package ch.retorte.intervalmusiccompositor.ui.mainscreen; import ch.retorte.intervalmusiccompositor.audiofile.IAudioFile; import ch.retorte.intervalmusiccompositor.commons.FormatTime; import ch.retorte.intervalmusiccompositor.commons.MessageFormatBundle; import ch.retorte.intervalmusiccompositor.compilation.CompilationParameters; import ch.retorte.intervalmusiccompositor.list.ListSortMode; import ch.retorte.intervalmusiccompositor.messagebus.DebugMessage; import ch.retorte.intervalmusiccompositor.messagebus.ErrorMessage; import ch.retorte.intervalmusiccompositor.messagebus.ProgressMessage; import ch.retorte.intervalmusiccompositor.messagebus.SubProcessProgressMessage; import ch.retorte.intervalmusiccompositor.spi.*; import ch.retorte.intervalmusiccompositor.spi.decoder.AudioFileDecoder; import ch.retorte.intervalmusiccompositor.spi.encoder.AudioFileEncoder; import ch.retorte.intervalmusiccompositor.spi.messagebus.MessageHandler; import ch.retorte.intervalmusiccompositor.spi.messagebus.MessageProducer; import ch.retorte.intervalmusiccompositor.spi.messagebus.MessageSubscriber; import ch.retorte.intervalmusiccompositor.spi.update.UpdateAvailabilityChecker; import ch.retorte.intervalmusiccompositor.ui.audiofilelist.DraggableAudioFileBreakListView; import ch.retorte.intervalmusiccompositor.ui.audiofilelist.DraggableAudioFileListView; import ch.retorte.intervalmusiccompositor.ui.audiofilelist.MusicFileChooser; import ch.retorte.intervalmusiccompositor.ui.debuglog.DebugLogWindow; import ch.retorte.intervalmusiccompositor.ui.graphics.BarChart; import ch.retorte.intervalmusiccompositor.ui.updatecheck.UpdateCheckDialog; import ch.retorte.intervalmusiccompositor.ui.utils.AudioFileEncoderConverter; import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.control.*; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.image.WritableImage; import javafx.scene.layout.Region; import javafx.stage.DirectoryChooser; import javafx.stage.Window; import java.io.File; import java.net.URI; import java.net.URL; import java.util.List; import java.util.ResourceBundle; import java.util.concurrent.ScheduledExecutorService; import static ch.retorte.intervalmusiccompositor.commons.Utf8Bundle.getBundle; import static ch.retorte.intervalmusiccompositor.ui.IntervalMusicCompositorUI.CORE_RESOURCE_BUNDLE_NAME; import static ch.retorte.intervalmusiccompositor.ui.IntervalMusicCompositorUI.UI_RESOURCE_BUNDLE_NAME; /** * Controller for the main screen of the JavaFX ui. */ public class MainScreenController implements Initializable { //---- FX Widgets // Menu @FXML private MenuItem menuLoadMusicFile; @FXML private MenuItem menuLoadBreakFile; @FXML private MenuItem menuQuit; @FXML private MenuItem menuVersion; @FXML private MenuItem menuHelp; @FXML private MenuItem menuAbout; @FXML private MenuItem menuCheckForUpdates; @FXML private MenuItem menuShowDebugLog; // Content @FXML private SplitPane container; // Music track @FXML private Label trackCount; @FXML private Button addMusicTrackButton; @FXML private DraggableAudioFileListView musicTrackListView; @FXML private Button sortTrackList; @FXML private Button shuffleTrackList; @FXML private Label trackListSortOrderIndicator; @FXML private ToggleGroup enumerationToggleGroup; // Break track @FXML private Button addBreakTrackButton; @FXML private DraggableAudioFileBreakListView breakTrackListView; // Visual @FXML private ImageView imageView; @FXML private Image starterBackgroundImage; @FXML private ProgressBar progressBar; @FXML private ProgressBar secondaryProgressBar; // Track controls @FXML private TabPane periodTabPane; @FXML private Tab simpleTab; @FXML private Spinner<Integer> soundPeriod; @FXML private Spinner<Integer> breakPeriod; @FXML private TextField soundPattern; @FXML private TextField breakPattern; @FXML private Spinner<Integer> iterations; @FXML private Label duration; @FXML private ToggleGroup blendModeToggleGroup; @FXML private Slider blendDuration; // File format @FXML private ChoiceBox<AudioFileEncoder> outputFileFormat; // Output directory @FXML private Button chooseOutputDirectory; @FXML private Label outputDirectory; // Actions @FXML private Button process; // Fields private MessageFormatBundle coreBundle = getBundle(CORE_RESOURCE_BUNDLE_NAME); private MessageFormatBundle bundle = getBundle(UI_RESOURCE_BUNDLE_NAME); private Ui ui; private ProgramControl programControl; private ApplicationData applicationData; private MusicListControl musicListControl; private MusicCompilationControl musicCompilationControl; private CompilationParameters compilationParameters; private MessageProducer messageProducer; private UpdateAvailabilityChecker updateAvailabilityChecker; private ScheduledExecutorService executorService; private List<AudioFileDecoder> audioFileDecoders; //---- Methods @Override public void initialize(URL location, ResourceBundle resourceBundle) { } public void initializeFieldsWith(Ui ui, ProgramControl programControl, ApplicationData applicationData, MusicListControl musicListControl, MusicCompilationControl musicCompilationControl, CompilationParameters compilationParameters, MessageSubscriber messageSubscriber, MessageProducer messageProducer, UpdateAvailabilityChecker updateAvailabilityChecker, ScheduledExecutorService executorService) { this.ui = ui; this.programControl = programControl; this.applicationData = applicationData; this.musicListControl = musicListControl; this.musicCompilationControl = musicCompilationControl; this.compilationParameters = compilationParameters; this.messageProducer = messageProducer; this.updateAvailabilityChecker = updateAvailabilityChecker; this.executorService = executorService; initializeMenu(); initializeMusicTrackList(); initializeTrackEnumeration(); initializeBreakTrackList(); initializeDurationControls(); initializeBlending(); initializeOutputFileFormat(); initializeOutputDirectory(); initializeControlButtons(); addMessageSubscribers(messageSubscriber); } private void initializeMenu() { menuLoadMusicFile.setOnAction(event -> openFileChooserFor(musicTrackListView)); menuLoadBreakFile.setOnAction(event -> openFileChooserFor(breakTrackListView)); menuQuit.setOnAction(event -> ui.quit()); menuVersion.setText(getVersionText()); menuHelp.setOnAction(event -> openHelpWebsite()); menuAbout.setOnAction(event -> openAboutWebsite()); menuCheckForUpdates.setOnAction(event -> openUpdateCheckDialog()); menuShowDebugLog.setOnAction(event -> openDebugLog()); } private String getVersionText() { return "Version " + applicationData.getProgramVersion(); } private void openHelpWebsite() { ui.openInDesktopBrowser(getHelpWebsiteUrl()); } private String getHelpWebsiteUrl() { return coreBundle.getString("web.website.help.url"); } private void openAboutWebsite() { ui.openInDesktopBrowser(getAboutWebsiteUrl()); } private void openUpdateCheckDialog() { UpdateCheckDialog updateCheckDialog = new UpdateCheckDialog(updateAvailabilityChecker, ui, bundle, coreBundle); updateCheckDialog.open(); } private String getAboutWebsiteUrl() { return coreBundle.getString("web.website.about.url"); } private void openDebugLog() { DebugLogWindow debugLogWindow = new DebugLogWindow(bundle, programControl, executorService); debugLogWindow.show(); } private void initializeMusicTrackList() { musicTrackListView.initializeWith(musicListControl.getMusicList(), bundle, messageProducer, musicListControl); musicTrackListView.addListChangeListener(newValue -> updateUiDataWidgets()); addMusicTrackButton.setOnAction(event -> openFileChooserFor(musicTrackListView)); } private void initializeTrackEnumeration() { sortTrackList.setOnAction(event -> { messageProducer.send(new DebugMessage(MainScreenController.this, "Pressed 'sort' button.")); musicListControl.sortMusicList(); updateUiDataWidgets(); }); shuffleTrackList.setOnAction(event -> { messageProducer.send(new DebugMessage(MainScreenController.this, "Pressed 'shuffle' button.")); musicListControl.shuffleMusicList(); updateUiDataWidgets(); }); trackListSortOrderIndicator.textProperty().addListener(debugHandlerWith(trackListSortOrderIndicator.getId())); enumerationToggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> compilationParameters.setEnumerationMode((String) newValue.getUserData())); enumerationToggleGroup.selectedToggleProperty().addListener(debugHandlerWith("enumerationToggleGroup")); enumerationToggleGroup.selectedToggleProperty().addListener(updateUiHandler()); } private void initializeBreakTrackList() { breakTrackListView.initializeWith(musicListControl.getBreakList(), bundle, messageProducer, musicListControl); addBreakTrackButton.setOnAction(event -> openFileChooserFor(breakTrackListView)); } private void initializeDurationControls() { periodTabPane.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { if (isSimpleTab(newValue)) { compilationParameters.setMusicPattern(soundPeriod.getValue()); compilationParameters.setBreakPattern(breakPeriod.getValue()); } else { compilationParameters.setMusicPattern(soundPattern.getText()); compilationParameters.setBreakPattern(breakPattern.getText()); } }); soundPeriod.valueProperty().addListener((observable, oldValue, newValue) -> compilationParameters.setMusicPattern(newValue)); breakPeriod.valueProperty().addListener((observable, oldValue, newValue) -> compilationParameters.setBreakPattern(newValue)); soundPattern.textProperty().addListener((observable, oldValue, newValue) -> compilationParameters.setMusicPattern(newValue)); breakPattern.textProperty().addListener((observable, oldValue, newValue) -> compilationParameters.setBreakPattern(newValue)); iterations.valueProperty().addListener((observable, oldValue, newValue) -> compilationParameters.setIterations(newValue)); prepareSpinnerForListening(soundPeriod); prepareSpinnerForListening(breakPeriod); prepareSpinnerForListening(iterations); periodTabPane.getSelectionModel().selectedItemProperty().addListener(debugHandlerWith(periodTabPane.getId())); periodTabPane.getSelectionModel().selectedItemProperty().addListener(updateUiHandler()); soundPeriod.valueProperty().addListener(debugHandlerWith(soundPeriod.getId())); soundPeriod.valueProperty().addListener(updateUiHandler()); breakPeriod.valueProperty().addListener(debugHandlerWith(breakPeriod.getId())); breakPeriod.valueProperty().addListener(updateUiHandler()); soundPattern.textProperty().addListener(debugHandlerWith(soundPattern.getId())); soundPattern.textProperty().addListener(updateUiHandler()); breakPattern.textProperty().addListener(debugHandlerWith(breakPattern.getId())); breakPattern.textProperty().addListener(updateUiHandler()); iterations.valueProperty().addListener(debugHandlerWith(iterations.getId())); iterations.valueProperty().addListener(updateUiHandler()); } private void initializeBlending() { blendModeToggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> compilationParameters.setBlendMode((String) newValue.getUserData())); blendModeToggleGroup.selectedToggleProperty().addListener(debugHandlerWith("blendModeToggleGroup")); blendModeToggleGroup.selectedToggleProperty().addListener(updateUiHandler()); blendDuration.valueProperty().addListener((observable, oldValue, newValue) -> compilationParameters.setBlendDuration(newValue.doubleValue())); blendDuration.valueProperty().addListener(debugHandlerWith(blendDuration.getId())); blendDuration.valueProperty().addListener(updateUiHandler()); } private void initializeOutputFileFormat() { outputFileFormat.valueProperty().addListener((observable, oldValue, newValue) -> compilationParameters.setEncoderIdentifier(newValue.getIdentificator())); outputFileFormat.valueProperty().addListener(debugHandlerWith(outputFileFormat.getId())); } private void initializeOutputDirectory() { outputDirectory.textProperty().addListener((observable, oldValue, newValue) -> compilationParameters.setOutputPath(newValue)); outputDirectory.textProperty().addListener(debugHandlerWith(outputDirectory.getId())); chooseOutputDirectory.setOnAction(event -> openDirectoryChooser()); } private void initializeControlButtons() { process.setOnAction(event -> musicCompilationControl.startCompilation(compilationParameters)); } private void openDirectoryChooser() { DirectoryChooser directoryChooser = new DirectoryChooser(); directoryChooser.setInitialDirectory(new File(compilationParameters.getOutputPath())); directoryChooser.setTitle(bundle.getString("ui.form.outfile_dialog_title")); File file = directoryChooser.showDialog(getWindow()); if (file != null && file.exists()) { outputDirectory.setText(file.getAbsolutePath()); } } private boolean isSimpleTab(Tab tab) { return tab.equals(simpleTab); } public void updateOutputFileFormatWith(ObservableList<AudioFileEncoder> audioFileEncoders) { outputFileFormat.setConverter(new AudioFileEncoderConverter(audioFileEncoders)); outputFileFormat.setItems(audioFileEncoders); outputFileFormat.getSelectionModel().select(0); } private void prepareSpinnerForListening(Spinner<Integer> spinner) { SpinnerValueFactory<Integer> valueFactory = spinner.getValueFactory(); TextFormatter<Integer> formatter = new TextFormatter<>(valueFactory.getConverter(), valueFactory.getValue()); spinner.getEditor().setTextFormatter(formatter); valueFactory.valueProperty().bindBidirectional(formatter.valueProperty()); } private void updateDurationEstimation() { setDurationTo(getDurationEstimation()); } private int getDurationEstimation() { return compilationParameters.getDurationEstimationSeconds(); } private void setDurationTo(int durationSeconds) { duration.setText(new FormatTime().getStrictFormattedTime(durationSeconds)); } public void setActive() { container.setDisable(false); setMenusDisabled(false); } public void setInactive() { container.setDisable(true); setMenusDisabled(true); } private void setMenusDisabled(boolean disabled) { menuLoadMusicFile.setDisable(disabled); menuLoadBreakFile.setDisable(disabled); } private void updateEnvelopeImage() { if (compilationParameters.hasUsableData()) { BarChart bar = new BarChart(720, 162); bar.generate(compilationParameters.getMusicPattern(), compilationParameters.getBreakPattern(), compilationParameters.getIterations(), false); imageView.setImage(bar.getWritableImage()); } else { imageView.setImage(starterBackgroundImage); } } private void updateSortModeLabel() { trackListSortOrderIndicator.setText(getLabelFor(compilationParameters.getListSortMode())); } private String getLabelFor(ListSortMode listSortMode) { switch (listSortMode) { case MANUAL: return bundle.getString("ui.form.music_list.list_mode.manual"); case SHUFFLE: return bundle.getString("ui.form.music_list.list_mode.shuffle"); case SORT: return bundle.getString("ui.form.music_list.list_mode.sort"); case SORT_REV: return bundle.getString("ui.form.music_list.list_mode.sort_rev"); default: return ""; } } public void setEnvelopeImage(WritableImage envelopeImage) { imageView.setImage(envelopeImage); } private void openFileChooserFor(DraggableAudioFileListView listView) { MusicFileChooser musicFileChooser = new MusicFileChooser(messageProducer, bundle, audioFileDecoders); List<File> chosenFile = musicFileChooser.chooseFileIn(getWindow()); chosenFile.forEach(file -> { IAudioFile newTrack = listView.addTrack(file); newTrack.addChangeListener(newValue -> Platform.runLater(() -> updateTrackCount())); }); } private Window getWindow() { return container.getScene().getWindow(); } public void updateAvailableDecodersWith(List<AudioFileDecoder> audioFileDecoders) { this.audioFileDecoders = audioFileDecoders; } private void addMessageSubscribers(MessageSubscriber messageSubscriber) { messageSubscriber.addHandler(new MessageHandler<ProgressMessage>() { @Override public void handle(ProgressMessage message) { updateProgressBar(message.getProgressInPercent(), message.getCurrentActivity()); } }); messageSubscriber.addHandler(new MessageHandler<ErrorMessage>() { @Override public void handle(ErrorMessage message) { showErrorMessage(message.getMessage()); setActive(); } }); messageSubscriber.addHandler(new MessageHandler<SubProcessProgressMessage>() { @Override public void handle(SubProcessProgressMessage message) { updateSubProgressBar(message.getPercentage()); } }); } private void showErrorMessage(String message) { Platform.runLater(() -> { Alert alert = new Alert(Alert.AlertType.ERROR); alert.setResizable(true); alert.setTitle(bundle.getString("ui.error.title")); alert.setContentText(message + "\n\n" + bundle.getString("ui.error.appendix")); alert.getDialogPane().getChildren().stream().filter(node -> node instanceof Label).forEach(node -> ((Label)node).setMinHeight(Region.USE_PREF_SIZE)); alert.show(); }); } private void updateProgressBar(Integer progressInPercent, String currentActivity) { progressBar.progressProperty().setValue((double) progressInPercent / 100); secondaryProgressBar.setVisible(false); } private void updateSubProgressBar(int percentage) { secondaryProgressBar.setVisible(true); secondaryProgressBar.progressProperty().setValue((double) percentage / 100); } private DebugMessageEventHandler debugHandlerWith(String id) { return new DebugMessageEventHandler(id, messageProducer); } private UpdateDependentUiWidgetsHandler updateUiHandler() { return new UpdateDependentUiWidgetsHandler(); } private void updateUiDataWidgets() { compilationParameters.setListSortMode(musicListControl.getSortMode()); updateDurationEstimation(); updateEnvelopeImage(); updateSortModeLabel(); updateTrackCount(); } private void updateTrackCount() { int tracks = musicListControl.getOkTracks(); String labelText = bundle.getString("ui.form.music_list.tracks_label_pl", tracks); if (tracks == 1) { labelText = bundle.getString("ui.form.music_list.tracks_label_sg", tracks); } trackCount.setText(labelText); } //---- Inner classes private class DebugMessageEventHandler implements ChangeListener<Object> { private String id; private MessageProducer messageProducer; DebugMessageEventHandler(String id, MessageProducer messageProducer) { this.id = id; this.messageProducer = messageProducer; } @Override public void changed(ObservableValue<?> observable, Object oldValue, Object newValue) { messageProducer.send(new DebugMessage(MainScreenController.this, "Changed contents of field '" + id + "' from: '" + oldValue + "' to '" + newValue + "'")); } } private class UpdateDependentUiWidgetsHandler implements ChangeListener<Object> { @Override public void changed(ObservableValue<?> observable, Object oldValue, Object newValue) { updateUiDataWidgets(); } } }