package com.twasyl.slideshowfx.controllers;
import com.twasyl.slideshowfx.app.SlideshowFX;
import com.twasyl.slideshowfx.concurrent.*;
import com.twasyl.slideshowfx.controls.SlideMenuItem;
import com.twasyl.slideshowfx.controls.Tour;
import com.twasyl.slideshowfx.controls.notification.NotificationCenter;
import com.twasyl.slideshowfx.controls.slideshow.Context;
import com.twasyl.slideshowfx.controls.slideshow.SlideshowStage;
import com.twasyl.slideshowfx.controls.stages.AboutStage;
import com.twasyl.slideshowfx.controls.stages.HelpStage;
import com.twasyl.slideshowfx.controls.stages.LogsStage;
import com.twasyl.slideshowfx.controls.stages.TemplateBuilderStage;
import com.twasyl.slideshowfx.dao.TaskDAO;
import com.twasyl.slideshowfx.engine.presentation.PresentationEngine;
import com.twasyl.slideshowfx.engine.presentation.Presentations;
import com.twasyl.slideshowfx.engine.presentation.configuration.Slide;
import com.twasyl.slideshowfx.engine.template.TemplateEngine;
import com.twasyl.slideshowfx.engine.template.configuration.SlideTemplate;
import com.twasyl.slideshowfx.global.configuration.GlobalConfiguration;
import com.twasyl.slideshowfx.hosting.connector.IHostingConnector;
import com.twasyl.slideshowfx.hosting.connector.exceptions.HostingConnectorException;
import com.twasyl.slideshowfx.hosting.connector.io.RemoteFile;
import com.twasyl.slideshowfx.io.SlideshowFXExtensionFilter;
import com.twasyl.slideshowfx.osgi.OSGiManager;
import com.twasyl.slideshowfx.server.SlideshowFXServer;
import com.twasyl.slideshowfx.server.service.AttendeeChatService;
import com.twasyl.slideshowfx.server.service.PresenterChatService;
import com.twasyl.slideshowfx.server.service.QuizService;
import com.twasyl.slideshowfx.server.service.TwitterService;
import com.twasyl.slideshowfx.services.AutoSavingService;
import com.twasyl.slideshowfx.utils.*;
import com.twasyl.slideshowfx.utils.beans.Pair;
import com.twasyl.slideshowfx.utils.beans.binding.WildcardBinding;
import com.twasyl.slideshowfx.utils.concurrent.SlideshowFXTask;
import com.twasyl.slideshowfx.utils.concurrent.TaskAction;
import com.twasyl.slideshowfx.utils.concurrent.actions.DisableAction;
import com.twasyl.slideshowfx.utils.concurrent.actions.EnableAction;
import com.twasyl.slideshowfx.utils.keys.KeyEventUtils;
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon;
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView;
import javafx.application.Platform;
import javafx.beans.binding.StringExpression;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.concurrent.Worker;
import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Cursor;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.*;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TextField;
import javafx.scene.input.*;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.stage.DirectoryChooser;
import javafx.stage.FileChooser;
import java.awt.*;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.nio.file.Files;
import java.util.List;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* This class is the controller of the {@code Slideshow.fxml} file. It defines all actions possible inside the view
* represented by the FXML.
*
* @author Thierry Wasyczenko
* @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<ActionEvent> addSlideActionEvent = event -> {
try {
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) {
final Slide addedSlide = presentation.addSlide((SlideTemplate) userData, view.getCurrentSlideNumber());
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);
}
};
private final EventHandler<ActionEvent> moveSlideActionEvent = event -> {
final PresentationEngine presentation = Presentations.getCurrentDisplayedPresentation();
final PresentationViewController view = SlideshowFXController.this.getCurrentPresentationView();
if(view != null && presentation != null) {
final SlideMenuItem menunItem = (SlideMenuItem) event.getSource();
final Slide slideToMove = presentation.getConfiguration().getSlideById(view.getCurrentSlideId());
final Slide beforeSlide = menunItem.getSlide();
presentation.moveSlide(slideToMove, beforeSlide);
final ReloadPresentationViewTask task = new ReloadPresentationViewTask(view);
SlideshowFXController.this.taskInProgress.setCurrentTask(task);
TaskDAO.getInstance().startTask(task);
SlideshowFXController.this.updateSlideSplitMenu();
}
};
@FXML private BorderPane root;
/* Main ToolBar elements */
@FXML private SplitMenuButton addSlideButton;
@FXML private SplitMenuButton moveSlideButton;
@FXML private ComboBox<String> serverIpAddress;
@FXML private TextField serverPort;
@FXML private TextField twitterHashtag;
@FXML private Button startServerButton;
/* Main application UI elements */
@FXML private TabPane openedPresentationsTabPane;
/* Main application menu elements */
@FXML private Menu uploadersMenu;
@FXML private Menu downloadersMenu;
@FXML private MenuItem openWebApplicationMenuItem;
/* Notification center */
@FXML private NotificationCenter taskInProgress;
/* List of controls */
@FXML private ObservableList<Object> saveElementsGroup;
@FXML private ObservableList<Object> openElementsGroup;
@FXML private ObservableList<Object> whenNoDocumentOpened;
/* All methods called by the FXML */
/**
* Loads a SlideshowFX template. This method displays an open dialog which only allows to open template files (with
* .sfxt archiveExtension) and then call the {@link #openTemplateOrPresentation(File)} method.
*
* @param event the event that triggered the call.
*/
@FXML private void loadTemplate(ActionEvent event) {
FileChooser chooser = new FileChooser();
chooser.getExtensionFilters().add(SlideshowFXExtensionFilter.TEMPLATE_FILTER);
File templateFile = chooser.showOpenDialog(null);
if (templateFile != null) {
try {
this.openTemplateOrPresentation(templateFile);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
/**
* Open a SlideshowFX presentation. This method displays an open dialog which only allows to open presentation files
* (with the .sfx archiveExtension) and then call {@link #openTemplateOrPresentation(File)} method.
*
* @param event the event that triggered the call.
*/
@FXML private void openPresentation(ActionEvent event) {
FileChooser chooser = new FileChooser();
chooser.getExtensionFilters().add(SlideshowFXExtensionFilter.PRESENTATION_FILES);
File file = chooser.showOpenDialog(null);
if (file != null) {
try {
this.openTemplateOrPresentation(file);
} catch (IllegalAccessException | FileNotFoundException e) {
LOGGER.log(Level.SEVERE, "Can not open the presentation", e);
}
}
}
/**
* Close the current displayed presentation.
* @param event The event that triggered the call.
*/
@FXML private void closePresentation(ActionEvent event) {
if(this.openedPresentationsTabPane.getSelectionModel().getSelectedItem() != null) {
final Tab tab = this.openedPresentationsTabPane.getSelectionModel().getSelectedItem();
// Fire a close request event on the tab if any EventHandler has been defined
final EventType<Event> closeRequestEventType = Tab.TAB_CLOSE_REQUEST_EVENT;
final Event closeRequestEvent = new Event(closeRequestEventType);
Event.fireEvent(tab, closeRequestEvent);
// Fire a closed event on the tab if any EventHandler has been defined
final EventType<Event> closedEventType = Tab.CLOSED_EVENT;
final Event closedEvent = new Event(closedEventType);
Event.fireEvent(tab, closedEvent);
this.openedPresentationsTabPane.getTabs().remove(tab);
}
}
/**
* Close the given presentation
* @param presentation The presentation to close.
* @param waitToFinish Indicates if the method should wait before exiting.
*/
public void closePresentation(final PresentationEngine presentation, final boolean waitToFinish) {
if(presentation != null && presentation.isModifiedSinceLatestSave()) {
final String message = presentation.getArchive() != null ?
String.format("Do you want to save the modifications on %1$s?", presentation.getArchive().getName()) :
"Do you want to save this presentation?";
final ButtonType answer = DialogHelper.showConfirmationAlert("Save the presentation", message);
if(answer == ButtonType.YES) {
SlideshowFXController.this.savePresentation(presentation, waitToFinish);
}
AutoSavingService.cancelFor(presentation);
}
}
/**
* Close all opened presentations. If a presentation hasn't been saved, the user is prompted if he wants to save
* the modifications.
* @param waitToFinish Indicates the method must wait for all presentations to be closed before exiting.
*/
public void closeAllPresentations(final boolean waitToFinish) {
PlatformHelper.run(() -> {
this.openedPresentationsTabPane.getTabs()
.filtered(tab -> tab.getUserData() != null && tab.getUserData() instanceof PresentationViewController)
.forEach(tab -> {
final PresentationEngine presentation = ((PresentationViewController) tab.getUserData()).getPresentation();
this.closePresentation(presentation, waitToFinish);
});
Platform.exit();
});
}
/**
* This method is called when a file is dropped on the main UI.
* It allows to open presentation or template to be opened by drag'n'drop.
*
* @param dragEvent The drag event associated to the drag.
*/
@FXML private void dragDroppedOnUI(DragEvent dragEvent) {
Dragboard board = dragEvent.getDragboard();
boolean dragSuccess = false;
if (board.hasFiles()) {
Optional<File> slideshowFXFile = board.getFiles().stream()
.filter(file -> file.getName().endsWith(PresentationEngine.DEFAULT_DOTTED_ARCHIVE_EXTENSION)
|| file.getName().endsWith(TemplateEngine.DEFAULT_DOTTED_ARCHIVE_EXTENSION))
.findFirst();
if (slideshowFXFile != null && slideshowFXFile.isPresent()) {
try {
SlideshowFXController.this.openTemplateOrPresentation(slideshowFXFile.get());
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
dragSuccess = true;
}
}
dragEvent.setDropCompleted(dragSuccess);
dragEvent.consume();
PlatformHelper.run(() -> {
this.openedPresentationsTabPane.getStyleClass().remove("validDragOver");
this.openedPresentationsTabPane.getStyleClass().remove("invalidDragOver");
});
}
/**
* This method is called when a file is dragged over the main UI.
* It allows to open presentation or template to be opened by drag'n'drop.
*
* @param dragEvent The drag event associated to the drag.
*/
@FXML private void dragOverUI(DragEvent dragEvent) {
if(dragEvent.getGestureSource() != this.openedPresentationsTabPane && dragEvent.getDragboard().hasFiles()) {
/**
* Check if either a template or a presentation is drag over the browser.
*/
Optional<File> slideshowFXFile = dragEvent.getDragboard().getFiles().stream()
.filter(file -> file.getName().endsWith(PresentationEngine.DEFAULT_DOTTED_ARCHIVE_EXTENSION)
|| file.getName().endsWith(TemplateEngine.DEFAULT_DOTTED_ARCHIVE_EXTENSION))
.findFirst();
if (slideshowFXFile != null && slideshowFXFile.isPresent()) {
dragEvent.acceptTransferModes(TransferMode.COPY_OR_MOVE);
PlatformHelper.run(() -> {
this.openedPresentationsTabPane.getStyleClass().remove("invalidDragOver");
if(!this.openedPresentationsTabPane.getStyleClass().contains("validDragOver")) {
this.openedPresentationsTabPane.getStyleClass().add("validDragOver");
}
});
} else {
PlatformHelper.run(() -> {
this.openedPresentationsTabPane.getStyleClass().remove("validDragOver");
if (!this.openedPresentationsTabPane.getStyleClass().contains("invalidDragOver")) {
this.openedPresentationsTabPane.getStyleClass().add("invalidDragOver");
}
});
}
dragEvent.consume();
}
}
/**
* This method is called the drag exits the main UI.
*
* @param dragEvent The drag event associated to the drag.
*/
@FXML private void dragExitedUI(DragEvent dragEvent) {
PlatformHelper.run(() -> {
this.openedPresentationsTabPane.getStyleClass().remove("validDragOver");
this.openedPresentationsTabPane.getStyleClass().remove("invalidDragOver");
});
}
/**
* Open the current presentation inside the default browser of the user.
* @param event The event associated to this request.
*/
@FXML private void openPresentationInBrowser(ActionEvent event) {
if(Desktop.isDesktopSupported()) {
final PresentationEngine presentation = Presentations.getCurrentDisplayedPresentation();
if(presentation != null) {
final File presentationFile = presentation.getConfiguration().getPresentationFile();
if(presentationFile != null && presentationFile.exists()) {
try {
Desktop.getDesktop().browse(presentationFile.toURI());
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Can not open working directory", e);
}
}
}
}
}
/**
* Open the current working directory in the file explorer of the system.
* @param event The event associated to the request.
*/
@FXML private void openWorkingDirectory(ActionEvent event) {
final PresentationEngine presentation = Presentations.getCurrentDisplayedPresentation();
if(presentation != null) {
final File workingDir = presentation.getWorkingDirectory();
if(workingDir != null && workingDir.exists()) {
if(Desktop.isDesktopSupported()) {
try {
Desktop.getDesktop().open(workingDir);
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Can not open working directory", e);
}
}
}
}
}
@FXML private void takeTour(ActionEvent event) {
final Tour tour = new Tour(SlideshowFX.getStage().getScene());
tour.addStep(new Tour.Step("#menuBar", "The menu bar of SlideshowFX."))
.addStep(new Tour.Step("#toolBar", "The toolbar gives you access to most commons features of SlideshowFX."))
.addStep(new Tour.Step("#loadTemplate", "Create a new presentation from a template."))
.addStep(new Tour.Step("#openPresentation", "Open an already existent presentation."))
.addStep(new Tour.Step("#saveButton", "Save the current presentation."))
.addStep(new Tour.Step("#printPresentation", "Print the current presentation."))
.addStep(new Tour.Step("#addSlideButton", "Add a slide after the current slide in the presentation."))
.addStep(new Tour.Step("#copySlide", "Copy the current slide after it."))
.addStep(new Tour.Step("#moveSlideButton", "Move a slide before another."))
.addStep(new Tour.Step("#deleteSlide", "Delete the current slide."))
.addStep(new Tour.Step("#reloadPresentation", "Reload the current presentation."))
.addStep(new Tour.Step("#slideshow", "Enter in the presentation mode."))
.addStep(new Tour.Step("#serverIpAddress", "The IP address of the embedded server. If nothing is provided, an automatic IP will be used."))
.addStep(new Tour.Step("#serverPort", "The port of the embedded server. If nothing is provided, 8080 will be used."))
.addStep(new Tour.Step("#twitterHashtag", "Look for the given hashtag on Twitter. If left blank, the Twitter service will not be started."))
.addStep(new Tour.Step("#startServerButton", "Start or stop the embedded server."))
.addStep(new Tour.Step("#browser", "Your presentation is displayed here."))
.addStep(new Tour.Step("#contentExtensionToolBar", "Extensions are added here. If you install new ones they will also appear here. An extension provides a feature that adds something to your presentation, like inserting an image."))
.addStep(new Tour.Step("#markupContentTypeBox", "The syntaxes available to define slides's content are located here. If you install new ones, they will also appear here."))
.addStep(new Tour.Step("#contentEditor", "It is here that you define the content for a slide."))
.addStep(new Tour.Step("#defineContent", "Define the content for the given element."))
.start();
}
/**
* Displays a dialog showing the information about SlideshowFX
* @param event The source of the event
*/
@FXML private void displayAbout(ActionEvent event) {
new AboutStage().show();
}
/**
* Displays an internal browser in a specific tab.
* @param event The source event.
*/
@FXML private void displayInternalBrowser(ActionEvent event) {
try {
final Parent root = FXMLLoader.load(ResourceHelper.getURL("/com/twasyl/slideshowfx/fxml/InternalBrowser.fxml"));
final Tab tab = new Tab("Internal browser", root);
this.openedPresentationsTabPane.getTabs().addAll(tab);
this.openedPresentationsTabPane.getSelectionModel().select(tab);
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Can not open internal browser", e);
}
}
@FXML private void displayWebApplication(final ActionEvent event) {
try {
final Parent root = FXMLLoader.load(ResourceHelper.getURL("/com/twasyl/slideshowfx/fxml/SlideshowFXWebApplication.fxml"));
final Tab tab = new Tab("Web application", root);
this.openedPresentationsTabPane.getTabs().addAll(tab);
this.openedPresentationsTabPane.getSelectionModel().select(tab);
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Can not open web application", e);
}
}
/**
* Displays the window allowing to display the logs.
* @param event The source event.
*/
@FXML private void displayLogs(final ActionEvent event) {
new LogsStage().show();
}
/**
* Displays the help stage.
* @param event The source event.
*/
@FXML private void displayHelp(final ActionEvent event) {
new HelpStage().show();
}
/**
* Displays the plugin center.
* @param event The source event.
*/
@FXML private void displayPluginCenter(final ActionEvent event) {
FXMLLoader loader = new FXMLLoader(ResourceHelper.getURL("/com/twasyl/slideshowfx/fxml/PluginCenter.fxml"));
try {
final Parent root = loader.load();
final PluginCenterController controller = loader.getController();
final ButtonType response = DialogHelper.showCancellableDialog("Plugin center", root);
if(response.equals(ButtonType.OK)) {
controller.validatePluginsConfiguration();
this.openedPresentationsTabPane.getTabs()
.stream()
.filter(tab -> tab.getUserData() != null && tab.getUserData() instanceof PresentationViewController)
.forEach(tab -> {
((PresentationViewController) tab.getUserData()).refreshMarkupSyntax();
((PresentationViewController) tab.getUserData()).refreshContentExtensions();
});
this.refreshHostingConnectors();
}
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Can not open plugin center view", e);
}
}
/**
* Copy the slide, update the menu of available slides and reload the presentation.
* The copy is delegated to {@link PresentationEngine#duplicateSlide(Slide)}.
*
* @param event
*/
@FXML private void copySlide(ActionEvent event) {
final PresentationEngine presentation = Presentations.getCurrentDisplayedPresentation();
final PresentationViewController view = getCurrentPresentationView();
if(presentation != null && view != null) {
final Slide source = presentation.getConfiguration().getSlideById(view.getCurrentSlideId());
this.copySlide(presentation, source);
this.updateSlideSplitMenu();
final ReloadPresentationViewTask task = new ReloadPresentationViewTask(view);
this.taskInProgress.setCurrentTask(task);
TaskDAO.getInstance().startTask(task);
}
}
/**
* Delete a slide from the presentation. The deletion is delegated to {@link PresentationEngine#deleteSlide(String)}.
*
* @param event
*/
@FXML private void deleteSlide(ActionEvent event) {
final PresentationEngine presentation = Presentations.getCurrentDisplayedPresentation();
final PresentationViewController view = this.getCurrentPresentationView();
if(presentation != null && view != null) {
final String slideNumberToDelete = view.getCurrentSlideNumber();
if(slideNumberToDelete != null) {
final ButtonType answer = DialogHelper.showConfirmationAlert("Delete slide", "Are you sure you want to delete the slide?");
if(answer == ButtonType.YES) {
final Slide slideBefore = presentation.getConfiguration().getSlideBefore(slideNumberToDelete);
presentation.deleteSlide(slideNumberToDelete);
if (slideBefore == null) this.reloadPresentation(view);
else reloadPresentationAndGoToSlide(view, slideBefore.getId());
}
}
}
}
/**
* Simply reload the presentation by calling {@link javafx.scene.web.WebEngine#reload()}.
*
* @param event
*/
@FXML private void reload(ActionEvent event) {
reloadPresentation(this.getCurrentPresentationView());
}
/**
* Save the current presentation. If the presentation has never been saved a save dialog is displayed.
* Then the presentation is saved where the user has chosen or opened the presentation.
* The saving is delegated to {@link #savePresentation(boolean)}
*
* @param event
*/
@FXML private void save(ActionEvent event) {
this.savePresentation(false);
}
/**
* Saves a copy of the existing presentation. A save dialog is displayed to the user.
* The saving is delegated to {@link #savePresentation(File, boolean)}.
*
* @param event
*/
@FXML private void saveAs(ActionEvent event) {
File presentationArchive = null;
FileChooser chooser = new FileChooser();
chooser.getExtensionFilters().add(SlideshowFXExtensionFilter.PRESENTATION_FILES);
presentationArchive = chooser.showSaveDialog(SlideshowFX.getStage());
this.savePresentation(presentationArchive, false);
}
/**
* Print the current presentation displayed.
*
* @param event
*/
@FXML private void print(ActionEvent event) {
final PresentationViewController view = this.getCurrentPresentationView();
if(view != null) view.printPresentation();
}
@FXML private void slideShow(ActionEvent event) {
this.startSlideshow(false);
}
@FXML private void slideshowFromCurrentSlide(ActionEvent event) {
this.startSlideshow(true);
}
/**
* This methods starts the chat when the ENTER key is pressed in the IP address, port number or Twitter's hashtag textfields.
*
* @param event
*/
@FXML private void startServerByKeyPressed(KeyEvent event) {
if(event.getCode().equals(KeyCode.ENTER)) this.startServer();
}
/**
* This methods starts the chat when the button for starting the chat is clicked.
*
* @param event
*/
@FXML private void startServerByButton(ActionEvent event) {
this.startServer();
}
/**
* This method is called in order to create a template from scratch.
* @param event The event associated to the click that should open the template builder.
*/
@FXML private void createTemplate(ActionEvent event) {
final TemplateEngine engine = new TemplateEngine();
engine.setWorkingDirectory(engine.generateWorkingDirectory());
if(engine.getWorkingDirectory().mkdir()) {
final File templateConfigurationFile = new File(engine.getWorkingDirectory(), engine.getConfigurationFilename());
try {
Files.createFile(templateConfigurationFile.toPath());
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Can not create template configuration file", e);
}
}
this.showTemplateBuilder(engine);
}
/**
* This method is called in order to open an existing template from scratch.
* @param event The event associated to the click that should open the template builder.
*/
@FXML private void openTemplate(ActionEvent event) {
final FileChooser chooser = new FileChooser();
chooser.setSelectedExtensionFilter(SlideshowFXExtensionFilter.TEMPLATE_FILTER);
final File file = chooser.showOpenDialog(null);
if(file != null) {
final TemplateEngine engine = new TemplateEngine();
engine.setWorkingDirectory(engine.generateWorkingDirectory());
engine.setArchive(file);
if(engine.getWorkingDirectory().mkdir()) {
try {
ZipUtils.unzip(file, engine.getWorkingDirectory());
this.showTemplateBuilder(engine);
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Can not unzip the template", e);
}
}
}
}
/**
* This method is called in order to edit the current template for the opened presentation.
* @param event The event associated to the click that should open the template builder.
*/
@FXML private void editTemplate(ActionEvent event) {
final PresentationEngine presentation = Presentations.getCurrentDisplayedPresentation();
if(presentation != null) {
final TemplateEngine engine = new TemplateEngine();
engine.setWorkingDirectory(presentation.getWorkingDirectory());
this.showTemplateBuilder(engine);
}
}
/**
* This method exits the application. If any opened presentation is not saved, the user will be asked if he wants to
* save the modifications or not.
*
* @param event
*/
@FXML private void exitApplication(ActionEvent event) {
this.closeAllPresentations(true);
}
/**
* This method shows a dialog for options of SlideshowFX.
* @param event
*/
@FXML private void showOptionsDialog(ActionEvent event) {
FXMLLoader loader = new FXMLLoader(ResourceHelper.getURL("/com/twasyl/slideshowfx/fxml/OptionsView.fxml"));
try {
final Parent root = loader.load();
final OptionsViewController controller = loader.getController();
final ButtonType response = DialogHelper.showCancellableDialog("Options", root);
if(response != null && response == ButtonType.OK) {
controller.saveOptions();
}
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Can not open options view", e);
}
}
/* All instance methods */
/**
* Open the dataFile. If the name ends with {@code .sfx} the file is considered as a presentation,
* if it ends with {@code .sfx} it is considered as a template.
*
* @param dataFile the file corresponding to either a template or a presentation.
* @throws IllegalArgumentException If the file is null.
* @throws FileNotFoundException If dataFile does not exist.
* @throws IllegalAccessException If the file can not be accessed.
*/
public void openTemplateOrPresentation(final File dataFile) throws IllegalArgumentException, IllegalAccessException, FileNotFoundException {
if (dataFile == null) throw new IllegalArgumentException("The dataFile can not be null");
if (!dataFile.exists()) throw new FileNotFoundException("The dataFile does not exist");
if (!dataFile.canRead()) throw new IllegalAccessException("The dataFile can not be accessed");
final SlideshowFXTask<PresentationEngine> loadingTask = dataFile.getName().endsWith(PresentationEngine.DEFAULT_DOTTED_ARCHIVE_EXTENSION)
? new LoadPresentationTask(dataFile) : dataFile.getName().endsWith(TemplateEngine.DEFAULT_DOTTED_ARCHIVE_EXTENSION) ?
new LoadTemplateTask(dataFile) : null;
if(loadingTask != null) {
TaskAction.forTask(loadingTask)
.when().stateIs(Worker.State.RUNNING).perform(DisableAction.forElements(this.whenNoDocumentOpened).and(this.openElementsGroup))
.when().stateIs(Worker.State.READY).perform(DisableAction.forElements(this.whenNoDocumentOpened).and(this.openElementsGroup))
.when().stateIs(Worker.State.SUCCEEDED).perform(EnableAction.forElements(this.whenNoDocumentOpened).and(this.openElementsGroup))
.when().stateIs(Worker.State.FAILED).perform(EnableAction.forElements(this.whenNoDocumentOpened).and(this.openElementsGroup))
.when().stateIs(Worker.State.CANCELLED).perform(EnableAction.forElements(this.whenNoDocumentOpened).and(this.openElementsGroup));
this.taskInProgress.setCurrentTask(loadingTask);
loadingTask.stateProperty().addListener((value, oldState, newState) -> {
if(newState != null && (
newState == Worker.State.FAILED ||
newState == Worker.State.CANCELLED ||
newState == Worker.State.SUCCEEDED) &&
loadingTask.getValue() != null) {
final FXMLLoader loader = new FXMLLoader(ResourceHelper.getURL("/com/twasyl/slideshowfx/fxml/PresentationView.fxml"));
try {
final Parent parent = loader.load();
final PresentationViewController controller = loader.getController();
controller.definePresentation(loadingTask.getValue());
final Tab tab = new Tab();
final WildcardBinding presentationModifiedBinding = new WildcardBinding(controller.presentationModifiedProperty());
final StringExpression tabTitle = controller.getPresentationName().concat(presentationModifiedBinding);
tab.textProperty().bind(tabTitle);
tab.setUserData(controller);
tab.setContent(parent);
tab.setOnCloseRequest(event -> SlideshowFXController.this.closePresentation(loadingTask.getValue(), false));
this.openedPresentationsTabPane.getTabs().addAll(tab);
this.openedPresentationsTabPane.getSelectionModel().select(tab);
this.updateSlideTemplatesSplitMenu();
this.updateSlideSplitMenu();
final AutoSavingService autoSavingService = new AutoSavingService(loadingTask.getValue());
if(GlobalConfiguration.isAutoSavingEnabled()) {
PlatformHelper.run(() -> autoSavingService.start());
}
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Can not load the view", e);
}
}
});
TaskDAO.getInstance().startTask(loadingTask);
}
}
/**
* Show the template builder window.
* @param engine the engine used for the template builder that will be created.
*/
private void showTemplateBuilder(final TemplateEngine engine) {
final TemplateBuilderStage stage = new TemplateBuilderStage(engine);
stage.setMaximized(true);
stage.show();
}
/**
* Update the control hosting the slides' templates. This method get the templates from the current displayed presentation.
* If no presentation is opened the list of templates is cleared.
*/
private void updateSlideTemplatesSplitMenu() {
this.addSlideButton.getItems().clear();
final PresentationEngine presentation = Presentations.getCurrentDisplayedPresentation();
if (presentation != null) {
List<SlideTemplate> templates = presentation.getTemplateConfiguration().getSlideTemplates();
if(templates != null) {
templates.forEach(template -> {
final MenuItem item = new MenuItem();
item.setText(template.getName());
item.setUserData(template);
item.setOnAction(addSlideActionEvent);
this.addSlideButton.getItems().add(item);
});
}
}
}
/**
* Update the list of existing slides for the current presentation. If no presentation is opened or no slides are
* defined, the control is cleared.
*/
protected void updateSlideSplitMenu() {
SlideshowFXController.this.moveSlideButton.getItems().clear();
final PresentationEngine presentation = Presentations.getCurrentDisplayedPresentation();
if (presentation != null) {
List<Slide> slides = presentation.getConfiguration().getSlides();
if(slides != null) {
slides.forEach(slide -> {
final SlideMenuItem menuItem = new SlideMenuItem(slide);
menuItem.setOnAction(SlideshowFXController.this.moveSlideActionEvent);
this.moveSlideButton.getItems().add(menuItem);
});
}
}
}
/**
* Copy a given {@link Slide slide} of the {@link PresentationEngine presentation}.
* @param presentation The presentation where the slide will be copied.
* @param slide The slide to copy
*/
private void copySlide(final PresentationEngine presentation, final Slide slide) {
try {
presentation.duplicateSlide(slide);
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Error when copying the slide", e);
}
}
/**
* Save the presentation that is currently displayed, if any. This method retrieves the displayed presentation and
* calls {@link #savePresentation(PresentationEngine, boolean)}.
* @param waitToFinish Indicates if the method should wait before exiting.
*/
private void savePresentation(boolean waitToFinish) {
this.savePresentation(Presentations.getCurrentDisplayedPresentation(), waitToFinish);
}
/**
* Save the given presentation. If the presentation hasn't been already saved, the user is prompted
* to choose where to save it. Once the choice is validated, the method calls {@link #savePresentation(File, boolean)}.
* @param presentation The presentation to save.
* @param waitToFinish Indicates if the method should wait before exiting.
*/
private void savePresentation(final PresentationEngine presentation, final boolean waitToFinish) {
if(presentation != null) {
File presentationArchive;
if (!presentation.isPresentationAlreadySaved()) {
FileChooser chooser = new FileChooser();
chooser.getExtensionFilters().add(SlideshowFXExtensionFilter.PRESENTATION_FILES);
presentationArchive = chooser.showSaveDialog(SlideshowFX.getStage());
final AutoSavingService autoSavingService = new AutoSavingService(presentation);
if(GlobalConfiguration.isAutoSavingEnabled()) {
PlatformHelper.run(() -> autoSavingService.start());
}
} else presentationArchive = presentation.getArchive();
this.savePresentation(presentationArchive, waitToFinish);
}
}
/**
* Save the current opened presentation to the given {@param archiveFile}. The process for
* saving the presentation is only started if the given {@param archiveFile} is not {@code null}. If the process is
* started, a {@link SavePresentationTask} is started with the current presentation
* and {@code archiveFile}.
* @param archiveFile The file to save the presentation in.
* @param waitToFinish Indicates if the method should wait before exiting.
*/
private void savePresentation(final File archiveFile, final boolean waitToFinish) {
if(archiveFile != null) {
final PresentationEngine presentation = Presentations.getCurrentDisplayedPresentation();
presentation.setArchive(archiveFile);
final SlideshowFXTask saveTask = new SavePresentationTask(presentation);
TaskAction.forTask(saveTask)
.when().stateIs(Worker.State.RUNNING).perform(DisableAction.forElements(this.saveElementsGroup).and(this.openElementsGroup))
.when().stateIs(Worker.State.SUCCEEDED).perform(EnableAction.forElements(this.saveElementsGroup).and(this.openElementsGroup))
.when().stateIs(Worker.State.FAILED).perform(EnableAction.forElements(this.saveElementsGroup).and(this.openElementsGroup))
.when().stateIs(Worker.State.CANCELLED).perform(EnableAction.forElements(this.saveElementsGroup).and(this.openElementsGroup));
this.taskInProgress.setCurrentTask(saveTask);
TaskDAO.getInstance().startTask(saveTask);
if(waitToFinish) {
PlatformHelper.run(() -> SlideshowFX.getStage().getScene().setCursor(Cursor.WAIT));
try {
saveTask.get();
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Can not wait for the presentation to be saved", e);
} finally {
PlatformHelper.run(() -> SlideshowFX.getStage().getScene().setCursor(Cursor.DEFAULT));
}
}
}
}
/**
* Reloads a presentation for a given view.
* @param view The view of the presentation to reload.
*/
private void reloadPresentation(final PresentationViewController view) {
reloadPresentationAndGoToSlide(view, null);
}
/**
* Reloads a presentation contained in a given {@link PresentationViewController view} and then go to a given slide
* identified by its id. If the provided ID is {@code null}, the presentation will only be reloaded.
* @param view The view of the presentation to reload.
* @param id The ID of the slide to go to when the presentation has been successfully reloaded.
*/
private void reloadPresentationAndGoToSlide(final PresentationViewController view, final String id) {
if(view != null) {
final ReloadPresentationViewTask task;
if(id != null && !id.isEmpty()) {
task = new ReloadPresentationViewAndGoToTask(view, id);
} else {
task = new ReloadPresentationViewTask(view);
}
SlideshowFXController.this.taskInProgress.setCurrentTask(task);
TaskDAO.getInstance().startTask(task);
}
}
/**
* Start the chat. This method takes the text entered in the IP, port and Twitter's hashtag fields to start the chat.
* If no text is entered for the IP address and the port number, the IP address of the computer is used and the port 80 is chosen.
*/
private void startServer() {
FontAwesomeIconView icon;
final Tooltip tooltip = new Tooltip();
if (SlideshowFXServer.getSingleton() != null) {
SlideshowFXServer.getSingleton().stop();
icon = new FontAwesomeIconView(FontAwesomeIcon.PLAY);
icon.setGlyphSize(20);
icon.setGlyphStyle("-fx-fill: green");
tooltip.setText("Start the server");
} else {
String ip = this.serverIpAddress.getValue();
if (ip == null || ip.isEmpty()) {
ip = NetworkUtils.getIP();
}
int port = 80;
if (this.serverPort.getText() != null && !this.serverPort.getText().isEmpty()) {
try {
port = Integer.parseInt(this.serverPort.getText());
} catch(NumberFormatException ex) {
LOGGER.log(Level.WARNING, "Can not parse given chat port, use the default one instead", ex);
}
}
this.serverIpAddress.setValue(ip);
this.serverPort.setText(port + "");
SlideshowFXServer.create(ip, port, this.twitterHashtag.getText()).start(
AttendeeChatService.class,
PresenterChatService.class,
QuizService.class,
TwitterService.class
);
icon = new FontAwesomeIconView(FontAwesomeIcon.POWER_OFF);
icon.setGlyphSize(20);
icon.setGlyphStyle("-fx-fill: app-color-orange");
tooltip.setText("Stop the server");
}
this.startServerButton.setGraphic(icon);
this.startServerButton.setTooltip(tooltip);
this.serverIpAddress.setDisable(!this.serverIpAddress.isDisable());
this.serverPort.setDisable(!this.serverPort.isDisable());
this.twitterHashtag.setDisable(!this.twitterHashtag.isDisable());
this.openWebApplicationMenuItem.setDisable(SlideshowFXServer.getSingleton() == null);
}
/**
* Refresh the {@link IHostingConnector hosting connectors} items in the UI. The items for downloading and
* uploading presentations will be updated.
*/
private void refreshHostingConnectors() {
this.downloadersMenu.getItems().clear();
this.uploadersMenu.getItems().clear();
OSGiManager.getInstance().getInstalledServices(IHostingConnector.class)
.stream()
.sorted((hostingConnector1, hostingConnector2) -> hostingConnector1.getName().compareTo(hostingConnector2.getName()))
.forEach(hostingConnector -> {
createUploaderMenuItem(hostingConnector);
createDownloaderMenuItem(hostingConnector);
});
}
/**
* Create the MenuItem that will be placed in the menu for uploaders, for the given {@code hostingConnector}. The {@code hostingConnector}
* is set as user data of the created MenuItem. The created MenuItem is added to the Upload menu.
* @param hostingConnector The hostingConnector attached to the MenuItem that will be created.
* @return A MenuItem for the {@code hostingConnector}
*/
private MenuItem createUploaderMenuItem(final IHostingConnector hostingConnector) {
final MenuItem uploaderMenuItem = new MenuItem(hostingConnector.getName());
uploaderMenuItem.setUserData(hostingConnector);
uploaderMenuItem.setOnAction(event -> {
PlatformHelper.run(() -> {
if (!hostingConnector.isAuthenticated()) {
try {
hostingConnector.authenticate();
} catch (HostingConnectorException e) {
final Pair<String, String> pair = e.getTitleAndMessage();
DialogHelper.showError(pair.getKey(), pair.getValue());
}
}
if (hostingConnector.isAuthenticated()) {
// Prompts the user where to upload the presentation
final RemoteFile destination;
try {
destination = hostingConnector.chooseFile(true, false);
if (destination != null) {
final UploadPresentationTask task = new UploadPresentationTask(Presentations.getCurrentDisplayedPresentation(),
hostingConnector, destination);
SlideshowFXController.this.taskInProgress.setCurrentTask(task);
TaskDAO.getInstance().startTask(task);
}
} catch (HostingConnectorException e) {
final Pair<String, String> pair = e.getTitleAndMessage();
DialogHelper.showError(pair.getKey(), pair.getValue());
}
}
});
});
this.uploadersMenu.getItems().add(uploaderMenuItem);
return uploaderMenuItem;
}
/**
* Create the MenuItem that will be placed in the menu for downloaders, for the given {@code hostingConnector}. The {@code hostingConnector}
* is set as user data of the created MenuItem. The created MenuItem is added to the Download menu.
* @param hostingConnector The hostingConnector attached to the MenuItem that will be created.
* @return A MenuItem for the {@code hostingConnector}
*/
private MenuItem createDownloaderMenuItem(final IHostingConnector hostingConnector) {
final MenuItem downloaderMenuItem = new MenuItem(hostingConnector.getName());
downloaderMenuItem.setUserData(hostingConnector);
downloaderMenuItem.setOnAction(event -> {
PlatformHelper.run(() -> {
if (!hostingConnector.isAuthenticated()) {
try {
hostingConnector.authenticate();
} catch (HostingConnectorException e) {
final Pair<String, String> pair = e.getTitleAndMessage();
DialogHelper.showError(pair.getKey(), pair.getValue());
}
}
if (hostingConnector.isAuthenticated()) {
// Prompts the user which file to download
try {
final RemoteFile presentationFile = hostingConnector.chooseFile(true, true);
if (presentationFile != null) {
// Prompts the user where the file should be downloaded
final DirectoryChooser chooser = new DirectoryChooser();
chooser.setTitle("Choose directory");
final File directory = chooser.showDialog(null);
if (directory != null) {
final DownloadPresentationTask task = new DownloadPresentationTask(
hostingConnector, directory, presentationFile);
task.stateProperty().addListener((value, oldState, newState) -> {
if (newState == Worker.State.SUCCEEDED && task.getValue() != null) {
ButtonType response = DialogHelper.showConfirmationAlert("Open file?", String.format("Do you want to open '%1$s' ?", task.getValue()));
if (response != null && response == ButtonType.YES) {
try {
this.openTemplateOrPresentation(task.getValue());
} catch (IOException | IllegalAccessException e) {
LOGGER.log(Level.SEVERE, "Error when opening file", e);
}
}
}
});
SlideshowFXController.this.taskInProgress.setCurrentTask(task);
TaskDAO.getInstance().startTask(task);
}
}
} catch (HostingConnectorException e) {
final Pair<String, String> pair = e.getTitleAndMessage();
DialogHelper.showError(pair.getKey(), pair.getValue());
}
}
});
});
this.downloadersMenu.getItems().add(downloaderMenuItem);
return downloaderMenuItem;
}
/**
* This method looks for the current focused view containing the presentation.
* In the current implementation the view is identified by the selected tab in the UI.
* @return The controller associated to the displayed view or {@code null} if no presentation is opened or focused.
*/
private PresentationViewController getCurrentPresentationView() {
PresentationViewController view = null;
final Tab selectedTab = this.openedPresentationsTabPane.getSelectionModel().getSelectedItem();
if(selectedTab != null) {
final Object userData = selectedTab.getUserData();
if(userData != null && userData instanceof PresentationViewController) {
view = (PresentationViewController) userData;
}
}
return view;
}
/**
* Start the slideshow.
* If the {@code fromCurrentSlide} parameter is set to {@code true}, the current slide is also determined.
*
* @param fromCurrentSlide Indicates if the slideshow must be started from the current slide or not.
*/
public void startSlideshow(boolean fromCurrentSlide) {
final PresentationViewController view = this.getCurrentPresentationView();
if(view != null) {
final PresentationEngine presentation = Presentations.getCurrentDisplayedPresentation();
final String currentSlideId = fromCurrentSlide ? view.getCurrentSlideId() : null;
if (presentation.getConfiguration() != null
&& presentation.getConfiguration().getPresentationFile() != null
&& presentation.getConfiguration().getPresentationFile().exists()) {
final Context context = new Context();
context.setStartAtSlideId(currentSlideId);
context.setPresentation(presentation);
final SlideshowStage stage = new SlideshowStage(context);
stage.onClose(() -> {
final String slideId = stage.getDisplayedSlideId();
view.goToSlide(slideId);
});
stage.show();
}
}
}
@Override
public void initialize(URL url, ResourceBundle resourceBundle) {
// We use reflection to disable all elements present in the list
final Consumer<Object> disableElementLambda = element -> {
try {
final Method setDisable = element.getClass().getMethod("setDisable", boolean.class);
setDisable.invoke(element, true);
} catch (NoSuchMethodException e) {
LOGGER.log(Level.FINE, "No setDisableMethod found", e);
} catch (InvocationTargetException e) {
LOGGER.log(Level.WARNING, "Can not disable element", e);
} catch (IllegalAccessException e) {
LOGGER.log(Level.WARNING, "Can not disable element", e);
}
};
this.whenNoDocumentOpened.forEach(disableElementLambda);
refreshHostingConnectors();
this.openedPresentationsTabPane.getSelectionModel().selectedItemProperty().addListener((value, oldSelection, newSelection) -> {
if(newSelection != null) {
final Object userData = newSelection.getUserData();
if(userData != null && userData instanceof PresentationViewController) {
final PresentationViewController view = (PresentationViewController) userData;
view.setAsCurrentPresentation();
this.updateSlideSplitMenu();
this.updateSlideTemplatesSplitMenu();
// Bind the title of the presentation with the application bar title
if(SlideshowFX.getStage().titleProperty().isBound()) SlideshowFX.getStage().titleProperty().unbind();
final WildcardBinding presentationModifiedBinding = new WildcardBinding(view.presentationModifiedProperty());
final StringExpression title = new SimpleStringProperty("SlideshowFX - ")
.concat(view.getPresentationName())
.concat(presentationModifiedBinding);
SlideshowFX.getStage().titleProperty().bind(title);
} else {
Presentations.setCurrentDisplayedPresentation(null);
}
} else {
this.updateSlideSplitMenu();
this.updateSlideTemplatesSplitMenu();
this.whenNoDocumentOpened.forEach(disableElementLambda);
if(SlideshowFX.getStage().titleProperty().isBound()) SlideshowFX.getStage().titleProperty().unbind();
SlideshowFX.getStage().setTitle("SlideshowFX");
}
});
this.openedPresentationsTabPane.getTabs().addListener((ListChangeListener) change -> {
final StackPane header = (StackPane) this.openedPresentationsTabPane.lookup(".tab-header-area");
if(header != null) {
if(this.openedPresentationsTabPane.getTabs().size() == 1) header.setPrefHeight(0);
else header.setPrefHeight(-1);
}
});
// Define global shortcuts of the application
this.root.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
boolean consumed = false;
if(event.isShortcutDown()) {
if(KeyEventUtils.isShortcutSequence("R", event)) {
consumed = true;
this.reload(null);
}
} else if(KeyCode.DELETE.equals(event.getCode())) {
consumed = true;
this.deleteSlide(null);
}
if(consumed) event.consume();
});
}
}