From 2282471e4e01f6aecbdb1bed33c9ebb6ccf99c97 Mon Sep 17 00:00:00 2001 From: Thierry Wasylczenko Date: Tue, 21 Feb 2017 20:27:42 +0100 Subject: [PATCH] Release version 1.4 --- .travis.yml | 2 +- CHANGELOG.textile | 15 ++ SlideshowFX-app/build.gradle | 2 +- .../slideshowfx/app/SlideshowFXPreloader.java | 60 ++++- .../ReloadPresentationViewAndGoToTask.java | 14 +- .../controllers/AboutViewController.java | 37 +-- .../controllers/HelpViewController.java | 9 +- .../PresentationViewController.java | 136 ++++++----- .../controllers/SlideshowFXController.java | 54 ++--- .../TemplateBuilderController.java | 118 ++------- .../controls/PresentationBrowser.java | 126 +++++++--- .../nodes/TemplateConfigurationFilePane.java | 16 +- .../controls/tree/FileTreeCell.java | 203 +++++++++++----- .../controls/tree/TemplateTreeView.java | 226 ++++++++++++++++-- .../documentation/SlideshowFX_user.asciidoc | 2 +- .../presentation/PresentationEngine.java | 141 ++++++----- .../controllers/PluginsViewController.java | 200 +++++----------- SlideshowFX-ui-controls/build.gradle | 1 + .../ui/controls/PluginFileButton.java | 98 +++----- .../com/twasyl/slideshowfx/utils/Jar.java | 150 ++++++++++++ build.gradle | 20 +- gradle/wrapper/gradle-wrapper.jar | Bin 54224 -> 54208 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 11 +- 24 files changed, 1017 insertions(+), 626 deletions(-) mode change 100644 => 100755 SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/app/SlideshowFXPreloader.java mode change 100644 => 100755 SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/concurrent/ReloadPresentationViewAndGoToTask.java mode change 100644 => 100755 SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controllers/AboutViewController.java mode change 100644 => 100755 SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controllers/HelpViewController.java mode change 100644 => 100755 SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controls/PresentationBrowser.java create mode 100755 SlideshowFX-utils/src/main/java/com/twasyl/slideshowfx/utils/Jar.java diff --git a/.travis.yml b/.travis.yml index 561a233f..7a6bc1fc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ matrix: - os: linux jdk: oraclejdk8 - os: osx - osx_image: xcode8 + osx_image: xcode8.2 before_install: diff --git a/CHANGELOG.textile b/CHANGELOG.textile index 81783a6b..7e148c31 100755 --- a/CHANGELOG.textile +++ b/CHANGELOG.textile @@ -4,6 +4,21 @@ p. Changes to the SlideshowFX software are listed by version within this documen h2. Versions +h3(#1_4). "Version 1.4":#1_4 + +h4. New and noteworthy + +* Create files and directories directly from the tree view of the template builder ("#26":https://github.com/twasyl/SlideshowFX/issues/26) +* Display the application version in the splash screen ("#29":https://github.com/twasyl/SlideshowFX/issues/29) +* Go to the newly added slide when adding a slide ("#30":https://github.com/twasyl/SlideshowFX/issues/30) + +h4. Bug fixes + +* The help can be displayed ("#24":https://github.com/twasyl/SlideshowFX/issues/24) +* The label for the slides' template directory within the template builder has been corrected ("#25":https://github.com/twasyl/SlideshowFX/issues/25) +* Allow to load the template's configuration when a slide has no template elements ("#27":https://github.com/twasyl/SlideshowFX/issues/27) +* Allow to insert a slide that has no template elements in a presentation ("#28":https://github.com/twasyl/SlideshowFX/issues/28) + h3(#1_3). "Version 1.3":#1_3 h4. New and noteworthy diff --git a/SlideshowFX-app/build.gradle b/SlideshowFX-app/build.gradle index f132707a..925a0f48 100755 --- a/SlideshowFX-app/build.gradle +++ b/SlideshowFX-app/build.gradle @@ -1,6 +1,6 @@ import com.sun.javafx.PlatformUtil -version = '1.3' +version = '1.4' dependencies { compile project(':SlideshowFX-engines') diff --git a/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/app/SlideshowFXPreloader.java b/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/app/SlideshowFXPreloader.java old mode 100644 new mode 100755 index dc181244..6a7fb910 --- a/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/app/SlideshowFXPreloader.java +++ b/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/app/SlideshowFXPreloader.java @@ -1,16 +1,25 @@ package com.twasyl.slideshowfx.app; +import com.twasyl.slideshowfx.utils.Jar; import com.twasyl.slideshowfx.utils.ResourceHelper; import javafx.animation.FadeTransition; import javafx.application.Preloader; +import javafx.geometry.Pos; import javafx.scene.Scene; +import javafx.scene.control.Label; import javafx.scene.image.Image; import javafx.scene.image.ImageView; -import javafx.scene.layout.BorderPane; +import javafx.scene.layout.StackPane; +import javafx.scene.text.Font; import javafx.stage.Stage; import javafx.stage.StageStyle; import javafx.util.Duration; +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.logging.Level; +import java.util.logging.Logger; + /** * This class is the custom preloader for SlideshowFX. It displays a splash screen that fade in and fade out * before the application starts. @@ -20,21 +29,14 @@ * @since SlideshowFX 1.0 */ public class SlideshowFXPreloader extends Preloader { - + private static Logger LOGGER = Logger.getLogger(SlideshowFXPreloader.class.getName()); private Stage currentStage; @Override public void start(Stage primaryStage) throws Exception { this.currentStage = primaryStage; - final Image splashImage = new Image(ResourceHelper.getInputStream("/com/twasyl/slideshowfx/images/splash.png")); - final ImageView view = new ImageView(splashImage); - - final BorderPane pane = new BorderPane(); - pane.centerProperty().set(view); - pane.setBackground(null); - pane.setOpacity(0); - + final StackPane pane = getRootView(); final Scene scene = new Scene(pane); scene.setFill(null); @@ -55,9 +57,45 @@ public void start(Stage primaryStage) throws Exception { fadeIn.play(); } + protected StackPane getRootView() { + final StackPane pane = new StackPane(); + pane.setAlignment(Pos.CENTER); + pane.setBackground(null); + pane.setOpacity(0); + + final Label version = getVersion(); + version.setTranslateY(110); + + pane.getChildren().addAll(getSplashImage(), version); + + return pane; + } + + protected ImageView getSplashImage() { + final Image splashImage = new Image(ResourceHelper.getInputStream("/com/twasyl/slideshowfx/images/splash.png")); + return new ImageView(splashImage); + } + + protected Label getVersion() { + final Font font = new Font(Font.getDefault().getName(), 15); + + final Label text = new Label(); + text.setFont(font); + + try { + try (final Jar jar = Jar.fromClass(getClass())) { + text.setText(jar.getImplementationVersion()); + } + } catch (IOException | URISyntaxException e) { + LOGGER.log(Level.SEVERE, "Can not determine application version", e); + } + + return text; + } + @Override public void handleStateChangeNotification(StateChangeNotification info) { - if(info.getType() == StateChangeNotification.Type.BEFORE_START) { + if (info.getType() == StateChangeNotification.Type.BEFORE_START) { final FadeTransition fadeOut = new FadeTransition(Duration.millis(500), this.currentStage.getScene().getRoot()); fadeOut.setFromValue(1.0); fadeOut.setToValue(0); diff --git a/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/concurrent/ReloadPresentationViewAndGoToTask.java b/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/concurrent/ReloadPresentationViewAndGoToTask.java old mode 100644 new mode 100755 index 50f0eff6..d3e0ecb0 --- a/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/concurrent/ReloadPresentationViewAndGoToTask.java +++ b/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/concurrent/ReloadPresentationViewAndGoToTask.java @@ -2,20 +2,28 @@ import com.twasyl.slideshowfx.controllers.PresentationViewController; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; +import java.util.logging.Logger; + /** * This tasks reloads the presentation view and then go to a given slide. If the {@link #presentationView} is null * , the task is considered as failed. * * @author Thierry Wasylczenko - * @version 1.0 + * @version 1.1 * @since SlideshowFX 1.0 */ public class ReloadPresentationViewAndGoToTask extends ReloadPresentationViewTask { + private static final Logger LOGGER = Logger.getLogger(ReloadPresentationViewAndGoToTask.class.getName()); public ReloadPresentationViewAndGoToTask(final PresentationViewController presentationView, final String slideId) { super(presentationView, () -> { - presentationView.goToSlide(slideId); - presentationView.reloadPresentationBrowser(); + final CompletableFuture reloadDone = presentationView.reloadPresentationBrowser(); + reloadDone.thenRun(() -> { + LOGGER.log(Level.FINE, "Going to slide " + slideId); + presentationView.goToSlide(slideId); + }); }); } } \ No newline at end of file diff --git a/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controllers/AboutViewController.java b/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controllers/AboutViewController.java old mode 100644 new mode 100755 index 664d8829..673eac3e --- a/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controllers/AboutViewController.java +++ b/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controllers/AboutViewController.java @@ -6,6 +6,7 @@ import com.twasyl.slideshowfx.osgi.OSGiManager; import com.twasyl.slideshowfx.plugin.InstalledPlugin; import com.twasyl.slideshowfx.snippet.executor.ISnippetExecutor; +import com.twasyl.slideshowfx.utils.Jar; import javafx.event.Event; import javafx.fxml.FXML; import javafx.fxml.Initializable; @@ -15,14 +16,11 @@ import javafx.scene.input.MouseEvent; import javafx.stage.WindowEvent; -import java.io.File; import java.io.IOException; import java.net.URISyntaxException; import java.net.URL; import java.util.ResourceBundle; -import java.util.jar.Attributes; import java.util.jar.JarFile; -import java.util.jar.Manifest; import java.util.logging.Level; import java.util.logging.Logger; @@ -30,16 +28,20 @@ * Controller class of the {@code AboutView.fxml} view. * * @author Thierry Wasylczenko + * @version 1.1 * @since SlideshowFX 1.0 - * @version 1.0 */ public class AboutViewController implements Initializable { private Logger LOGGER = Logger.getLogger(AboutViewController.class.getName()); - @FXML private Parent root; - @FXML private Label slideshowFXVersion; - @FXML private Label javaVersion; - @FXML private TableView plugins; + @FXML + private Parent root; + @FXML + private Label slideshowFXVersion; + @FXML + private Label javaVersion; + @FXML + private TableView plugins; @FXML public void exitByClick(final MouseEvent event) { @@ -69,31 +71,18 @@ public void initialize(URL location, ResourceBundle resources) { /** * Get the version of the application. The version is stored within the {@code MANIFEST.MF} file of the {@link JarFile} * of the application. + * * @return The version of the application stored in the {@code MANIFEST.MF} file or {@code null} if it can not be found. */ private String getApplicationVersion() { String appVersion = null; - try(final JarFile jarFile = new JarFile(getJARLocation())) { - final Manifest manifest = jarFile.getManifest(); - final Attributes attrs = manifest.getMainAttributes(); - if(attrs != null) { - appVersion = attrs.getValue("Implementation-Version"); - } + try (final Jar jar = Jar.fromClass(getClass())) { + appVersion = jar.getImplementationVersion(); } catch (IOException | URISyntaxException e) { LOGGER.log(Level.SEVERE, "Can not get application's version", e); } return appVersion; } - - /** - * Get the {@link File} that corresponds to the JAR file of the application. - * @return The file corresponding to JRA file of the application. - * @throws URISyntaxException If the location can not be determined. - */ - private File getJARLocation() throws URISyntaxException { - final File file = new File(getClass().getProtectionDomain().getCodeSource().getLocation().toURI().getPath()); - return file; - } } diff --git a/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controllers/HelpViewController.java b/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controllers/HelpViewController.java old mode 100644 new mode 100755 index f1f4863f..f799be6b --- a/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controllers/HelpViewController.java +++ b/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controllers/HelpViewController.java @@ -13,13 +13,15 @@ * Controller class of the {@code HelpView.fxml} view. * * @author Thierry Wasylczenko + * @version 1.1 * @since SlideshowFX 1.0 - * @version 1.0 */ public class HelpViewController implements Initializable { - @FXML private WebView userDocumentationBrowser; - @FXML private WebView developerDocumentationBrowser; + @FXML + private WebView userDocumentationBrowser; + @FXML + private WebView developerDocumentationBrowser; @Override public void initialize(URL location, ResourceBundle resources) { @@ -71,7 +73,6 @@ protected Attributes getAsciidoctorAttributes() { .tableOfContents(Placement.LEFT) .styleSheetName("slideshowfx.css") .stylesDir(ResourceHelper.getExternalForm("/com/twasyl/slideshowfx/documentation/css")) - .imagesDir(ResourceHelper.getExternalForm("/com/twasyl/slideshowfx/documentation/images")) .noFooter(true) .get(); diff --git a/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controllers/PresentationViewController.java b/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controllers/PresentationViewController.java index fe5ec2e9..b738d1a8 100755 --- a/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controllers/PresentationViewController.java +++ b/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controllers/PresentationViewController.java @@ -48,16 +48,17 @@ import java.util.Iterator; import java.util.Optional; import java.util.ResourceBundle; +import java.util.concurrent.CompletableFuture; import java.util.logging.Level; import java.util.logging.Logger; /** - * This class is the controller of the {@code PresentationView.fxml} file. It defines all actions possible inside the view - * represented by the FXML. - * - * @author Thierry Wasyczenko - * @version 1.1 - * @since SlideshowFX 1.0 + * This class is the controller of the {@code PresentationView.fxml} file. It defines all actions possible inside the view + * represented by the FXML. + * + * @author Thierry Wasyczenko + * @version 1.2 + * @since SlideshowFX 1.0 */ public class PresentationViewController implements Initializable { private static final Logger LOGGER = Logger.getLogger(PresentationViewController.class.getName()); @@ -67,37 +68,48 @@ public class PresentationViewController implements Initializable { private final ReadOnlyStringProperty presentationName = new SimpleStringProperty(); private final ReadOnlyBooleanProperty presentationModified = new SimpleBooleanProperty(false); - @FXML private PresentationBrowser browser; - @FXML private TextField slideNumber; - @FXML private TextField fieldName; - @FXML private HBox markupContentTypeBox; - @FXML private ToolBar contentExtensionToolBar; - @FXML private ToggleGroup markupContentType = new ToggleGroup(); - @FXML private SlideContentEditor contentEditor; - @FXML private Button defineContent; + @FXML + private PresentationBrowser browser; + @FXML + private TextField slideNumber; + @FXML + private TextField fieldName; + @FXML + private HBox markupContentTypeBox; + @FXML + private ToolBar contentExtensionToolBar; + @FXML + private ToggleGroup markupContentType = new ToggleGroup(); + @FXML + private SlideContentEditor contentEditor; + @FXML + private Button defineContent; /* All methods called by the FXML */ /** * This method is called by the Define button of the FXML. The selected syntax is retrieved as well as the content. - * The treatment is then delegated to the {@link #updateSlide(com.twasyl.slideshowfx.markup.IMarkup, String)} method. + * The treatment is then delegated to the {@link #updateSlide(IMarkup, String)} method. * * @param event - * @throws javax.xml.transform.TransformerException - * @throws java.io.IOException - * @throws javax.xml.parsers.ParserConfigurationException - * @throws org.xml.sax.SAXException + * @throws TransformerException + * @throws IOException + * @throws ParserConfigurationException + * @throws SAXException */ - @FXML private void updateSlideWithText(ActionEvent event) throws TransformerException, IOException, ParserConfigurationException, SAXException { + @FXML + private void updateSlideWithText(ActionEvent event) throws TransformerException, IOException, ParserConfigurationException, SAXException { this.updateSlide(); } /** * Define and manages variables that are available for the presentation. Variable allow to insert elements which * values will be replaced inside the presentation. + * * @param event The source event calling this method. */ - @FXML private void definePresentationVariables(ActionEvent event) { + @FXML + private void definePresentationVariables(ActionEvent event) { final PresentationVariablesPanel variablesPanel = new PresentationVariablesPanel(this.presentationEngine.getConfiguration()); final ButtonType insert = new ButtonType("Insert", ButtonBar.ButtonData.OTHER); @@ -105,14 +117,15 @@ public class PresentationViewController implements Initializable { final ButtonType answer = DialogHelper.showDialog("Insert a variable", variablesPanel, ButtonType.CANCEL, insert, ButtonType.OK); // Insert the token inside the editor - if(answer != null && answer == insert) { + if (answer != null && answer == insert) { final Pair variable = variablesPanel.getSelectedVariable(); - if(variable != null) this.contentEditor.appendContentEditorValue(String.format("${%1$s}", variable.getKey())); + if (variable != null) + this.contentEditor.appendContentEditorValue(String.format("${%1$s}", variable.getKey())); } // If cancel wasn't clicked, updates all variables in the presentation and updates it the presentation file - if(answer != ButtonType.CANCEL) { + if (answer != ButtonType.CANCEL) { this.presentationEngine.getConfiguration().setVariables(variablesPanel.getVariables()); this.presentationEngine.getConfiguration() @@ -128,6 +141,7 @@ public class PresentationViewController implements Initializable { * This method updates a slide of the presentation. The markup and the originalContent are * deduced from the user interface. If all parameters can be deduced, then {@link #updateSlide(IMarkup, String)} is * called, otherwise nothing is performed. + * * @throws TransformerException * @throws IOException * @throws ParserConfigurationException @@ -136,7 +150,7 @@ public class PresentationViewController implements Initializable { private void updateSlide() throws TransformerException, IOException, ParserConfigurationException, SAXException { RadioButton selectedMarkup = (RadioButton) this.markupContentType.getSelectedToggle(); - if(selectedMarkup != null) { + if (selectedMarkup != null) { this.updateSlide((IMarkup) selectedMarkup.getUserData(), this.contentEditor.getContentEditorValue()); } } @@ -148,12 +162,12 @@ private void updateSlide() throws TransformerException, IOException, ParserConfi * with the HTML content converted in Base64. * A screenshot of the slide is taken to update the menu of available slides. * - * @param markup The markup with which the new content was generated. + * @param markup The markup with which the new content was generated. * @param originalContent The original content, in Base64, with which the slide will be updated. - * @throws javax.xml.transform.TransformerException - * @throws java.io.IOException - * @throws javax.xml.parsers.ParserConfigurationException - * @throws org.xml.sax.SAXException + * @throws TransformerException + * @throws IOException + * @throws ParserConfigurationException + * @throws SAXException */ private void updateSlide(final IMarkup markup, final String originalContent) throws TransformerException, IOException, ParserConfigurationException, SAXException { final String elementId = String.format("%1$s-%2$s", this.slideNumber.getText(), this.fieldName.getText()); @@ -176,7 +190,7 @@ private void updateSlide(final IMarkup markup, final String originalContent) thr WritableImage thumbnail = this.browser.snapshot(null, null); this.presentationEngine.getConfiguration().updateSlideThumbnail(this.slideNumber.getText(), thumbnail); - if(this.parent != null) this.parent.updateSlideSplitMenu(); + if (this.parent != null) this.parent.updateSlideSplitMenu(); this.presentationEngine.setModifiedSinceLatestSave(true); } @@ -184,8 +198,8 @@ private void updateSlide(final IMarkup markup, final String originalContent) thr /** * Update the JavaFX UI with the data from the element that has been clicked in the HTML page. * - * @param slideNumber The slide number of the slide that has been clicked in the HTML page - * @param field The field of the slide that has been clicked in the HTML page. + * @param slideNumber The slide number of the slide that has been clicked in the HTML page + * @param field The field of the slide that has been clicked in the HTML page. * @param currentElementContent The current content of the element clicked in the HTML page. */ public void prefillContentDefinition(String slideNumber, String field, String currentElementContent) { @@ -229,24 +243,24 @@ public void prefillContentDefinition(String slideNumber, String field, String cu /** * This method is called by the presentation in order to execute a code snippet. The executor is identified by the - * {@code snippetExecutorCode} and retrieved in the OSGi context to get the {@link com.twasyl.slideshowfx.snippet.executor.ISnippetExecutor} + * {@code snippetExecutorCode} and retrieved in the OSGi context to get the {@link ISnippetExecutor} * instance that will execute the code. * The code to execute is passed to this method in Base64 using the {@code base64CodeSnippet} parameter. The execution * result will be pushed back to the presentation in the HTML element {@code consoleOutputId}. * * @param snippetExecutorCode The unique identifier of the executor that will execute the code. - * @param base64CodeSnippet The code snippet to execute, given in Base64. - * @param consoleOutputId The HTML element that will be updated with the execution result. + * @param base64CodeSnippet The code snippet to execute, given in Base64. + * @param consoleOutputId The HTML element that will be updated with the execution result. */ public void executeCodeSnippet(final String snippetExecutorCode, final String base64CodeSnippet, final String consoleOutputId) { - if(snippetExecutorCode != null) { + if (snippetExecutorCode != null) { final Optional snippetExecutor = OSGiManager.getInstance().getInstalledServices(ISnippetExecutor.class) .stream() .filter(executor -> snippetExecutorCode.equals(executor.getCode())) .findFirst(); - if(snippetExecutor.isPresent()) { + if (snippetExecutor.isPresent()) { final String decodedString = new String(Base64.getDecoder().decode(base64CodeSnippet), GlobalConfiguration.getDefaultCharset()); final CodeSnippet codeSnippetDecoded = CodeSnippet.toObject(decodedString); final ObservableList consoleOutput = snippetExecutor.get().execute(codeSnippetDecoded); @@ -273,6 +287,7 @@ public void executeCodeSnippet(final String snippetExecutorCode, final String ba * added to the panel of markups as well as in the ToggleGroup for all markups. * Note that the RadioButton will not request focus when it is clicked. This avoid the cursor to leave an eventual * text edition area. + * * @param markup The markup to create the RadioButton for * @return The created RadioButton. */ @@ -294,6 +309,7 @@ public void requestFocus() { /** * Creates a Button for the given content extension so the user will be able to insert new type of content in a slide. * The Button is added to the ToolBar of content extensions. + * * @param contentExtension The content extension to create the Button for. * @return The created Button. */ @@ -367,10 +383,10 @@ public void refreshMarkupSyntax() { final Iterator it = this.markupContentTypeBox.getChildren().iterator(); Node child; - while(it.hasNext()) { + while (it.hasNext()) { child = it.next(); - if(child instanceof RadioButton) it.remove(); + if (child instanceof RadioButton) it.remove(); } // Creating RadioButtons for each markup bundle installed @@ -386,10 +402,10 @@ public void refreshContentExtensions() { final Iterator iterator = this.contentExtensionToolBar.getItems().iterator(); Node child; - while(iterator.hasNext()) { + while (iterator.hasNext()) { child = iterator.next(); - if(child instanceof Button && child.getUserData() instanceof IContentExtension) iterator.remove(); + if (child instanceof Button && child.getUserData() instanceof IContentExtension) iterator.remove(); } // Creating Buttons for each extension bundle installed @@ -400,10 +416,12 @@ public void refreshContentExtensions() { } /** - * This method refreshed the browser displaying the presentation. + * Reload the browser displaying the presentation. + * + * @return A {@link CompletableFuture} which will be completed when the browser is no more loading it's content. */ - public void reloadPresentationBrowser() { - this.browser.reload(); + public CompletableFuture reloadPresentationBrowser() { + return this.browser.reload(); } /** @@ -411,7 +429,7 @@ public void reloadPresentationBrowser() { * exists, nothing if done. */ public void loadPresentationInBrowser() { - if(this.presentationEngine.getConfiguration().getPresentationFile() != null + if (this.presentationEngine.getConfiguration().getPresentationFile() != null && this.presentationEngine.getConfiguration().getPresentationFile().exists()) { this.browser.loadPresentation(this.presentationEngine); } @@ -419,11 +437,12 @@ public void loadPresentationInBrowser() { /** * Defines the presentation for the given view and load it in the browser. + * * @param presentation The presentation associated to the view. - * @throws java.lang.NullPointerException If {@code presentation} is {@code null}. + * @throws NullPointerException If {@code presentation} is {@code null}. */ - public void definePresentation(final PresentationEngine presentation) { - if(presentation == null) throw new NullPointerException("The presentation can not be null"); + public void definePresentation(final PresentationEngine presentation) { + if (presentation == null) throw new NullPointerException("The presentation can not be null"); this.presentationEngine = presentation; this.loadPresentationInBrowser(); @@ -452,11 +471,14 @@ public void definePresentation(final PresentationEngine presentation) { } /** - * Get the presentation name. This will typically be the name of the {@link com.twasyl.slideshowfx.engine.presentation.PresentationEngine#getArchive()} + * Get the presentation name. This will typically be the name of the {@link PresentationEngine#getArchive()} * object, or "Untitled" if it doesn't exist. + * * @return The name of this presentation. */ - public ReadOnlyStringProperty getPresentationName() { return this.presentationName; } + public ReadOnlyStringProperty getPresentationName() { + return this.presentationName; + } /** * Indicates if the presentation has been modified since the latest time it has been saved. @@ -464,17 +486,20 @@ public void definePresentation(final PresentationEngine presentation) { * @return The property indicating if the presentation has been modified since the latest save. */ - public ReadOnlyBooleanProperty presentationModifiedProperty() { return presentationModified; } + public ReadOnlyBooleanProperty presentationModifiedProperty() { + return presentationModified; + } /** * Get the slide number of the slide currently displayed. + * * @return The slide number of the current displayed slide or {@code null} if no slide is displayed. */ public String getCurrentSlideNumber() { String slideNumber = null; final String slideId = this.getCurrentSlideId(); - if(slideId != null && !slideId.isEmpty()) { + if (slideId != null && !slideId.isEmpty()) { slideNumber = slideId.substring(this.presentationEngine.getTemplateConfiguration().getSlideIdPrefix().length()); } @@ -483,6 +508,7 @@ public String getCurrentSlideNumber() { /** * Get the ID of the slide currently displayed. + * * @return The ID of the slide currently displayed or {@code null} if no slide is displayed. */ public String getCurrentSlideId() { @@ -491,10 +517,11 @@ public String getCurrentSlideId() { /** * Go to a specific slide ID. If the given ID is {@code null} or empty, nothing will be performed. + * * @param slideId The ID of the slide to go to. */ public void goToSlide(final String slideId) { - if(slideId != null && !slideId.isEmpty()) { + if (slideId != null && !slideId.isEmpty()) { this.browser.slide(slideId); } } @@ -515,6 +542,7 @@ public void setAsCurrentPresentation() { /** * Get the presentation associated to this view. + * * @return The presentation associated to this view. */ public PresentationEngine getPresentation() { diff --git a/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controllers/SlideshowFXController.java b/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controllers/SlideshowFXController.java index 13090507..0282e2bf 100755 --- a/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controllers/SlideshowFXController.java +++ b/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controllers/SlideshowFXController.java @@ -85,57 +85,53 @@ * represented by the FXML. * * @author Thierry Wasyczenko - * @version 1.2 + * @version 1.3 * @since SlideshowFX 1.0 */ public class SlideshowFXController implements Initializable { private static final Logger LOGGER = Logger.getLogger(SlideshowFXController.class.getName()); - private final EventHandler addSlideActionEvent = new EventHandler() { - @Override - public void handle(ActionEvent actionEvent) { - try { + private final EventHandler addSlideActionEvent = event -> { + try { - final PresentationEngine presentation = Presentations.getCurrentDisplayedPresentation(); - final PresentationViewController view = SlideshowFXController.this.getCurrentPresentationView(); - final Object userData = ((MenuItem) actionEvent.getSource()).getUserData(); + final PresentationEngine presentation = Presentations.getCurrentDisplayedPresentation(); + final PresentationViewController view = SlideshowFXController.this.getCurrentPresentationView(); + final Object userData = ((MenuItem) event.getSource()).getUserData(); - if (userData instanceof SlideTemplate && view != null && presentation != null) { - presentation.addSlide((SlideTemplate) userData, view.getCurrentSlideNumber()); + if (userData instanceof SlideTemplate && view != null && presentation != null) { + final Slide addedSlide = presentation.addSlide((SlideTemplate) userData, view.getCurrentSlideNumber()); - final ReloadPresentationViewTask task = new ReloadPresentationViewTask(view); + if(addedSlide != null) { + final ReloadPresentationViewTask task = new ReloadPresentationViewAndGoToTask(view, addedSlide.getId()); SlideshowFXController.this.taskInProgress.setCurrentTask(task); TaskDAO.getInstance().startTask(task); SlideshowFXController.this.updateSlideSplitMenu(); } - } catch (IOException e) { - LOGGER.log(Level.SEVERE, "Error when adding a slide", e); } + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Error when adding a slide", e); } }; - private final EventHandler moveSlideActionEvent = new EventHandler() { - @Override - public void handle(ActionEvent actionEvent) { - final PresentationEngine presentation = Presentations.getCurrentDisplayedPresentation(); - final PresentationViewController view = SlideshowFXController.this.getCurrentPresentationView(); - - if(view != null && presentation != null) { - final SlideMenuItem menunItem = (SlideMenuItem) actionEvent.getSource(); - final Slide slideToMove = presentation.getConfiguration().getSlideById(view.getCurrentSlideId()); - final Slide beforeSlide = menunItem.getSlide(); + private final EventHandler moveSlideActionEvent = event -> { + final PresentationEngine presentation = Presentations.getCurrentDisplayedPresentation(); + final PresentationViewController view = SlideshowFXController.this.getCurrentPresentationView(); - presentation.moveSlide(slideToMove, beforeSlide); + if(view != null && presentation != null) { + final SlideMenuItem menunItem = (SlideMenuItem) event.getSource(); + final Slide slideToMove = presentation.getConfiguration().getSlideById(view.getCurrentSlideId()); + final Slide beforeSlide = menunItem.getSlide(); - final ReloadPresentationViewTask task = new ReloadPresentationViewTask(view); - SlideshowFXController.this.taskInProgress.setCurrentTask(task); - TaskDAO.getInstance().startTask(task); + presentation.moveSlide(slideToMove, beforeSlide); - SlideshowFXController.this.updateSlideSplitMenu(); - } + final ReloadPresentationViewTask task = new ReloadPresentationViewTask(view); + SlideshowFXController.this.taskInProgress.setCurrentTask(task); + TaskDAO.getInstance().startTask(task); + SlideshowFXController.this.updateSlideSplitMenu(); } + }; @FXML private BorderPane root; diff --git a/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controllers/TemplateBuilderController.java b/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controllers/TemplateBuilderController.java index 8b112d70..8028fa7e 100755 --- a/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controllers/TemplateBuilderController.java +++ b/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controllers/TemplateBuilderController.java @@ -10,7 +10,10 @@ import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.Initializable; -import javafx.scene.control.*; +import javafx.scene.control.ButtonType; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; +import javafx.scene.control.TreeItem; import javafx.scene.input.MouseButton; import javafx.stage.DirectoryChooser; import javafx.stage.FileChooser; @@ -20,7 +23,6 @@ import java.io.IOException; import java.net.URL; import java.nio.file.Files; -import java.util.Arrays; import java.util.Optional; import java.util.ResourceBundle; import java.util.logging.Level; @@ -30,7 +32,7 @@ * Controller class used for the Template Builder. * * @author Thierry Wasylczenko - * @version 1.1 + * @version 1.2 * @since SlideshowFX 1.0 */ public class TemplateBuilderController implements Initializable { @@ -74,7 +76,7 @@ private void addFolderToTreeView(ActionEvent event) { final File directory = chooser.showDialog(null); if (directory != null) { - this.addContentToTreeView(directory); + this.templateContentTreeView.appendContentToTreeView(directory); } } @@ -90,7 +92,7 @@ private void addFileToTreeView(ActionEvent event) { final File file = chooser.showOpenDialog(null); if (file != null) { - this.addContentToTreeView(file); + this.templateContentTreeView.appendContentToTreeView(file); } } @@ -208,55 +210,7 @@ private void deleteFromTreeView(ActionEvent event) { */ @FXML private void createDirectory(ActionEvent event) { - final TextField field = new TextField(); - field.setPromptText("Directory name"); - - ButtonType response = DialogHelper.showCancellableDialog("Create a directory", field); - - if (response != null && response == ButtonType.OK) { - if (!field.getText().trim().isEmpty()) { - TreeItem parent = this.templateContentTreeView.getSelectionModel().getSelectedItem(); - - if (parent == null) parent = this.templateContentTreeView.getRoot(); - else { - // Ensure the selected item contain a directory. If it contains a file, the parent is taken. - if (parent.getValue().isFile()) { - parent = parent.getParent(); - } - } - - /** - * Split the text by / and create each directory and append it to the TreeView. - */ - - TreeItem tmpItem; - for (String name : field.getText().trim().split("/")) { - final File tmpFile = new File(parent.getValue(), name); - tmpItem = new TreeItem<>(tmpFile); - - if (!tmpFile.exists()) { - final boolean result = tmpFile.mkdir(); - if (result) { - // Avoid duplicates in the tree - Optional> sameItem = parent.getChildren() - .stream() - .filter(item -> item.getValue().equals(tmpFile)) - .findFirst(); - - if (!sameItem.isPresent()) { - parent.getChildren().add(tmpItem); - parent = tmpItem; - } else { - parent = sameItem.get(); - } - - } else { - LOGGER.log(Level.INFO, "Could not create directory: " + tmpFile.getAbsolutePath()); - } - } - } - } - } + this.templateContentTreeView.promptUserAndCreateNewDirectory(); } /** @@ -268,53 +222,7 @@ private void createDirectory(ActionEvent event) { */ @FXML private void createFile(ActionEvent event) { - final TextField field = new TextField(); - field.setPromptText("File name"); - - ButtonType response = DialogHelper.showCancellableDialog("Create a file", field); - - if (response != null && response == ButtonType.OK) { - if (!field.getText().trim().isEmpty()) { - TreeItem parent = this.templateContentTreeView.getSelectionModel().getSelectedItem(); - - if (parent == null) parent = this.templateContentTreeView.getRoot(); - else { - // Ensure the selected item contain a directory. If it contains a file, the parent is taken. - if (parent.getValue().isFile()) { - parent = parent.getParent(); - } - } - - final File newFile = new File(parent.getValue(), field.getText().trim()); - try { - Files.createFile(newFile.toPath()); - final TreeItem newFileItem = new TreeItem<>(newFile); - - parent.getChildren().add(newFileItem); - } catch (IOException e) { - LOGGER.log(Level.SEVERE, "Can not create the empty file", e); - } - } - } - } - - /** - * This method adds the given content to the TreeView. It detects if a TreeItem containing a directory - * is selected in the TreeView to add the content to the selection. If not, the content is added to the root - * of the TreeView. - * - * @param content The content to add to the TreeView. - */ - private void addContentToTreeView(File content) { - if (content != null && content.exists()) { - TreeItem parent = this.templateContentTreeView.getSelectionModel().getSelectedItem(); - - if (parent == null || !parent.getValue().isDirectory()) { - parent = this.templateContentTreeView.getRoot(); - } - - this.templateContentTreeView.appendContentToTreeView(content, parent); - } + this.templateContentTreeView.promptUserAndCreateNewFile(); } /** @@ -349,8 +257,14 @@ public void setTemplateEngine(TemplateEngine templateEngine) { final File[] children = this.templateEngine.getWorkingDirectory().listFiles(); if (children != null) { - Arrays.stream(children).forEach(child -> this.addContentToTreeView(child)); + + for (File child : children) { + this.templateContentTreeView.appendContentToTreeView(child, root); + } } + + this.templateContentTreeView.closeItem(root); + root.setExpanded(true); } } diff --git a/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controls/PresentationBrowser.java b/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controls/PresentationBrowser.java old mode 100644 new mode 100755 index a5767918..c156bd13 --- a/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controls/PresentationBrowser.java +++ b/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controls/PresentationBrowser.java @@ -4,6 +4,7 @@ import com.twasyl.slideshowfx.engine.template.configuration.TemplateConfiguration; import com.twasyl.slideshowfx.server.SlideshowFXServer; import com.twasyl.slideshowfx.utils.DialogHelper; +import com.twasyl.slideshowfx.utils.PlatformHelper; import javafx.beans.binding.DoubleBinding; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; @@ -24,6 +25,7 @@ import netscape.javascript.JSObject; import java.util.Base64; +import java.util.concurrent.CompletableFuture; import java.util.logging.Level; import java.util.logging.Logger; @@ -36,7 +38,7 @@ * browser under the name returned by {@link TemplateConfiguration#getJsObject()} variable stored in the {@link #presentationProperty()}. * * @author Thierry Wasylczenko - * @version 1.0.0 + * @version 1.1 * @since SlideshowFX 1.0 */ public final class PresentationBrowser extends StackPane { @@ -62,48 +64,69 @@ public PresentationBrowser() { * used to display the presentation and is never null. * CAUTION: in order to manipulate the presentation, you should use the methods present in the {@link PresentationBrowser} * class and not the internal browser. + * * @return The internal browser of this {@link PresentationBrowser}. */ - public WebView getInternalBrowser() { return this.internalBrowser; } + public WebView getInternalBrowser() { + return this.internalBrowser; + } /** * The presentation associated to this browser. + * * @return The property of the presentation associated to this browser. */ - public ObjectProperty presentationProperty() { return presentation; } + public ObjectProperty presentationProperty() { + return presentation; + } /** * Get the presentation associated to this browser. + * * @return The presentation associated to this browser or {@code null} if it hasn't been defined yet. */ - public PresentationEngine getPresentation() { return presentation.get(); } + public PresentationEngine getPresentation() { + return presentation.get(); + } /** * Defines the presentation associated to this browser. + * * @param presentation The presentation associated to this browser. */ - public void setPresentation(PresentationEngine presentation) { this.presentation.set(presentation); } + public void setPresentation(PresentationEngine presentation) { + this.presentation.set(presentation); + } /** * The backend associated to this browser. The backend is defined as member of the page displayed under the name * returned by the {@link TemplateConfiguration#getJsObject()} method of the current {@link #presentationProperty()}. + * * @return The property for backend object that has been defined. */ - public ObjectProperty backendProperty() { return backend; } + public ObjectProperty backendProperty() { + return backend; + } /** * The backend associated to this browser. The backend is defined as member of the page displayed under the name * returned by the {@link TemplateConfiguration#getJsObject()} method of the current {@link #presentationProperty()}. + * * @return The backend object that has been defined or {@code null} if it hasn't been defined yet. */ - public Object getBackend() { return backend.get(); } + public Object getBackend() { + return backend.get(); + } /** * Defines the backend associated to this browser. The backend is defined as member of the page displayed under the * name returned by the {@link TemplateConfiguration#getJsObject()} method of the current {@link #presentationProperty()}. + * * @param backend The backend to set as member of the displayed page. */ - public void setBackend(Object backend) { this.backend.set(backend); } + public void setBackend(Object backend) { + this.backend.set(backend); + } /** * Initializes the node indicating the status of the page's loading. @@ -155,10 +178,11 @@ private final void initializeBrowser() { * as well as the {@link #presentationProperty()}. * The backend is defined under the name returned by the * {@link TemplateConfiguration#getJsObject()} method of the current {@link #presentationProperty()}. + * * @param backend The backend object to inject into the page. */ private final void injectBackend(Object backend) { - if(backend != null + if (backend != null && this.internalBrowser.getEngine().getLoadWorker().getState() == Worker.State.SUCCEEDED && this.getPresentation() != null && this.getPresentation().getTemplateConfiguration() != null @@ -168,7 +192,7 @@ private final void injectBackend(Object backend) { // Only inject the backend if it is not already present final Object member = window.getMember(this.getPresentation().getTemplateConfiguration().getJsObject()); - if("undefined".equals(member)) { + if ("undefined".equals(member)) { window.setMember(PresentationBrowser.this.getPresentation().getTemplateConfiguration().getJsObject(), backend); } } @@ -178,10 +202,11 @@ private final void injectBackend(Object backend) { * Injects the {@code server} inside the displayed page under the named returned by the * {@link TemplateConfiguration#getSfxServerObject()} method for the current {@link #presentationProperty()} only if * the given {@code server}is not {@code null}. + * * @param server The server to inject within the displayed page. */ private final void injectServer(final SlideshowFXServer server) { - if(server != null + if (server != null && this.internalBrowser.getEngine().getLoadWorker().getState() == Worker.State.SUCCEEDED && this.getPresentation() != null && this.getPresentation().getTemplateConfiguration() != null @@ -191,7 +216,7 @@ private final void injectServer(final SlideshowFXServer server) { // Only inject the server if it is not already present final Object member = window.getMember(this.getPresentation().getTemplateConfiguration().getSfxServerObject()); - if("undefined".equals(member)) { + if ("undefined".equals(member)) { window.setMember(this.getPresentation().getTemplateConfiguration().getSfxServerObject(), server); } } @@ -199,24 +224,34 @@ private final void injectServer(final SlideshowFXServer server) { /** * Indicates if the user can interact with the internal browser, meaning click on it and so on. + * * @return The property indicating if user interaction is allowed for the internal browser. */ - public BooleanProperty interactionAllowedProperty() { return interactionAllowed; } + public BooleanProperty interactionAllowedProperty() { + return interactionAllowed; + } /** * Indicates if the user can interact with the internal browser, meaning click on it and so on. + * * @return {@code true} if user interactions are allowed for the internal browser, {@code false} otherwise. */ - public boolean isInteractionAllowed() { return interactionAllowed.get(); } + public boolean isInteractionAllowed() { + return interactionAllowed.get(); + } /** * Defines if user interactions are allowed for the internal browser. + * * @param interactionAllowed {@code true} if user interactions are allowed, {@code false} otherwise. */ - public void setInteractionAllowed(boolean interactionAllowed) { this.interactionAllowed.set(interactionAllowed); } + public void setInteractionAllowed(boolean interactionAllowed) { + this.interactionAllowed.set(interactionAllowed); + } /** * Loads the given presentation inside the browser. + * * @param presentation The presentation to load. */ public final void loadPresentation(final PresentationEngine presentation) { @@ -224,7 +259,7 @@ public final void loadPresentation(final PresentationEngine presentation) { } public final void loadPresentationAndDo(final PresentationEngine presentation, Runnable action) { - if(presentation != null) { + if (presentation != null) { this.presentation.set(presentation); final ChangeListener stateListener = new ChangeListener() { @@ -236,8 +271,8 @@ public void changed(ObservableValue observable, Worker.S PresentationBrowser.this.injectServer(SlideshowFXServer.getSingleton()); try { - if(action != null) action.run(); - } catch(JSException jsex) { + if (action != null) action.run(); + } catch (JSException jsex) { LOGGER.log(Level.SEVERE, "Error while executing an action in the internal browser", jsex); } } @@ -248,11 +283,30 @@ public void changed(ObservableValue observable, Worker.S this.internalBrowser.getEngine().load(presentation.getConfiguration().getPresentationFile().toURI().toASCIIString()); } } + /** * Simply reloads the page displayed in the browser, not necessarily the {@link #presentationProperty()}. + * + * @return A {@link CompletableFuture} which will be complete when the browser is no more loading. */ - public final void reload() { - this.internalBrowser.getEngine().reload(); + public final CompletableFuture reload() { + final Worker loadWorker = this.internalBrowser.getEngine().getLoadWorker(); + final CompletableFuture reloadDone = new CompletableFuture<>(); + + final ChangeListener stateListener = new ChangeListener() { + @Override + public void changed(ObservableValue state, Worker.State oldState, Worker.State newState) { + if (newState != null && newState != Worker.State.RUNNING && newState != Worker.State.SCHEDULED && newState != Worker.State.READY) { + loadWorker.stateProperty().removeListener(this); + reloadDone.complete(true); + } + } + }; + loadWorker.stateProperty().addListener(stateListener); + + PlatformHelper.run(() -> this.internalBrowser.getEngine().reload()); + + return reloadDone; } /** @@ -264,7 +318,7 @@ public final void print() { if (job != null) { if (job.showPrintDialog(null)) { - if(this.getPresentation().getArchive() != null) { + if (this.getPresentation().getArchive() != null) { final String extension = ".".concat(this.getPresentation().getArchiveExtension()); final int indexOfExtension = this.getPresentation().getArchive().getName().indexOf(extension); final String jobName = this.getPresentation().getArchive().getName().substring(0, indexOfExtension); @@ -288,6 +342,7 @@ public final void print() { * Get the current ID of the slide displayed. The ID is retrieved from the JavaScript method identified by the name * returned by the {@link TemplateConfiguration#getGetCurrentSlideMethod()} method of the current * {@link #presentationProperty()}. + * * @return The ID of the slide currently displayed, depending on the implementation of the JavaScript method for getting * it. */ @@ -301,6 +356,7 @@ public final String getCurrentSlideId() { * must not be Base64 encoded. * The JavaScript method identified by the name returned by the {@link TemplateConfiguration#getContentDefinerMethod()} * of the current {@link #presentationProperty()} will be called to define the content. + * * @param slideNumber The number of the slide to define the content for an element. * @param elementName The name of the element to define the content for. * @param htmlContent The HTML content, not Base64 encoded, for the element to define. @@ -321,31 +377,33 @@ public final void defineContent(final String slideNumber, final String elementNa * The JavaScript method identified by the name returned by the * {@link TemplateConfiguration#getUpdateCodeSnippetConsoleMethod()} method of the current {@link #presentationProperty()} * will be called in order to update the console output. + * * @param consoleOutputId The ID of the console to update. - * @param consoleLine The line to insert in the console output. + * @param consoleLine The line to insert in the console output. */ public final void updateCodeSnippetConsole(final String consoleOutputId, final String consoleLine) { this.internalBrowser.getEngine().executeScript( - String.format("%1$s('%2$s', '%3$s');", - this.getPresentation().getTemplateConfiguration().getUpdateCodeSnippetConsoleMethod(), - consoleOutputId, - Base64.getEncoder().encodeToString(consoleLine.getBytes(getDefaultCharset())) - )); + String.format("%1$s('%2$s', '%3$s');", + this.getPresentation().getTemplateConfiguration().getUpdateCodeSnippetConsoleMethod(), + consoleOutputId, + Base64.getEncoder().encodeToString(consoleLine.getBytes(getDefaultCharset())) + )); } /** * Go to the slide identified by the given {@code slideId}. + * * @param slideId The ID of the slide to go to. */ public void slide(final String slideId) { - if(slideId != null) { + if (slideId != null) { this.internalBrowser.getEngine().executeScript( - String.format( - "%1$s('%2$s');", - this.getPresentation().getTemplateConfiguration().getGotoSlideMethod(), - slideId - ) - ); + String.format( + "%1$s('%2$s');", + this.getPresentation().getTemplateConfiguration().getGotoSlideMethod(), + slideId + ) + ); } } } diff --git a/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controls/builder/nodes/TemplateConfigurationFilePane.java b/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controls/builder/nodes/TemplateConfigurationFilePane.java index d09dabd6..55c9bc1b 100755 --- a/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controls/builder/nodes/TemplateConfigurationFilePane.java +++ b/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controls/builder/nodes/TemplateConfigurationFilePane.java @@ -34,7 +34,7 @@ * In order to get the configuration as a string, the method {@link #getAsString()} must be used. * * @author Thierry Wasylczenko - * @version 1.0 + * @version 1.1 * @since SlideshowFX 1.3 */ public class TemplateConfigurationFilePane extends VBox { @@ -55,7 +55,7 @@ public class TemplateConfigurationFilePane extends VBox { // 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("Presentation directory", 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); @@ -332,11 +332,13 @@ public void fillWithFile(final File file) { definition.setName(template.getName()); definition.setFile(template.getFile().getName()); - for (SlideElementTemplate elementTemplate : template.getElements()) { - final SlideElementDefinition slideElementDefinition = definition.addSlideElement(); - slideElementDefinition.setElementId(elementTemplate.getId()); - slideElementDefinition.setHtmlId(elementTemplate.getHtmlId()); - slideElementDefinition.setDefaultContent(elementTemplate.getDefaultContent()); + 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) { diff --git a/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controls/tree/FileTreeCell.java b/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controls/tree/FileTreeCell.java index 416ffa41..7254ce81 100755 --- a/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controls/tree/FileTreeCell.java +++ b/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controls/tree/FileTreeCell.java @@ -3,6 +3,7 @@ import com.twasyl.slideshowfx.utils.DialogHelper; import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon; import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView; +import javafx.scene.Node; import javafx.scene.control.*; import javafx.scene.layout.HBox; @@ -18,7 +19,7 @@ * cell. * * @author Thierry Wasylczenko - * @version 1.1 + * @version 1.2 * @since SlideshowFX 1.0 */ public class FileTreeCell extends TreeCell { @@ -26,9 +27,137 @@ public class FileTreeCell extends TreeCell { public FileTreeCell() { super(); + } + + @Override + protected void updateItem(File item, boolean empty) { + super.updateItem(item, empty); + + initializeDragEvents(item); + initializeGraphic(); + defineCellText(); + defineContextMenu(); + } + + /** + * Initialize drag events to the current {@link FileTreeCell}. Drag events are only defined if the given item is a + * a {@link File#isDirectory() directory}. If the item is not a directory, then this method ensures that all + * drag events are removed from this cell. + * + * @param item The file for which drag events will be eventually set. + */ + protected void initializeDragEvents(final File item) { + if (item != null && item.isDirectory()) { + setOnDragOver(((TemplateTreeView) getTreeView()).getOnDragOverItem()); + setOnDragDropped(((TemplateTreeView) getTreeView()).getOnDragDroppedItem()); + setOnDragDone(((TemplateTreeView) getTreeView()).getOnDragDoneItem()); + setOnDragExited(((TemplateTreeView) getTreeView()).getOnDragExitedItem()); + } else { + setOnDragOver(null); + setOnDragDropped(null); + setOnDragDone(null); + setOnDragExited(null); + } + } + + /** + * Defines the {@link #setGraphic(Node) graphic} of this cell according the availability of the value and the type + * of file this cell is hosting: a file or a directory. + */ + protected void initializeGraphic() { + if (this.getTreeItem() != null && this.getTreeItem().getValue() != null) { + + if(this.getTreeItem().getValue().isDirectory()) { + if (this.getTreeItem().isExpanded()) { + setGraphic(new FontAwesomeIconView(FontAwesomeIcon.FOLDER_OPEN)); + } else { + setGraphic(new FontAwesomeIconView(FontAwesomeIcon.FOLDER)); + } + } else { + setGraphic(new FontAwesomeIconView(FontAwesomeIcon.FILE_TEXT_ALT)); + } + } else { + setGraphic(null); + } + } - final ContextMenu contextMenu = new ContextMenu(); + /** + * Define the {@link #setText(String) text} of this cell. If the {@link TreeView#getRoot() root} of the {@link TreeView} + * hosting this cell is equal to the current {@link #getTreeItem() item}, then {@code /} is defined as text, + * otherwise it is the {@link File#getName() name} of the provided file. + */ + protected void defineCellText() { + if (isEmpty()) { + setText(""); + } else if (getTreeItem() == null) { + setText("null"); + } else if (getTreeView().getRoot() == getTreeItem()) { + setText("/"); + } else { + setText(getTreeItem().getValue().getName()); + } + } + /** + * Define the {@link ContextMenu} of this cell. This method can be called each time the item of the cell changes. + */ + protected void defineContextMenu() { + if (getContextMenu() == null) { + this.setContextMenu(new ContextMenu()); + } + + getContextMenu().getItems().clear(); + + final TemplateTreeView treeView = (TemplateTreeView) getTreeView(); + + if (getItem() != null && getItem().isDirectory()) { + getContextMenu().getItems().add(this.createNewFileMenuItem()); + getContextMenu().getItems().add(this.createNewDirectoryMenuItem()); + } + + if (treeView.isItemRenamingAllowed(getTreeItem())) { + getContextMenu().getItems().add(this.createRenameMenuItem()); + } + + if (treeView.isItemDeletionEnabled(getTreeItem())) { + getContextMenu().getItems().add(this.createDeleteMenuItem()); + } + } + + /** + * Creates the {@link MenuItem} that will allow to create a new file under the directory this cell is hosting. + * + * @return The {@link MenuItem} allowing to create a new file. + */ + protected MenuItem createNewFileMenuItem() { + final MenuItem newFile = new MenuItem("New file"); + newFile.setOnAction(event -> { + final TemplateTreeView treeView = (TemplateTreeView) getTreeView(); + treeView.promptUserAndCreateNewFile(); + }); + return newFile; + } + + /** + * Creates the {@link MenuItem} that will allow to create a new directory under the directory this cell is hosting. + * + * @return The {@link MenuItem} allowing to create a new directory. + */ + protected MenuItem createNewDirectoryMenuItem() { + final MenuItem newFile = new MenuItem("New directory"); + newFile.setOnAction(event -> { + final TemplateTreeView treeView = (TemplateTreeView) getTreeView(); + treeView.promptUserAndCreateNewDirectory(); + }); + return newFile; + } + + /** + * Creates the {@link MenuItem} that will allow to rename the file or directory this cell is hosting. + * + * @return The {@link MenuItem} allowing to rename contents. + */ + protected MenuItem createRenameMenuItem() { final MenuItem renameItem = new MenuItem("Rename"); renameItem.setOnAction(event -> { try { @@ -49,7 +178,15 @@ public FileTreeCell() { LOGGER.log(Level.SEVERE, "Can not rename item", e); } }); + return renameItem; + } + /** + * Creates the {@link MenuItem} that will allow to delete the file or directory this cell is hosting. + * + * @return The {@link MenuItem} allowing to delete contents. + */ + protected MenuItem createDeleteMenuItem() { final MenuItem deleteItem = new MenuItem("Delete"); deleteItem.setOnAction(event -> { try { @@ -61,66 +198,6 @@ public FileTreeCell() { LOGGER.log(Level.SEVERE, "Can not delete item", e); } }); - - this.setContextMenu(contextMenu); - - this.treeItemProperty().addListener((value, oldValue, newValue) -> { - if (newValue != null) { - contextMenu.getItems().clear(); - - final TemplateTreeView ttv = (TemplateTreeView) this.getTreeView(); - - if (ttv.isItemRenamingAllowed(newValue)) contextMenu.getItems().add(renameItem); - else contextMenu.getItems().remove(renameItem); - - if (ttv.isItemDeletionEnabled(newValue)) contextMenu.getItems().add(deleteItem); - else contextMenu.getItems().remove(deleteItem); - } - }); - } - - @Override - protected void updateItem(File item, boolean empty) { - super.updateItem(item, empty); - - if (empty) { - setText(""); - setGraphic(null); - } else if (item == null) { - setText("null"); - setGraphic(null); - } else { - /** - * The drag is only allowed if the file is a directory - */ - if (item.isDirectory()) { - setOnDragOver(((TemplateTreeView) getTreeView()).getOnDragOverItem()); - setOnDragDropped(((TemplateTreeView) getTreeView()).getOnDragDroppedItem()); - setOnDragDone(((TemplateTreeView) getTreeView()).getOnDragDoneItem()); - setOnDragExited(((TemplateTreeView) getTreeView()).getOnDragExitedItem()); - - if (this.getTreeItem() != null && this.getTreeItem().isExpanded()) { - setGraphic(new FontAwesomeIconView(FontAwesomeIcon.FOLDER_OPEN)); - } else { - setGraphic(new FontAwesomeIconView(FontAwesomeIcon.FOLDER)); - } - } else { - setOnDragOver(null); - setOnDragDropped(null); - setOnDragDone(null); - setOnDragExited(null); - - setGraphic(null); - } - - /** - * If this TreeItem is the root of the TreeView, display "/" otherwise display the name of the file. - */ - if (getTreeView().getRoot() == getTreeItem()) { - setText("/"); - } else { - setText(item.getName()); - } - } + return deleteItem; } } diff --git a/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controls/tree/TemplateTreeView.java b/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controls/tree/TemplateTreeView.java index 2f57ca78..4331d73c 100755 --- a/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controls/tree/TemplateTreeView.java +++ b/SlideshowFX-app/src/main/java/com/twasyl/slideshowfx/controls/tree/TemplateTreeView.java @@ -1,12 +1,16 @@ package com.twasyl.slideshowfx.controls.tree; import com.twasyl.slideshowfx.engine.template.TemplateEngine; +import com.twasyl.slideshowfx.ui.controls.ExtendedTextField; +import com.twasyl.slideshowfx.ui.controls.validators.Validators; +import com.twasyl.slideshowfx.utils.DialogHelper; import com.twasyl.slideshowfx.utils.io.DeleteFileVisitor; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.css.PseudoClass; import javafx.event.EventHandler; import javafx.scene.Node; +import javafx.scene.control.ButtonType; import javafx.scene.control.TreeCell; import javafx.scene.control.TreeItem; import javafx.scene.control.TreeView; @@ -14,12 +18,12 @@ import javafx.scene.input.Dragboard; import javafx.scene.input.MouseEvent; import javafx.scene.input.TransferMode; +import javafx.scene.layout.HBox; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Arrays; import java.util.logging.Level; import java.util.logging.Logger; @@ -31,7 +35,7 @@ * filesystem. * * @author Thierry Wasylczenko - * @version 1.1 + * @version 1.2 * @since SlideshowFX 1.0 */ public class TemplateTreeView extends TreeView { @@ -170,36 +174,230 @@ public void setOnItemClick(EventHandler onItemClick) { } /** - * This method adds the given file to the parent TreeItem. If the file is a directory, + * This method adds the given file to the selected item in the tree view. If there is no selection, the root of the + * tree view will be used. + * If the file is a directory, all files included in the directory will be added to the tree view for a TreeItem + * corresponding the the current given file. + * This method also copy the given file to the temporary archive folder. + * + * @param file The content to add to the TreeView. + * @return Return the {@link TreeItem} that has been created. + */ + public TreeItem appendContentToTreeView(File file) { + final TreeItem parent = this.getParentDirectoryOfSelection(); + + return this.appendContentToTreeView(file, parent); + } + + /** + * This method adds the given file to the parent {@link TreeItem}. If the file is a directory, * all files included in the directory will be added to the TreeView for a TreeItem corresponding the the current given file. * This method also copy the given file to the temporary archive folder. * * @param file The content to add to the TreeView. * @param parent The item that is the parent of the content to add. + * @return Return the {@link TreeItem} that has been created. */ - public void appendContentToTreeView(File file, TreeItem parent) { + public TreeItem appendContentToTreeView(File file, TreeItem parent) { File relativeToParent = new File(parent.getValue(), file.getName()); - final TreeItem treeItem = new TreeItem<>(relativeToParent); + TreeItem treeItem = new TreeItem<>(relativeToParent); try { if (file.isDirectory()) { Files.createDirectories(relativeToParent.toPath()); - Arrays.stream(file.listFiles()) - .forEach(subFile -> this.appendContentToTreeView(subFile, treeItem)); + for (final File child : file.listFiles()) { + this.appendContentToTreeView(child, treeItem); + } } else { Files.copy(file.toPath(), relativeToParent.toPath()); } - if (this.getRoot().equals(parent)) { - parent.setExpanded(true); - } else { - parent.setExpanded(false); - } parent.getChildren().add(treeItem); + this.getSelectionModel().select(treeItem); } catch (IOException e) { LOGGER.log(Level.SEVERE, "Can not copy content", e); + treeItem = null; + } + return treeItem; + } + + /** + * This methods will prompt the user a name of a file and create an empty file under the current selection. + */ + public void promptUserAndCreateNewFile() { + final ExtendedTextField fileName = new ExtendedTextField("File name", true); + fileName.setValidator(Validators.isNotEmpty()); + + final HBox pane = new HBox(5, fileName); + + final ButtonType answer = DialogHelper.showCancellableDialog("Add a new file", pane); + + if (answer == ButtonType.OK && fileName.isValid()) { + this.createFileUnderSelection(fileName.getText()); + } + } + + /** + * This methods will prompt the user a name of a file and create an empty file under the current selection. + */ + public void promptUserAndCreateNewDirectory() { + final ExtendedTextField directoryName = new ExtendedTextField("Directory name", true); + directoryName.setValidator(Validators.isNotEmpty()); + + final HBox pane = new HBox(5, directoryName); + + final ButtonType answer = DialogHelper.showCancellableDialog("Add a new directory", pane); + + if (answer == ButtonType.OK && directoryName.isValid()) { + this.createDirectoryUnderSelection(directoryName.getText()); + } + } + + /** + * Creates an empty file named according the given {@code fileName}. The file is created under the current selection. + * If the current selection is a directory, an empty file will be created inside this directory. + * If the current selection is a file, the first parent will be determined and the file will be created into this + * parent. + * If there is no selection, then the file will be created under the root. + * + * @param fileName The name of the file that must be created. + */ + public void createFileUnderSelection(final String fileName) { + if (fileName != null && !fileName.trim().isEmpty()) { + final TreeItem parent = getParentDirectoryOfSelection(); + + if (parent != null) { + final File newFile = new File(parent.getValue(), fileName.trim()); + + try { + Files.createFile(newFile.toPath()); + final TreeItem newFileItem = new TreeItem<>(newFile); + + parent.getChildren().add(newFileItem); + this.getSelectionModel().select(newFileItem); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Can not create the empty file", e); + } + } else { + LOGGER.log(Level.WARNING, "Can not determine where the file must be created"); + } + } + } + + /** + * Creates an empty directory named according the given {@code directoryName}. The file is created under the + * current selection. + * If the current selection is a directory, an empty directory will be created inside this directory. + * If the current selection is a file, the first parent will be determined and the directory will be created into + * this parent. + * If there is no selection, then the directory will be created under the root. + *

+ * The directory's name can be a path where each directory to create is separated by a {@code /}. For instance: + * {@code dir/subdir} will create a subdir directory in a dir directory, itself created under the selection. + * + * @param directoryName The name of the directory that must be created. + */ + public void createDirectoryUnderSelection(final String directoryName) { + final TreeItem parent = getParentDirectoryOfSelection(); + + if (directoryName != null && !directoryName.trim().isEmpty() && parent != null) { + final String[] directories = directoryName.trim().split("/"); + + TreeItem createdDirectory = parent; + int index = 0; + + do { + createdDirectory = this.createDirectoryUnderParent(createdDirectory, directories[index++]); + } while (createdDirectory != null && index < directories.length); + } + } + + /** + * Creates an empty directory under the given parent. The parent must not be {@code null} and it's value must be + * non {@code null} and be a directory. + * The name of the directory must be non {@code null} and not empty. + * + * @param parent The parent of the directory to create. + * @param directoryName The name of the directory to create. + * @return Return the created directory. + */ + private TreeItem createDirectoryUnderParent(final TreeItem parent, final String directoryName) { + TreeItem newDirectoryItem = null; + + if (directoryName != null && !directoryName.trim().isEmpty()) { + if (parent != null && parent.getValue() != null) { + if (parent.getValue().isDirectory()) { + final File newDirectory = new File(parent.getValue(), directoryName.trim()); + + if (!newDirectory.exists()) { + try { + Files.createDirectory(newDirectory.toPath()); + newDirectoryItem = new TreeItem<>(newDirectory); + + parent.getChildren().add(newDirectoryItem); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Can not create the empty file", e); + } + } else { + newDirectoryItem = parent.getChildren() + .stream() + .filter(item -> item.getValue().equals(newDirectory)) + .findFirst() + .orElse(null); + } + + if (newDirectoryItem != null) { + this.getSelectionModel().select(newDirectoryItem); + } + } else { + LOGGER.log(Level.WARNING, "Can not create a directory because the parent is not a directory"); + } + } else { + LOGGER.log(Level.WARNING, "Can not determine where the file must be created"); + } + } + + return newDirectoryItem; + } + + /** + * Determine the parent of the current selection that is a directory. If the selection is a directory itself, + * then it is returned. If the selection is a file, then it's parent is returned. If there is no selection, the + * root of this {@link TemplateTreeView} is returned. + * + * @return The parent of the selection that is a directory. + */ + protected TreeItem getParentDirectoryOfSelection() { + final TreeItem selection = this.getSelectionModel().getSelectedItem(); + final TreeItem parent; + + if (selection == null) { + parent = getRoot(); + } else if (selection.getValue() != null && selection.getValue().isDirectory()) { + parent = selection; + } else if (selection.getValue() != null && selection.getValue().isFile()) { + parent = selection.getParent(); + } else { + parent = null; + } + + return parent; + } + + /** + * Closes recursively the given {@link TreeItem}. + * + * @param item The item to close recusively. + */ + public void closeItem(final TreeItem item) { + if (item != null) { + item.setExpanded(false); + + if(!item.isLeaf()) { + item.getChildren().forEach(this::closeItem); + } } } @@ -265,7 +463,7 @@ public boolean isItemRenamingAllowed(TreeItem item) { if (item != this.getRoot()) { final File configurationFile = new File(this.getEngine().getWorkingDirectory(), this.getEngine().getConfigurationFilename()); - canRename = !item.getValue().equals(configurationFile); + canRename = item != null && item.getValue() != null && !item.getValue().equals(configurationFile); } return canRename; @@ -280,7 +478,7 @@ public boolean isItemRenamingAllowed(TreeItem item) { public boolean isItemDeletionEnabled(TreeItem item) { boolean canDelete = false; - if (item != this.getRoot()) { + if (item != null && item != this.getRoot()) { final File configurationFile = new File(this.getEngine().getWorkingDirectory(), this.getEngine().getConfigurationFilename()); canDelete = !item.getValue().equals(configurationFile); diff --git a/SlideshowFX-app/src/main/resources/com/twasyl/slideshowfx/documentation/SlideshowFX_user.asciidoc b/SlideshowFX-app/src/main/resources/com/twasyl/slideshowfx/documentation/SlideshowFX_user.asciidoc index ebbe4013..b0b85a90 100755 --- a/SlideshowFX-app/src/main/resources/com/twasyl/slideshowfx/documentation/SlideshowFX_user.asciidoc +++ b/SlideshowFX-app/src/main/resources/com/twasyl/slideshowfx/documentation/SlideshowFX_user.asciidoc @@ -186,7 +186,7 @@ When you click on the button of one of these plugins in the tool bar next to the ==== Hosting connector plugins -Hosting connector plugins allow to save and download presentations to and from a _cloud storage platform_. Currently SlideshowFX supports https://www.dropbox.com/[Dropbox] and https://www.google.com/drive/[Google Drive]. +Hosting connector plugins allow to save and download presentations to and from a _cloud storage platform_. Currently SlideshowFX supports https://www.box.com/[Box], https://www.dropbox.com/[Dropbox] and https://www.google.com/drive/[Google Drive]. ===== Configuration diff --git a/SlideshowFX-engines/src/main/java/com/twasyl/slideshowfx/engine/presentation/PresentationEngine.java b/SlideshowFX-engines/src/main/java/com/twasyl/slideshowfx/engine/presentation/PresentationEngine.java index 8ba47361..9acb304b 100755 --- a/SlideshowFX-engines/src/main/java/com/twasyl/slideshowfx/engine/presentation/PresentationEngine.java +++ b/SlideshowFX-engines/src/main/java/com/twasyl/slideshowfx/engine/presentation/PresentationEngine.java @@ -38,7 +38,7 @@ * The extension of a presentation is {@code sfx}. * * @author Thierry Wasylczenko - * @version 1.1 + * @version 1.2 * @since SlideshowFX 1.0 */ public class PresentationEngine extends AbstractEngine { @@ -84,7 +84,7 @@ public boolean checkConfiguration() throws EngineException { @Override public PresentationConfiguration readConfiguration(Reader reader) throws NullPointerException, IllegalArgumentException, IOException { - if(reader == null) throw new NullPointerException("The configuration reader can not be null"); + if (reader == null) throw new NullPointerException("The configuration reader can not be null"); final PresentationConfiguration presentationConfiguration = new PresentationConfiguration(); presentationConfiguration.setPresentationFile(new File(this.getWorkingDirectory(), PresentationConfiguration.DEFAULT_PRESENTATION_FILENAME)); @@ -94,7 +94,7 @@ public PresentationConfiguration readConfiguration(Reader reader) throws NullPoi presentationConfiguration.setId(presentationJson.getLong(PresentationConfiguration.PRESENTATION_ID, System.currentTimeMillis())); - if(presentationJson.getJsonArray(PresentationConfiguration.PRESENTATION_CUSTOM_RESOURCES) != null) { + if (presentationJson.getJsonArray(PresentationConfiguration.PRESENTATION_CUSTOM_RESOURCES) != null) { presentationJson.getJsonArray(PresentationConfiguration.PRESENTATION_CUSTOM_RESOURCES) .forEach(customResource -> { final Resource resource = new Resource( @@ -106,7 +106,7 @@ public PresentationConfiguration readConfiguration(Reader reader) throws NullPoi }); } - if(presentationJson.getJsonArray(PresentationConfiguration.PRESENTATION_VARIABLES) != null) { + if (presentationJson.getJsonArray(PresentationConfiguration.PRESENTATION_VARIABLES) != null) { presentationJson.getJsonArray(PresentationConfiguration.PRESENTATION_VARIABLES) .forEach(variableJson -> { final Pair variable = new Pair<>(); @@ -127,7 +127,7 @@ public PresentationConfiguration readConfiguration(Reader reader) throws NullPoi try { final File thumbnailFile = this.getThumbnailFile(slide); - if(thumbnailFile.exists()) { + if (thumbnailFile.exists()) { slide.setThumbnail(this.getThumbnailImage(thumbnailFile)); } } catch (IOException e) { @@ -156,7 +156,8 @@ public PresentationConfiguration readConfiguration(Reader reader) throws NullPoi /** * Get the thumbnail file for the given slide. This methods only creates a {@link File} without checking its existence. * The file is supposed to be found in the {@link TemplateConfiguration#getSlidesThumbnailDirectory() thumbnail directory} - * of the template configuration of this presentation. + * of the template configuration of this presentation. + * * @param slide The slide to get the thumbnail file for. * @return The supposed file corresponding to the thumbnail. */ @@ -167,6 +168,7 @@ private File getThumbnailFile(final Slide slide) { /** * Get the {@link WritableImage image} located in the {@link File thumbnail file}. + * * @param thumbnailFile The file of the image. * @return The thumbnail image. * @throws IOException If something went wrong. @@ -179,9 +181,9 @@ private WritableImage getThumbnailImage(final File thumbnailFile) throws IOExcep @Override public void writeConfiguration(Writer writer) throws NullPointerException, IOException { - if(writer == null) throw new NullPointerException("The configuration to write into can not be null"); + if (writer == null) throw new NullPointerException("The configuration to write into can not be null"); - if(this.configuration != null) { + if (this.configuration != null) { final JsonObject presentationJson = new JsonObject(); final JsonArray slidesJson = new JsonArray(); final JsonArray customResourcesJson = new JsonArray(); @@ -248,10 +250,11 @@ public void writeConfiguration(Writer writer) throws NullPointerException, IOExc @Override public void loadArchive(File file) throws IllegalArgumentException, NullPointerException, IOException, IllegalAccessException { - if(file == null) throw new NullPointerException("The archive file can not be null"); - if(!file.exists()) throw new FileNotFoundException("The archive file does not exist"); - if(!file.canRead()) throw new IllegalAccessException("The archive file can not be read"); - if(!file.getName().endsWith(this.getArchiveExtension())) throw new IllegalArgumentException("The extension of the archive is not valid"); + if (file == null) throw new NullPointerException("The archive file can not be null"); + if (!file.exists()) throw new FileNotFoundException("The archive file does not exist"); + if (!file.canRead()) throw new IllegalAccessException("The archive file can not be read"); + if (!file.getName().endsWith(this.getArchiveExtension())) + throw new IllegalArgumentException("The extension of the archive is not valid"); this.setModifiedSinceLatestSave(false); @@ -280,7 +283,7 @@ public void loadArchive(File file) throws IllegalArgumentException, NullPointerE tokens.put(TEMPLATE_SFX_JAVASCRIPT_RESOURCES_TOKEN, this.buildJavaScriptResourcesToInclude()); // Replacing the template tokens - try(final StringWriter writer = new StringWriter()) { + try (final StringWriter writer = new StringWriter()) { final Template documentTemplate = templateConfiguration.getTemplate(this.templateEngine.getConfiguration().getFile().getName()); documentTemplate.process(tokens, writer); @@ -297,7 +300,7 @@ public void loadArchive(File file) throws IllegalArgumentException, NullPointerE // Append the custom resources this.configuration.getCustomResources() .stream() - .forEach(resource -> this.addCustomResource(resource)); + .forEach(this::addCustomResource); // Append the slides' content to the presentation tokens.clear(); @@ -305,7 +308,7 @@ public void loadArchive(File file) throws IllegalArgumentException, NullPointerE tokens.put(TEMPLATE_SLIDE_ID_PREFIX_TOKEN, this.templateEngine.getConfiguration().getSlideIdPrefix()); tokens.putAll(this.configuration.getVariables().stream().collect(Collectors.toMap(Pair::getKey, Pair::getValue))); - for(Slide s : this.configuration.getSlides()) { + for (Slide s : this.configuration.getSlides()) { templateConfiguration.setDirectoryForTemplateLoading(s.getTemplate().getFile().getParentFile()); try (final StringWriter writer = new StringWriter()) { @@ -338,8 +341,8 @@ public synchronized void saveArchive(File file) throws IllegalArgumentException, this.writeConfiguration(); LOGGER.fine("Create slides thumbnails"); - if(!this.templateEngine.getConfiguration().getSlidesThumbnailDirectory().exists()) { - if(!this.templateEngine.getConfiguration().getSlidesThumbnailDirectory().mkdirs()) { + if (!this.templateEngine.getConfiguration().getSlidesThumbnailDirectory().exists()) { + if (!this.templateEngine.getConfiguration().getSlidesThumbnailDirectory().mkdirs()) { LOGGER.log(Level.SEVERE, "Can not create slides thumbnails directory"); } } else { @@ -371,6 +374,7 @@ public synchronized void saveArchive(File file) throws IllegalArgumentException, /** * Indicates if the presentation has already been saved by testing if the {@link #getArchive()} * method returns {@code null} or not. + * * @return {@code true} if {@link #getArchive()} is not {@code null}, {@code false} otherwise. */ public boolean isPresentationAlreadySaved() { @@ -380,6 +384,7 @@ public boolean isPresentationAlreadySaved() { /** * Indicates if the presentation has been modified since the latest save. If the presentation has never been saved, * then the presentation is considered modified. + * * @return {@code true} if the presentation has been modified since the latest save, {@code false} otherwise. */ public boolean isModifiedSinceLatestSave() { @@ -388,6 +393,7 @@ public boolean isModifiedSinceLatestSave() { /** * Set if the presentation has been modified since its latest save. + * * @param modifiedSinceLatestSave {@code true} to indicate a modification, {@code false} otherwise. */ public void setModifiedSinceLatestSave(boolean modifiedSinceLatestSave) { @@ -401,7 +407,7 @@ public void setModifiedSinceLatestSave(boolean modifiedSinceLatestSave) { * in order this engine to be used to create the new presentation. * * @param templateArchive The template archive file to create the presentation from. - * @throws IOException If an error occurred when processing the archive. + * @throws IOException If an error occurred when processing the archive. * @throws IllegalAccessException If an error occurred when processing the archive. */ public void createFromTemplate(File templateArchive) throws IOException, IllegalAccessException { @@ -424,7 +430,7 @@ public void createFromTemplate(File templateArchive) throws IOException, Illegal final Map tokens = new HashMap<>(); tokens.put(TEMPLATE_SFX_JAVASCRIPT_RESOURCES_TOKEN, this.buildJavaScriptResourcesToInclude()); - try(final StringWriter writer = new StringWriter()) { + try (final StringWriter writer = new StringWriter()) { final Template documentTemplate = templateConfiguration.getTemplate(this.templateEngine.getConfiguration().getFile().getName()); documentTemplate.process(tokens, writer); @@ -443,44 +449,48 @@ public void createFromTemplate(File templateArchive) throws IOException, Illegal * * @return The configuration of the template. */ - public TemplateConfiguration getTemplateConfiguration() { return this.templateEngine.getConfiguration(); } + public TemplateConfiguration getTemplateConfiguration() { + return this.templateEngine.getConfiguration(); + } /** * Add a slide to the presentation and save the presentation. If {@code afterSlideNumber} is {@code null} or not * found, the slide is added at the end of the presentation, otherwise it is added after the given slide number. - * @param template The template of slide to add. + * + * @param template The template of slide to add. * @param afterSlideNumber The slide number to insert the new slide after. * @return The new added slide. * @throws IOException If an error occurred when saving the presentation. */ public Slide addSlide(SlideTemplate template, String afterSlideNumber) throws IOException { - if(template == null) throw new IllegalArgumentException("The templateConfiguration for creating a slide can not be null"); + if (template == null) + throw new IllegalArgumentException("The templateConfiguration for creating a slide can not be null"); this.setModifiedSinceLatestSave(true); final Pair createdSlide = this.createSlide(template); - if(afterSlideNumber == null) { + if (afterSlideNumber == null) { this.configuration.getSlides().add(createdSlide.getKey()); } else { ListIterator slidesIterator = this.configuration.getSlides().listIterator(); this.configuration.getSlideByNumber(afterSlideNumber); int index = -1; - while(slidesIterator.hasNext()) { - if(slidesIterator.next().getSlideNumber().equals(afterSlideNumber)) { + while (slidesIterator.hasNext()) { + if (slidesIterator.next().getSlideNumber().equals(afterSlideNumber)) { index = slidesIterator.nextIndex(); break; } } - if(index > -1) { + if (index > -1) { this.configuration.getSlides().add(index, createdSlide.getKey()); } else { this.configuration.getSlides().add(createdSlide.getKey()); } } - if(afterSlideNumber == null || afterSlideNumber.isEmpty()) { + if (afterSlideNumber == null || afterSlideNumber.isEmpty()) { this.configuration.getDocument() .getElementById(this.templateEngine.getConfiguration().getSlidesContainer()) .append(createdSlide.getValue().outerHtml()); @@ -497,15 +507,16 @@ public Slide addSlide(SlideTemplate template, String afterSlideNumber) throws IO /** * Delete the slide with the slideNumber and save the presentation. + * * @param slideNumber The slide number to delete. */ public void deleteSlide(String slideNumber) { - if(slideNumber == null) throw new IllegalArgumentException("Slide number can not be null"); + if (slideNumber == null) throw new IllegalArgumentException("Slide number can not be null"); this.setModifiedSinceLatestSave(true); Slide slideToRemove = this.configuration.getSlideByNumber(slideNumber); - if(slideToRemove != null) { + if (slideToRemove != null) { this.configuration.getSlides().remove(slideToRemove); this.configuration.getDocument() .getElementById(slideToRemove.getId()).remove(); @@ -516,19 +527,20 @@ public void deleteSlide(String slideNumber) { /** * Duplicates the given slide and add it to the presentation. The presentation is temporary saved. + * * @param slide The slide to duplicate. * @return The duplicated slide. */ public Slide duplicateSlide(Slide slide) throws IOException { - if(slide == null) throw new IllegalArgumentException("The slide to duplicate can not be null"); + if (slide == null) throw new IllegalArgumentException("The slide to duplicate can not be null"); this.setModifiedSinceLatestSave(true); final Pair duplicatedSlide = this.createSlide(slide.getTemplate()); // Add the slide to the presentation's slides int index = this.configuration.getSlides().indexOf(slide); - if(index != -1) { - if(index == this.configuration.getSlides().size() - 1) { + if (index != -1) { + if (index == this.configuration.getSlides().size() - 1) { this.configuration.getSlides().add(duplicatedSlide.getKey()); } else { this.configuration.getSlides().add(index + 1, duplicatedSlide.getKey()); @@ -563,14 +575,15 @@ public Slide duplicateSlide(Slide slide) throws IOException { * the slide is moved at the end of the presentation. If slideToMove is equal to beforeSlide * nothing is done. * If an operation has been performed, the presentation is temporary saved. + * * @param slideToMove The slide to move * @param beforeSlide The slide before slideToMove is moved * @throws IllegalArgumentException if the slideToMove is null */ public void moveSlide(Slide slideToMove, Slide beforeSlide) { - if(slideToMove == null) throw new IllegalArgumentException("The slideToMove to move can not be null"); + if (slideToMove == null) throw new IllegalArgumentException("The slideToMove to move can not be null"); - if(!slideToMove.equals(beforeSlide)) { + if (!slideToMove.equals(beforeSlide)) { this.setModifiedSinceLatestSave(true); this.configuration.getSlides().remove(slideToMove); @@ -582,7 +595,7 @@ public void moveSlide(Slide slideToMove, Slide beforeSlide) { .getElementById(slideToMove.getId()) .remove(); - if(beforeSlide == null) { + if (beforeSlide == null) { this.configuration.getSlides().add(slideToMove); this.configuration.getDocument() .getElementById(this.templateEngine.getConfiguration().getSlidesContainer()) @@ -603,10 +616,11 @@ public void moveSlide(Slide slideToMove, Slide beforeSlide) { /** * This method adds the given resource to the collection of resources present in {@link #getConfiguration()} as well * as in the presentation's document. + * * @param resource The resource to add in the collection and the document. */ public void addCustomResource(Resource resource) { - if(resource != null + if (resource != null && resource.getContent() != null && !resource.getContent().trim().isEmpty()) { @@ -620,16 +634,16 @@ public void addCustomResource(Resource resource) { final String htmlString = resource.buildHTMLString(location); final String resourceHtml = Jsoup.parseBodyFragment(htmlString).body().html(); - if(!this.configuration.getDocument().head().html().contains(resourceHtml)) { + if (!this.configuration.getDocument().head().html().contains(resourceHtml)) { this.configuration.getDocument().head().append(htmlString); } } } public void savePresentationFile() { - try(final FileOutputStream fileOutputStream = new FileOutputStream(this.configuration.getPresentationFile()); - final OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream, GlobalConfiguration.getDefaultCharset()); - final Writer writer = new BufferedWriter(outputStreamWriter)) { + try (final FileOutputStream fileOutputStream = new FileOutputStream(this.configuration.getPresentationFile()); + final OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream, GlobalConfiguration.getDefaultCharset()); + final Writer writer = new BufferedWriter(outputStreamWriter)) { writer.write(this.configuration.getDocument().html()); writer.flush(); } catch (IOException e) { @@ -639,6 +653,7 @@ public void savePresentationFile() { /** * This method loads all JavaScript resources that should be inserted in a template and return them in a String. + * * @return The String containing the content of all JavaScript resources needed for a template */ private String buildJavaScriptResourcesToInclude() { @@ -654,14 +669,15 @@ private String buildJavaScriptResourcesToInclude() { /** * Create a {@link Slide slide} from the given {@link SlideTemplate template}. + * * @param template The template to create the slide from. * @return A {@link Pair} where the key is the created {@link Slide} object and the value the HTML code get from the * parsed template. - * @throws IOException If an error occurs when parsing the template. + * @throws IOException If an error occurs when parsing the template. * @throws NullPointerException If the given {@code template} is {@code null}. */ private Pair createSlide(final SlideTemplate template) throws NullPointerException, IOException { - if(template == null) throw new NullPointerException("The template can not be null"); + if (template == null) throw new NullPointerException("The template can not be null"); this.setModifiedSinceLatestSave(true); final Pair result = new Pair<>(); @@ -673,35 +689,36 @@ private Pair createSlide(final SlideTemplate template) throws Nu // Process the SlideElements by replacing their ID and setting their content final Configuration defaultConfiguration = TemplateProcessor.getDefaultConfiguration(); - Arrays.stream(template.getElements()) - .forEach(element -> { - try (final StringWriter writer = new StringWriter(); - final StringReader reader = new StringReader(element.getHtmlId())) { - - final Template elementTemplate = new Template("element template", reader, defaultConfiguration); - elementTemplate.process(tokens, writer); - writer.flush(); - - result.getKey().updateElement(writer.toString(), "HTML", element.getDefaultContent(), element.getDefaultContent()) - .setTemplate(element); - } catch (IOException | TemplateException e) { - LOGGER.log(Level.WARNING, "Can not parse element", e); - } - }); - + if (template.getElements() != null) { + Arrays.stream(template.getElements()) + .forEach(element -> { + try (final StringWriter writer = new StringWriter(); + final StringReader reader = new StringReader(element.getHtmlId())) { + + final Template elementTemplate = new Template("element template", reader, defaultConfiguration); + elementTemplate.process(tokens, writer); + writer.flush(); + + result.getKey().updateElement(writer.toString(), "HTML", element.getDefaultContent(), element.getDefaultContent()) + .setTemplate(element); + } catch (IOException | TemplateException e) { + LOGGER.log(Level.WARNING, "Can not parse element", e); + } + }); + } // Add dynamic attributes to the tokens by asking their values to the user // TODO INCUBATING tokens.clear(); - if(result.getKey().getTemplate().getDynamicAttributes() != null && result.getKey().getTemplate().getDynamicAttributes().length > 0) { + if (result.getKey().getTemplate().getDynamicAttributes() != null && result.getKey().getTemplate().getDynamicAttributes().length > 0) { Scanner scanner = new Scanner(System.in); String value; - for(DynamicAttribute attribute : result.getKey().getTemplate().getDynamicAttributes()) { + for (DynamicAttribute attribute : result.getKey().getTemplate().getDynamicAttributes()) { System.out.print(attribute.getPromptMessage() + " "); value = scanner.nextLine(); - if(value == null || value.trim().isEmpty()) { + if (value == null || value.trim().isEmpty()) { tokens.put(attribute.getTemplateExpression(), ""); } else { tokens.put(attribute.getTemplateExpression(), String.format("%1$s=\"%2$s\"", attribute.getAttribute(), value.trim())); @@ -716,7 +733,7 @@ private Pair createSlide(final SlideTemplate template) throws Nu tokens.put(TEMPLATE_SFX_CALLBACK_TOKEN, TEMPLATE_SFX_CALLBACK_CALL); tokens.putAll(this.configuration.getVariables().stream().collect(Collectors.toMap(Pair::getKey, Pair::getValue))); - try(final StringWriter writer = new StringWriter()) { + try (final StringWriter writer = new StringWriter()) { final Template slideTemplate = defaultConfiguration.getTemplate(template.getFile().getName()); slideTemplate.process(tokens, writer); writer.flush(); diff --git a/SlideshowFX-setup/src/main/java/com/twasyl/slideshowfx/setup/controllers/PluginsViewController.java b/SlideshowFX-setup/src/main/java/com/twasyl/slideshowfx/setup/controllers/PluginsViewController.java index 0adc6e3d..4906dc2a 100755 --- a/SlideshowFX-setup/src/main/java/com/twasyl/slideshowfx/setup/controllers/PluginsViewController.java +++ b/SlideshowFX-setup/src/main/java/com/twasyl/slideshowfx/setup/controllers/PluginsViewController.java @@ -1,7 +1,6 @@ package com.twasyl.slideshowfx.setup.controllers; import com.twasyl.slideshowfx.ui.controls.PluginFileButton; -import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon; import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView; import javafx.beans.property.IntegerProperty; import javafx.beans.property.ObjectProperty; @@ -15,33 +14,21 @@ import javafx.scene.control.CheckBox; import javafx.scene.control.TitledPane; import javafx.scene.control.Tooltip; -import javafx.scene.image.Image; -import javafx.scene.image.ImageView; import javafx.scene.layout.TilePane; -import java.io.*; +import java.io.File; import java.net.URL; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.ResourceBundle; -import java.util.jar.Attributes; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; -import java.util.jar.Manifest; -import java.util.logging.Level; +import java.util.*; import java.util.logging.Logger; /** - * Controller for the {PluginsView.xml} file. + * Controller for the {@code PluginsView.xml} file. * * @author Thierry Wasylczenko + * @version 1.2 * @since SlideshowFX 1.0 - * @version 1.1 */ public class PluginsViewController implements Initializable { - private static final Logger LOGGER = Logger.getLogger(PluginsViewController.class.getName()); - protected static final PseudoClass INVALID_STATE = PseudoClass.getPseudoClass("invalid"); protected final String MARKUP_PLUGINS_DIRECTORY_NAME = "markups"; @@ -49,18 +36,28 @@ public class PluginsViewController implements Initializable { protected final String CONTENT_EXTENSION_PLUGINS_DIRECTORY_NAME = "extensions"; protected final String HOSTING_CONNECTOR_PLUGINS_DIRECTORY_NAME = "hostingConnectors"; - @FXML private TitledPane markupPluginsContainer; - @FXML private FontAwesomeIconView markupErrorSign; - - @FXML private TilePane markupPlugins; - @FXML private TilePane contentExtensionPlugins; - @FXML private TilePane snippetExecutorPlugins; - @FXML private TilePane hostingConnectorsPlugins; - - @FXML private CheckBox installAllMarkupPlugins; - @FXML private CheckBox installAllContentExtensionPlugins; - @FXML private CheckBox installAllSnippetExecutorPlugins; - @FXML private CheckBox installAllHostingConnectorPlugins; + @FXML + private TitledPane markupPluginsContainer; + @FXML + private FontAwesomeIconView markupErrorSign; + + @FXML + private TilePane markupPlugins; + @FXML + private TilePane contentExtensionPlugins; + @FXML + private TilePane snippetExecutorPlugins; + @FXML + private TilePane hostingConnectorsPlugins; + + @FXML + private CheckBox installAllMarkupPlugins; + @FXML + private CheckBox installAllContentExtensionPlugins; + @FXML + private CheckBox installAllSnippetExecutorPlugins; + @FXML + private CheckBox installAllHostingConnectorPlugins; private final ObjectProperty pluginsDirectory = new SimpleObjectProperty<>(); private final List pluginsToInstall = new ArrayList<>(); @@ -68,6 +65,7 @@ public class PluginsViewController implements Initializable { /** * Get the list of the plugins the user has chosen to install. Each {@link File} corresponds to the plugin file. + * * @return The list of the plugins the user has chosen to install. */ public List getPluginsToInstall() { @@ -77,6 +75,7 @@ public List getPluginsToInstall() { /** * Set the directory that contains the plugins to be installed. The directory is not the directory of specialized * plugins but the directory that contains the other directories containing those specialized plugins. + * * @param directory The directory containing the plugins. * @return This instance of the controller. */ @@ -87,30 +86,37 @@ public PluginsViewController setPluginsDirectory(final File directory) { /** * Return the number of markup plugins that are selected in the view. + * * @return The property indicating the number of markup plugins selected in the view. */ - public IntegerProperty numberOfSelectedMarkup() { return this.numberOfSelectedMarkup; } + public IntegerProperty numberOfSelectedMarkup() { + return this.numberOfSelectedMarkup; + } - @FXML private void actionOnInstallAllMarkupPlugins(final ActionEvent event) { + @FXML + private void actionOnInstallAllMarkupPlugins(final ActionEvent event) { this.actionOnInstallAllPlugins(this.installAllMarkupPlugins.isSelected(), this.markupPlugins); } - @FXML private void actionOnInstallAllContentExtensionPlugins(final ActionEvent event) { + @FXML + private void actionOnInstallAllContentExtensionPlugins(final ActionEvent event) { this.actionOnInstallAllPlugins(this.installAllContentExtensionPlugins.isSelected(), this.contentExtensionPlugins); } - @FXML private void actionOnInstallAllSnippetExecutorPlugins(final ActionEvent event) { + @FXML + private void actionOnInstallAllSnippetExecutorPlugins(final ActionEvent event) { this.actionOnInstallAllPlugins(this.installAllSnippetExecutorPlugins.isSelected(), this.snippetExecutorPlugins); } - @FXML private void actionOnInstallAllHostingConnectorPlugins(final ActionEvent event) { + @FXML + private void actionOnInstallAllHostingConnectorPlugins(final ActionEvent event) { this.actionOnInstallAllPlugins(this.installAllHostingConnectorPlugins.isSelected(), this.hostingConnectorsPlugins); } @Override public void initialize(URL location, ResourceBundle resources) { this.numberOfSelectedMarkup.addListener((value, oldNumber, newNumber) -> { - if(newNumber.intValue() == 0) this.makeMarkupPluginsContainerInvalid(); + if (newNumber.intValue() == 0) this.makeMarkupPluginsContainerInvalid(); else this.makeMarkupPluginsContainerValid(); }); @@ -138,8 +144,9 @@ protected final void makeMarkupPluginsContainerValid() { /** * Fill the {@link Node} that will list the available markup plugins. - * @see #fillPluginsView(String, TilePane, CheckBox) + * * @return An {@link IntegerProperty} indicating the number of selected markup plugins in the view. + * @see #fillPluginsView(String, TilePane, CheckBox) */ protected final IntegerProperty fillMarkupPluginsView() { return this.fillPluginsView(MARKUP_PLUGINS_DIRECTORY_NAME, this.markupPlugins, this.installAllMarkupPlugins); @@ -147,8 +154,9 @@ protected final IntegerProperty fillMarkupPluginsView() { /** * Fill the {@link Node} that will list the available content extension plugins. - * @see #fillPluginsView(String, TilePane, CheckBox) + * * @return An {@link IntegerProperty} indicating the number of selected content extension plugins in the view. + * @see #fillPluginsView(String, TilePane, CheckBox) */ protected final IntegerProperty fillContentExtensionPluginsView() { return this.fillPluginsView(CONTENT_EXTENSION_PLUGINS_DIRECTORY_NAME, this.contentExtensionPlugins, this.installAllContentExtensionPlugins); @@ -156,8 +164,9 @@ protected final IntegerProperty fillContentExtensionPluginsView() { /** * Fill the {@link Node} that will list the available snippet executor plugins. - * @see #fillPluginsView(String, TilePane, CheckBox) + * * @return An {@link IntegerProperty} indicating the number of selected snippet executor plugins in the view. + * @see #fillPluginsView(String, TilePane, CheckBox) */ protected final IntegerProperty fillSnippetExecutorPluginsView() { return this.fillPluginsView(SNIPPET_EXECUTORS_PLUGINS_DIRECTORY_NAME, this.snippetExecutorPlugins, this.installAllSnippetExecutorPlugins); @@ -165,8 +174,9 @@ protected final IntegerProperty fillSnippetExecutorPluginsView() { /** * Fill the {@link Node} that will list the available hosting connector plugins. - * @see #fillPluginsView(String, TilePane, CheckBox) + * * @return An {@link IntegerProperty} indicating the number of selected hosting connector plugins in the view. + * @see #fillPluginsView(String, TilePane, CheckBox) */ protected final IntegerProperty fillHostingConnectorPluginsView() { return this.fillPluginsView(HOSTING_CONNECTOR_PLUGINS_DIRECTORY_NAME, this.hostingConnectorsPlugins, this.installAllHostingConnectorPlugins); @@ -174,9 +184,10 @@ protected final IntegerProperty fillHostingConnectorPluginsView() { /** * Fill the given {@code view} with the plugins contained within the {@code specializedPluginsDirectoryName}. + * * @param specializedPluginsDirectoryName The name of directory containing the plugins to list. - * @param view The view to be filled. - * @param installAllPluginsBox The checkbox allowing to select/unselect all plugins in the {@code view}. + * @param view The view to be filled. + * @param installAllPluginsBox The checkbox allowing to select/unselect all plugins in the {@code view}. * @return An {@link IntegerProperty} indicating the number of selected plugins in the view. */ protected final IntegerProperty fillPluginsView(final String specializedPluginsDirectoryName, final TilePane view, final CheckBox installAllPluginsBox) { @@ -188,16 +199,15 @@ protected final IntegerProperty fillPluginsView(final String specializedPluginsD Arrays.stream(specializedPluginsDir.listFiles()) .filter(file -> file.getName().endsWith(".jar")) .map(file -> new PluginFileButton(file)) - .sorted((button1, button2) -> button1.getLabel().compareTo(button2.getLabel())) + .sorted(Comparator.comparing(PluginFileButton::getLabel)) .forEach(button -> { button.selectedProperty().addListener((selectedValue, oldSelected, newSelected) -> { - if(newSelected) { + if (newSelected) { this.pluginsToInstall.add(button.getFile()); numberOfSelectedPlugins.set(numberOfSelectedPlugins.get() + 1); - } - else { + } else { this.pluginsToInstall.remove(button.getFile()); - if(numberOfSelectedPlugins.get() > 0) { + if (numberOfSelectedPlugins.get() > 0) { numberOfSelectedPlugins.set(numberOfSelectedPlugins.get() - 1); } } @@ -211,102 +221,11 @@ protected final IntegerProperty fillPluginsView(final String specializedPluginsD return numberOfSelectedPlugins; } - /** - * Get the attributes contained in the {@code MANIFEST.MF} of the plugin. - * @param plugin The JAR file of the plugin. - * @return The attributes contained in the {@code MANIFEST.MF} file of the plugin. - */ - protected final Attributes getManifestAttributes(final File plugin) { - Attributes manifestAttributes = null; - - try { - final JarFile jarFile = new JarFile(plugin); - final Manifest manifest = jarFile.getManifest(); - manifestAttributes = manifest.getMainAttributes(); - } catch(IOException ex) { - LOGGER.log(Level.WARNING, "Can not extract manifest attributes", ex); - } - - return manifestAttributes; - } - - /** - * Get the value of an attribute stored within a collection of {@code attributes}. If the value is {@code null} or - * empty, the default value will be returned. - * @param attributes The whole collection of attributes. - * @param name The name of the attribute to retrieve the value for. - * @param defaultValue The default value to return if the original value is {@code null} or empty. - * @return The value of the attribute. - */ - protected final String getManifestAttributeValue(final Attributes attributes, final String name, final String defaultValue) { - final String value = attributes.getValue(name); - - if(value == null || value.isEmpty()) return defaultValue; - else return value; - } - - /** - * Get the icon of the plugin stored within the JAR file as an array of bytes. If no icon is present, an empty array - * is returned. - * @param plugin The JAR file of the plugin. - * @return The icon of the plugin. - */ - protected final byte[] getIconFromJar(final File plugin) { - final ByteArrayOutputStream iconOut = new ByteArrayOutputStream(); - - try { - final JarFile jarFile = new JarFile(plugin); - final JarEntry icon = jarFile.getJarEntry("META-INF/icon.png"); - - if(icon != null) { - final InputStream iconIn = jarFile.getInputStream(icon); - final byte[] buffer = new byte[512]; - int numberOfBytesRead; - - while ((numberOfBytesRead = iconIn.read(buffer)) != -1) { - iconOut.write(buffer, 0, numberOfBytesRead); - } - - iconOut.flush(); - iconOut.close(); - } - } catch(IOException ex) { - LOGGER.log(Level.WARNING, "Can not the icon from JAR", ex); - } - - return iconOut.toByteArray(); - } - - /** - * Create the {@code Node} that will contain the icon of the plugin. - * @param plugin The JAR file of the plugin. - * @param attributes The manifest attributes of the plugin JAR file. - * @return The element containing the icon of the plugin. - */ - protected final Node buildIconNode(final File plugin, final Attributes attributes) { - Node icon = null; - final byte[] iconFromJar = this.getIconFromJar(plugin); - - if(iconFromJar != null && iconFromJar.length > 0) { - final ByteArrayInputStream input = new ByteArrayInputStream(iconFromJar); - final Image image = new Image(input, 50, 50, true, true); - icon = new ImageView(image); - } else { - final String fontIconName = this.getManifestAttributeValue(attributes, "Setup-Wizard-Icon-Name", ""); - - if(!fontIconName.isEmpty()) { - icon = new FontAwesomeIconView(FontAwesomeIcon.valueOf(fontIconName)); - ((FontAwesomeIconView) icon).setGlyphSize(50); - } - } - - return icon; - } - /** * Check or uncheck the given {@code box} according the fact all plugins are selected or not in the given {@code view}. + * * @param view The view determining of the box should be checked or not. - * @param box The box to check or not. + * @param box The box to check or not. */ protected final void manageCheckBoxStateForPlugins(final TilePane view, final CheckBox box) { final Node unselectedNode = view.getChildren() @@ -320,8 +239,9 @@ protected final void manageCheckBoxStateForPlugins(final TilePane view, final Ch /** * Check/Uncheck all plugins in the view according the {@code install} value. + * * @param install Indicates if the plugins should be installed or not. - * @param view The view to update. + * @param view The view to update. */ protected final void actionOnInstallAllPlugins(final boolean install, final TilePane view) { view.getChildren() diff --git a/SlideshowFX-ui-controls/build.gradle b/SlideshowFX-ui-controls/build.gradle index 95ce921e..30b622fb 100755 --- a/SlideshowFX-ui-controls/build.gradle +++ b/SlideshowFX-ui-controls/build.gradle @@ -1,6 +1,7 @@ version = '1.2' dependencies { + compile project(':SlideshowFX-utils') compile configurations.fontawesomefx testCompile configurations.junit diff --git a/SlideshowFX-ui-controls/src/main/java/com/twasyl/slideshowfx/ui/controls/PluginFileButton.java b/SlideshowFX-ui-controls/src/main/java/com/twasyl/slideshowfx/ui/controls/PluginFileButton.java index 0ab15a30..4acb97cf 100755 --- a/SlideshowFX-ui-controls/src/main/java/com/twasyl/slideshowfx/ui/controls/PluginFileButton.java +++ b/SlideshowFX-ui-controls/src/main/java/com/twasyl/slideshowfx/ui/controls/PluginFileButton.java @@ -1,5 +1,6 @@ package com.twasyl.slideshowfx.ui.controls; +import com.twasyl.slideshowfx.utils.Jar; import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon; import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView; import javafx.geometry.Pos; @@ -15,8 +16,6 @@ import java.io.*; import java.util.jar.Attributes; import java.util.jar.JarEntry; -import java.util.jar.JarFile; -import java.util.jar.Manifest; import java.util.logging.Level; import java.util.logging.Logger; @@ -25,7 +24,7 @@ * {@code plugin-file-button}. * * @author Thierry Wasylczenko - * @version 1.0 + * @version 1.1 * @since SlideshowFX 1.1 */ public class PluginFileButton extends ToggleButton { @@ -33,19 +32,22 @@ public class PluginFileButton extends ToggleButton { private static final double BUTTON_SIZE = 80; - private final File pluginFile; + private Jar pluginFile; private final String label; private final String version; private final String description; public PluginFileButton(final File pluginFile) { - this.pluginFile = pluginFile; - final Attributes manifestAttributes = this.getManifestAttributes(); + try { + this.pluginFile = new Jar(pluginFile); + } catch (IOException e) { + throw new IllegalArgumentException("Invalid JAR file", e); + } - final Node icon = this.buildIconNode(manifestAttributes); - this.label = this.getManifestAttributeValue(manifestAttributes, "Setup-Wizard-Label", this.pluginFile.getName()); - this.version = this.getManifestAttributeValue(manifestAttributes, "Bundle-Version", ""); - this.description = this.getManifestAttributeValue(manifestAttributes, "Bundle-Description", ""); + final Node icon = this.buildIconNode(this.pluginFile.getManifestAttributes()); + this.label = this.pluginFile.getManifestAttributeValue("Setup-Wizard-Label", this.pluginFile.getFile().getName()); + this.version = this.pluginFile.getManifestAttributeValue("Bundle-Version", ""); + this.description = this.pluginFile.getManifestAttributeValue("Bundle-Description", ""); this.setPrefSize(BUTTON_SIZE, BUTTON_SIZE); this.setMinSize(BUTTON_SIZE, BUTTON_SIZE); @@ -56,7 +58,7 @@ public PluginFileButton(final File pluginFile) { final VBox graphics = new VBox(2); graphics.setAlignment(Pos.CENTER); - if(icon != null) { + if (icon != null) { graphics.getChildren().add(icon); } else { graphics.getChildren().add(getLabelNode()); @@ -70,19 +72,25 @@ public PluginFileButton(final File pluginFile) { final StringBuilder tooltipText = new StringBuilder(label).append(":\n") .append(description).append(".\n"); - if(newSelected) tooltipText.append("Will be installed"); + if (newSelected) tooltipText.append("Will be installed"); else tooltipText.append("Will not be installed"); tooltipText.append('.'); Tooltip tooltip = this.getTooltip(); - if(tooltip == null) { + if (tooltip == null) { tooltip = new Tooltip(); this.setTooltip(tooltip); } tooltip.setText(tooltipText.toString()); }); + + try { + this.pluginFile.close(); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Can not close plugin file", e); + } } protected Text getVersionNode() { @@ -105,66 +113,35 @@ protected Text getLabelNode() { /** * Get the file associated to this button. + * * @return The file associated to this button. */ public File getFile() { - return this.pluginFile; + return this.pluginFile.getFile(); } /** * Get the label of this plugin. + * * @return The label of this plugin. */ - public String getLabel() { return this.label; } - - /** - * Get the attributes contained in the {@code MANIFEST.MF} of the plugin. - * @return The attributes contained in the {@code MANIFEST.MF} file of the plugin. - */ - protected final Attributes getManifestAttributes() { - Attributes manifestAttributes = null; - - try { - final JarFile jarFile = new JarFile(this.pluginFile); - final Manifest manifest = jarFile.getManifest(); - manifestAttributes = manifest.getMainAttributes(); - } catch(IOException ex) { - LOGGER.log(Level.WARNING, "Can not extract manifest attributes", ex); - } - - return manifestAttributes; - } - - /** - * Get the value of an attribute stored within a collection of {@code attributes}. If the value is {@code null} or - * empty, the default value will be returned. - * @param attributes The whole collection of attributes. - * @param name The name of the attribute to retrieve the value for. - * @param defaultValue The default value to return if the original value is {@code null} or empty. - * @return The value of the attribute. - */ - protected final String getManifestAttributeValue(final Attributes attributes, final String name, final String defaultValue) { - final String value = attributes.getValue(name); - - if(value == null || value.isEmpty()) return defaultValue; - else return value; + public String getLabel() { + return this.label; } /** * Get the icon of the plugin stored within the JAR file as an array of bytes. If no icon is present, an empty array * is returned. - * @param plugin The JAR file of the plugin. + * * @return The icon of the plugin. */ - protected final byte[] getIconFromJar(final File plugin) { + protected final byte[] getIconFromJar() { final ByteArrayOutputStream iconOut = new ByteArrayOutputStream(); - try { - final JarFile jarFile = new JarFile(plugin); - final JarEntry icon = jarFile.getJarEntry("META-INF/icon.png"); + final JarEntry icon = this.pluginFile.getEntry("META-INF/icon.png"); - if(icon != null) { - final InputStream iconIn = jarFile.getInputStream(icon); + if (icon != null) { + try (final InputStream iconIn = this.pluginFile.getInputStream(icon)) { final byte[] buffer = new byte[512]; int numberOfBytesRead; @@ -174,9 +151,9 @@ protected final byte[] getIconFromJar(final File plugin) { iconOut.flush(); iconOut.close(); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Can not the icon from JAR", e); } - } catch(IOException ex) { - LOGGER.log(Level.WARNING, "Can not the icon from JAR", ex); } return iconOut.toByteArray(); @@ -184,21 +161,22 @@ protected final byte[] getIconFromJar(final File plugin) { /** * Create the {@code Node} that will contain the icon of the plugin. + * * @param attributes The manifest attributes of the plugin JAR file. * @return The element containing the icon of the plugin. */ protected final Node buildIconNode(final Attributes attributes) { Node icon = null; - final byte[] iconFromJar = this.getIconFromJar(this.pluginFile); + final byte[] iconFromJar = this.getIconFromJar(); - if(iconFromJar != null && iconFromJar.length > 0) { + if (iconFromJar != null && iconFromJar.length > 0) { final ByteArrayInputStream input = new ByteArrayInputStream(iconFromJar); final Image image = new Image(input, 50, 50, true, true); icon = new ImageView(image); } else { - final String fontIconName = this.getManifestAttributeValue(attributes, "Setup-Wizard-Icon-Name", ""); + final String fontIconName = this.pluginFile.getManifestAttributeValue("Setup-Wizard-Icon-Name", ""); - if(!fontIconName.isEmpty()) { + if (!fontIconName.isEmpty()) { icon = new FontAwesomeIconView(FontAwesomeIcon.valueOf(fontIconName)); ((FontAwesomeIconView) icon).setGlyphSize(50); } diff --git a/SlideshowFX-utils/src/main/java/com/twasyl/slideshowfx/utils/Jar.java b/SlideshowFX-utils/src/main/java/com/twasyl/slideshowfx/utils/Jar.java new file mode 100755 index 00000000..2b534913 --- /dev/null +++ b/SlideshowFX-utils/src/main/java/com/twasyl/slideshowfx/utils/Jar.java @@ -0,0 +1,150 @@ +package com.twasyl.slideshowfx.utils; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Class representing a JAR file and allowing to manipulate it's attributes easily. + * + * @author Thierry Wasylczenko + * @version 1.0 + * @since SlideshowFX 1.4 + */ +public class Jar implements AutoCloseable { + private static final Logger LOGGER = Logger.getLogger(Jar.class.getName()); + + protected final JarFile jar; + protected final File file; + protected Manifest manifest = null; + protected Attributes manifestAttributes = null; + + /** + * Creates a {@link Jar} object from the given JAR file. + * + * @param file The JAR file. + * @throws IOException If an error occurs. + */ + public Jar(final File file) throws IOException { + this.file = file; + this.jar = new JarFile(this.file); + } + + /** + * Get the {@link Jar} for the given {@link Class}. If the class is {@code null}, then {@code null} will be + * returned. + * + * @param clazz The class to get the JAR for. + * @return The {@link Jar} instance. + * @throws URISyntaxException If the {@link File} of the JAR can not be determined from the class. + * @throws IOException If the {@link Jar} can not be constructed. + */ + public static Jar fromClass(final Class clazz) throws URISyntaxException, IOException { + final File file = new File(clazz.getProtectionDomain().getCodeSource().getLocation().toURI().getPath()); + return new Jar(file); + } + + @Override + public void close() throws IOException { + if (this.jar != null) { + this.jar.close(); + } + } + + /** + * Get the {@link File} of this JAR. + * + * @return The {@link File} of this JAR. + */ + public File getFile() { + return file; + } + + /** + * Get the {@link InputStream} associated to the given {@link JarEntry entry}. + * + * @param entry The entry to get the input stream for. + * @return The {@link InputStream} for the given entry. + * @throws IOException + */ + public InputStream getInputStream(final JarEntry entry) throws IOException { + return this.jar.getInputStream(entry); + } + + /** + * Get the {@link Manifest} of this JAR. + * + * @return The {@link Manifest} of this JAR. + */ + public final Manifest getManifest() { + if (this.manifest == null) { + try { + this.manifest = this.jar.getManifest(); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Can not retrieve the MANIFEST file of the JAR", e); + } + } + + return this.manifest; + } + + /** + * Get the attributes contained in the {@code MANIFEST.MF} of the JAR. + * + * @return The attributes contained in the {@code MANIFEST.MF} file of the JAR. + */ + public final Attributes getManifestAttributes() { + if (this.manifestAttributes == null) { + final Manifest manifest = getManifest(); + + if (manifest != null) { + this.manifestAttributes = manifest.getMainAttributes(); + } + } + + return this.manifestAttributes; + } + + /** + * Get the value of an attribute stored in the MANIFEST. If the value is {@code null} or + * empty, the default value will be returned. + * + * @param name The name of the attribute to retrieve the value for. + * @param defaultValue The default value to return if the original value is {@code null} or empty. + * @return The value of the attribute. + */ + public final String getManifestAttributeValue(final String name, final String defaultValue) { + final Attributes attributes = this.getManifestAttributes(); + final String value = attributes == null ? null : attributes.getValue(name); + + if (value == null || value.isEmpty()) return defaultValue; + else return value; + } + + /** + * Get an entry of this JAR. + * + * @param entryName The name of the entry to get. + * @return The {@link JarEntry} or {@code null} if not found. + */ + public final JarEntry getEntry(final String entryName) { + final JarEntry entry = this.jar.getJarEntry("META-INF/icon.png"); + return entry; + } + + /** + * Get the value of the attribute {@link Attributes.Name#IMPLEMENTATION_VERSION}. + * + * @return The value of the attribute {@link Attributes.Name#IMPLEMENTATION_VERSION} or {@code null} if not found. + */ + public final String getImplementationVersion() { + return getManifestAttributeValue(Attributes.Name.IMPLEMENTATION_VERSION.toString(), null); + } +} diff --git a/build.gradle b/build.gradle index 9ffae2f5..3fbe4376 100755 --- a/build.gradle +++ b/build.gradle @@ -46,7 +46,7 @@ allprojects { drive 'com.google.apis:google-api-services-drive:v3-rev55-1.22.0' dropbox 'com.dropbox.core:dropbox-core-sdk:2.1.2' felix 'org.apache.felix:org.apache.felix.framework:5.6.1' - fontawesomefx 'de.jensd:fontawesomefx-fontawesome:4.6.1-2' + fontawesomefx 'de.jensd:fontawesomefx-fontawesome:4.7.0' freemarker 'org.freemarker:freemarker:2.3.25-incubating' jsoup 'org.jsoup:jsoup:1.10.2' markdown 'com.github.rjeschke:txtmark:0.13' @@ -131,16 +131,16 @@ ext { asciidoctorMarkupBintrayUploadEnabled = false htmlMarkupBintrayUploadEnabled = false markdownMarkupBintrayUploadEnabled = false - textileMarkupBintrayUploadEnabled = true + textileMarkupBintrayUploadEnabled = false boxHostingConnectorBintrayUploadEnabled = false driveHostingConnectorBintrayUploadEnabled = false - dropboxHostingConnectorBintrayUploadEnabled = true + dropboxHostingConnectorBintrayUploadEnabled = false - alertContentExtensionBintrayUploadEnabled = true - codeContentExtensionBintrayUploadEnabled = true - imageContentExtensionBintrayUploadEnabled = true - linkContentExtensionBintrayUploadEnabled = true + alertContentExtensionBintrayUploadEnabled = false + codeContentExtensionBintrayUploadEnabled = false + imageContentExtensionBintrayUploadEnabled = false + linkContentExtensionBintrayUploadEnabled = false quizContentExtensionBintrayUploadEnabled = false quoteContentExtensionBintrayUploadEnabled = false @@ -242,10 +242,10 @@ ext { apply plugin: 'org.asciidoctor.convert' apply plugin: 'distribution' -version = '1.3' +version = '1.4' wrapper { - gradleVersion = '3.3' + gradleVersion = '3.4' } asciidoctorj { @@ -261,7 +261,7 @@ asciidoctor { setanchors: '', sectlinks: '', linkcss: false, - 'slideshowfx_version': '1.3', + 'slideshowfx_version': project.version, 'asciidoctor-source': new File(project(':SlideshowFX-app').projectDir, 'src/main/resources/com/twasyl/slideshowfx/documentation').absolutePath, 'javafx-version': '8 update 121', 'jdk-version': '8 update 121', diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d6e2637affb74a80bfbe87bd2da57e81b2f3c661..926ad9b0e8b55c6b64608b644c33170f374fef58 100644 GIT binary patch delta 4749 zcmZ`*2{@G78~>PWlTr3DMnn{{L}d#}N~97J(b&S^igA@nCMsPC@l}>kQFN1ijUhMt zz9mZ5ZW1ZWzndie&o`m({sKF8e~j~HhoH6G5Tpvh_LYLL7oQ~kn2)H7NPv!2 z`>V0f>ph2#Xk@@Ur8gEt2^mJ3JWQ_E3yCkvb{H+#YHLjS5N288kZ5)kPl&YK>lcer2{VCuKiASDg?4GkyM2mu&Kc!f2 z`$>)PDfK8T!Ou@#Hsp{uIZjzO{Uacg-2Uek9%5CH(G8oh z@DQueT?WpbROkM^1H7)5^O90Mx1w9Rbt2E;4`-fyBA7|45)T=-ne?Rx^oli674qaW z)OKHw*nVT2axALq70rsrGwf`hIZ5Nd`2^{x{Bju=!<(|_n)8E#4}K46JM8J06hVJ{ z*x-N)Id)t_U+smut`?Jye1siVrzmw6s@$N(w!|f%@8$xO3 z4r$f1&z%P@+tE^j3%TdWC*A#O%1eqe22%nl1J{C&u?K{VJ$^1zB|w}#I({j_L)%jS zs>$BU+uW)`mtWs*eoP-B8PJ=pEslF0blE=k=f1c7>1!>{zWKZ-Oo+|gg_AL2I8dH) z&?Rhr)#NDa7^yVkT}6R&7tT{_eD+azyMDQ2u5Sx<6|EgV{B^^2+@5oq($fv&PIel_ z3Z(i(1-6*9a?0l5aoG`A&kD_i2*O7*mOi$l`JZW(Mp#1M89hcK{=!Gr~A9u-n zoNt~@O84hGr9&oHOgxZEFSfZzbIV>=E0;EGoss&mg+2dXsPZ3KO?DS3(=}hyJ@xTa zCkf?RTW-x+^Ok|)gZYGJ?>>T5!zAZv)f-f^{+Yje)?O^p+3Df+g~RonuDK5BQI!Ud z*Xsx#@=i6LA+E%jNK#^DY7$+X>z*iY#*JiR*PUvYO$KV&c)k(VN;Bg>TF7pa*_Zxq z{LR&-K&`DU)yFkydi%9peMVE2*`!Rd=g`Brl755KxO1Kkc3h>Z3cY2&nJ4|;pYu`b z%WlQ>w0*%j=O@a*vpxnf**M`84szPlZ{Ec869hISQB zb*rKIJrh>{0f`4cbh@5A+5fIIKHI3ZsAlfRYVoVPYWirWRj#E~g4)qLgo z$My=d){*>ux*Ca*91V;?w@d2`ie|~-u-Z53^z;sQee3`6)jgcG4W;w7aOFIt2R)UF zaG5#xc|&KIV3W67qFdWH{+V61{j5K}woy&LhfMIeKD`(?-c9-5i?4wPzg}LO%UhQ> z@qU#5c{@GOXS2T?U9W4vmMq}6W^(A;j&QTAP){kJ?}nF@3WOb7xe$C+i@8ZaFh`u}s zF?XBO%SCNuIhW`%ijle98rg(@KGw*(iic%^5oiupu|d!t*fe1S9M&M6tL}9G4T7)J zhR`zn01T%A&jh7^d2K`ExCsAr=CH?`19du zAX*+;#7H4$+Y428(4cW} zzImM;%n!79!8~Nl!YTm$YWWa`#kP(M5irBVz+6vZ-+?_!%V9{Ts0&oh!31PK2iScu zV9^RO`htajYaD;-BENB9B5fp^d^uwEFLm%$wt#S<**kfh_-`OwSU~{IBLuyN;Ot5v zc%Wj{YLrXxrwStIVaY7azEYx+B#l~p_u;6|BLM9IQxf4=8Zf0&9`>~+e&O+o(4Awx9fQ<5Of3wLF#A>#3BNsl7#1pMEGtM z8z!=>DkQSiqD9n)I*Gku@Mu4?W(b)UeyTceo3Gp z1Zzw3z*)-_%9#pwvP_0UvZNKUxCS{nCj)bxU`k=k#wjkiDu{{0qXsb|v8vA((7N?8X0WH7PWQ+9+e)X6z5IE&m<2=8J+uR(;R5#ji=+!l(*(SwR9 zAOyU(&G|zVK*xhpR$T%jA7zg4-8Q9vNhkuNtkG#t1~6HU<*<9s@We?ec%KU&CMX0X zdA80v3fPGSn~1HRn^XWI!}xpLYtdVWfZziSNPgW<0u4jBzm07%zsRBrt zO^DM=LxhI7p}J+|c|dUS9yTqhpwLoq%L|Ro2ShCk+cpXK3<$+MzpR;0K=o*gaotQ4=(Z3UXh=_9XdA-0wzz8ROG7tH9q+x z(ZJE?*R23uevl{Z!UsXs;GANUs&D~-B>!Urb%kAj)BrnIKvih}G6cT5qnNfXz~mLS zU`Q+0AA~yxoYcU8X5(P`ZLJl6Jq4U!v_azP!QG>U0W`%kt@yQwFV-D1&j}1c1|XLJ zr(`om-w`ft*@{W!Tnmy)YBKyWc@w;|b?N?M@*HYK7WLAYMj~1z(3_^;CNw+>obm@R z%?^wa{S2o5Sca(l@?)qV=o-WUJSzdTDj3ieShr0R&ChsjqJk9tMGyowwS9TuaP7uK z@u>}Qw7!se>imj?R-|x5iZHbo zU|O1QU%Gsm`C8Y3xZ(R0oJ$edtV0~*}d Oh8Z0^?2n!y|NkE{g9UE@ delta 4771 zcmZ`+2{@E(7k9Pzqn!Vr1Xj zPzg~)pHj$|`k!~w=Y946&viXBbFOorbMAAV1R0yUez7fwb@%0+M+;rDmT64qHmj(RecoF5TD(a=y9@tcJTCK zWoj}-U3XJAkFTb6ny3oNc%ZQk_e#JvKA~(_S;g=0#^>!4Pm7jB^>Axj9r9hiefgx^ zCaLy(z5AqgAGuQUFtokgMzwsdCq~xo@s)k0TpwHrjTcQ965NKz6q8JFY@L``qQPmC}m3i39i5OK9@D_cd*Znb?`V z%UJaqeFG=is?=J1N1{QGaCIgGnxpSQse`|_BBOEm9i;L1PSK(S7j&J zowycUo8|1-VtIS(Xkwj?V1>&%j;61ds$|R@g>r62*LtsuLm(V*=Or!-$TvEXc~hpD+rFGnxE1%*D%lly45JlCC?)?6Q}N#mQHw zslC`WOW2#KeT)5Oh{fV7{nG7rSEWfMH?W9tPscFitLu4Ks-{INq7jI1! z&dtAylPV4C`H`HnGjYC|s$^x)Q71ZLlW{j{TZxVBiDxhJrQgg&WP4r>?e|(Vn7Ysy z{3ftx_S+{k9^cxcs^cV-XK;rWxxY*bkN}^#Rk;~vz*ai0*zCv#**JbaZ-Xu56 zjv1!eb4q6;&KiZ?Q=bXU5@vgO{-Jnje1!_lGVdl!m;bSFM&UD;{z~yA_$<98zjGK2 z<;@QM%DvIkdx2qWh^hVn?hfm7qv8*lVYnXdGKinQ%-i&K-Trweu(8)^oy$(4Ib97@UmE=a0+;@L5jGZhp3kwCdl zYyFY*U@^K;eB=bkz`UbhahccZTN&NRH? zi}RfP_%^stm?u@y`L>>BhS(!kem<*Ac*a{`C#}}ZPQKI1fAj6GBTq_H7O8KpS@0GS z?;m54v!-x-G&|;=W>em&V&ydXTzS2$XspUp;}izCgvqV-h&knD*TheVWqQkH+H__3 ze`ch67uatvFncXz?7iA!9{un*^%A_W$K}(yfKi{RN4m|*jfW(nyt9K27U|hal^#%Y ziaq*v(5S{sKhQ$lc%LG%xO)KKebhf;t{i?Wn_MiEA(HFQo7Q{J@q+)8ek1#>3E9lt zv;5+VLwE48V!K7dBp(FW_L)b0P)!irz0GL%xhwv(+XWS88@v8)w$2hAcOdTMU)+=L zT@zyzQNr7D+SvD8%u{{>p(evEd?{B%Y>CH_>+btlMun$v-i1?1!>lH!xqVx?PcmKV{1c>G&2^4kDgjJzYL(Yr}OuAqsQA*xPZ}H7HeYoB;GZ{=%?H8 zgc;~>R34g;G)nnyYM);Lh%*SL#Byxp3uLy3CWl5IS@4j1501AG7X)paJx06|BmjU2he>e3nX3S$bbu(C z?qT?`C_4G0XI$KJK%}Q&N@61JSNKJ@9YwmbM3Li<99}IBkp(y*=(QjO ziDNu=gRKw#c4)6>RZ(!J9QN~DT~uZ-(QBkN5Hv-GATf-oj4m)mvSWsonh6_Ft59X;Re zC<5@pU}7uy_YwGbi|p@eBlixrg`k6&JA;NIOkO$12pj=`TZ2Bhm%kwhIXVk)UCTPO zXX75XsRV#c12>H7zfk00$hg4beq>mtl^2b79~7eXH&a5vK_!5Ls);f1^oK?Kwc+sge~%MAdR#jaT{jRUBn)!}aw7mV zgP;u~)=uDz3W3{C3*+#55FB4C2n%UieX$0y_BK?QEocu;BCH=GDd&r=XA?JiKuI zA4_D=LmZz3j6*?1u|`G3bfcVZF$~+zZiMa3UAmI9hlo)s^dL&(`#?#C{d(4;>6O=& z`XvK;1%RFohMsCA9nW1!{Da