All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.sciss.mellite.impl.code.CodeFrameImpl.scala Maven / Gradle / Ivy

/*
 *  CodeFrameImpl.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.impl.code

import de.sciss.desktop.{Menu, Util}
import de.sciss.icons.raphael
import de.sciss.lucre.edit.{EditProgram, UndoManager}
import de.sciss.lucre.expr.CellView
import de.sciss.lucre.expr.graph.Ex
import de.sciss.lucre.swing.LucreSwing.deferTx
import de.sciss.lucre.swing.View
import de.sciss.lucre.swing.edit.EditVar
import de.sciss.lucre.swing.impl.ComponentHolder
import de.sciss.lucre.synth.Txn
import de.sciss.lucre.{BooleanObj, Disposable, Expr, Obj, Source, Txn => LTxn}
import de.sciss.mellite.edit.EditAddProcOutput
import de.sciss.mellite.impl.WorkspaceWindowImpl
import de.sciss.mellite.{ActionBounce, AttrMapView, CanBounce, CodeFrame, CodeView, GUI, ProcOutputsView, RunnerToggleButton, SplitPaneView, UniverseHandler, UniverseObjView, ViewState}
import de.sciss.model.Model
import de.sciss.proc.Code.Example
import de.sciss.proc.impl.MkSynthGraphSource
import de.sciss.proc.{Action, Code, Control, Proc, SoundProcesses, Universe, Widget}
import de.sciss.synth.SynthGraph
import de.sciss.synth.proc.graph.ScanOut

import java.awt.event.{ComponentAdapter, ComponentEvent, ComponentListener}
import scala.collection.immutable.{Seq => ISeq}
import scala.swing.{BorderPanel, Button, Component, FlowPanel, Orientation, TabbedPane}
import scala.util.control.NonFatal

object CodeFrameImpl extends CodeFrame.Companion {
  def install(): Unit =
    CodeFrame.peer = this

  // ---- adapter for editing a Proc's source ----

  override def proc[T <: Txn[T]](obj: Proc[T])
                       (implicit tx: T, universeHandler: UniverseHandler[T],
                        compiler: Code.Compiler): CodeFrame[T] =
    universeHandler(obj, CodeFrame) {
      import universeHandler.universe

      val codeObj = mkSource(obj = obj, codeTpe = Code.Proc)({
        val gv: SynthGraph = obj.graph.value
        val txt     = /*if (gv.isEmpty) "" else*/ try {
          MkSynthGraphSource(gv)
        } catch {
          case NonFatal(ex) =>
            s"// $ex"
        }
        if (txt.isEmpty)
          Code.Proc.defaultSource
        else
          s"// source code automatically extracted\n\n$txt"
      })
      val swapObjOpt  = codeObj.attr.$[Code.Obj](CodeView.attrSwap)
      val built0      = swapObjOpt.isEmpty

      val objH    = tx.newHandle(obj)
      val code0   = (swapObjOpt getOrElse codeObj).value match {
        case cs: Code.Proc => cs
        case other => sys.error(s"Proc source code does not produce SynthGraph: ${other.tpe.humanName}")
      }

      val handler = new CodeView.Handler[T, Unit, SynthGraph] {
        override def in(): Unit = ()

        override def save(in: Unit, out: SynthGraph)(implicit tx: T, undo: UndoManager[T]): Unit = {
          val obj = objH()
          // we automatically create missing outputs for the
          // ScanOut elements we see here
          lazy val oldKeys = obj.outputs.keys
          val newKeys = out.sources.collect {
            case ScanOut(key, _) if !oldKeys.contains(key) => key
          }
          val editName  = "Change SynthGraph"
          val editNameC = if (newKeys.isEmpty) editName else s"$editName and Outputs"
          undo.capture(editNameC) {
            EditVar.exprUndo[T, SynthGraph, Proc.GraphObj](editName, obj.graph,
              Proc.GraphObj.newConst[T](out))

            if (newKeys.nonEmpty) {
              newKeys.iterator.foreach(EditAddProcOutput[T](obj, _))
            }
          }
        }

        def dispose()(implicit tx: T): Unit = ()
      }

      implicit val undo: UndoManager[T] = UndoManager()
      val outputsView = ProcOutputsView [T](obj)
      val attrView    = AttrMapView     [T](obj)
      val viewPower   = RunnerToggleButton[T](obj)
      val rightView   = SplitPaneView(attrView, outputsView, Orientation.Vertical)

      newInstance(obj, objH, codeObj, code0, built0 = built0, handler = Some(handler), bottom = viewPower :: Nil,
        rightViewOpt = Some(("In/Out", rightView)), canBounce = true,
      )
    }

  // ---- adapter for editing a Control.Graph's source ----

  override def control[T <: Txn[T]](obj: Control[T])
                          (implicit tx: T, universeHandler: UniverseHandler[T],
                           compiler: Code.Compiler): CodeFrame[T] =
    universeHandler(obj, CodeFrame) {
      import universeHandler.universe

      val codeObj = mkSource(obj = obj, codeTpe = Code.Control)({
        val gv: Control.Graph = obj.graph.value
        if (gv.controls.isEmpty)
          Code.Control.defaultSource
        else
          s"// Warning: source code could not be automatically extracted!\n\n"
      })
      val swapObjOpt  = codeObj.attr.$[Code.Obj](CodeView.attrSwap)
      val built0      = swapObjOpt.isEmpty

      val objH    = tx.newHandle(obj)
      val code0   = (swapObjOpt getOrElse codeObj).value match {
        case cs: Code.Control => cs
        case other => sys.error(s"Control source code does not produce Control.Graph: ${other.tpe.humanName}")
      }

      val handler = new CodeView.Handler[T, Unit, Control.Graph] {
        override def in(): Unit = ()

        override def save(in: Unit, out: Control.Graph)(implicit tx: T, undo: UndoManager[T]): Unit = {
          val obj = objH()
          EditVar.exprUndo[T, Control.Graph, Control.GraphObj]("Change Control Graph", obj.graph,
            Control.GraphObj.newConst[T](out))
        }

        def dispose()(implicit tx: T): Unit = ()
      }

      implicit val undo: UndoManager[T] = UndoManager()
      val attrView    = AttrMapView       [T](obj)
      val viewPower   = RunnerToggleButton[T](obj)
      val rightView   = attrView // SplitPaneView(attrView, outputsView, Orientation.Vertical)

      newInstance(obj, objH, codeObj, code0, built0 = built0, handler = Some(handler), bottom = viewPower :: Nil,
        rightViewOpt = Some(("In/Out", rightView)), canBounce = true,
      )
    }

  // ---- adapter for editing an Action.Graph's source ----

  override def action[T <: Txn[T]](obj: Action[T])
                          (implicit tx: T, universeHandler: UniverseHandler[T],
                           compiler: Code.Compiler): CodeFrame[T] =
    universeHandler(obj, CodeFrame) {
      import universeHandler.universe

      val codeObj = mkSource(obj = obj, codeTpe = Code.Action)({
        val gv: Action.Graph = obj.graph.value
        if (gv.controls.isEmpty)
          Code.Action.defaultSource
        else
          s"// Warning: source code could not be automatically extracted!\n\n"
      })
      val swapObjOpt  = codeObj.attr.$[Code.Obj](CodeView.attrSwap)
      val built0      = swapObjOpt.isEmpty

      val objH    = tx.newHandle(obj)
      val code0   = (swapObjOpt getOrElse codeObj).value match {
        case cs: Code.Action => cs
        case other => sys.error(s"Action source code does not produce Action.Graph: ${other.tpe.humanName}")
      }

      val handler = new CodeView.Handler[T, Unit, Action.Graph] {
        override def in(): Unit = ()

        override def save(in: Unit, out: Action.Graph)(implicit tx: T, undo: UndoManager[T]): Unit = {
          val obj = objH()
          EditVar.exprUndo[T, Action.Graph, Action.GraphObj]("Change Action Graph", obj.graph,
            Action.GraphObj.newConst[T](out))
        }

        def dispose()(implicit tx: T): Unit = ()
      }

      implicit val undo: UndoManager[T] = UndoManager()
      val attrView    = AttrMapView[T](obj)
      val viewPower   = RunnerToggleButton[T](obj, isAction = true)
      val rightView   = attrView // SplitPaneView(attrView, outputsView, Orientation.Vertical)

      newInstance(obj, objH, codeObj, code0, built0 = built0, handler = Some(handler), bottom = viewPower :: Nil,
        rightViewOpt = Some(("In/Out", rightView)), canBounce = false,
      )
    }

  // ---- adapter for editing an Expr.Program's source ----

  override def program[T <: Txn[T], A, E[~ <: LTxn[~]] <: Expr[~, A]](obj: E[T] with Expr.Program[T, A])
                         (implicit tx: T, universeHandler: UniverseHandler[T],
                          compiler: Code.Compiler, tpe: Expr.Type[A, E]): CodeFrame[T] =
    universeHandler(obj, CodeFrame) {
      import universeHandler.universe

      val codeTpe     = Code.Program.find(tpe).getOrElse(sys.error(s"Now code type for $tpe"))
      val codeObj     = mkSource(obj = obj, codeTpe = codeTpe)()
      val swapObjOpt  = codeObj.attr.$[Code.Obj](CodeView.attrSwap)
      val built0      = swapObjOpt.isEmpty

      val objH    = tx.newHandle(obj)(tpe.programFormat[T])
      val code0: Code.T[Unit, Ex[A]] = (swapObjOpt getOrElse codeObj).value match {
        case cs if cs.tpe == codeTpe => cs.asInstanceOf[codeTpe.Repr]
        case other => sys.error(s"Expression source code does not produce program: ${other.tpe.humanName}")
      }

      val handler = new CodeView.Handler[T, Unit, Ex[A]] {
        override def in(): Unit = ()

        override def save(in: Unit, out: Ex[A])(implicit tx: T, undo: UndoManager[T]): Unit = {
          val obj = objH()
          EditProgram(s"Change ${codeTpe.humanName}", tpe)(obj, out)
        }

        def dispose()(implicit tx: T): Unit = ()
      }

      implicit val undo: UndoManager[T] = UndoManager()
      val attrView  = AttrMapView[T](obj)
      val viewEval  = View.wrap[T, Button] {
        val actionEval = new swing.Action("Evaluate") { self =>
          import universe.cursor
          def apply(): Unit = {
            val res = cursor.step { implicit tx =>
              val obj = objH()
              obj.value
            }
            println(res)
          }
        }
        GUI.toolButton(actionEval, raphael.Shapes.Quote)
      }

      val bottom    = viewEval :: Nil
      val rightView = attrView // SplitPaneView(attrView, outputsView, Orientation.Vertical)

      type In   = Unit
      type Out  = Ex[A]

      newInstance[T, In, Out](obj: Obj[T], objH, codeObj, code0, built0 = built0, handler = Some(handler),
        bottom = bottom, numLines = 6, rightViewOpt = Some(("In", rightView)), canBounce = true,
      )
    }

  // ---- general constructor ----

  override def apply[T <: Txn[T]](obj: Code.Obj[T], bottom: ISeq[View[T]])
                                 (implicit tx: T, handler: UniverseHandler[T], compiler: Code.Compiler): CodeFrame[T] =
    apply[T](obj, bottom, canBounce = false)

  def apply[T <: Txn[T]](obj: Code.Obj[T], bottom: ISeq[View[T]], canBounce: Boolean)
                        (implicit tx: T, universeHandler: UniverseHandler[T],
                         compiler: Code.Compiler): CodeFrame[T] =
    universeHandler(obj, CodeFrame) {
      val codeObj     = obj
      val swapObjOpt  = codeObj.attr.$[Code.Obj](CodeView.attrSwap)
      val built0      = swapObjOpt.isEmpty

      val code0 = (swapObjOpt getOrElse codeObj).value
      implicit val undo: UndoManager[T] = UndoManager()
      val objH    = tx.newHandle(obj)

      newInstance[T, code0.In, code0.Out](
        pObj            = obj,
        pObjH           = objH,
        cObj            = obj,
        code0           = code0.asInstanceOf[Code.T[code0.In, code0.Out]],  // Scala 2.12 woes
        built0          = built0,
        handler         = None,
        bottom          = bottom,
        rightViewOpt    = None,
        debugMenuItems  = Nil,
        canBounce       = canBounce
      )
    }

  private class PlainView[T <: Txn[T]](objH: Source[T, Obj[T]], codeView: View[T],
                                       rightViewOpt: Option[(String, View[T])],
                                       showEditor: Boolean, bottom: ISeq[View[T]])
                                      (implicit val universe: Universe[T],
                                       val undoManager: UndoManager[T])
    extends View.Editable[T] with UniverseObjView[T] with ComponentHolder[Component] {

    type C = Component

    override def obj(implicit tx: T): Obj[T] = objH()

    override def viewState: Set[ViewState] = Set.empty

    def init()(implicit tx: T): this.type = {
      deferTx(initGUI())
      this
    }

    private def initGUI(): Unit = {
      val pane = rightViewOpt.fold[C](codeView.component) { case (rightTitle, rightView) =>
        val _tabs = new TabbedPane
        _tabs.peer.putClientProperty("styleId", "attached")  // XXX TODO: obsolete
        _tabs.focusable  = false
        val pageEditC   = if (showEditor) codeView.component else {
          new BorderPanel {
            private def addEditor(): Unit =
              add(codeView.component, BorderPanel.Position.Center)

            lazy val cl: ComponentListener = new ComponentAdapter {
              override def componentShown(e: ComponentEvent): Unit = {
                if (peer.isShowing) {
                  peer.removeComponentListener(cl)
                  addEditor()
                  revalidate()
                }
              }
            }
            peer.addComponentListener(cl)
          }
        }

        val pageEdit    = new TabbedPane.Page("Editor"  , pageEditC, null)
        val pageRender  = new TabbedPane.Page(rightTitle, rightView .component, null)
        _tabs.pages     += pageEdit
        _tabs.pages     += pageRender
        Util.addTabNavigation(_tabs)

        if (showEditor) {
          codeView.component.requestFocus()
        } else {
          _tabs.selection.index = 1
          // paneEdit.preferredSize = renderer.component.preferredSize
        }

        _tabs
      }

      component = if (bottom.isEmpty) pane else new BorderPanel {
        add(pane, BorderPanel.Position.Center)

        {
          val botC          = bottom.map(_.component)
          val panelBottom   = new FlowPanel(FlowPanel.Alignment.Trailing)(botC: _*)
          panelBottom.hGap  = 4
          panelBottom.vGap  = 2
          add(panelBottom, BorderPanel.Position.South)
        }
      }
    }

    def dispose()(implicit tx: T): Unit = {
      codeView.dispose()
      rightViewOpt.foreach(_._2.dispose())
      bottom.foreach(_.dispose())
    }
  }

  private final class CanBounceView[T <: Txn[T]](objH: Source[T, Obj[T]], codeView: View[T],
                                                 rightViewOpt: Option[(String, View[T])],
                                                 showEditor: Boolean, bottom: ISeq[View[T]])
                                                (implicit universe: Universe[T],
                                                 undoManager: UndoManager[T])
    extends PlainView[T](objH, codeView, rightViewOpt, showEditor = showEditor, bottom = bottom) with CanBounce {

    object actionBounce extends ActionBounce[T](this, objH)
  }

  // trying to minimize IntelliJ false error highlights
  private final type CodeT[In0, Out0] = Code { type In = In0; type Out = Out0 }

  /**
    * @param rightViewOpt optional title and component for a second tab view
    */
  def make[T <: Txn[T], In0, Out0](pObj          : Obj[T],
                                   pObjH          : Source[T, Obj[T]],
                                   cObj           : Code.Obj[T],
                                   code0          : CodeT[In0, Out0],
                                   built0         : Boolean,
                                   handler        : Option[CodeView.Handler[T, In0, Out0]],
                                   bottom         : ISeq[View[T]],
                                   rightViewOpt   : Option[(String, View[T])] = None,
                                   debugMenuItems : ISeq[swing.Action]        = Nil,
                                   canBounce      : Boolean
                                  )
                                  (implicit tx: T, universeHandler: UniverseHandler[T],
                                   undoManager: UndoManager[T], compiler: Code.Compiler): CodeFrame[T] =
    universeHandler(pObj, CodeFrame) {
      newInstance[T, In0, Out0](
        pObj            = pObj,
        pObjH           = pObjH,
        cObj            = cObj,
        code0           = code0,
        built0          = built0,
        handler         = handler,
        bottom          = bottom,
        rightViewOpt    = rightViewOpt,
        debugMenuItems  = debugMenuItems,
        canBounce       = canBounce
      )
    }

  def titleView[T <: Txn[T]](obj: Obj[T], peer: CodeView[T, _], contextName: String)
                            (implicit tx: T): CellView[T, String] =
    new TitleView[T](obj, peer, contextName = contextName, tx0 = tx)

  private class TitleView[T <: Txn[T]](obj: Obj[T], peer: CodeView[T, _], contextName: String, tx0: T)
    extends CellView[T, String] { title =>

    private[this] val name    = CellView.name(obj)(tx0)
    private[this] val postfix = if (contextName.isEmpty) contextName else s" : $contextName Code"

    private class React(fun: T => String => Unit, tx0: T) extends Disposable[T] {
      private[this] val obsName = name.react { implicit tx => _ =>
        fun(tx)(title())
      } (tx0)

      private def fireFromUI(): Unit = {
        import peer.cursor
        SoundProcesses.step[T]("Update title") { implicit tx =>
          fun(tx)(title())
        }
      }

      private[this] val obsPeer: Model.Listener[CodeView.Update] = {
        case CodeView.DirtyChange(_) => fireFromUI()
        case CodeView.BuiltChange(_) => fireFromUI()
      }

      deferTx {
        peer.addListener(obsPeer)
      } (tx0)

      override def dispose()(implicit tx: T): Unit = {
        obsName.dispose()
        deferTx {
          peer.removeListener(obsPeer)
        }
      }
    }

    override def react(fun: T => String => Unit)(implicit tx: T): Disposable[T] = new React(fun, tx)

    override def apply()(implicit tx: T): String = {
      val dirty   = peer.dirty
      val built   = !dirty && peer.built
      val prefix  = if (dirty) "*" else if (built) "" else "~"
      s"$prefix${name()}$postfix"
    }
  }

  /** Same as `make`, but does not return existing views from handler */
  def newInstance[T <: Txn[T], In0, Out0](pObj           : Obj[T],
                                          pObjH          : Source[T, Obj[T]],
                                          cObj           : Code.Obj[T],
                                          code0          : CodeT[In0, Out0],
                                          built0         : Boolean,
                                          handler        : Option[CodeView.Handler[T, In0, Out0]],
                                          bottom         : ISeq[View[T]],
                                          numLines       : Int                        = 24,
                                          rightViewOpt   : Option[(String, View[T])]  = None,
                                          debugMenuItems : ISeq[swing.Action]         = Nil,
                                          canBounce      : Boolean
                            )
                                         (implicit tx: T, universeHandler: UniverseHandler[T],
                             undoManager: UndoManager[T], compiler: Code.Compiler): CodeFrame[T] = {
      import universeHandler.universe
      val showEditor  = pObj.attr.$[BooleanObj](Widget.attrEditMode).forall(_.value)
      val bottomCode  = if (showEditor) bottom else Nil
      val bottomView  = if (showEditor) Nil else bottom
      val codeView    = CodeView(cObj, code0, built0 = built0, bottom = bottomCode, numLines = numLines)(handler)
  
      val view = if (canBounce)
        new CanBounceView (pObjH, codeView, rightViewOpt, showEditor = showEditor, bottom = bottomView)
      else
        new PlainView     (pObjH, codeView, rightViewOpt, showEditor = showEditor, bottom = bottomView)
  
      view.init()
      val contextName = code0.tpe.humanName
      val res = new FrameImpl(codeView = codeView, view = view, debugMenuItems = debugMenuItems,
        examples = code0.tpe.examples
      )
      res.init().setTitle(titleView(pObj, codeView, contextName = contextName))
      res
    }

  // ---- util ----

  def mkSource[T <: Txn[T]](obj: Obj[T], codeTpe: Code.Type, key: String = Code.attrSource)(init: => String = codeTpe.defaultSource)
                           (implicit tx: T): Code.Obj[T] = {
    // if there is no source code attached,
    // create a new code object and add it to the attribute map.
    // let's just do that without undo manager
    val codeObj = obj.attr.get(key) match {
      case Some(c: Code.Obj[T]) => c
      case _ =>
        val source  = init
        val code    = Code(codeTpe.id, source)
        val c       = Code.Obj.newVar(Code.Obj.newConst[T](code))
        obj.attr.put(key, c)
        c
    }
    codeObj
  }

  // ---- frame impl ----

  private final class FrameImpl[T <: Txn[T]](val codeView   : CodeView[T, _],
                                             val view       : UniverseObjView[T],
                                             debugMenuItems : ISeq[swing.Action],
                                             examples       : ISeq[Example]
                                            )(implicit val handler: UniverseHandler[T])
    extends CodeFrame[T] with WorkspaceWindowImpl[T] with CodeFrameBase[T] {

    // XXX TODO
    override def supportsNewWindow: Boolean = false

    override def newWindow()(implicit tx: T): Repr[T] = throw new UnsupportedOperationException

    override protected def initGUI(): Unit = {
      super.initGUI()
      if (debugMenuItems.nonEmpty) {
        val mf = window.handler.menuFactory
        mf.get("actions").foreach {
          case g: Menu.Group =>
            val winOpt = Some(window)
            debugMenuItems.iterator.zipWithIndex.foreach { case (a, ai) =>
              g.add(winOpt, Menu.Item(s"debug-${ai + 1}", a))
            }
          case _ =>
        }
      }
      mkExamplesMenu(examples)
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy