/* * Copyright 2013-2014 Odysseus Software GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.musicmount.fx; import java.io.File; import java.net.MalformedURLException; import java.util.logging.Level; import java.util.logging.Logger; import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.concurrent.Service; import javafx.concurrent.Task; import javafx.concurrent.WorkerStateEvent; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.CheckBox; import javafx.scene.control.Label; import javafx.scene.control.PasswordField; import javafx.scene.control.ProgressIndicator; import javafx.scene.control.TextField; import javafx.scene.control.Tooltip; import javafx.scene.layout.BorderPane; import javafx.scene.layout.ColumnConstraintsBuilder; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javafx.scene.text.Text; import javafx.stage.DirectoryChooser; import org.musicmount.live.MusicMountLive; import org.musicmount.util.BonjourService; public class FXLiveController { static final Logger LOGGER = Logger.getLogger(FXLiveController.class.getName()); private Pane pane; private TextField musicFolderTextField; private Button musicFolderChooseButton; private TextField portTextField; private TextField userTextField; private PasswordField passwordField; private CheckBox bonjourCheckBox; private CheckBox retinaCheckBox; private CheckBox groupingCheckBox; private CheckBox fullCheckBox; private CheckBox noTrackIndexCheckBox; private CheckBox noVariousArtistsCheckBox; private CheckBox unknownGenreCheckBox; private ProgressIndicator progressIndicator; private Button runButton; private Text statusText; private final FXCommandModel model; private final BonjourService bonjourService; private final MusicMountLive live; private final Service<Object> buildService = new Service<Object>() { @Override protected Task<Object> createTask() { return new Task<Object>() { @Override protected Object call() throws Exception { live.start(model.getMusicFolder(), model.getServerPort().intValue(), getUser(), getPassword()); return null; } }; } }; private final Service<Object> liveService = new Service<Object>() { @Override protected Task<Object> createTask() { return new Task<Object>() { @Override protected Object call() throws Exception { if (bonjourService != null && model.isBonjour()) { startBonjour(); } live.await(); return null; } @Override public boolean cancel(boolean mayInterruptIfRunning) { try { if (bonjourService != null && model.isBonjour()) { stopBonjour(); } live.stop(); } catch (Exception e) { return false; } return true; } }; } }; public FXLiveController(final FXCommandModel model) { this.model = model; this.bonjourService = createBonjour(); this.live = new MusicMountLive(model.getBuildConfig()); this.pane = createView(); live.setProgressHandler(new FXProgressHandler(statusText, progressIndicator, live.getProgressHandler())); musicFolderTextField.textProperty().addListener(new ChangeListener<String>() { @Override public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) { model.setMusicFolder(model.toFolder(newValue)); updateRunButton(); } }); musicFolderChooseButton.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { DirectoryChooser directoryChooser = new DirectoryChooser(); directoryChooser.setTitle("Music Folder"); File directory = directoryChooser.showDialog(musicFolderChooseButton.getScene().getWindow()); if (directory != null) { musicFolderTextField.setText(directory.getAbsolutePath()); } } }); portTextField.textProperty().addListener(new ChangeListener<String>() { @Override public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) { try { model.setServerPort(Integer.valueOf(portTextField.getText())); } catch (NumberFormatException e) { model.setServerPort(null); } updateRunButton(); } }); bonjourCheckBox.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { model.setBonjour(bonjourCheckBox.isSelected()); } }); userTextField.textProperty().addListener(new ChangeListener<String>() { @Override public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) { updateUserAndPassword(); updateRunButton(); } }); passwordField.textProperty().addListener(new ChangeListener<String>() { @Override public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) { updateUserAndPassword(); updateRunButton(); } }); retinaCheckBox.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { live.getConfig().setRetina(retinaCheckBox.isSelected()); } }); groupingCheckBox.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { live.getConfig().setGrouping(groupingCheckBox.isSelected()); } }); fullCheckBox.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { live.getConfig().setFull(fullCheckBox.isSelected()); } }); noTrackIndexCheckBox.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { live.getConfig().setNoTrackIndex(noTrackIndexCheckBox.isSelected()); } }); noVariousArtistsCheckBox.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { live.getConfig().setNoVariousArtists(noVariousArtistsCheckBox.isSelected()); } }); unknownGenreCheckBox.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { live.getConfig().setUnknownGenre(unknownGenreCheckBox.isSelected()); } }); buildService.setOnRunning(new EventHandler<WorkerStateEvent>() { public void handle(WorkerStateEvent event) { statusText.setText(null); runButton.setDisable(true); runButton.setText("Analyzing..."); disableControls(true); } }); buildService.setOnSucceeded(new EventHandler<WorkerStateEvent>() { public void handle(WorkerStateEvent event) { statusText.setText("Mount analysis done"); Platform.runLater(new Runnable() { @Override public void run() { liveService.reset(); liveService.start(); } }); } }); buildService.setOnFailed(new EventHandler<WorkerStateEvent>() { public void handle(WorkerStateEvent event) { live.getProgressHandler().endTask(); statusText.setText("Mount analysis failed"); if (buildService.getException() != null) { buildService.getException().printStackTrace(); } runButton.setText("Start Server"); runButton.setDisable(false); disableControls(false); } }); liveService.setOnRunning(new EventHandler<WorkerStateEvent>() { public void handle(WorkerStateEvent event) { try { statusText.setText("Server started - " + live.getSiteURL(live.getHostName("<hostname>"), model.getServerPort().intValue())); } catch (MalformedURLException e) { statusText.setText("Server started - Failed to determine site URL: " + e.getMessage()); } runButton.setDisable(false); runButton.setText("Stop Server"); disableControls(true); } }); liveService.setOnSucceeded(new EventHandler<WorkerStateEvent>() { public void handle(WorkerStateEvent event) { statusText.setText("Server stopped"); runButton.setDisable(false); runButton.setText("Start Server"); disableControls(false); } }); liveService.setOnFailed(new EventHandler<WorkerStateEvent>() { public void handle(WorkerStateEvent event) { if (liveService.getException() != null) { liveService.getException().printStackTrace(); statusText.setText("Server failed: " + liveService.getException().getMessage()); } else { statusText.setText("Server failed"); } runButton.setText("Start Server"); runButton.setDisable(false); disableControls(false); } }); runButton.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { if (liveService.isRunning()) { liveService.cancel(); } else if (!buildService.isRunning()) { buildService.reset(); buildService.start(); } } }); updateAll(); } private BonjourService createBonjour() { try { return new BonjourService(true); } catch (Exception e) { LOGGER.log(Level.WARNING, "Failed to create Bonjour service", e); return null; } } private void startBonjour() { String host = bonjourService.getHostName(); String name = String.format("Live @ %s", live.getHostName(host)); try { // bonjourService.start(name, model.getServerPort(), live.getSitePath(), getUser()); // bug in MM 1.5.2 bonjourService.start(name, live.getSiteURL(host, model.getServerPort().intValue()), getUser()); LOGGER.info("Bonjour service started."); } catch (Exception e) { LOGGER.log(Level.WARNING, "Failed to start Bonjour service", e); } } private void stopBonjour() { try { bonjourService.stop(); LOGGER.info("Bonjour service stopped."); } catch (Exception e) { LOGGER.log(Level.WARNING, "Failed to stop Bonjour service", e); } } void disableControls(boolean disable) { musicFolderTextField.setDisable(disable); musicFolderChooseButton.setDisable(disable); portTextField.setDisable(disable); userTextField.setDisable(disable); passwordField.setDisable(disable); bonjourCheckBox.setDisable(disable || bonjourService == null); retinaCheckBox.setDisable(disable); groupingCheckBox.setDisable(disable); fullCheckBox.setDisable(disable); noTrackIndexCheckBox.setDisable(disable); noVariousArtistsCheckBox.setDisable(disable); unknownGenreCheckBox.setDisable(disable); } Pane createView() { GridPane grid = new GridPane(); grid.setAlignment(Pos.CENTER); grid.setHgap(5); grid.setVgap(10); // grid.setGridLinesVisible(true); grid.getColumnConstraints().add(0, ColumnConstraintsBuilder.create().hgrow(Priority.NEVER).build()); grid.getColumnConstraints().add(1, ColumnConstraintsBuilder.create().hgrow(Priority.ALWAYS).percentWidth(33).build()); grid.getColumnConstraints().add(2, ColumnConstraintsBuilder.create().hgrow(Priority.ALWAYS).build()); grid.getColumnConstraints().add(3, ColumnConstraintsBuilder.create().hgrow(Priority.ALWAYS).percentWidth(33).build()); grid.getColumnConstraints().add(4, ColumnConstraintsBuilder.create().hgrow(Priority.NEVER).build()); /* * music folder */ Label musicFolderLabel = new Label("Music Folder"); musicFolderTextField = new TextField(); musicFolderTextField.setPromptText("Input directory, containing your music collection"); musicFolderChooseButton = new Button("..."); grid.add(musicFolderLabel, 0, 1); GridPane.setHalignment(musicFolderLabel, HPos.RIGHT); grid.add(musicFolderTextField, 1, 1, 3, 1); grid.add(musicFolderChooseButton, 4, 1); /* * server port */ Label portLabel = new Label("Server Port"); grid.add(portLabel, 0, 2); GridPane.setHalignment(portLabel, HPos.RIGHT); portTextField = new TextField(); portTextField.setPromptText("Number"); grid.add(portTextField, 1, 2); /* * bonjour */ Label bonjourLabel = new Label("Bonjour"); grid.add(bonjourLabel, 2, 2); GridPane.setHalignment(bonjourLabel, HPos.RIGHT); bonjourCheckBox = new CheckBox("Publish Site"); bonjourCheckBox.setDisable(bonjourService == null); bonjourCheckBox.setTooltip(new Tooltip("Register as local Bonjour service")); grid.add(bonjourCheckBox, 3, 2); /* * user */ Label userLabel = new Label("User"); grid.add(userLabel, 0, 3); GridPane.setHalignment(userLabel, HPos.RIGHT); userTextField = new TextField(); userTextField.setPromptText("Optional"); grid.add(userTextField, 1, 3); /* * password */ Label passwordLabel = new Label("Password"); grid.add(passwordLabel, 2, 3); GridPane.setHalignment(passwordLabel, HPos.RIGHT); passwordField = new PasswordField(); passwordField.setPromptText("Optional"); grid.add(passwordField, 3, 3); /* * options */ Label optionsLabel = new Label("Options"); grid.add(optionsLabel, 0, 4); GridPane.setHalignment(optionsLabel, HPos.RIGHT); retinaCheckBox = new CheckBox("Retina Images"); retinaCheckBox.setTooltip(new Tooltip("Double image resolution, better for tables")); grid.add(retinaCheckBox, 1, 4); groupingCheckBox = new CheckBox("Track Grouping"); groupingCheckBox.setTooltip(new Tooltip("Use grouping tag to group album tracks")); grid.add(groupingCheckBox, 1, 5); fullCheckBox = new CheckBox("Full Parse"); fullCheckBox.setTooltip(new Tooltip("Force full parse, don't use asset store")); grid.add(fullCheckBox, 1, 6); noTrackIndexCheckBox = new CheckBox("No Track Index"); noTrackIndexCheckBox.setTooltip(new Tooltip("Do not generate a track index")); grid.add(noTrackIndexCheckBox, 3, 4); noVariousArtistsCheckBox = new CheckBox("No 'Various Artists' Item"); noVariousArtistsCheckBox.setTooltip(new Tooltip("Exclude 'Various Artists' from album artist index")); grid.add(noVariousArtistsCheckBox, 3, 5); unknownGenreCheckBox = new CheckBox("Add 'Unknown' Genre"); unknownGenreCheckBox.setTooltip(new Tooltip("Report missing genre as 'Unknown'")); grid.add(unknownGenreCheckBox, 3, 6); /* * progress */ progressIndicator = new ProgressIndicator(); progressIndicator.setPrefWidth(30); progressIndicator.setVisible(false); VBox progressBox = new VBox(); progressBox.setFillWidth(false); progressBox.setAlignment(Pos.BOTTOM_CENTER); progressBox.getChildren().add(progressIndicator); grid.add(progressBox, 0, 5, 1, 3); /* * run button */ runButton = new Button("Start Server"); runButton.setId("live-button"); runButton.getStyleClass().add("run-button"); HBox runButtonHBox = new HBox(10); runButtonHBox.setAlignment(Pos.BOTTOM_RIGHT); runButtonHBox.getChildren().add(runButton); grid.add(runButtonHBox, 3, 7, 2, 1); GridPane.setVgrow(runButtonHBox, Priority.ALWAYS); BorderPane borderPane = new BorderPane(); borderPane.setCenter(grid); BorderPane.setMargin(grid, new Insets(10)); Text titleText = new Text("Run MusicMount Live Server"); titleText.setId("live-title"); titleText.getStyleClass().add("tool-title"); borderPane.setTop(titleText); BorderPane.setMargin(titleText, new Insets(15, 10, 0, 10)); statusText = new Text(); statusText.setId("build-status"); statusText.getStyleClass().add("status-text"); borderPane.setBottom(statusText); BorderPane.setMargin(statusText, new Insets(5, 10, 10, 10)); return borderPane; } void updateAll() { updateMusicFolder(); updatePort(); updateBonjour(); updateUserAndPassword(); updateOptions(); updateRunButton(); } void updateMusicFolder() { musicFolderTextField.setText(model.getMusicFolder() != null ? model.getMusicFolder().toString() : null); } void updatePort() { portTextField.setText(model.getServerPort() != null ? model.getServerPort().toString() : null); } void updateBonjour() { bonjourCheckBox.setSelected(model.isBonjour()); } void updateUserAndPassword() { userTextField.setPromptText(getPassword() == null ? "Optional" : "Required"); passwordField.setPromptText(getUser() == null ? "Optional" : "Required"); } void updateOptions() { fullCheckBox.setSelected(live.getConfig().isFull()); groupingCheckBox.setSelected(live.getConfig().isGrouping()); noTrackIndexCheckBox.setSelected(live.getConfig().isNoTrackIndex()); noVariousArtistsCheckBox.setSelected(live.getConfig().isNoVariousArtists()); retinaCheckBox.setSelected(live.getConfig().isRetina()); unknownGenreCheckBox.setSelected(live.getConfig().isUnknownGenre()); } void updateRunButton() { runButton.setDisable(buildService.isRunning() || liveService.isRunning() || !model.isValidLiveModel()); } String getUser() { return userTextField.getText() == null || userTextField.getText().trim().isEmpty() ? null : userTextField.getText().trim(); } String getPassword() { return passwordField.getText() == null || passwordField.getText().trim().isEmpty() ? null : passwordField.getText().trim(); } public Pane getPane() { return pane; } public Service<?> getLiveService() { return liveService; } public Service<?> getBuildService() { return buildService; } }