Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
de.sciss.mellite.edit.Edits.scala Maven / Gradle / Ivy
/*
* Edits.scala
* (Mellite)
*
* Copyright (c) 2012-2023 Hanns Holger Rutz. All rights reserved.
*
* This software is published under the GNU Affero General Public License v3+
*
*
* For further information, please contact Hanns Holger Rutz at
* [email protected]
*/
package de.sciss.mellite.edit
import de.sciss.lucre.edit.{EditAttrMap, EditFolder, EditGrapheme, EditTimeline, UndoManager}
import de.sciss.lucre.swing.edit.EditVar
import de.sciss.lucre.{BooleanObj, DoubleObj, DoubleVector, Expr, Folder, IntObj, LongObj, Obj, SpanLikeObj, StringObj, Txn, expr}
import de.sciss.mellite.Log.log
import de.sciss.mellite.TimelineTool.{EmptyFade, Fade, Gain, Move, Mute, Resize}
import de.sciss.mellite.{GraphemeTool, ObjTimelineView, ProcActions, TimelineView}
import de.sciss.proc.{AudioCue, CurveObj, EnvSegment, FadeSpec, Grapheme, ObjKeys, Proc, Timeline}
import de.sciss.span.{Span, SpanLike}
import de.sciss.synth.Curve
import scala.collection.immutable.{Seq => ISeq}
import scala.math.{max, min}
import scala.reflect.ClassTag
object Edits {
def setBus[T <: Txn[T]](objects: Iterable[Obj[T]], intExpr: IntObj[T])
(implicit tx: T, undo: UndoManager[T]): Unit = {
val name = "Set Bus"
undo.capture(name) {
objects.iterator.map { obj =>
EditAttrMapExpr[T, Int, IntObj](name, obj, ObjKeys.attrBus, Some(intExpr))
}
}
}
// def setSynthGraph[T <: Txn[T]](processes: Iterable[Proc[T]], codeElem: Code.Obj[T])
// (implicit tx: T, undo: UndoManager[T],
// compiler: Code.Compiler): Unit = {
// val code = codeElem.value
// code match {
// case csg: Code.Proc =>
// val sg = try {
// csg.execute {} // XXX TODO: compilation blocks, not good!
// } catch {
// case NonFatal(e) =>
// e.printStackTrace()
// return None
// }
//
// var scanInKeys = Set.empty[String]
// var scanOutKeys = Set.empty[String]
//
// sg.sources.foreach {
// case synth.proc.graph.ScanIn (key) => scanInKeys += key
// case synth.proc.graph.ScanOut (key, _) => scanOutKeys += key
// case synth.proc.graph.ScanInFix(key, _) => scanInKeys += key
// case _ =>
// }
//
// // sg.sources.foreach(println)
// if (scanInKeys .nonEmpty) log.debug(s"SynthDef has the following scan in keys: ${scanInKeys .mkString(", ")}")
// if (scanOutKeys.nonEmpty) log.debug(s"SynthDef has the following scan out keys: ${scanOutKeys.mkString(", ")}")
//
// val editName = "Set Synth Graph"
// val attrNameOpt = codeElem.attr.get(ObjKeys.attrName)
// val edits = List.newBuilder[UndoableEdit]
//
// processes.foreach { p =>
// val graphEx = Proc.GraphObj.newConst[T](sg) // XXX TODO: ideally would link to code updates
// val edit1 = EditVar.Expr[T, SynthGraph, Proc.GraphObj](editName, p.graph, graphEx)
// edits += edit1
// if (attrNameOpt.nonEmpty) {
// val edit2 = EditAttrMap("Set Object Name", p, ObjKeys.attrName, attrNameOpt)
// edits += edit2
// }
//
// ???! // SCAN
//// def check(scans: Scans[T], keys: Set[String], isInput: Boolean): Unit = {
//// val toRemove = scans.iterator.collect {
//// case (key, scan) if !keys.contains(key) && scan.isEmpty => key
//// }
//// if (toRemove.nonEmpty) toRemove.foreach { key =>
//// edits += EditRemoveScan(p, key = key, isInput = isInput)
//// }
////
//// val existing = scans.iterator.collect {
//// case (key, _) if keys contains key => key
//// }
//// val toAdd = keys -- existing.toSet
//// if (toAdd.nonEmpty) toAdd.foreach { key =>
//// edits += EditAddScan(p, key = key, isInput = isInput)
//// }
//// }
////
//// val proc = p
//// check(proc.inputs , scanInKeys , isInput = true )
//// check(proc.outputs, scanOutKeys, isInput = false)
// }
//
// CompoundEdit(edits.result(), editName)
//
// case _ => None
// }
// }
def setName[T <: Txn[T]](obj: Obj[T], nameOpt: Option[StringObj[T]])
(implicit tx: T, undo: UndoManager[T]): Unit =
EditAttrMapExpr[T, String, StringObj]("Rename Object", obj, ObjKeys.attrName, nameOpt)
def addLink[T <: Txn[T]](source: Proc.Output[T], sink: Proc[T], key: String = Proc.mainIn)
(implicit tx: T, undo: UndoManager[T]): Unit = {
log.debug(s"Link $source to $sink / $key")
// source.addSink(Scan.Link.Scan(sink))
// EditAddScanLink(source = source /* , sourceKey */ , sink = sink /* , sinkKey */)
val aSink = sink.attr
aSink.get(key) match {
case Some(f: Folder[T]) =>
EditFolder.appendUndo(parent = f, child = source)
case Some(other) =>
val f = Folder[T]()
f.addLast(other)
f.addLast(source)
EditAttrMap.putUndo(/*"Add Link",*/ aSink, key = key, value = f)
case None =>
EditAttrMap.putUndo(/*"Add Link",*/ aSink, key = key, value = source)
}
}
def removeLink[T <: Txn[T]](link: Link[T])
(implicit tx: T, undo: UndoManager[T]): Unit = {
log.debug(s"Unlink $link")
link.sinkType match {
case SinkDirect() =>
EditAttrMap.removeUndo(/*"Remove Link",*/ link.sink.attr, key = link.key)
case SinkFolder(f, index) =>
EditFolder.removeAtUndo(/*"Link",*/ parent = f, index = index /*, child = link.source*/)
}
}
sealed trait SinkType[T <: Txn[T]]
final case class SinkDirect[T <: Txn[T]]() extends SinkType[T]
final case class SinkFolder[T <: Txn[T]](f: Folder[T], index: Int) extends SinkType[T]
final case class Link[T <: Txn[T]](source: Proc.Output[T], sink: Proc[T], key: String, sinkType: SinkType[T])
def findLink[T <: Txn[T]](out: Proc[T], in: Proc[T], keys: ISeq[String] = Proc.mainIn :: Nil)
(implicit tx: T): Option[Link[T]] = {
val attr = in.attr
val it = out.outputs.iterator.flatMap { out =>
keys.iterator.flatMap { key =>
val sinkTypeOpt = attr.get(key).flatMap {
case `out` => Some(SinkDirect[T]())
case f: Folder[T] =>
val idx = f.indexOf(out)
if (idx < 0) None else Some(SinkFolder[T](f, index = idx))
case _ => None
}
sinkTypeOpt.map(tpe => Link(source = out, sink = in, key = key, sinkType = tpe))
} // .headOption
}
if (it.isEmpty) None else Some(it.next()) // XXX TODO -- why no `headOption` on iterator?
}
def linkOrUnlink[T <: Txn[T]](out: Proc[T], in: Proc[T])
(implicit tx: T, undo: UndoManager[T]): Boolean = {
findLink(out = out, in = in).fold[Boolean] {
out.outputs.get(Proc.mainOut).exists { out =>
val key = Proc.mainIn
addLink(source = out, sink = in, key = key)
true
}
} { link =>
removeLink(link)
true
}
}
// def unlinkAndRemove[T <: Txn[T]](timeline: proc.Timeline.Modifiable[T], span: SpanLikeObj[T], obj: Obj[T])
// (implicit tx: T, cursor: Cursor[T]): UndoableEdit = {
// val scanEdits = obj match {
// case _: Proc[T] =>
// // val proc = objT
// // // val scans = proc.scans
// // val edits1 = proc.inputs.iterator.toList.flatMap { case (key, scan) =>
// // scan.iterator.collect {
// // case Scan.Link.Scan(source) =>
// // removeLink(source, scan)
// // }.toList
// // }
// // val edits2 = proc.outputs.iterator.toList.flatMap { case (key, scan) =>
// // scan.iterator.collect {
// // case Scan.Link.Scan(sink) =>
// // removeLink(scan, sink)
// // } .toList
// // }
// // edits1 ++ edits2
// ???! // SCAN
//
// case _ => Nil
// }
// val name = "Remove Object"
// val objEdit = EditTimelineRemoveObj(name, timeline, span, obj)
// CompoundEdit(scanEdits :+ objEdit, name).get // XXX TODO - not nice, `get`
// }
// private def any2stringadd: Any = ()
def adjustAttr[T <: Txn[T], A, Repr[~ <: Txn[~]] <: Expr[~, A]](obj: Obj[T], key: String, arg: A, editName: String,
default: A)(combine: (A, A) => A)
(implicit tx: T, undo: UndoManager[T], tpe: Expr.Type[A, Repr],
ct: ClassTag[Repr[T]]): Boolean =
obj.attr.$[Repr](key) match {
case Some(tpe.Var(vr)) =>
// XXX TODO could be more elaborate; for now preserve just one level of variables
EditVar.exprUndo[T, A, Repr](editName, vr, tpe.newConst(combine(vr().value, arg)))
true
case other =>
import de.sciss.equal.Implicits._
val v = combine(other.fold(default)(_.value), arg)
val valueOpt = if (v === default) None else Some(tpe.newVar[T](tpe.newConst(v)))
if (other.isEmpty && valueOpt.isEmpty) false else {
EditAttrMapExpr[T, A, Repr](editName, obj, key, valueOpt)
true
}
}
def gain[T <: Txn[T]](obj: Obj[T], amount: Gain)
(implicit tx: T, undo: UndoManager[T]): Boolean = {
import amount.factor
if (factor == 1f) false else {
adjustAttr[T, Double, DoubleObj](obj, key = ObjKeys.attrGain, arg = factor.toDouble, editName = "Adjust Gain",
default = 1.0)(_ * _)
}
}
def mute[T <: Txn[T]](obj: Obj[T], state: Mute)
(implicit tx: T, undo: UndoManager[T]): Boolean = {
import state.engaged
adjustAttr[T, Boolean, BooleanObj](obj, key = ObjKeys.attrMute, arg = engaged, editName = "Adjust Mute",
default = false)((_, now) => now)
}
def resize[T <: Txn[T]](span: SpanLikeObj[T], obj: Obj[T], amount: Resize, minStart: Long)
(implicit tx: T, undo: UndoManager[T]): Boolean = {
import de.sciss.equal.Implicits._
val nameResize = "Resize"
var edited = false
undo.capture(nameResize) {
// time
span match {
case SpanLikeObj.Var(vr) =>
import amount.{deltaStart, deltaStop}
val oldSpan = span.value
// val minStart = timelineModel.bounds.start
val dStartC = if (deltaStart >= 0) deltaStart else oldSpan match {
case Span.HasStart(oldStart) => math.max(-(oldStart - minStart) , deltaStart)
case _ => 0L
}
val dStopC = if (deltaStop >= 0) deltaStop else oldSpan match {
case Span.HasStop (oldStop) => math.max(-(oldStop - minStart + 32 /* MinDur */), deltaStop)
case _ => 0L
}
if (dStartC != 0L || dStopC != 0L) {
// val imp = ExprImplicits[T]
// XXX TODO -- the variable contents should ideally be looked at
// during the edit performance
val oldSpan = vr()
val newSpan = oldSpan.value match {
case Span.From (start) => Span.From (start + dStartC)
case Span.Until(stop ) => Span.Until(stop + dStopC )
case Span(start, stop) =>
val newStart = start + dStartC
Span(newStart, math.max(newStart + 32 /* MinDur */, stop + dStopC))
case other => other
}
val newSpanEx = SpanLikeObj.newConst[T](newSpan)
if (newSpanEx !== oldSpan) {
EditVar.exprUndo[T, SpanLike, SpanLikeObj](nameResize, vr, newSpanEx)
edited = true
if (dStartC != 0L) obj match {
case objT: Proc[T] =>
ProcActions.getAudioRegion(objT).foreach { audioCue =>
// Crazy heuristics
audioCue match {
case AudioCue.Obj.Shift(peer, amt) =>
import expr.Ops._
amt match {
case LongObj.Var(amtVr) =>
// XXX TODO why we use EditVar.Expr here and EditAttrMap elsewhere?
// I think we should always edit the variable and not copy the contents
// of the variable to a new variable?
EditVar.exprUndo[T, Long, LongObj](nameResize, amtVr, amtVr() + dStartC)
edited = true
case _ =>
val newCue = AudioCue.Obj.Shift(peer, LongObj.newVar[T](amt + dStartC))
EditAttrMap.putUndo(/*nameResize,*/ objT.attr, Proc.graphAudio, value = newCue)
edited = true
}
case other =>
val newCue = AudioCue.Obj.Shift(other, LongObj.newVar[T](dStartC))
EditAttrMap.putUndo(/*nameResize,*/ objT.attr, Proc.graphAudio, value = newCue)
edited = true
}
}
case _ =>
}
}
}
case _ =>
}
val nameTrack = "Adjust Track Placement"
// track
val deltaTrack = amount.deltaTrackStart
if (deltaTrack != 0) {
edited |= adjustAttr[T, Int, IntObj](obj, key = ObjTimelineView.attrTrackIndex, arg = deltaTrack,
editName = nameTrack, default = 0)(_ + _)
}
val deltaTrackH = amount.deltaTrackStop - amount.deltaTrackStart
if (deltaTrackH != 0) {
edited |= adjustAttr[T, Int, IntObj](obj, key = ObjTimelineView.attrTrackHeight, arg = deltaTrackH,
editName = "Adjust Track Height", default = TimelineView.DefaultTrackHeight)(_ + _)
}
}
edited
}
def fade[T <: Txn[T]](span: SpanLikeObj[T], obj: Obj[T], amount: Fade)
(implicit tx: T, undo: UndoManager[T]): Boolean = {
import amount._
val attr = obj.attr
val exprIn = attr.$[FadeSpec.Obj](ObjKeys.attrFadeIn )
val exprOut = attr.$[FadeSpec.Obj](ObjKeys.attrFadeOut)
val valIn = exprIn .fold(EmptyFade)(_.value)
val valOut = exprOut.fold(EmptyFade)(_.value)
val total = span.value match {
case Span(start, stop) => stop - start
case _ => Long.MaxValue
}
val dIn = max(-valIn.numFrames, min(total - (valIn.numFrames + valOut.numFrames), deltaFadeIn))
val valInC = valIn.curve match {
case Curve.linear => 0f
case Curve.parametric(curvature) => curvature
case _ => Float.NaN
}
val dInC = if (valInC.isNaN) 0f else max(-20, min(20, deltaFadeInCurve + valInC)) - valInC
var edited = false
undo.capture("Adjust Fade") {
val newValIn = if (dIn != 0L || dInC != 0f) {
val curve = if (valInC.isNaN) valIn.curve else {
val newInC = valInC + dInC
if (newInC == 0f) Curve.linear else Curve.parametric(newInC)
}
val numFr = valIn.numFrames + dIn
val arg = FadeSpec(numFr, curve, valIn.floor)
edited |= adjustAttr[T, FadeSpec, FadeSpec.Obj](obj, key = ObjKeys.attrFadeIn, arg = arg,
editName = "Adjust Fade-In", default = FadeSpec(0L))((_, now) => now)
arg
} else valIn
// XXX TODO: DRY
val dOut = max(-valOut.numFrames, min(total - newValIn.numFrames, deltaFadeOut))
val valOutC = valOut.curve match {
case Curve.linear => 0f
case Curve.parametric(curvature) => curvature
case _ => Float.NaN
}
val dOutC = if (valOutC.isNaN) 0f else max(-20, min(20, deltaFadeOutCurve + valOutC)) - valOutC
if (dOut != 0L || dOutC != 0f) {
val curve = if (valOutC.isNaN) valOut.curve else {
val newOutC = valOutC + dOutC
if (newOutC == 0f) Curve.linear else Curve.parametric(newOutC)
}
val numFr = valOut.numFrames + dOut
val arg = FadeSpec(numFr, curve, valOut.floor)
edited |= adjustAttr[T, FadeSpec, FadeSpec.Obj](obj, key = ObjKeys.attrFadeOut, arg = arg,
editName = "Adjust Fade-Out", default = FadeSpec(0L))((_, now) => now)
}
}
edited
}
def timelineMoveOrCopy[T <: Txn[T]](span: SpanLikeObj[T], obj: Obj[T], timeline: Timeline[T], amount: Move, minStart: Long)
(implicit tx: T, undo: UndoManager[T]): Boolean =
if (amount.copy) timelineCopyImpl(span, obj, timeline, amount, minStart = minStart)
else timelineMoveImpl(span, obj, timeline, amount, minStart = minStart)
def graphemeMoveOrCopy[T <: Txn[T]](time: LongObj[T], obj: Obj[T], grapheme: Grapheme[T],
amount: GraphemeTool.Move, minStart: Long)
(implicit tx: T, undo: UndoManager[T]): Boolean =
if (amount.copy) ??? // graphemeCopyImpl(time, obj, grapheme, amount, minStart = minStart)
else graphemeMoveImpl(time, obj, grapheme, amount, minStart = minStart)
// ---- private ----
private def timelineCopyImpl[T <: Txn[T]](span: SpanLikeObj[T], obj: Obj[T], timeline: Timeline[T], amount: Move, minStart: Long)
(implicit tx: T, undo: UndoManager[T]): Boolean =
timeline.modifiableOption.exists { tlMod =>
val objCopy = ProcActions.copy[T](obj, connectInput = true)
import amount._
if (deltaTrack != 0) {
val newTrack: IntObj[T] = IntObj.newVar[T](
obj.attr.$[IntObj](ObjTimelineView.attrTrackIndex).map(_.value).getOrElse(0) + deltaTrack
)
objCopy.attr.put(ObjTimelineView.attrTrackIndex, newTrack)
}
val deltaC = calcSpanDeltaClipped(span, amount, minStart = minStart)
val newSpan: SpanLikeObj[T] = SpanLikeObj.newVar[T](
span.value.shift(deltaC)
)
EditTimeline.addUndo(/*"Insert Region",*/ tlMod, newSpan, elem = objCopy)
true
}
private def calcSpanDeltaClipped[T <: Txn[T]](span: SpanLikeObj[T], amount: Move, minStart: Long)
(implicit tx: T): Long = {
import amount.deltaTime
if (deltaTime >= 0) deltaTime else span.value match {
case Span.HasStart(oldStart) => math.max(-(oldStart - minStart ), deltaTime)
case Span.HasStop (oldStop ) => math.max(-(oldStop - minStart + 32), deltaTime)
case _ => 0 // e.g., Span.All
}
}
private def calcPosDeltaClipped[T <: Txn[T]](time: LongObj[T], amount: GraphemeTool.Move, minStart: Long)
(implicit tx: T): Long = {
import amount.deltaTime
if (deltaTime >= 0) deltaTime else {
val oldTime = time.value
math.max(-(oldTime - minStart), deltaTime)
}
}
private def timelineMoveImpl[T <: Txn[T]](span: SpanLikeObj[T], obj: Obj[T], timeline: Timeline[T], amount: Move,
minStart: Long)
(implicit tx: T, undo: UndoManager[T]): Boolean = {
val name = "Move"
var edited = false
undo.capture(name) {
import amount._
if (deltaTrack != 0) {
edited |= adjustAttr[T, Int, IntObj](obj, key = ObjTimelineView.attrTrackIndex, arg = deltaTrack,
editName = "Adjust Track Placement", default = 0)(_ + _)
}
val deltaC = calcSpanDeltaClipped(span, amount, minStart = minStart)
if (deltaC != 0L) {
// val imp = ExprImplicits[T]
span match {
case SpanLikeObj.Var(vr) =>
// s.transform(_ shift deltaC)
import expr.Ops._
val newSpan = vr() shift deltaC
EditVar.exprUndo[T, SpanLike, SpanLikeObj](name, vr, newSpan)
edited = true
case _ =>
}
}
}
edited
}
private def graphemeMoveImpl[T <: Txn[T]](time: LongObj[T], value: Obj[T], grapheme: Grapheme[T],
amount: GraphemeTool.Move, minStart: Long)
(implicit tx: T, undo: UndoManager[T]): Boolean = {
val name = "Move"
var wasRemoved = false
var edited = false
undo.capture(name) {
def removeOld(grMod: Grapheme.Modifiable[T]): Unit = {
require (!wasRemoved)
EditGrapheme.removeUndo(/*name,*/ grMod, time, value)
edited = true
wasRemoved = true
}
import amount._
val newValueOpt: Option[Obj[T]] = if (deltaModelY == 0) None else {
// in the case of const, just overwrite, in the case of
// var, check the value stored in the var, and update the var
// instead (recursion). otherwise, it will be some combinatorial
// expression, and we could decide to construct a binary op instead!
// XXX TODO -- do not hard code supported cases; should be handled by GraphemeObjView ?
def checkDouble(objT: DoubleObj[T]): Option[DoubleObj[T]] = {
import expr.Ops._
objT match {
case DoubleObj.Var(vr) =>
val newValue = vr() + deltaModelY
EditVar.applyUndo(name, vr, newValue)
edited = true
None
case _ =>
grapheme.modifiableOption.map { grMod =>
removeOld(grMod)
val v = objT.value + deltaModelY
DoubleObj.newVar[T](v) // "upgrade" to var
}
}
}
def checkDoubleVector(objT: DoubleVector[T]): Option[DoubleVector[T]] =
objT match {
case DoubleVector.Var(vr) =>
val v = vr().value.map(_ + deltaModelY)
// val newValue = DoubleVector.newConst[T](v)
EditVar.applyUndo[T, DoubleVector[T], DoubleVector.Var[T]](name, vr, v)
edited = true
None
case _ =>
grapheme.modifiableOption.map { grMod =>
removeOld(grMod)
val v = objT.value.map(_ + deltaModelY)
DoubleVector.newVar[T](v) // "upgrade" to var
}
}
// XXX TODO --- gosh what a horrible matching orgy
value match {
case objT: DoubleObj [T] => checkDouble (objT)
case objT: DoubleVector [T] => checkDoubleVector(objT)
case objT: EnvSegment.Obj[T] =>
objT match {
case EnvSegment.Obj.ApplySingle(start, curve) =>
val newStartOpt = checkDouble(start)
newStartOpt.map { newStart =>
EnvSegment.Obj.ApplySingle(newStart, curve)
}
case EnvSegment.Obj.ApplyMulti(start, curve) =>
val newStartOpt = checkDoubleVector(start)
newStartOpt.map { newStart =>
EnvSegment.Obj.ApplyMulti(newStart, curve)
}
case EnvSegment.Obj.Var(vr) =>
val seg = vr().value
val v = seg.startLevels match {
case Seq(single) =>
EnvSegment.Single (single + deltaModelY , seg.curve)
case multi =>
EnvSegment.Multi (multi.map(_ + deltaModelY), seg.curve)
}
EditVar.applyUndo[T, EnvSegment.Obj[T], EnvSegment.Obj.Var[T]](name, vr, v)
edited = true
None
case _ =>
grapheme.modifiableOption.map { grMod =>
removeOld(grMod)
val seg = objT.value
val curve = CurveObj.newVar[T](seg.curve)
seg.startLevels match {
case Seq(single) =>
val singleVar = DoubleObj.newVar[T](single)
EnvSegment.Obj.ApplySingle(singleVar, curve)
case multi =>
val multiVar = DoubleVector.newVar[T](multi)
EnvSegment.Obj.ApplyMulti(multiVar, curve)
}
}
}
case _ =>
None
}
}
val deltaC = calcPosDeltaClipped(time, amount, minStart = minStart)
val hasDeltaTime = deltaC != 0L
if (hasDeltaTime || newValueOpt.isDefined) {
val newTimeOpt: Option[LongObj[T]] = time match {
case LongObj.Var(vr) =>
import expr.Ops._
val newTime = vr() + deltaC
EditVar.exprUndo[T, Long, LongObj](name, vr, newTime)
edited = true
None
case _ if hasDeltaTime =>
grapheme.modifiableOption.map { grMod =>
if (newValueOpt.isEmpty) { // wasn't removed yet
removeOld(grMod)
}
val v = time.value + deltaC
LongObj.newConst[T](v) // "upgrade" to var
}
case _ =>
None
}
if (newTimeOpt.isDefined || newValueOpt.isDefined) {
val newTime = newTimeOpt .getOrElse(time)
val newValue = newValueOpt .getOrElse(value)
grapheme.modifiableOption.foreach { grMod =>
EditGrapheme.add(/*name,*/ grMod, newTime, newValue)
edited = true
}
}
}
}
edited
}
}