diff --git a/common/src/main/kotlin/org/wycliffeassociates/otter/common/domain/content/PluginActions.kt b/common/src/main/kotlin/org/wycliffeassociates/otter/common/domain/content/PluginActions.kt index a0e576c709..a1bf0d70c1 100644 --- a/common/src/main/kotlin/org/wycliffeassociates/otter/common/domain/content/PluginActions.kt +++ b/common/src/main/kotlin/org/wycliffeassociates/otter/common/domain/content/PluginActions.kt @@ -131,6 +131,11 @@ class PluginActions @Inject constructor( } } + fun record(take: Take, params: PluginParameters): Single { + return launchPlugin(PluginType.RECORDER, take, params) + .map { it.second } + } + fun import( audio: AssociatedAudio, projectAudioDir: File, diff --git a/jvm/controls/src/main/kotlin/org/wycliffeassociates/otter/jvm/controls/event/ChunkingPageEvents.kt b/jvm/controls/src/main/kotlin/org/wycliffeassociates/otter/jvm/controls/event/ChunkingPageEvents.kt index 0f07b677aa..0fbf82ff71 100644 --- a/jvm/controls/src/main/kotlin/org/wycliffeassociates/otter/jvm/controls/event/ChunkingPageEvents.kt +++ b/jvm/controls/src/main/kotlin/org/wycliffeassociates/otter/jvm/controls/event/ChunkingPageEvents.kt @@ -42,4 +42,10 @@ class UndoChunkingPageEvent: FXEvent() class RedoChunkingPageEvent: FXEvent() class GoToNextChapterEvent: FXEvent() class GoToPreviousChapterEvent: FXEvent() -class OpenInPluginEvent: FXEvent() \ No newline at end of file +class OpenInPluginEvent: FXEvent() + +/** + * Use this event to avoid unwanted refresh of steps or chunk list + * when returning from an external plugin. + */ +class ReturnFromPluginEvent: FXEvent() \ No newline at end of file diff --git a/jvm/workbookapp/src/main/kotlin/org/wycliffeassociates/otter/jvm/workbookapp/ui/narration/NarrationViewModel.kt b/jvm/workbookapp/src/main/kotlin/org/wycliffeassociates/otter/jvm/workbookapp/ui/narration/NarrationViewModel.kt index 43b9c5b962..4a88ee36ce 100644 --- a/jvm/workbookapp/src/main/kotlin/org/wycliffeassociates/otter/jvm/workbookapp/ui/narration/NarrationViewModel.kt +++ b/jvm/workbookapp/src/main/kotlin/org/wycliffeassociates/otter/jvm/workbookapp/ui/narration/NarrationViewModel.kt @@ -37,7 +37,6 @@ import javafx.collections.ObservableList import javafx.scene.canvas.Canvas import javafx.scene.canvas.GraphicsContext import org.slf4j.LoggerFactory -import org.wycliffeassociates.otter.assets.initialization.DEFAULT_RECORDER_NAME import org.wycliffeassociates.otter.common.audio.AudioFileReader import org.wycliffeassociates.otter.common.audio.DEFAULT_SAMPLE_RATE import org.wycliffeassociates.otter.common.data.ColorTheme @@ -666,8 +665,9 @@ class NarrationViewModel : ViewModel() { } fun recordAgain(verseIndex: Int): NarrationStateTransition? { - val selectedPluginName = audioPluginViewModel.selectedRecorderProperty.value.name - return if (selectedPluginName != DEFAULT_RECORDER_NAME) { + val selectedPlugin = audioPluginViewModel.getPlugin(PluginType.RECORDER) + .blockingGet() + return if (!selectedPlugin.isNativePlugin()) { openInAudioPlugin(verseIndex) null } else { @@ -809,8 +809,9 @@ class NarrationViewModel : ViewModel() { } fun record(index: Int): NarrationStateTransition? { - val selectedPluginName = audioPluginViewModel.selectedRecorderProperty.value.name - return if (selectedPluginName != DEFAULT_RECORDER_NAME) { + val selectedPlugin = audioPluginViewModel.getPlugin(PluginType.RECORDER) + .blockingGet() + return if (!selectedPlugin.isNativePlugin()) { openInAudioPlugin(index) null } else { @@ -902,6 +903,7 @@ class NarrationViewModel : ViewModel() { .flatMapSingle { plugin -> pluginOpenedProperty.set(true) workbookDataStore.activeTakeNumberProperty.set(1) + workbookDataStore.activeChunkProperty.set(chunksList[verseIndex]) FX.eventbus.fire(PluginOpenedEvent(pluginType, plugin.isNativePlugin())) audioPluginViewModel.edit(file) } diff --git a/jvm/workbookapp/src/main/kotlin/org/wycliffeassociates/otter/jvm/workbookapp/ui/screens/ChunkingTranslationPage.kt b/jvm/workbookapp/src/main/kotlin/org/wycliffeassociates/otter/jvm/workbookapp/ui/screens/ChunkingTranslationPage.kt index 6172dd6891..b80adbe7e7 100644 --- a/jvm/workbookapp/src/main/kotlin/org/wycliffeassociates/otter/jvm/workbookapp/ui/screens/ChunkingTranslationPage.kt +++ b/jvm/workbookapp/src/main/kotlin/org/wycliffeassociates/otter/jvm/workbookapp/ui/screens/ChunkingTranslationPage.kt @@ -27,6 +27,7 @@ import org.wycliffeassociates.otter.jvm.controls.event.ChunkingStepSelectedEvent import org.wycliffeassociates.otter.jvm.controls.event.GoToNextChapterEvent import org.wycliffeassociates.otter.jvm.controls.event.GoToPreviousChapterEvent import org.wycliffeassociates.otter.jvm.controls.event.NavigateChapterEvent +import org.wycliffeassociates.otter.jvm.controls.event.ReturnFromPluginEvent import org.wycliffeassociates.otter.jvm.controls.model.ChunkingStep import org.wycliffeassociates.otter.jvm.workbookapp.ui.screens.translation.BlindDraft import org.wycliffeassociates.otter.jvm.workbookapp.ui.screens.translation.Chunking @@ -150,11 +151,26 @@ class ChunkingTranslationPage : View() { override fun onDock() { super.onDock() - viewModel.dockPage() + when (viewModel.pluginOpenedProperty.value) { + true -> { + // returning from plugin + FX.eventbus.fire(ReturnFromPluginEvent()) + } + false -> { + viewModel.dockPage() + } + } } override fun onUndock() { super.onUndock() - viewModel.undockPage() + when (viewModel.pluginOpenedProperty.value) { + true -> { + // no-op, opening plugin + } + false -> { + viewModel.undockPage() + } + } } } \ No newline at end of file diff --git a/jvm/workbookapp/src/main/kotlin/org/wycliffeassociates/otter/jvm/workbookapp/ui/screens/translation/BlindDraft.kt b/jvm/workbookapp/src/main/kotlin/org/wycliffeassociates/otter/jvm/workbookapp/ui/screens/translation/BlindDraft.kt index 33af73f162..d9e1e7502a 100644 --- a/jvm/workbookapp/src/main/kotlin/org/wycliffeassociates/otter/jvm/workbookapp/ui/screens/translation/BlindDraft.kt +++ b/jvm/workbookapp/src/main/kotlin/org/wycliffeassociates/otter/jvm/workbookapp/ui/screens/translation/BlindDraft.kt @@ -18,6 +18,7 @@ */ package org.wycliffeassociates.otter.jvm.workbookapp.ui.screens.translation +import com.github.thomasnield.rxkotlinfx.toLazyBinding import javafx.beans.property.SimpleObjectProperty import javafx.geometry.Side import javafx.scene.Node @@ -30,6 +31,7 @@ import org.slf4j.LoggerFactory import org.wycliffeassociates.otter.jvm.controls.Shortcut import org.wycliffeassociates.otter.jvm.controls.TakeSelectionAnimationMediator import org.wycliffeassociates.otter.jvm.controls.customizeScrollbarSkin +import org.wycliffeassociates.otter.jvm.controls.dialog.PluginOpenedPage import org.wycliffeassociates.otter.jvm.controls.media.simpleaudioplayer import org.wycliffeassociates.otter.jvm.controls.styles.tryImportStylesheet import org.wycliffeassociates.otter.jvm.workbookapp.ui.components.ChunkTakeCard @@ -39,8 +41,11 @@ import org.wycliffeassociates.otter.jvm.controls.event.TakeAction import org.wycliffeassociates.otter.jvm.controls.event.UndoChunkingPageEvent import org.wycliffeassociates.otter.jvm.utils.ListenerDisposer import org.wycliffeassociates.otter.jvm.utils.onChangeWithDisposer +import org.wycliffeassociates.otter.jvm.workbookapp.plugin.PluginOpenedEvent import org.wycliffeassociates.otter.jvm.workbookapp.ui.viewmodel.BlindDraftViewModel import org.wycliffeassociates.otter.jvm.workbookapp.ui.viewmodel.RecorderViewModel +import org.wycliffeassociates.otter.jvm.workbookapp.ui.viewmodel.SettingsViewModel +import org.wycliffeassociates.otter.jvm.workbookapp.ui.viewmodel.TranslationViewModel2 import org.wycliffeassociates.otter.jvm.workbookapp.ui.viewmodel.WorkbookDataStore import tornadofx.* @@ -50,12 +55,16 @@ class BlindDraft : View() { val viewModel: BlindDraftViewModel by inject() val recorderViewModel: RecorderViewModel by inject() val workbookDataStore: WorkbookDataStore by inject() + val settingsViewModel: SettingsViewModel by inject() + val translationViewModel: TranslationViewModel2 by inject() + private val mainSectionProperty = SimpleObjectProperty(null) private val takesView = buildTakesArea() private val recordingView = buildRecordingArea() private val hideSourceAudio = mainSectionProperty.booleanBinding { it == recordingView } private val eventSubscriptions = mutableListOf() private val listenerDisposers = mutableListOf() + private val pluginOpenedPage = createPluginOpenedPage() override val root = borderpane { addClass("blind-draft") @@ -131,10 +140,11 @@ class BlindDraft : View() { graphic = FontIcon(MaterialDesign.MDI_MICROPHONE) action { - viewModel.onRecordNew() - mainSectionProperty.set(recordingView) - recorderViewModel.onViewReady(takesView.width.toInt()) // use the width of the existing component - recorderViewModel.toggle() + viewModel.onRecordNew { + mainSectionProperty.set(recordingView) + recorderViewModel.onViewReady(takesView.width.toInt()) // use the width of the existing component + recorderViewModel.toggle() + } } } } @@ -167,24 +177,64 @@ class BlindDraft : View() { override fun onDock() { super.onDock() - logger.info("Blind Draft docked.") recorderViewModel.waveformCanvas = recordingView.waveformCanvas recorderViewModel.volumeCanvas = recordingView.volumeCanvas mainSectionProperty.set(takesView) - viewModel.dockBlindDraft() + when (viewModel.pluginOpenedProperty.value) { + true -> { + // navigate back from plugin + viewModel.pluginOpenedProperty.set(false) + translationViewModel.loadingStepProperty.set(false) + } + false -> { + logger.info("Blind Draft docked.") + viewModel.dockBlindDraft() + } + } subscribeEvents() } override fun onUndock() { super.onUndock() - logger.info("Blind Draft undocked.") + when (viewModel.pluginOpenedProperty.value) { + true -> { + /* no-op, opening plugin */ + } + false -> { + logger.info("Blind Draft undocked.") + viewModel.undockBlindDraft() + } + } unsubscribeEvents() - viewModel.undockBlindDraft() if (mainSectionProperty.value == recordingView) { recorderViewModel.cancel() } } + private fun createPluginOpenedPage(): PluginOpenedPage { + return find().apply { + licenseProperty.bind(workbookDataStore.sourceLicenseProperty) + sourceTextProperty.bind(workbookDataStore.sourceTextBinding()) + sourceContentTitleProperty.bind(workbookDataStore.activeTitleBinding()) + orientationProperty.bind(settingsViewModel.orientationProperty) + sourceOrientationProperty.bind(settingsViewModel.sourceOrientationProperty) + + sourceSpeedRateProperty.bind( + workbookDataStore.activeWorkbookProperty.select { + it.translation.sourceRate.toLazyBinding() + } + ) + + targetSpeedRateProperty.bind( + workbookDataStore.activeWorkbookProperty.select { + it.translation.targetRate.toLazyBinding() + } + ) + + playerProperty.bind(viewModel.sourcePlayerProperty) + } + } + private fun subscribeEvents() { addShortcut() @@ -213,6 +263,12 @@ class BlindDraft : View() { subscribe { viewModel.redo() }.also { eventSubscriptions.add(it) } + + subscribe { pluginInfo -> + if (!pluginInfo.isNative) { + workspace.dock(pluginOpenedPage) + } + }.let { eventSubscriptions.add(it) } } private fun unsubscribeEvents() { diff --git a/jvm/workbookapp/src/main/kotlin/org/wycliffeassociates/otter/jvm/workbookapp/ui/screens/translation/ChapterReview.kt b/jvm/workbookapp/src/main/kotlin/org/wycliffeassociates/otter/jvm/workbookapp/ui/screens/translation/ChapterReview.kt index 20684df987..ad9a738781 100644 --- a/jvm/workbookapp/src/main/kotlin/org/wycliffeassociates/otter/jvm/workbookapp/ui/screens/translation/ChapterReview.kt +++ b/jvm/workbookapp/src/main/kotlin/org/wycliffeassociates/otter/jvm/workbookapp/ui/screens/translation/ChapterReview.kt @@ -57,6 +57,7 @@ import org.wycliffeassociates.otter.jvm.workbookapp.plugin.PluginOpenedEvent import org.wycliffeassociates.otter.jvm.workbookapp.ui.narration.SnackBarEvent import org.wycliffeassociates.otter.jvm.workbookapp.ui.viewmodel.ChapterReviewViewModel import org.wycliffeassociates.otter.jvm.workbookapp.ui.viewmodel.SettingsViewModel +import org.wycliffeassociates.otter.jvm.workbookapp.ui.viewmodel.TranslationViewModel2 import org.wycliffeassociates.otter.jvm.workbookapp.ui.viewmodel.WorkbookDataStore import tornadofx.* @@ -67,6 +68,7 @@ class ChapterReview : View() { val viewModel: ChapterReviewViewModel by inject() val settingsViewModel: SettingsViewModel by inject() private val workbookDataStore: WorkbookDataStore by inject() + private val translationViewModel: TranslationViewModel2 by inject() private lateinit var waveform: MarkerWaveform private val audioScrollBar = createAudioScrollBar( @@ -203,10 +205,11 @@ class ChapterReview : View() { when (viewModel.pluginOpenedProperty.value) { true -> { // navigate back from plugin viewModel.pluginOpenedProperty.set(false) + translationViewModel.loadingStepProperty.set(false) viewModel.reloadAudio().subscribe() } - else -> { + false -> { logger.info("Final Review docked.") viewModel.subscribeOnWaveformImagesProperty.set(::subscribeOnWaveformImages) viewModel.cleanupWaveformProperty.set(waveform::cleanup) diff --git a/jvm/workbookapp/src/main/kotlin/org/wycliffeassociates/otter/jvm/workbookapp/ui/screens/translation/PeerEdit.kt b/jvm/workbookapp/src/main/kotlin/org/wycliffeassociates/otter/jvm/workbookapp/ui/screens/translation/PeerEdit.kt index f54f0f1a91..6f973fc183 100644 --- a/jvm/workbookapp/src/main/kotlin/org/wycliffeassociates/otter/jvm/workbookapp/ui/screens/translation/PeerEdit.kt +++ b/jvm/workbookapp/src/main/kotlin/org/wycliffeassociates/otter/jvm/workbookapp/ui/screens/translation/PeerEdit.kt @@ -19,6 +19,7 @@ package org.wycliffeassociates.otter.jvm.workbookapp.ui.screens.translation import com.github.thomasnield.rxkotlinfx.observeOnFx +import com.github.thomasnield.rxkotlinfx.toLazyBinding import com.github.thomasnield.rxkotlinfx.toObservable import com.sun.javafx.util.Utils import io.reactivex.rxkotlin.addTo @@ -34,8 +35,10 @@ import org.kordamp.ikonli.materialdesign.MaterialDesign import org.slf4j.LoggerFactory import org.wycliffeassociates.otter.jvm.controls.Shortcut import org.wycliffeassociates.otter.jvm.controls.createAudioScrollBar +import org.wycliffeassociates.otter.jvm.controls.dialog.PluginOpenedPage import org.wycliffeassociates.otter.jvm.controls.event.TranslationNavigationEvent import org.wycliffeassociates.otter.jvm.controls.event.RedoChunkingPageEvent +import org.wycliffeassociates.otter.jvm.controls.event.ReturnFromPluginEvent import org.wycliffeassociates.otter.jvm.controls.event.UndoChunkingPageEvent import org.wycliffeassociates.otter.jvm.controls.media.simpleaudioplayer import org.wycliffeassociates.otter.jvm.controls.model.pixelsToFrames @@ -44,9 +47,12 @@ import org.wycliffeassociates.otter.jvm.controls.waveform.MarkerWaveform import org.wycliffeassociates.otter.jvm.controls.waveform.startAnimationTimer import org.wycliffeassociates.otter.jvm.utils.ListenerDisposer import org.wycliffeassociates.otter.jvm.utils.onChangeWithDisposer +import org.wycliffeassociates.otter.jvm.workbookapp.plugin.PluginOpenedEvent import org.wycliffeassociates.otter.jvm.workbookapp.ui.viewmodel.PeerEditViewModel import org.wycliffeassociates.otter.jvm.workbookapp.ui.viewmodel.RecorderViewModel import org.wycliffeassociates.otter.jvm.workbookapp.ui.viewmodel.SettingsViewModel +import org.wycliffeassociates.otter.jvm.workbookapp.ui.viewmodel.TranslationViewModel2 +import org.wycliffeassociates.otter.jvm.workbookapp.ui.viewmodel.WorkbookDataStore import tornadofx.* open class PeerEdit : View() { @@ -55,6 +61,8 @@ open class PeerEdit : View() { val viewModel: PeerEditViewModel by inject() val settingsViewModel: SettingsViewModel by inject() val recorderViewModel: RecorderViewModel by inject() + val workbookDataStore: WorkbookDataStore by inject() + val translationViewModel: TranslationViewModel2 by inject() private lateinit var waveform: MarkerWaveform private val audioScrollBar = createAudioScrollBar( @@ -68,6 +76,7 @@ open class PeerEdit : View() { private val mainSectionProperty = SimpleObjectProperty(null) private val playbackView = createPlaybackView() private val recordingView = createRecordingView() + private val pluginOpenedPage = createPluginOpenedPage() private val eventSubscriptions = mutableListOf() private val listenerDisposers = mutableListOf() @@ -141,10 +150,11 @@ open class PeerEdit : View() { disableWhen { viewModel.isPlayingProperty } action { - viewModel.onRecordNew() - mainSectionProperty.set(recordingView) - recorderViewModel.onViewReady(container.width.toInt()) // use the width of the existing component - recorderViewModel.toggle() + viewModel.onRecordNew { + mainSectionProperty.set(recordingView) + recorderViewModel.onViewReady(container.width.toInt()) // use the width of the existing component + recorderViewModel.toggle() + } } } } @@ -200,24 +210,42 @@ open class PeerEdit : View() { override fun onDock() { super.onDock() - logger.info("Checking docked.") recorderViewModel.waveformCanvas = recordingView.waveformCanvas recorderViewModel.volumeCanvas = recordingView.volumeCanvas mainSectionProperty.set(playbackView) timer = startAnimationTimer { viewModel.calculatePosition() } - viewModel.subscribeOnWaveformImagesProperty.set(::subscribeOnWaveformImages) - viewModel.cleanupWaveformProperty.set(waveform::cleanup) - viewModel.dock() + + when (viewModel.pluginOpenedProperty.value) { + true -> { // navigate back from plugin + viewModel.pluginOpenedProperty.set(false) + translationViewModel.loadingStepProperty.set(false) + } + + false -> { + logger.info("Checking docked.") + viewModel.subscribeOnWaveformImagesProperty.set(::subscribeOnWaveformImages) + viewModel.cleanupWaveformProperty.set(waveform::cleanup) + viewModel.dock() + } + } subscribeEvents() subscribeOnThemeChange() } override fun onUndock() { super.onUndock() - logger.info("Checking undocked.") timer?.stop() + + when (viewModel.pluginOpenedProperty.value) { + true -> { + /* no-op, opening plugin */ + } + false ->{ + logger.info("Checking undocked.") + viewModel.undock() + } + } unsubscribeEvents() - viewModel.undock() if (mainSectionProperty.value == recordingView) { recorderViewModel.cancel() } @@ -245,6 +273,22 @@ open class PeerEdit : View() { viewModel.redo() }.also { eventSubscriptions.add(it) } + subscribe { pluginInfo -> + if (!pluginInfo.isNative) { + workspace.dock(pluginOpenedPage) + } + }.let { eventSubscriptions.add(it) } + + subscribe { + /* When leaving the plugin, the flow is: Plugin undock > main page dock > PeerEdit dock. + * If the there's no change to the audio (empty), we want to keep the current Peer Edit view unchanged, + * which means avoiding refresh and navigation. Therefore, we only set it + * AFTER the main page has accessed this property and fired off the event. + * This ensures the correct value of the property when navigating away from PeerEdit. + * */ + viewModel.pluginOpenedProperty.set(false) + }.let { eventSubscriptions.add(it) } + subscribe { viewModel.cleanupWaveform() }.also { eventSubscriptions.add(it) } @@ -273,6 +317,30 @@ open class PeerEdit : View() { .addTo(viewModel.disposable) } + private fun createPluginOpenedPage(): PluginOpenedPage { + return find().apply { + licenseProperty.bind(workbookDataStore.sourceLicenseProperty) + sourceTextProperty.bind(workbookDataStore.sourceTextBinding()) + sourceContentTitleProperty.bind(workbookDataStore.activeTitleBinding()) + orientationProperty.bind(settingsViewModel.orientationProperty) + sourceOrientationProperty.bind(settingsViewModel.sourceOrientationProperty) + + sourceSpeedRateProperty.bind( + workbookDataStore.activeWorkbookProperty.select { + it.translation.sourceRate.toLazyBinding() + } + ) + + targetSpeedRateProperty.bind( + workbookDataStore.activeWorkbookProperty.select { + it.translation.targetRate.toLazyBinding() + } + ) + + playerProperty.bind(viewModel.sourcePlayerProperty) + } + } + private fun addShortcut() { workspace.shortcut(Shortcut.PLAY_SOURCE.value) { viewModel.sourcePlayerProperty.value?.toggle() diff --git a/jvm/workbookapp/src/main/kotlin/org/wycliffeassociates/otter/jvm/workbookapp/ui/viewmodel/AudioPluginViewModel.kt b/jvm/workbookapp/src/main/kotlin/org/wycliffeassociates/otter/jvm/workbookapp/ui/viewmodel/AudioPluginViewModel.kt index 74c87b31d5..d9d53a097b 100644 --- a/jvm/workbookapp/src/main/kotlin/org/wycliffeassociates/otter/jvm/workbookapp/ui/viewmodel/AudioPluginViewModel.kt +++ b/jvm/workbookapp/src/main/kotlin/org/wycliffeassociates/otter/jvm/workbookapp/ui/viewmodel/AudioPluginViewModel.kt @@ -81,6 +81,11 @@ class AudioPluginViewModel : ViewModel() { ) } + fun record(take: Take): Single { + val params = constructPluginParameters() + return pluginActions.record(take, params) + } + fun import(recordable: Recordable, take: File): Completable { return pluginActions.import( audio = recordable.audio, diff --git a/jvm/workbookapp/src/main/kotlin/org/wycliffeassociates/otter/jvm/workbookapp/ui/viewmodel/BlindDraftViewModel.kt b/jvm/workbookapp/src/main/kotlin/org/wycliffeassociates/otter/jvm/workbookapp/ui/viewmodel/BlindDraftViewModel.kt index a5772e3237..8758fba606 100644 --- a/jvm/workbookapp/src/main/kotlin/org/wycliffeassociates/otter/jvm/workbookapp/ui/viewmodel/BlindDraftViewModel.kt +++ b/jvm/workbookapp/src/main/kotlin/org/wycliffeassociates/otter/jvm/workbookapp/ui/viewmodel/BlindDraftViewModel.kt @@ -23,9 +23,11 @@ import io.reactivex.Single import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxkotlin.addTo import javafx.application.Platform +import javafx.beans.property.SimpleBooleanProperty import javafx.beans.property.SimpleObjectProperty import javafx.collections.transformation.FilteredList import org.slf4j.LoggerFactory +import org.wycliffeassociates.otter.common.audio.AudioFile import org.wycliffeassociates.otter.common.audio.AudioFileFormat import org.wycliffeassociates.otter.common.audio.wav.IWaveFileCreator import org.wycliffeassociates.otter.common.data.primitives.ContentType @@ -35,6 +37,7 @@ import org.wycliffeassociates.otter.common.data.workbook.Take import org.wycliffeassociates.otter.common.device.IAudioPlayer import org.wycliffeassociates.otter.common.domain.IUndoable import org.wycliffeassociates.otter.common.domain.content.FileNamer +import org.wycliffeassociates.otter.common.domain.content.PluginActions import org.wycliffeassociates.otter.common.domain.content.Recordable import org.wycliffeassociates.otter.common.domain.content.WorkbookFileNamerBuilder import org.wycliffeassociates.otter.jvm.device.audio.AudioConnectionFactory @@ -45,7 +48,12 @@ import org.wycliffeassociates.otter.common.domain.translation.TranslationTakeDel import org.wycliffeassociates.otter.common.domain.translation.TranslationTakeRecordAction import org.wycliffeassociates.otter.common.domain.translation.TranslationTakeSelectAction import org.wycliffeassociates.otter.common.domain.model.UndoableActionHistory +import org.wycliffeassociates.otter.common.domain.plugins.IAudioPlugin +import org.wycliffeassociates.otter.common.persistence.repositories.PluginType +import org.wycliffeassociates.otter.jvm.workbookapp.plugin.PluginClosedEvent +import org.wycliffeassociates.otter.jvm.workbookapp.plugin.PluginOpenedEvent import org.wycliffeassociates.otter.jvm.workbookapp.ui.model.TakeCardModel +import org.wycliffeassociates.otter.jvm.workbookapp.ui.narration.SnackBarEvent import org.wycliffeassociates.otter.jvm.workbookapp.ui.viewmodel.RecorderViewModel.Result import tornadofx.* import java.io.File @@ -57,6 +65,7 @@ class BlindDraftViewModel : ViewModel() { @Inject lateinit var waveFileCreator: IWaveFileCreator + @Inject lateinit var audioConnectionFactory: AudioConnectionFactory @@ -65,6 +74,7 @@ class BlindDraftViewModel : ViewModel() { val translationViewModel: TranslationViewModel2 by inject() val recorderViewModel: RecorderViewModel by inject() val chapterReviewViewModel: ChapterReviewViewModel by inject() + val audioPluginViewModel: AudioPluginViewModel by inject() val sourcePlayerProperty = SimpleObjectProperty() val currentChunkProperty = SimpleObjectProperty() @@ -72,6 +82,7 @@ class BlindDraftViewModel : ViewModel() { val takes = observableListOf() val selectedTake = FilteredList(takes) { it.selected } val availableTakes = FilteredList(takes) { !it.selected } + val pluginOpenedProperty = SimpleBooleanProperty(false) private val recordedTakeProperty = SimpleObjectProperty() private val actionHistory = UndoableActionHistory() @@ -99,6 +110,7 @@ class BlindDraftViewModel : ViewModel() { actionHistory.clear() }.also { disposableListeners.add(it) } + translationViewModel.pluginOpenedProperty.bind(pluginOpenedProperty) translationViewModel.loadingStepProperty.set(false) } @@ -115,6 +127,7 @@ class BlindDraftViewModel : ViewModel() { } sourcePlayerProperty.unbind() currentChunkProperty.set(null) + translationViewModel.pluginOpenedProperty.unbind() translationViewModel.updateSourceText().subscribe() selectedTakeDisposable.clear() disposables.clear() @@ -122,13 +135,22 @@ class BlindDraftViewModel : ViewModel() { disposableListeners.clear() } - fun onRecordNew() { - newTakeFile() - .observeOnFx() - .subscribe { take -> - recordedTakeProperty.set(take) - recorderViewModel.targetFileProperty.set(take.file) - } + fun onRecordNew(toggleViewCallback: () -> Unit = {}) { + val pluginType = PluginType.RECORDER + val selectedPlugin = audioPluginViewModel.getPlugin(pluginType) + .blockingGet() + if (!selectedPlugin.isNativePlugin()) { + recordWithExternalPlugin(selectedPlugin, pluginType) + } else { + newTakeFile() + .observeOnFx() + .subscribe { take -> + recordedTakeProperty.set(take) + recorderViewModel.targetFileProperty.set(take.file) + } + toggleViewCallback() + } + } fun onRecordFinish(result: Result) { @@ -320,6 +342,54 @@ class BlindDraftViewModel : ViewModel() { } } + private fun recordWithExternalPlugin(plugin: IAudioPlugin, pluginType: PluginType) { + pluginOpenedProperty.set(true) + workbookDataStore.activeTakeNumberProperty.set(1) + FX.eventbus.fire(PluginOpenedEvent(pluginType, plugin.isNativePlugin())) + newTakeFile() + .flatMap { take -> + recordedTakeProperty.set(take) + audioPluginViewModel.record(take) + } + .observeOnFx() + .doOnError { e -> + logger.error("Error in processing take with plugin type: $pluginType", e) + } + .onErrorReturn { PluginActions.Result.NO_PLUGIN } + .subscribe { result -> + logger.info("Returned from plugin with result: $result") + + when (result) { + PluginActions.Result.NO_PLUGIN -> { + FX.eventbus.fire(SnackBarEvent(messages["noEditor"])) + } + + PluginActions.Result.SUCCESS -> { + // handle nonempty take returned from plugin + val file = recordedTakeProperty.value.file + if (AudioFile(file).totalFrames > 0) { + workbookDataStore.chunk?.let { chunk -> + val op = TranslationTakeRecordAction( + chunk, + recordedTakeProperty.value, + chunk.audio.getSelectedTake() + ) + actionHistory.execute(op) + onUndoableAction() + loadTakes(chunk) + } + } + } + + else -> { + // no audio - no op + } + } + recordedTakeProperty.set(null) + FX.eventbus.fire(PluginClosedEvent(pluginType)) + } + } + private fun onUndoableAction() { translationViewModel.canUndoProperty.set(true) translationViewModel.canRedoProperty.set(false) diff --git a/jvm/workbookapp/src/main/kotlin/org/wycliffeassociates/otter/jvm/workbookapp/ui/viewmodel/ChapterReviewViewModel.kt b/jvm/workbookapp/src/main/kotlin/org/wycliffeassociates/otter/jvm/workbookapp/ui/viewmodel/ChapterReviewViewModel.kt index 3a2a5fbc17..cc96478215 100644 --- a/jvm/workbookapp/src/main/kotlin/org/wycliffeassociates/otter/jvm/workbookapp/ui/viewmodel/ChapterReviewViewModel.kt +++ b/jvm/workbookapp/src/main/kotlin/org/wycliffeassociates/otter/jvm/workbookapp/ui/viewmodel/ChapterReviewViewModel.kt @@ -147,14 +147,13 @@ class ChapterReviewViewModel : ViewModel(), IMarkerViewModel { init { (app as IDependencyGraphProvider).dependencyGraph.inject(this) - - translationViewModel.pluginOpenedProperty.bind(pluginOpenedProperty) } fun dock() { sourcePlayerProperty.bind(audioDataStore.sourceAudioPlayerProperty) workbookDataStore.activeChunkProperty.set(null) translationViewModel.currentMarkerProperty.bind(highlightedMarkerIndexProperty) + translationViewModel.pluginOpenedProperty.bind(pluginOpenedProperty) Completable .fromAction { @@ -191,6 +190,7 @@ class ChapterReviewViewModel : ViewModel(), IMarkerViewModel { ?.writeMarkers() ?.blockingAwait() + translationViewModel.pluginOpenedProperty.unbind() translationViewModel.currentMarkerProperty.unbind() translationViewModel.currentMarkerProperty.set(-1) cleanup() @@ -325,6 +325,10 @@ class ChapterReviewViewModel : ViewModel(), IMarkerViewModel { // no-op } } + + /* set pluginOpenedProperty to false to allow invoking dock() + which refreshes the chapter audio. */ + pluginOpenedProperty.set(false) FX.eventbus.fire(PluginClosedEvent(pluginType)) } } diff --git a/jvm/workbookapp/src/main/kotlin/org/wycliffeassociates/otter/jvm/workbookapp/ui/viewmodel/PeerEditViewModel.kt b/jvm/workbookapp/src/main/kotlin/org/wycliffeassociates/otter/jvm/workbookapp/ui/viewmodel/PeerEditViewModel.kt index 9e5d2bff07..ed266dedfc 100644 --- a/jvm/workbookapp/src/main/kotlin/org/wycliffeassociates/otter/jvm/workbookapp/ui/viewmodel/PeerEditViewModel.kt +++ b/jvm/workbookapp/src/main/kotlin/org/wycliffeassociates/otter/jvm/workbookapp/ui/viewmodel/PeerEditViewModel.kt @@ -19,7 +19,6 @@ package org.wycliffeassociates.otter.jvm.workbookapp.ui.viewmodel import com.github.thomasnield.rxkotlinfx.observeOnFx -import com.github.thomasnield.rxkotlinfx.toObservable import com.sun.glass.ui.Screen import io.reactivex.Observable import io.reactivex.disposables.CompositeDisposable @@ -30,6 +29,8 @@ import javafx.beans.property.SimpleIntegerProperty import javafx.beans.property.SimpleObjectProperty import javafx.scene.image.Image import javafx.scene.paint.Color +import org.slf4j.LoggerFactory +import org.wycliffeassociates.otter.common.audio.AudioFile import org.wycliffeassociates.otter.common.data.getWaveformColors import org.wycliffeassociates.otter.common.data.primitives.CheckingStatus import org.wycliffeassociates.otter.common.data.primitives.ContentType @@ -38,8 +39,11 @@ import org.wycliffeassociates.otter.common.data.workbook.Take import org.wycliffeassociates.otter.common.device.IAudioPlayer import org.wycliffeassociates.otter.common.domain.IUndoable import org.wycliffeassociates.otter.common.domain.audio.OratureAudioFile +import org.wycliffeassociates.otter.common.domain.content.PluginActions import org.wycliffeassociates.otter.common.domain.translation.TranslationTakeApproveAction import org.wycliffeassociates.otter.common.domain.model.UndoableActionHistory +import org.wycliffeassociates.otter.common.domain.plugins.IAudioPlugin +import org.wycliffeassociates.otter.common.persistence.repositories.PluginType import org.wycliffeassociates.otter.jvm.controls.controllers.AudioPlayerController import org.wycliffeassociates.otter.jvm.controls.controllers.ScrollSpeed import org.wycliffeassociates.otter.jvm.controls.waveform.IWaveformViewModel @@ -50,11 +54,14 @@ import org.wycliffeassociates.otter.jvm.utils.onChangeAndDoNowWithDisposer import org.wycliffeassociates.otter.jvm.workbookapp.di.IDependencyGraphProvider import org.wycliffeassociates.otter.jvm.controls.model.ChunkingStep import org.wycliffeassociates.otter.jvm.controls.waveform.WAVEFORM_MAX_HEIGHT -import org.wycliffeassociates.otter.jvm.utils.onChangeWithDisposer +import org.wycliffeassociates.otter.jvm.workbookapp.plugin.PluginClosedEvent +import org.wycliffeassociates.otter.jvm.workbookapp.plugin.PluginOpenedEvent import tornadofx.* import javax.inject.Inject class PeerEditViewModel : ViewModel(), IWaveformViewModel { + private val logger = LoggerFactory.getLogger(javaClass) + @Inject lateinit var audioConnectionFactory: AudioConnectionFactory @@ -65,6 +72,7 @@ class PeerEditViewModel : ViewModel(), IWaveformViewModel { val blindDraftViewModel: BlindDraftViewModel by inject() val recorderViewModel: RecorderViewModel by inject() val chapterReviewViewModel: ChapterReviewViewModel by inject() + val audioPluginViewModel: AudioPluginViewModel by inject() override val waveformAudioPlayerProperty = SimpleObjectProperty() override val positionProperty = SimpleDoubleProperty(0.0) @@ -76,6 +84,7 @@ class PeerEditViewModel : ViewModel(), IWaveformViewModel { val chunkConfirmed = SimpleBooleanProperty(false) val sourcePlayerProperty = SimpleObjectProperty() val isPlayingProperty = SimpleBooleanProperty(false) + val pluginOpenedProperty = SimpleBooleanProperty(false) val disposable = CompositeDisposable() lateinit var waveform: Observable @@ -114,6 +123,7 @@ class PeerEditViewModel : ViewModel(), IWaveformViewModel { }.also { disposableListeners.add(it) } sourcePlayerProperty.bind(audioDataStore.sourceAudioPlayerProperty) + translationViewModel.pluginOpenedProperty.bind(pluginOpenedProperty) translationViewModel.loadingStepProperty.set(false) } @@ -125,6 +135,7 @@ class PeerEditViewModel : ViewModel(), IWaveformViewModel { sourcePlayerProperty.unbind() currentChunkProperty.set(null) selectedTakeDisposable.clear() + translationViewModel.pluginOpenedProperty.unbind() disposable.clear() disposableListeners.forEach { it.dispose() } disposableListeners.clear() @@ -206,13 +217,22 @@ class PeerEditViewModel : ViewModel(), IWaveformViewModel { translationViewModel.canRedoProperty.set(actionHistory.canRedo()) } - fun onRecordNew() { - blindDraftViewModel.newTakeFile() - .observeOnFx() - .subscribe { take -> - newTakeProperty.set(take) - recorderViewModel.targetFileProperty.set(take.file) - } + fun onRecordNew(toggleViewCallback: () -> Unit = {}) { + val pluginType = PluginType.RECORDER + val selectedPlugin = audioPluginViewModel.getPlugin(pluginType) + .blockingGet() + if (!selectedPlugin.isNativePlugin()) { + recordWithExternalPlugin(selectedPlugin, pluginType) + } else { + blindDraftViewModel.newTakeFile() + .observeOnFx() + .subscribe { take -> + newTakeProperty.set(take) + recorderViewModel.targetFileProperty.set(take.file) + } + + toggleViewCallback() + } } fun onRecordFinish(result: RecorderViewModel.Result) { @@ -300,6 +320,41 @@ class PeerEditViewModel : ViewModel(), IWaveformViewModel { createWaveformImages(audio) subscribeOnWaveformImages() } + private fun recordWithExternalPlugin(plugin: IAudioPlugin, pluginType: PluginType) { + pluginOpenedProperty.set(true) + workbookDataStore.activeTakeNumberProperty.set(1) + FX.eventbus.fire(PluginOpenedEvent(pluginType, plugin.isNativePlugin())) + blindDraftViewModel.newTakeFile() + .flatMap { take -> + newTakeProperty.set(take) + // doesn't need to create take since .record() will do + audioPluginViewModel.record(take) + } + .observeOnFx() + .doOnError { e -> + logger.error("Error in processing take with plugin type: $pluginType", e) + } + .onErrorReturn { PluginActions.Result.NO_PLUGIN } + .subscribe { result -> + logger.info("Returned from plugin with result: $result") + + val take = newTakeProperty.value + if (AudioFile(take.file).totalFrames > 0) { + /* set pluginOpenedProperty to false will allow invoking dock(), + which updates chunk status and auto navigates to incomplete chunk. + This only applies to non-empty recording. */ + pluginOpenedProperty.set(false) + currentChunkProperty.value.audio.insertTake(take) + chapterReviewViewModel.invalidateChapterTake() + // any change(s) to chunk's take requires checking again + translationViewModel.selectedStepProperty.set(null) + translationViewModel.navigateStep(ChunkingStep.PEER_EDIT) + } + + newTakeProperty.set(null) + FX.eventbus.fire(PluginClosedEvent(pluginType)) + } + } private fun createWaveformImages(audio: OratureAudioFile) { cleanupWaveform() @@ -332,4 +387,5 @@ class PeerEditViewModel : ViewModel(), IWaveformViewModel { else -> CheckingStatus.UNCHECKED } } -} \ No newline at end of file +} +