diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/LabelActionType.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/LabelActionType.java index feb63ff62..4f2ad551f 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/LabelActionType.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/LabelActionType.java @@ -9,7 +9,9 @@ public enum LabelActionType implements ActionType { Lock(true), Merge, Split, - SelectAll(true); + SelectAll(true), + Replace, + Delete; //TODO Caleb: consider moving this to ActionType. Maybe others too private final boolean readOnly; diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/MaskedSource.java b/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/MaskedSource.java index 83dabd03e..482216dc6 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/MaskedSource.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/MaskedSource.java @@ -88,6 +88,7 @@ import net.imglib2.view.ExtendedRealRandomAccessibleRealInterval; import net.imglib2.view.IntervalView; import net.imglib2.view.Views; +import org.checkerframework.common.reflection.qual.Invoke; import org.janelia.saalfeldlab.fx.Tasks; import org.janelia.saalfeldlab.fx.UtilityTask; import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread; @@ -637,10 +638,10 @@ public void applyMaskOverIntervals( intervalOverCanvas, acceptAsPainted); - synchronized (progressBinding) { - var progress = completedTasks.incrementAndGet() / (double)expectedTasks; + InvokeOnJavaFXApplicationThread.invoke(() -> { + final double progress = completedTasks.incrementAndGet() / (double)expectedTasks; progressBinding.set(progress); - } + }); final Map blocksByLabelByLevel = this.affectedBlocksByLabel[maskInfo.level]; synchronized (blocksByLabelByLevel) { @@ -669,10 +670,10 @@ public void applyMaskOverIntervals( acceptAsPainted, propagationExecutor) ).get(); - synchronized (progressBinding) { - var progress = completedTasks.incrementAndGet() / (double)expectedTasks; + InvokeOnJavaFXApplicationThread.invoke(() -> { + final double progress = completedTasks.incrementAndGet() / (double)expectedTasks; progressBinding.set(progress); - } + }); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } @@ -687,9 +688,7 @@ public void applyMaskOverIntervals( throw new RuntimeException(e); } } - synchronized (progressBinding) { - progressBinding.set(1.0); - } + InvokeOnJavaFXApplicationThread.invoke(() -> progressBinding.set(1.0)); synchronized (this) { setCurrentMask(null); diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/BindingKeys.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/BindingKeys.kt index b9057e142..178d817f0 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/BindingKeys.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/BindingKeys.kt @@ -109,6 +109,7 @@ enum class LabelSourceStateKeys(lateInitNamedKeyCombo : LateInitNamedKeyCombinat SELECT_ALL_IN_CURRENT_VIEW ( CONTROL_DOWN + SHIFT_DOWN + A), LOCK_SEGMENT ( L), NEXT_ID ( N), + DELETE_ID ( SHIFT_DOWN + BACK_SPACE), COMMIT_DIALOG ( C + CONTROL_DOWN), MERGE_ALL_SELECTED ( ENTER + CONTROL_DOWN), ARGB_STREAM__INCREMENT_SEED ( C), diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/actions/paint/ReplaceLabel.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/actions/paint/ReplaceLabel.kt new file mode 100644 index 000000000..62e23c556 --- /dev/null +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/actions/paint/ReplaceLabel.kt @@ -0,0 +1,225 @@ +package org.janelia.saalfeldlab.paintera.control.actions.paint + +import io.github.oshai.kotlinlogging.KotlinLogging +import javafx.beans.property.SimpleDoubleProperty +import javafx.beans.property.SimpleStringProperty +import javafx.event.ActionEvent +import javafx.scene.control.ButtonType +import javafx.scene.control.Dialog +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import net.imglib2.FinalInterval +import net.imglib2.Interval +import net.imglib2.type.Type +import net.imglib2.type.numeric.IntegerType +import net.imglib2.type.numeric.integer.UnsignedLongType +import net.imglib2.type.volatiles.VolatileUnsignedLongType +import net.imglib2.util.Intervals +import net.imglib2.view.IntervalView +import org.janelia.saalfeldlab.fx.actions.verifyPermission +import org.janelia.saalfeldlab.fx.extensions.createObservableBinding +import org.janelia.saalfeldlab.fx.extensions.nonnull +import org.janelia.saalfeldlab.fx.extensions.nonnullVal +import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread +import org.janelia.saalfeldlab.labels.blocks.LabelBlockLookupKey +import org.janelia.saalfeldlab.paintera.Paintera +import org.janelia.saalfeldlab.paintera.control.actions.MenuAction +import org.janelia.saalfeldlab.paintera.control.actions.PaintActionType +import org.janelia.saalfeldlab.paintera.control.actions.onAction +import org.janelia.saalfeldlab.paintera.data.mask.MaskInfo +import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource +import org.janelia.saalfeldlab.paintera.data.mask.SourceMask +import org.janelia.saalfeldlab.paintera.data.n5.N5DataSource +import org.janelia.saalfeldlab.paintera.paintera +import org.janelia.saalfeldlab.paintera.state.label.ConnectomicsLabelState +import org.janelia.saalfeldlab.util.convert +import org.janelia.saalfeldlab.util.grids.LabelBlockLookupAllBlocks +import org.janelia.saalfeldlab.util.interval +import kotlin.math.nextUp +import net.imglib2.type.label.Label as Imglib2Label + +object ReplaceLabel : MenuAction("_Replace or Delete Label...") { + + private val LOG = KotlinLogging.logger { } + + private val state = ReplaceLabelState() + + private val replacementLabelProperty = state.replacementLabel + private val replacementLabel by replacementLabelProperty.nonnullVal() + + private val activateReplacementProperty = state.activeReplacementLabel.apply { + subscribe { _, activate -> + if (activate) + activateReplacementLabel(0L, replacementLabel) + else + activateReplacementLabel(replacementLabel, 0L) + } + } + private val activateReplacement by activateReplacementProperty.nonnull() + + private val progressProperty = SimpleDoubleProperty() + private val progressTextProperty = SimpleStringProperty() + + init { + verifyPermission(PaintActionType.Erase, PaintActionType.Background, PaintActionType.Fill) + onAction(state) { + showDialog() + } + } + + private fun > ReplaceLabelState.generateReplaceLabelMask(newLabel: Long, vararg fragments: Long) = with(maskedSource) { + val dataSource = getDataSource(0, 0) + + val fragmentsSet = fragments.toHashSet() + dataSource.convert(UnsignedLongType(Imglib2Label.INVALID)) { src, target -> + val value = if (src.integerLong in fragmentsSet) newLabel else Imglib2Label.INVALID + target.set(value) + }.interval(dataSource) + } + + + private fun > ReplaceLabelState.deleteActiveFragment() = replaceActiveFragment(0L) + private fun > ReplaceLabelState.deleteActiveSegment() = replaceActiveSegment(0L) + private fun > ReplaceLabelState.deleteAllActiveFragments() = replaceAllActiveFragments(0L) + private fun > ReplaceLabelState.deleteAllActiveSegments() = replaceAllActiveSegments(0L) + private fun > ReplaceLabelState.replaceActiveFragment(newLabel: Long) = replaceLabels(newLabel, activeFragment) + private fun > ReplaceLabelState.replaceActiveSegment(newLabel: Long) = replaceLabels(newLabel, *fragmentsForActiveSegment) + private fun > ReplaceLabelState.replaceAllActiveFragments(newLabel: Long) = replaceLabels(newLabel, *allActiveFragments) + private fun > ReplaceLabelState.replaceAllActiveSegments(newLabel: Long) = replaceLabels(newLabel, *fragmentsForActiveSegment) + + + private fun > ReplaceLabelState.replaceLabels(newLabel: Long, vararg oldLabels: Long) = with(maskedSource) { + val blocks = blocksForLabels(0, *oldLabels) + val replacedLabelMask = generateReplaceLabelMask(newLabel, *oldLabels) + + val sourceMask = SourceMask( + MaskInfo(0, 0), + replacedLabelMask, + replacedLabelMask.convert(VolatileUnsignedLongType(Imglib2Label.INVALID)) { input, output -> + output.set(input.integerLong) + output.isValid = true + }.interval(replacedLabelMask), + null, + null + ) {} + + + val numBlocks = blocks.size + val progressTextBinding = progressProperty.createObservableBinding { + val blocksDone = (it.doubleValue() * numBlocks).nextUp().toLong().coerceAtMost(numBlocks.toLong()) + "Blocks: $blocksDone / $numBlocks" + } + InvokeOnJavaFXApplicationThread { + progressTextProperty.unbind() + progressTextProperty.bind(progressTextBinding) + } + + setMask(sourceMask) { it == newLabel } + applyMaskOverIntervals( + sourceMask, + blocks, + progressProperty + ) { it == newLabel } + + requestRepaintOverIntervals(blocks) + sourceState.refreshMeshes() + } + + private fun activateReplacementLabel(current: Long, next: Long) { + val selectedIds = state.paintContext.selectedIds + if (current != selectedIds.lastSelection) { + selectedIds.deactivate(current) + } + if (activateReplacement && next > 0L) { + selectedIds.activateAlso(selectedIds.lastSelection, next) + } + } + + fun showDialog() = state.showDialog() + + private fun ReplaceLabelState<*>.showDialog() { + Dialog().apply { + isResizable = true + Paintera.registerStylesheets(dialogPane) + dialogPane.buttonTypes += ButtonType.APPLY + dialogPane.buttonTypes += ButtonType.CANCEL + title = name?.replace("_", "") + + progressTextProperty.unbind() + progressTextProperty.set("") + + progressProperty.unbind() + progressProperty.set(0.0) + + val replaceLabelUI = ReplaceLabelUI(state) + replaceLabelUI.progressBarProperty.bind(progressProperty) + replaceLabelUI.progressLabelText.bind(progressTextProperty) + + dialogPane.content = replaceLabelUI + dialogPane.lookupButton(ButtonType.APPLY).also { applyButton -> + applyButton.disableProperty().bind(paintera.baseView.isDisabledProperty) + applyButton.cursorProperty().bind(paintera.baseView.node.cursorProperty()) + applyButton.addEventFilter(ActionEvent.ACTION) { event -> + event.consume() + val replacementLabel = state.replacementLabel.value + val fragmentsToReplace = state.fragmentsToReplace.toLongArray() + if (replacementLabel != null && replacementLabel >= 0 && fragmentsToReplace.isNotEmpty()) { + CoroutineScope(Dispatchers.Default).async { + replaceLabels(replacementLabel, *fragmentsToReplace) + if (activateReplacement) + state.sourceState.selectedIds.activate(replacementLabel) + } + } + } + } + dialogPane.lookupButton(ButtonType.CANCEL).apply { + disableProperty().bind(paintContext.dataSource.isApplyingMaskProperty()) + } + dialogPane.scene.window.setOnCloseRequest { + if (paintContext.dataSource.isApplyingMaskProperty().get()) + it.consume() + } + }.show() + } + + private fun ReplaceLabelState<*>.requestRepaintOverIntervals(sourceIntervals: List? = null) { + val globalInterval = sourceIntervals + ?.reduce(Intervals::union) + ?.let { maskedSource.getSourceTransformForMask(MaskInfo(0, 0)).estimateBounds(it) } + + paintera.baseView.orthogonalViews().requestRepaint(globalInterval) + } + + fun ReplaceLabelState<*>.blocksForLabels(scale0: Int, vararg labels: Long): List = with(maskedSource) { + val blocksFromSource = labels.flatMap { sourceState.labelBlockLookup.read(LabelBlockLookupKey(scale0, it)).toList() } + + /* Read from canvas access (if in canvas) */ + val cellGrid = getCellGrid(0, scale0) + val cellIntervals = cellGrid.cellIntervals().randomAccess() + val cellPos = LongArray(cellGrid.numDimensions()) + val blocksFromCanvas = labels.flatMap { + getModifiedBlocks(scale0, it).toArray().map { block -> + cellGrid.getCellGridPositionFlat(block, cellPos) + FinalInterval(cellIntervals.setPositionAndGet(*cellPos)) + } + } + + return blocksFromSource + blocksFromCanvas + } + +} + +private fun > MaskedSource>.addReplaceMaskAsSource( + replacedFragmentMask: IntervalView +): ConnectomicsLabelState { + val metadataState = (underlyingSource() as? N5DataSource)?.getMetadataState()!! + return paintera.baseView.addConnectomicsLabelSource( + replacedFragmentMask, + metadataState.resolution, + metadataState.translation, + 1L, + "fragmentMask", + LabelBlockLookupAllBlocks.fromSource(underlyingSource()) + )!! +} diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/actions/paint/ReplaceLabelState.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/actions/paint/ReplaceLabelState.kt new file mode 100644 index 000000000..5a26aa4da --- /dev/null +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/actions/paint/ReplaceLabelState.kt @@ -0,0 +1,93 @@ +package org.janelia.saalfeldlab.paintera.control.actions.paint + +import javafx.beans.property.BooleanProperty +import javafx.beans.property.LongProperty +import javafx.beans.property.SimpleBooleanProperty +import javafx.beans.property.SimpleLongProperty +import javafx.collections.FXCollections +import javafx.collections.ObservableList +import javafx.event.Event +import net.imglib2.type.numeric.IntegerType +import org.janelia.saalfeldlab.fx.actions.Action +import org.janelia.saalfeldlab.paintera.control.actions.ActionState +import org.janelia.saalfeldlab.paintera.control.actions.verify +import org.janelia.saalfeldlab.paintera.control.modes.PaintLabelMode +import org.janelia.saalfeldlab.paintera.control.tools.paint.StatePaintContext +import org.janelia.saalfeldlab.paintera.paintera +import org.janelia.saalfeldlab.paintera.state.label.ConnectomicsLabelState + +interface ReplaceLabelUIState { + + val activeFragment: Long + val activeSegment: Long + val allActiveFragments: LongArray + val allActiveSegments: LongArray + val fragmentsForActiveSegment: LongArray + val fragmentsForAllActiveSegments: LongArray + + val fragmentsToReplace: ObservableList + val replacementLabel: LongProperty + val activeReplacementLabel: BooleanProperty + + fun fragmentsForSegment(segment: Long): LongArray + fun nextId(): Long + +} + +class ReplaceLabelState() : ActionState(), ReplaceLabelUIState + where T : IntegerType { + internal lateinit var sourceState: ConnectomicsLabelState<*, *> + internal lateinit var paintContext: StatePaintContext + + internal val maskedSource + get() = paintContext.dataSource + + internal val assignment + get() = paintContext.assignment + + private val selectedIds + get() = paintContext.selectedIds + + override val activeFragment + get() = selectedIds.lastSelection + + override val activeSegment + get() = assignment.getSegment(activeFragment) + + override val fragmentsForActiveSegment: LongArray + get() = assignment.getFragments(activeSegment).toArray() + + override val allActiveFragments: LongArray + get() = selectedIds.activeIds.toArray() + + override val allActiveSegments + get() = allActiveFragments.asSequence() + .map { assignment.getSegment(it) } + .toSet() + .toLongArray() + + override val fragmentsForAllActiveSegments + get() = allActiveSegments.asSequence() + .flatMap { assignment.getFragments(it).toArray().asSequence() } + .toSet() + .toLongArray() + + override fun fragmentsForSegment(segment: Long): LongArray { + return assignment.getFragments(segment).toArray() + } + + override val fragmentsToReplace: ObservableList = FXCollections.observableArrayList() + override val replacementLabel: LongProperty = SimpleLongProperty(0L) + override val activeReplacementLabel = SimpleBooleanProperty(false) + + override fun nextId() = sourceState.nextId() + + override fun Action.verifyState() { + verify(::sourceState, "Label Source is Active") { paintera.currentSource as? ConnectomicsLabelState<*, *> } + verify(::paintContext, "Paint Label Mode has StatePaintContext") { PaintLabelMode.statePaintContext as StatePaintContext } + + verify("Paint Label Mode is Active") { paintera.currentMode is PaintLabelMode } + verify("Paintera is not disabled") { !paintera.baseView.isDisabledProperty.get() } + verify("Mask not in use") { !paintContext.dataSource.isMaskInUseBinding().get() } + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/actions/paint/ReplaceLabelUI.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/actions/paint/ReplaceLabelUI.kt new file mode 100644 index 000000000..38386ffa8 --- /dev/null +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/actions/paint/ReplaceLabelUI.kt @@ -0,0 +1,402 @@ +package org.janelia.saalfeldlab.paintera.control.actions.paint + +import javafx.beans.property.* +import javafx.collections.FXCollections +import javafx.collections.ObservableList +import javafx.event.ActionEvent +import javafx.event.EventHandler +import javafx.geometry.Insets +import javafx.geometry.Pos +import javafx.scene.Node +import javafx.scene.Scene +import javafx.scene.control.* +import javafx.scene.control.cell.TextFieldListCell +import javafx.scene.layout.HBox +import javafx.scene.layout.HBox.setHgrow +import javafx.scene.layout.Pane +import javafx.scene.layout.Priority +import javafx.scene.layout.VBox +import javafx.stage.Stage +import javafx.util.StringConverter +import javafx.util.converter.LongStringConverter +import kotlinx.coroutines.delay +import org.controlsfx.control.SegmentedButton +import org.janelia.saalfeldlab.fx.extensions.createObservableBinding +import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread +import org.janelia.saalfeldlab.paintera.control.actions.paint.ReplaceLabelUI.ReplaceIdSelection.Companion.getReplaceIdSelectionButtons +import org.janelia.saalfeldlab.paintera.control.actions.paint.ReplaceLabelUI.ReplaceIdSelection.entries +import org.janelia.saalfeldlab.paintera.control.actions.paint.ReplaceLabelUI.ReplaceTargetSelection.Companion.getReplaceTargetSelectionButtons +import org.janelia.saalfeldlab.paintera.control.actions.paint.ReplaceLabelUI.ReplaceTargetSelection.entries +import kotlin.math.nextUp + +private const val ACTIVE_FRAGMENT_TOOLTIP = "Replace the currently active Fragment" +private const val ACTIVE_FRAGMENTS_TOOLTIP = "Replace all currently active Fragments" +private const val ACTIVE_SEGMENT_TOOLTIP = "Replace all fragments for the currently active Segment" +private const val ACTIVE_SEGMENTS_TOOLTIP = "Replace all fragments from all currently active Segments" +private const val RESET_SELECTION_TOOLTIP = "Clear Fragment and Segment Replacement Lists" + +class ReplaceLabelUI( + val state: ReplaceLabelUIState +) : VBox() { + + private enum class ReplaceIdSelection( + val text: String, + val tooltip: String, + val fragments: ReplaceLabelUIState.() -> LongArray, + val segments: ReplaceLabelUIState.() -> LongArray + ) { + ACTIVE_FRAGMENT( + "Active Fragment", + ACTIVE_FRAGMENT_TOOLTIP, + { longArrayOf(activeFragment) }, + { longArrayOf() } + ), + ACTIVE_FRAGMENTS( + "All Active Fragments", + ACTIVE_FRAGMENTS_TOOLTIP, + { allActiveFragments }, + { longArrayOf() } + ), + ACTIVE_SEGMENT( + "Segment For Active Fragment", + ACTIVE_SEGMENT_TOOLTIP, + { fragmentsForActiveSegment }, + { longArrayOf(activeSegment) } + ), + ACTIVE_SEGMENTS( + "Segments For All Active Fragments", + ACTIVE_SEGMENTS_TOOLTIP, + { fragmentsForAllActiveSegments }, + { allActiveSegments } + ), + RESET( + "Reset", + RESET_SELECTION_TOOLTIP, + { longArrayOf() }, + { longArrayOf() } + ); + + fun fragmentsToReplace(state: ReplaceLabelUIState) = state.run { fragments() } + fun segmentsToReplace(state: ReplaceLabelUIState) = state.run { segments() } + + fun button(ui: ReplaceLabelUI) = Button().apply { + setHgrow(this, Priority.ALWAYS) + text = this@ReplaceIdSelection.text + tooltip = Tooltip().apply { + text = this@ReplaceIdSelection.tooltip + } + userData = this@ReplaceIdSelection + onAction = EventHandler { + ui.fragmentsToReplace.setAll(*fragmentsToReplace(ui.state).toTypedArray()) + ui.segmentsToReplace.setAll(*segmentsToReplace(ui.state).toTypedArray()) + } + } + + companion object { + fun ReplaceLabelUI.getReplaceIdSelectionButtons() = entries.filter { it != RESET }.map { it.button(this) } + } + } + + private enum class ReplaceTargetSelection(val text: String, val tooltip: String, val label: Long? = null) { + REPLACE("Replace Label", "Specify Replacement Label"), + DELETE("Delete Label", "Replace Label with 0", 0); + + val ReplaceLabelUI.button: ToggleButton + get() = ToggleButton().apply { + text = this@ReplaceTargetSelection.text + tooltip = Tooltip(this@ReplaceTargetSelection.tooltip) + userData = this@ReplaceTargetSelection + onAction = EventHandler { + replaceWithIdFormatter.value = label + } + } + + companion object { + fun ReplaceLabelUI.getReplaceTargetSelectionButtons() = entries.map { it.run { button } } + } + } + + private val fragmentsToReplace = state.fragmentsToReplace + private val segmentsToReplace = FXCollections.observableArrayList() + + private val replaceIdButtonBar = let { + val buttons = getReplaceIdSelectionButtons().toTypedArray() + /* default replace ID behavior is ACTIVE_SEGMENT */ + buttons.first { it.userData == ReplaceIdSelection.ACTIVE_SEGMENT }.fire() + HBox(*buttons) + } + + + private val replaceActionButtonBar = let { + val buttons = getReplaceTargetSelectionButtons().toTypedArray() + /* default replace action is DELETE */ + buttons.first { toggle -> toggle.userData == ReplaceTargetSelection.DELETE }.isSelected = true + SegmentedButton(*buttons) + } + + private val deleteToggled = replaceActionButtonBar.toggleGroup + .selectedToggleProperty() + .createObservableBinding { it.value?.userData == ReplaceTargetSelection.DELETE } + + private val replaceWithIdFormatter = InvalidLongLabelFormatter().also { + state.replacementLabel.unbind() + state.replacementLabel.bind(it.valueProperty()) + } + private val replaceWithIdField = TextField().hGrow { + promptText = "ID to Replace Fragment/Segment IDs with... " + textFormatter = replaceWithIdFormatter + disableProperty().bind(deleteToggled) + } + + + private val fragmentsListView = ListView(fragmentsToReplace).apply { + isEditable = true + cellFactory = TextFieldListCell.forListView(LongStringConverter()).apply { + } + } + + private val addFragmentField = TextField().hGrow { + promptText = "Add a Fragment ID to Replace..." + textFormatter = InvalidLongLabelFormatter() + onAction = EventHandler { submitFragmentHandler() } + } + + private val segmentsListView = ListView(segmentsToReplace).apply { + isEditable = true + cellFactory = TextFieldListCell.forListView(LongStringConverter()) + } + private val addSegmentField = TextField().hGrow { + promptText = "Add a Segment ID to Replace..." + textFormatter = InvalidLongLabelFormatter() + onAction = EventHandler { submitSegmentHandler() } + } + + private val submitSegmentHandler: () -> Unit = { + addSegmentField.run { + commitValue() + (textFormatter as? InvalidLongLabelFormatter)?.run { + value?.let { addSegment(it) } + value = null + } + } + } + + private val submitFragmentHandler: () -> Unit = { + addFragmentField.run { + commitValue() + (textFormatter as? InvalidLongLabelFormatter)?.run { + value?.let { addFragment(it) } + value = null + } + } + } + + private fun addSegment(segment: Long) { + fragmentsToReplace += state.fragmentsForSegment(segment) + .filter { it !in fragmentsToReplace } + .toSet() + segment.takeIf { it !in segmentsToReplace }?.let { segmentsToReplace += it } + } + + private fun addFragment(fragment: Long) { + fragment.takeIf { it !in fragmentsToReplace }?.let { fragmentsToReplace += it } + } + + private val setNextNewID = EventHandler { + replaceWithIdFormatter.value = state.nextId() + } + + val progressBarProperty = SimpleDoubleProperty(0.0) + val progressLabelText = SimpleStringProperty("Progress: ") + + init { + + hvGrow() + spacing = 10.0 + padding = Insets(10.0) + children += VBox().apply { + spacing = 5.0 + children += HBox().apply { + children += Label("Replace Labels By: ").also { alignment = Pos.BOTTOM_CENTER } + children += Pane().hGrow() + children += ReplaceIdSelection.RESET.button(this@ReplaceLabelUI).apply { + onAction = onAction?.let { oldAction -> + // wrap to also set the target value to 0L on reset + EventHandler { + oldAction.handle(it) + replaceWithIdFormatter.value = 0 + } + } + } + } + children += replaceIdButtonBar.hGrow() + } + + children += VBox().apply { + spacing = 5.0 + children += Label("Delete Or Replace ") + children += HBox().apply { + spacing = 5.0 + children += replaceActionButtonBar + children += Pane().hGrow() + children += Label("Activate Label After Replacement?").apply { + disableProperty().bind(deleteToggled) + } + children += CheckBox().apply { + state.activeReplacementLabel.bind(selectedProperty()) + disableProperty().bind(deleteToggled) + deleteToggled.subscribe { it -> + if (it) isSelected = false + } + } + } + children += HBox().hGrow { + padding = Insets(5.0, 0.0, 0.0, 0.0) + spacing = 5.0 + children += Label("Replace With ID: ") + children += replaceWithIdField.hGrow() + children += Button("Next New ID").apply { + onAction = setNextNewID + disableProperty().bind(deleteToggled) + } + } + } + children += HBox().hvGrow { + children += VBox().hvGrow { + alignment = Pos.CENTER + children += Label("Fragments to Replace") + children += fragmentsListView.hvGrow() + children += HBox().hGrow { + children += addFragmentField + children += Button("Add Fragment ID").apply { + onAction = EventHandler { submitFragmentHandler() } + } + } + } + children += VBox().hvGrow { + alignment = Pos.CENTER + children += Label("Segments to Replace") + children += segmentsListView.hvGrow() + children += HBox().hGrow { + children += addSegmentField + children += Button("Add Segment ID").apply { + onAction = EventHandler { submitSegmentHandler() } + + } + } + } + } + children += HBox().hGrow { + spacing = 10.0 + children += Label().hGrow().apply { + alignment = Pos.CENTER_LEFT + textProperty().bind(progressLabelText) + maxWidthProperty().bind(this@hGrow.widthProperty().createObservableBinding { it.doubleValue() * .2 }) + } + children += ProgressBar().hGrow { + maxWidth = Double.MAX_VALUE + progressProperty().bind(progressBarProperty) + } + } + } + + companion object { + + private class InvalidLongLabelFormatter : TextFormatter( + object : StringConverter() { + override fun toString(`object`: Long?) = `object`?.takeIf { it >= 0 }?.let { "$it" } + override fun fromString(string: String?) = string?.toLongOrNull() + }, + null, + { + try { + it.text.toLong() + } catch (_: NumberFormatException) { + it.text = "" + } + it + } + ) + + private fun T.hGrow(apply: (T.() -> Unit)? = { }): T { + setHgrow(this, Priority.ALWAYS) + apply?.invoke(this) + return this + } + + private fun T.vGrow(apply: (T.() -> Unit)? = { }): T { + setVgrow(this, Priority.ALWAYS) + apply?.invoke(this) + return this + } + + private fun T.hvGrow(apply: (T.() -> Unit)? = { }): T { + hGrow() + vGrow() + apply?.invoke(this) + return this + } + } +} + +fun main() { + InvokeOnJavaFXApplicationThread { + + val state = object : ReplaceLabelUIState { + override val activeFragment: Long + get() = 123 + override val activeSegment: Long + get() = 456 + override val allActiveFragments: LongArray + get() = longArrayOf(1, 2, 3, 4) + override val allActiveSegments: LongArray + get() = longArrayOf(1, 2, 3) + override val fragmentsForActiveSegment: LongArray + get() = longArrayOf(1, 2) + override val fragmentsForAllActiveSegments: LongArray + get() = longArrayOf(1, 2, 3, 4, 5, 6, 7, 8, 9) + override val activeReplacementLabel = SimpleBooleanProperty(false) + + override val fragmentsToReplace: ObservableList = FXCollections.observableArrayList() + override val replacementLabel: LongProperty = SimpleLongProperty() + + override fun fragmentsForSegment(segment: Long): LongArray { + return LongArray((0..10).random()) { (0..100L).random() } + } + + var next = 0L + + override fun nextId(): Long { + return ++next + } + } + + val root = VBox() + root.apply { + children += Button("Reload").apply { + onAction = EventHandler { + root.children.removeIf { it is ReplaceLabelUI } + root.children.add(ReplaceLabelUI(state)) + } + } + children += ReplaceLabelUI(state) + } + + InvokeOnJavaFXApplicationThread { + delay(200) + var prev = root.children.firstNotNullOf { it as? ReplaceLabelUI }.progressBarProperty.get() + while( prev < 1.0 ) { + prev = prev + .05 + root.children.firstNotNullOf { it as? ReplaceLabelUI }.progressBarProperty.set(prev) + delay(200) + } + } + + + val scene = Scene(root) + val stage = Stage() + stage.scene = scene + stage.show() + } +} + diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/PaintLabelMode.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/PaintLabelMode.kt index a7c867768..1d2bdc01b 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/PaintLabelMode.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/PaintLabelMode.kt @@ -32,7 +32,7 @@ import org.janelia.saalfeldlab.paintera.control.ShapeInterpolationController import org.janelia.saalfeldlab.paintera.control.actions.AllowedActions import org.janelia.saalfeldlab.paintera.control.actions.LabelActionType import org.janelia.saalfeldlab.paintera.control.actions.PaintActionType -import org.janelia.saalfeldlab.paintera.control.actions.paint.SmoothAction.onAction +import org.janelia.saalfeldlab.paintera.control.actions.paint.ReplaceLabel import org.janelia.saalfeldlab.paintera.control.tools.Tool import org.janelia.saalfeldlab.paintera.control.tools.paint.* import org.janelia.saalfeldlab.paintera.control.tools.paint.PaintTool.Companion.createPaintStateContext @@ -71,6 +71,7 @@ object PaintLabelMode : AbstractToolMode() { *getToolTriggers().toTypedArray(), enterShapeInterpolationMode, getSelectNextIdActions(), + getDeleteReplaceIdActions(), getResetMaskAction(), ) } @@ -147,6 +148,7 @@ object PaintLabelMode : AbstractToolMode() { switchTool(null) selectViewerBefore { switchModes() } } + else -> switchModes() } } @@ -184,7 +186,7 @@ object PaintLabelMode : AbstractToolMode() { } } - override fun switchTool(tool: Tool?) : Job? { + override fun switchTool(tool: Tool?): Job? { val switchToolJob = super.switchTool(tool) /*SAM Tool restrict the active ViewerPanel, so we don't want it changing on mouseover of the other views, for example */ (tool as? SamTool)?.let { runBlocking { switchToolJob?.join() } } @@ -216,15 +218,16 @@ object PaintLabelMode : AbstractToolMode() { } } + private fun getDeleteReplaceIdActions() = painteraActionSet("delete_label", LabelActionType.Delete) { + KEY_PRESSED(DELETE_ID) { + verify("ReplaceLabel is valid") { ReplaceLabel.isValid(it) } + onAction { ReplaceLabel.showDialog() } + } + } + private fun getResetMaskAction() = painteraActionSet("Force Mask Reset", PaintActionType.Paint, ignoreDisable = true) { KEY_PRESSED(KeyCode.SHIFT, KeyCode.ESCAPE) { - verify { - statePaintContext?.let { state -> - (state.dataSource as? MaskedSource<*, *>)?.let { maskedSource -> - maskedSource.currentMask?.let { true } ?: false - } ?: false - } ?: false - } + verify("has current mask") { (statePaintContext as? MaskedSource<*, *>)?.currentMask != null } onAction { InvokeOnJavaFXApplicationThread { PainteraAlerts.confirmation("Yes", "No", false, paintera.pane.scene.window).apply { diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/menus/PainteraMenus.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/menus/PainteraMenus.kt index b2bdd1a5f..73ab89e3a 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/menus/PainteraMenus.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/menus/PainteraMenus.kt @@ -13,7 +13,8 @@ import javafx.scene.control.SeparatorMenuItem import org.janelia.saalfeldlab.fx.extensions.LazyForeignValue import org.janelia.saalfeldlab.fx.ui.MatchSelectionMenu import org.janelia.saalfeldlab.paintera.Paintera -import org.janelia.saalfeldlab.paintera.control.actions.paint.SmoothAction +import org.janelia.saalfeldlab.paintera.control.actions.paint.ReplaceLabel +import org.janelia.saalfeldlab.paintera.control.actions.paint.SmoothLabel import org.janelia.saalfeldlab.paintera.paintera import org.janelia.saalfeldlab.paintera.ui.FontAwesome import org.janelia.saalfeldlab.paintera.ui.PainteraAlerts @@ -80,7 +81,7 @@ private val viewMenu by LazyForeignValue(::paintera) { viewer3DMenu ) } -private val actionMenu by LazyForeignValue(::paintera) { Menu("_Actions", null, SmoothAction.menuItem) } //, DilateAction.menuItem) } +private val actionMenu by LazyForeignValue(::paintera) { Menu("_Actions", null, SmoothLabel.menuItem, ReplaceLabel.menuItem) } private val helpMenu by LazyForeignValue(::paintera) { Menu("_Help", null, SHOW_README.menu, SHOW_KEY_BINDINGS.menu, showVersion) } val menuBar by LazyForeignValue(::paintera) {