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.impl.markdown.MarkdownRenderViewImpl.scala Maven / Gradle / Ivy
/*
* MarkdownRenderViewImpl.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.markdown
import de.sciss.desktop.{Desktop, KeyStrokes, OptionPane, Util}
import de.sciss.icons.raphael
import de.sciss.lucre.Txn.peer
import de.sciss.lucre.impl.ObservableImpl
import de.sciss.lucre.swing.LucreSwing.{deferTx, requireEDT}
import de.sciss.lucre.swing.impl.ComponentHolder
import de.sciss.lucre.swing.{View, Window}
import de.sciss.lucre.{Cursor, Disposable, Obj, Source, synth, Txn => LTxn}
import de.sciss.mellite.impl.component.{NavigationHistory, ZoomSupport}
import de.sciss.mellite.{GUI, MarkdownFrame, MarkdownRenderView, ObjListView, UniverseHandler, ViewState}
import de.sciss.proc.{Markdown, Universe}
import de.sciss.{desktop, proc}
import org.pegdown.{Extensions, PegDownProcessor}
import javax.swing.event.HyperlinkEvent
import scala.collection.immutable.{Seq => ISeq}
import scala.concurrent.stm.Ref
import scala.swing.Swing._
import scala.swing.event.Key
import scala.swing.{Action, BorderPanel, Component, FlowPanel, ScrollPane, Swing}
object MarkdownRenderViewImpl extends MarkdownRenderView.Companion {
def install(): Unit =
MarkdownRenderView.peer = this
def apply[T <: synth.Txn[T]](init: Markdown[T], bottom: ISeq[View[T]], embedded: Boolean)
(implicit tx: T, handler: UniverseHandler[T]): MarkdownRenderView[T] =
new Impl[T](bottom, embedded = embedded).init(init)
def basic[T <: LTxn[T]](init: Markdown[T], bottom: ISeq[View[T]], embedded: Boolean)
(implicit tx: T, cursor: Cursor[T]): MarkdownRenderView.Basic[T] =
new BasicImpl[T](bottom, embedded = embedded).init(init)
private final class Impl[T <: synth.Txn[T]](bottom: ISeq[View[T]], embedded: Boolean)
(implicit val universeHandler: UniverseHandler[T])
extends Base[T](bottom, embedded) with MarkdownRenderView[T] { impl =>
override def obj(implicit tx: T): Markdown[T] = markdown
override def viewState: Set[ViewState] = Set.empty
override implicit val universe: Universe[T] = universeHandler.universe
protected def mkEditButton(): Option[Component] = {
if (embedded) None else {
val actionEdit = Action(null) {
cursor.step { implicit tx =>
MarkdownFrame.editor(markdown)
}
}
val ggEdit = GUI.toolButton(actionEdit, raphael.Shapes.Edit)
Some(ggEdit)
}
}
protected def viewAttr(obj: Obj[T])(implicit tx: T): Option[Window[T]] = {
val listView = ObjListView(obj)
if (listView.isViewable) {
listView.openView(Window.find(impl))
} else {
None
}
}
}
private final class BasicImpl[T <: LTxn[T]](bottom: ISeq[View[T]], embedded: Boolean)
(implicit val cursor: Cursor[T])
extends Base[T](bottom, embedded) {
protected def mkEditButton(): Option[Component] = None
protected def viewAttr(obj: Obj[T])(implicit tx: T): Option[Window[T]] = None
}
private final case class Percent(value: Int) {
override def toString: String = s"$value%"
// def fraction: Double = value * 0.01
}
private abstract class Base[T <: LTxn[T]](bottom: ISeq[View[T]], embedded: Boolean)
extends MarkdownRenderView.Basic[T]
with ZoomSupport
with ComponentHolder[Component]
with ObservableImpl[T, MarkdownRenderView.Update[T]] { impl =>
type C = Component
// ---- abstract ----
def cursor: Cursor[T]
protected def mkEditButton(): Option[Component]
protected def viewAttr(obj: Obj[T])(implicit tx: T): Option[Window[T]]
// ---- impl ----
private[this] val mdRef = Ref.make[(Source[T, Markdown[T]], Disposable[T])]()
private[this] var _editor: HTMLEditorPaneWithZoom = _
private[this] val nav = NavigationHistory.empty[T, Source[T, Markdown[T]]]
private[this] var actionBwd: Action = _
private[this] var actionFwd: Action = _
private[this] var obsNav: Disposable[T] = _
def dispose()(implicit tx: T): Unit = {
mdRef()._2.dispose()
obsNav .dispose()
}
def markdown(implicit tx: T): Markdown[T] = mdRef()._1.apply()
def init(obj: Markdown[T])(implicit tx: T): this.type = {
deferTx(initGUI())
markdown = obj
obsNav = nav.react { implicit tx => upd =>
deferTx {
actionBwd.enabled = upd.canGoBack
actionFwd.enabled = upd.canGoForward
}
}
this
}
def setInProgress(md: Markdown[T], value: String)(implicit tx: T): Unit = {
val obs = md.changed.react { implicit tx => upd =>
val newText = upd.now
deferTx(setText(newText))
}
val old = mdRef.swap(tx.newHandle(md) -> obs)
if (old != null) old._2.dispose()
deferTx(setText(value))
}
def markdown_=(md: Markdown[T])(implicit tx: T): Unit =
setMarkdown(md, reset = true)
private def setMarkdownFromNav()(implicit tx: T): Unit =
nav.current.foreach { mdH =>
val md = mdH()
setInProgress(md, md.value)
}
private def setMarkdown(md: Markdown[T], reset: Boolean)(implicit tx: T): Unit = {
setInProgress(md, md.value)
val mdH = tx.newHandle(md)
if (reset) nav.resetTo(mdH) else nav.push(mdH)
}
private def setText(text: String): Unit = {
requireEDT()
// Note: unless we have line wrap in the editor,
// we should not use hard wraps in the rendering.
// Note: task list items are not correctly rendered
// with WebLaF (checkboxes are always unselected)
val mdp = new PegDownProcessor(Extensions.SMARTYPANTS | /*Extensions.HARDWRAPS |*/ Extensions.TABLES |
Extensions.FENCED_CODE_BLOCKS /*| Extensions.TASKLISTITEMS*/)
val html = mdp.markdownToHtml(text)
_editor.text = html
_editor.peer.setCaretPosition(0)
}
protected def setZoomFactor(f: Float): Unit =
_editor.zoom = f
private def initGUI(): Unit = {
_editor = new HTMLEditorPaneWithZoom("") {
editable = false
border = Swing.EmptyBorder(8)
preferredSize = (500, 500)
peer.addHyperlinkListener((e: HyperlinkEvent) => {
if (e.getEventType == HyperlinkEvent.EventType.ACTIVATED) {
// println(s"description: ${e.getDescription}")
// println(s"source elem: ${e.getSourceElement}")
// println(s"url : ${e.getURL}")
// val link = e.getDescription
// val ident = if (link.startsWith("ugen.")) link.substring(5) else link
// lookUpHelp(ident)
val url = e.getURL
if (url != null) {
Desktop.browseURI(url.toURI)
} else {
val key = e.getDescription
val either: Either[String, Unit] = impl.cursor.step { implicit tx =>
val obj = markdown
obj.attr.get(key).fold[Either[String, Unit]] {
import proc.Implicits._
Left(s"Attribute '$key' not found in Markdown object '${obj.name}'")
} {
case md: Markdown[T] =>
nav.push(tx.newHandle(md))
setMarkdownFromNav()
fire(MarkdownRenderView.FollowedLink(impl, md))
Right(())
case other =>
val opt = viewAttr(other)
opt match {
case Some(_) => Right(())
case None =>
import proc.Implicits._
Left(s"Object '${other.name}' in attribute '$key' is not viewable")
}
}
}
either.left.foreach { message =>
val opt = OptionPane.message(message, OptionPane.Message.Error)
opt.show(desktop.Window.find(impl.component), "Markdown Link")
}
}
}
})
}
actionBwd = Action(null) {
cursor.step { implicit tx =>
if (nav.canGoBack) {
nav.backward()
setMarkdownFromNav()
}
}
}
actionBwd.enabled = false
actionFwd = Action(null) {
cursor.step { implicit tx =>
if (nav.canGoForward) {
nav.forward()
setMarkdownFromNav()
}
}
}
actionFwd.enabled = false
val paneRender = new ScrollPane(_editor)
paneRender.peer.putClientProperty("styleId", "undecorated")
val ggBwd = GUI.toolButton(actionBwd, raphael.Shapes.Backward)
val ggFwd = GUI.toolButton(actionFwd, raphael.Shapes.Forward )
val ggZoom = initZoomWithComboBox()
if (!embedded) {
import KeyStrokes._
Util.addGlobalKey(ggBwd, alt + Key.Left)
Util.addGlobalKey(ggFwd, alt + Key.Left)
}
val bot1: List[Component] = if (bottom.isEmpty) Nil else bottom.iterator.map(_.component).toList
val bot2 = mkEditButton().fold(bot1)(_ :: bot1)
val bot3 = HGlue :: ggZoom :: ggBwd :: ggFwd :: bot2
val panelBottom = new FlowPanel(FlowPanel.Alignment.Trailing)(bot3: _*)
val pane = new BorderPanel {
add(paneRender , BorderPanel.Position.Center)
add(panelBottom , BorderPanel.Position.South )
}
component = pane
}
}
}