de.sciss.mellite.gui.impl.grapheme.GraphemeViewImpl.scala Maven / Gradle / Ivy
/*
* GraphemeViewImpl.scala
* (Mellite)
*
* Copyright (c) 2012-2016 Hanns Holger Rutz. All rights reserved.
*
* This software is published under the GNU General Public License v3+
*
*
* For further information, please contact Hanns Holger Rutz at
* [email protected]
*/
package de.sciss.mellite
package gui
package impl
package grapheme
import java.awt
import java.awt.{Font, Graphics2D, LinearGradientPaint, RenderingHints}
import java.util.Locale
import javax.swing.UIManager
import de.sciss.audiowidgets.TimelineModel
import de.sciss.audiowidgets.impl.TimelineModelImpl
import de.sciss.desktop
import de.sciss.desktop.{UndoManager, Window}
import de.sciss.icons.raphael
import de.sciss.lucre.bitemp.{BiGroup, BiPin}
import de.sciss.lucre.stm.{Cursor, Disposable, TxnLike}
import de.sciss.lucre.swing.deferTx
import de.sciss.lucre.swing.impl.ComponentHolder
import de.sciss.lucre.synth.Sys
import de.sciss.lucre.{GraphemeHasIterator, stm}
import de.sciss.model.Change
import de.sciss.span.Span
import de.sciss.synth.proc.{Grapheme, Proc, TimeRef}
import scala.collection.immutable.{SortedMap => ISortedMap}
import scala.concurrent.stm.{Ref, TMap, TSet}
import scala.swing.Swing._
import scala.swing.{Action, BorderPanel, BoxPanel, Component, Orientation}
object GraphemeViewImpl {
private val colrBg = awt.Color.darkGray
private val colrRegionOutline = new awt.Color(0x68, 0x68, 0x68)
private val colrRegionOutlineSel= awt.Color.blue
private val pntRegionBg = new LinearGradientPaint(0f, 1f, 0f, 62f,
Array[Float](0f, 0.23f, 0.77f, 1f), Array[awt.Color](new awt.Color(0x5E, 0x5E, 0x5E), colrRegionOutline,
colrRegionOutline, new awt.Color(0x77, 0x77, 0x77)))
private val pntRegionBgSel = new LinearGradientPaint(0f, 1f, 0f, 62f,
Array[Float](0f, 0.23f, 0.77f, 1f), Array[awt.Color](new awt.Color(0x00, 0x00, 0xE6), colrRegionOutlineSel,
colrRegionOutlineSel, new awt.Color(0x1A, 0x1A, 0xFF)))
private val DEBUG = true
private val NoMove = TrackTool.Move(deltaTime = 0L, deltaTrack = 0, copy = false)
import de.sciss.mellite.{logTimeline => logT}
private type EntryProc[S <: Sys[S]] = BiGroup.Entry[S, Proc[S]]
def apply[S <: Sys[S]](obj: Grapheme[S])
(implicit tx: S#Tx, workspace: Workspace[S], cursor: stm.Cursor[S],
undo: UndoManager): GraphemeView[S] = {
val sampleRate = TimeRef.SampleRate
val tlm = new TimelineModelImpl(Span(0L, (sampleRate * 60 * 60).toLong), sampleRate)
tlm.visible = Span(0L, (sampleRate * 60 * 2).toLong)
val grapheme = obj
val graphemeH = tx.newHandle(obj)
var disposables = List.empty[Disposable[S#Tx]]
val viewMap = TMap.empty[Long, GraphemeObjView[S]] // tx.newInMemoryIDMap[GraphemeObjView[S]]
val selectionModel = SelectionModel[S, GraphemeObjView[S]]
val grView = new Impl[S](graphemeH, viewMap, tlm, selectionModel)
import GraphemeHasIterator._
grapheme.iterator.foreach { case (time, entry) =>
if (DEBUG) println(s"ADD $time / ${TimeRef.framesToSecs(time)}, ${entry.value}")
grView.objAdded(time, entry, repaint = false)
}
val obsGrapheme = grapheme.changed.react { implicit tx => upd =>
upd.changes.foreach {
case BiPin.Added (time, entry) =>
if (DEBUG) println(s"Added $time, $entry")
grView.objAdded(time, entry, repaint = true)
case BiPin.Removed(time, entry) =>
if (DEBUG) println(s"Removed $time, $entry")
grView.objRemoved(time, entry)
case BiPin.Moved (timeChange, entry) =>
if (DEBUG) println(s"Moved $entry, $timeChange")
grView.objMoved(entry, timeCh = timeChange)
}
}
disposables ::= obsGrapheme
grView.disposables.set(disposables)(tx.peer)
deferTx(grView.guiInit())
grView
}
private final class Impl[S <: Sys[S]](val graphemeH : stm.Source[S#Tx, Grapheme[S]],
val viewMap : TMap[Long, GraphemeObjView[S]],
val timelineModel : TimelineModel,
val selectionModel: SelectionModel[S, GraphemeObjView[S]])
(implicit val workspace: Workspace[S], val cursor: Cursor[S],
val undoManager: UndoManager)
extends GraphemeActions[S]
with GraphemeView[S]
with ComponentHolder[Component] {
impl =>
import cursor.step
private var viewRange = ISortedMap.empty[Long, GraphemeObjView[S]]
private val viewSet = TSet.empty[GraphemeObjView[S]]
private var canvasView: View = _
val disposables = Ref(List.empty[Disposable[S#Tx]])
// private lazy val toolCursor = TrackTool.cursor [S](canvasView)
// private lazy val toolMove = TrackTool.move [S](canvasView)
def grapheme(implicit tx: S#Tx) = graphemeH()
def plainGroup(implicit tx: S#Tx) = grapheme
def window: Window = component.peer.getClientProperty("de.sciss.mellite.Window").asInstanceOf[Window]
def canvasComponent: Component = canvasView.canvasComponent
def dispose()(implicit tx: S#Tx): Unit = {
deferTx {
viewRange = ISortedMap.empty
}
disposables.swap(Nil)(tx.peer).foreach(_.dispose())
viewSet.foreach(_.dispose())(tx.peer)
clearSet(viewSet)
// this is already included in `disposables`:
// viewMap.dispose()
}
private def clearSet[A](s: TSet[A])(implicit tx: S#Tx): Unit =
s.retain(_ => false)(tx.peer) // no `clear` method
def guiInit(): Unit = {
canvasView = new View
val actionAttr: Action = Action(null) {
withSelection { implicit tx =>
seq => {
seq.foreach { view =>
AttrMapFrame(view.obj)
}
None
}
}
}
actionAttr.enabled = false
val ggAttr = GUI.toolButton(actionAttr, raphael.Shapes.Wrench, "Attributes Editor")
ggAttr.focusable = false
val transportPane = new BoxPanel(Orientation.Horizontal) {
contents ++= Seq(
HStrut(4),
// TrackTools.palette(canvasView.trackTools, Vector(
// toolCursor, toolMove, toolResize, toolGain, toolFade /* , toolSlide*/ ,
// toolMute, toolAudition, toolFunction, toolPatch)),
// HStrut(4),
ggAttr,
HGlue
)
}
selectionModel.addListener {
case _ =>
val hasSome = selectionModel.nonEmpty
actionAttr .enabled = hasSome
actionMoveObjectToCursor.enabled = hasSome
}
timelineModel.addListener {
case TimelineModel.Selection(_, span) if span.before.isEmpty != span.now.isEmpty =>
val hasSome = span.now.nonEmpty
actionClearSpan .enabled = hasSome
actionRemoveSpan.enabled = hasSome
}
val pane2 = canvasView.component
// pane2.dividerSize = 4
// pane2.border = null
// pane2.oneTouchExpandable = true
val pane = new BorderPanel {
import BorderPanel.Position._
layoutManager.setVgap(2)
add(transportPane, North )
add(pane2 , Center)
// add(ggTrackPos , East )
// add(globalView.component, West )
}
component = pane
// DocumentViewHandler.instance.add(this)
}
private def repaintAll(): Unit = canvasView.canvasComponent.repaint()
def objAdded(time: Long, entry: Grapheme.Entry[S], repaint: Boolean)(implicit tx: S#Tx): Unit = {
import TxnLike.peer
logT(s"objAdded($time / ${TimeRef.framesToSecs(time)}, $entry)")
// entry.span
// val proc = entry.value
// val pv = ProcView(entry, viewMap, scanMap)
val view = GraphemeObjView(entry.key, entry.value)
viewMap.put(time, view)
viewSet.add(view)(tx.peer)
def doAdd(): Unit = {
viewRange += time -> view
if (repaint) repaintAll() // XXX TODO: optimize dirty rectangle
}
if (repaint)
deferTx(doAdd())
else
doAdd()
// XXX TODO -- do we need to remember the disposable?
view.react { implicit tx => {
case ObjView.Repaint(_) => objUpdated(view)
case _ =>
}}
}
private def warnViewNotFound(action: String, entry: Grapheme.Entry[S]): Unit =
Console.err.println(s"Warning: Grapheme - $action. View for object $entry not found.")
def objRemoved(time: Long, entry: Grapheme.Entry[S])(implicit tx: S#Tx): Unit = {
import TxnLike.peer
logT(s"objRemoved($time, $entry)")
viewMap.remove(time).fold {
warnViewNotFound("remove", entry)
} { view =>
viewSet.remove(view)(tx.peer)
deferTx {
viewRange -= time
repaintAll() // XXX TODO: optimize dirty rectangle
}
view.dispose()
}
}
// insignificant changes are ignored, therefore one can just move the span without the track
// by using trackCh = Change(0,0), and vice versa
def objMoved(entry: Grapheme.Entry[S], timeCh: Change[Long])
(implicit tx: S#Tx): Unit = {
import TxnLike.peer
logT(s"objMoved(${timeCh.before} / ${TimeRef.framesToSecs(timeCh.before)} -> ${timeCh.now} / ${TimeRef.framesToSecs(timeCh.now)}, $entry)")
viewMap.remove(timeCh.before).fold {
warnViewNotFound("move", entry)
} { view =>
viewMap.put(timeCh.now, view)
deferTx {
viewRange -= timeCh.before
view.timeValue = timeCh .now
viewRange += timeCh.now -> view
repaintAll() // XXX TODO: optimize dirty rectangle
}
}
}
private def objUpdated(view: GraphemeObjView[S]): Unit = {
// if (view.isGlobal)
// globalView.updated(view)
// else
repaintAll() // XXX TODO: optimize dirty rectangle
}
private final class View extends GraphemeCanvas[S] {
canvasImpl =>
// import AbstractGraphemeView._
def timelineModel = impl.timelineModel
def selectionModel = impl.selectionModel
def grapheme(implicit tx: S#Tx) = impl.plainGroup
def findView(pos: Long): Option[GraphemeObjView[S]] =
viewRange.range(pos, Long.MaxValue).headOption.map(_._2)
// def findRegions(r: TrackTool.Rectangular): Iterator[GraphemeObjView[S]] = {
// val views = intersect(r.span)
// views.filter(pv => pv.trackIndex < r.trackIndex + r.trackHeight && (pv.trackIndex + pv.trackHeight) > r.trackIndex)
// }
protected def commitToolChanges(value: Any): Unit = {
logT(s"Commit tool changes $value")
val editOpt = step { implicit tx =>
value match {
// case t: TrackTool.Cursor => toolCursor commit t
// case t: TrackTool.Move =>
// val res = toolMove.commit(t)
// res
case _ => None
}
}
editOpt.foreach(undoManager.add)
}
private var _toolState = Option.empty[Any]
private var moveState = NoMove
protected def toolState = _toolState
protected def toolState_=(state: Option[Any]): Unit = {
_toolState = state
moveState = NoMove
state.foreach {
case s: TrackTool.Move => moveState = s
case _ =>
}
}
object canvasComponent extends Component /* with DnD[S] */ /* with sonogram.PaintController */ {
protected def graphemeModel = impl.timelineModel
protected def workspace = impl.workspace
// private var currentDrop = Option.empty[DnD.Drop[S]]
font = {
val f = UIManager.getFont("Slider.font", Locale.US)
if (f != null) f.deriveFont(math.min(f.getSize2D, 9.5f)) else new Font("SansSerif", Font.PLAIN, 9)
}
preferredSize = {
val b = desktop.Util.maximumWindowBounds
(b.width >> 1, b.height >> 1)
}
// protected def updateDnD(drop: Option[DnD.Drop[S]]): Unit = {
// currentDrop = drop
// repaint()
// }
//
// protected def acceptDnD(drop: DnD.Drop[S]): Boolean = performDrop(drop)
def imageObserver = peer
override protected def paintComponent(g: Graphics2D): Unit = {
super.paintComponent(g)
val w = peer.getWidth
val h = peer.getHeight
g.setColor(colrBg) // g.setPaint(pntChecker)
g.fillRect(0, 0, w, h)
val total = graphemeModel.bounds
val clipOrig = g.getClip
val cr = clipOrig.getBounds
val visStart = screenToFrame(cr.x).toLong
val visStop = screenToFrame(cr.x + cr.width).toLong + 1 // plus one to avoid glitches
val sel = selectionModel
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
// warning: iterator, we need to traverse twice!
viewRange.range(visStart, visStop).foreach { case (_, view) =>
val selected = sel.contains(view)
def drawProc(start: Long, x1: Int, x2: Int, move: Long): Unit = {
// val pTrk = if (selected) math.max(0, view.trackIndex + moveState.deltaTrack) else view.trackIndex
val py = 0 // trackToScreen(pTrk)
val px = x1
val pw = x2 - x1
val ph = peer.getHeight // trackToScreen(pTrk + view.trackHeight) - py
// clipped coordinates
val px1C = math.max(px + 1, cr.x - 2)
val px2C = math.min(px + pw, cr.x + cr.width + 3)
if (px1C < px2C) { // skip this if we are not overlapping with clip
g.translate(px, py)
g.setColor(if (selected) colrRegionOutlineSel else colrRegionOutline)
g.fillRoundRect(0, 0, pw, ph, 5, 5)
g.setPaint(if (selected) pntRegionBgSel else pntRegionBg)
g.fillRoundRect(1, 1, pw - 2, ph - 2, 4, 4)
g.translate(-px, -py)
g.setColor(colrBg)
g.drawLine(px - 1, py, px - 1, py + ph - 1) // better distinguish directly neighbouring views
// val hndl = 0
// val innerH = ph - (hndl + 1)
// val innerY = py + hndl
// g.clipRect(px + 1, innerY, pw - 2, innerH)
// g.setClip(clipOrig)
}
}
def adjustStart(start: Long): Long =
if (selected) {
val dt0 = moveState.deltaTime // + resizeState.deltaStart
if (dt0 >= 0) dt0 else {
math.max(-(start - total.start), dt0)
}
} else 0L
def adjustStop(stop: Long): Long =
if (selected) {
val dt0 = moveState.deltaTime // + resizeState.deltaStop
if (dt0 >= 0) dt0 else {
math.max(-(stop - total.start + TimelineView.MinDur), dt0)
}
} else 0L
def adjustMove(start: Long): Long =
if (selected) {
val dt0 = moveState.deltaTime
if (dt0 >= 0) dt0 else {
math.max(-(start - total.start), dt0)
}
} else 0L
// view.spanValue match {
// case Span(start, stop) =>
val start = view.timeValue
val stop = start
val dStart = adjustStart(start)
val dStop = adjustStop (stop )
val newStart = start + dStart
val newStop = math.max(newStart + TimelineView.MinDur, stop + dStop)
val x1 = frameToScreen(newStart).toInt
val x2 = frameToScreen(newStop ).toInt
drawProc(start, x1, x2, adjustMove(start))
//
// case Span.From(start) =>
// val dStart = adjustStart(start)
// val newStart = start + dStart
// val x1 = frameToScreen(newStart).toInt
// drawProc(start, x1, w + 5, adjustMove(start))
//
// case Span.Until(stop) =>
// val dStop = adjustStop(stop)
// val newStop = stop + dStop
// val x2 = frameToScreen(newStop).toInt
// drawProc(Long.MinValue, -5, x2, 0L)
//
// case Span.All =>
// drawProc(Long.MinValue, -5, w + 5, 0L)
//
// case _ => // don't draw Span.Void
// }
}
// --- grapheme cursor and selection ---
paintPosAndSelection(g, h)
}
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy