src/Tools/jEdit/src/document_dockable.scala
author wenzelm
Wed, 01 Feb 2023 21:23:54 +0100
changeset 77174 1eb55d6809b3
parent 77173 f1063cdb0093
child 77184 861777e58b77
permissions -rw-r--r--
more general program start message; progress on "Creating directory";

/*  Title:      Tools/jEdit/src/document_dockable.scala
    Author:     Makarius

Dockable window for document build support.
*/

package isabelle.jedit


import isabelle._

import java.awt.BorderLayout
import java.awt.event.{ComponentEvent, ComponentAdapter}

import scala.swing.{ScrollPane, TextArea, Label, TabbedPane, BorderPanel, Component}
import scala.swing.event.SelectionChanged

import org.gjt.sp.jedit.{jEdit, View}


object Document_Dockable {
  /* state */

  object State {
    def init(): State = State()
  }

  sealed case class State(
    pending: Boolean = false,
    process: Future[Unit] = Future.value(()),
    progress: Progress = new Progress,
    output_results: Command.Results = Command.Results.empty,
    output_main: XML.Body = Nil,
    output_more: XML.Body = Nil
  ) {
    def running: Boolean = !process.is_finished

    def run(process: Future[Unit], progress: Progress, reset_pending: Boolean): State =
      copy(process = process, progress = progress, pending = if (reset_pending) false else pending)

    def output(results: Command.Results, body: XML.Body): State =
      copy(output_results = results, output_main = body, output_more = Nil)

    def finish(body: XML.Body): State =
      copy(process = Future.value(()), output_more = body)

    def output_body: XML.Body =
      output_main :::
      (if (output_main.nonEmpty && output_more.nonEmpty) Pretty.Separator else Nil) :::
      output_more

    def reset(): State = {
      process.cancel()
      progress.stop()
      State.init()
    }
  }
}

class Document_Dockable(view: View, position: String) extends Dockable(view, position) {
  dockable =>

  GUI_Thread.require {}


  /* component state -- owned by GUI thread */

  private val current_state = Synchronized(Document_Dockable.State.init())

  private val process_indicator = new Process_Indicator
  private val pretty_text_area = new Pretty_Text_Area(view)
  private val message_pane = new TabbedPane

  private def show_state(): Unit = GUI_Thread.later {
    val st = current_state.value

    pretty_text_area.update(Document.Snapshot.init, st.output_results, st.output_body)

    if (st.running) process_indicator.update("Running document build process ...", 15)
    else if (st.pending) process_indicator.update("Waiting for pending document content ...", 5)
    else process_indicator.update(null, 0)
  }

  private def show_page(page: TabbedPane.Page): Unit = GUI_Thread.later {
    show_state()
    message_pane.selection.page = page
  }


  /* text area with zoom/resize */

  override def detach_operation: Option[() => Unit] = pretty_text_area.detach_operation

  private val zoom = new Font_Info.Zoom { override def changed(): Unit = handle_resize() }
  private def handle_resize(): Unit = GUI_Thread.require { pretty_text_area.zoom(zoom) }

  private val delay_resize: Delay =
    Delay.first(PIDE.session.update_delay, gui = true) { handle_resize() }

  addComponentListener(new ComponentAdapter {
    override def componentResized(e: ComponentEvent): Unit = delay_resize.invoke()
    override def componentShown(e: ComponentEvent): Unit = delay_resize.invoke()
  })


  /* progress */

  class Log_Progress extends Program_Progress(default_title = "build") {
    progress =>

    override def detect_program(s: String): Option[String] =
      Document_Build.detect_program_start(s)

    private val delay: Delay =
      Delay.first(PIDE.session.output_delay) {
        if (!stopped) {
          output_process(progress)
          show_state()
        }
      }

    override def echo(msg: String): Unit = { super.echo(msg); delay.invoke() }
    override def stop_program(): Unit = { super.stop_program(); delay.invoke() }
  }


  /* document build process */

  private def init_state(): Unit =
    current_state.change { st =>
      st.progress.stop()
      Document_Dockable.State(progress = new Log_Progress)
    }

  private def cancel_process(): Unit =
    current_state.change { st => st.process.cancel(); st }

  private def await_process(): Unit =
    current_state.guarded_access(st => if (st.process.is_finished) None else Some((), st))

  private def output_process(progress: Log_Progress): Unit = {
    val (results, body) = progress.output()
    current_state.change(_.output(results, body))
  }

  private def pending_process(): Unit =
    current_state.change { st =>
      if (st.pending) st
      else {
        delay_auto_build.revoke()
        delay_build.invoke()
        st.copy(pending = true)
      }
    }

  private def finish_process(output: XML.Body): Unit =
    current_state.change { st =>
      if (st.pending) {
        delay_auto_build.revoke()
        delay_build.invoke()
      }
      st.finish(output)
    }

  private def run_process(reset_pending: Boolean = false)(body: Log_Progress => Unit): Boolean = {
    val started =
      current_state.change_result { st =>
        if (st.process.is_finished) {
          st.progress.stop()
          val progress = new Log_Progress
          val process =
            Future.thread[Unit](name = "Document_Dockable.process") {
              await_process()
              body(progress)
            }
          (true, st.run(process, progress, reset_pending))
        }
        else (false, st)
      }
    show_state()
    started
  }

  private def load_document(session: String): Boolean = {
    val options = PIDE.options.value
    run_process() { _ =>
      try {
        val session_background =
          Document_Build.session_background(
            options, session, dirs = JEdit_Sessions.session_dirs)
        PIDE.editor.document_setup(Some(session_background))

        finish_process(Nil)
        GUI_Thread.later {
          refresh_theories()
          show_page(input_page)
        }
      }
      catch {
        case exn: Throwable if !Exn.is_interrupt(exn) =>
          finish_process(List(Protocol.error_message(Exn.print(exn))))
          show_page(output_page)
      }
    }
  }

  private def document_build(
    document_session: Document_Editor.Session,
    progress: Log_Progress
  ): Unit = {
    val session_background = document_session.get_background
    val snapshot = document_session.get_snapshot
    val session_context = JEdit_Sessions.open_session_context(document_snapshot = Some(snapshot))
    try {
      val context =
        Document_Build.context(session_context,
          document_session = Some(session_background.base),
          document_selection = document_session.selection,
          progress = progress)

      Isabelle_System.make_directory(Document_Editor.document_output_dir())
      val doc = context.build_document(document_session.get_variant, verbose = true)

      File.write(Document_Editor.document_output().log, doc.log)
      Bytes.write(Document_Editor.document_output().pdf, doc.pdf)
      Document_Editor.view_document()
    }
    finally { session_context.close() }
  }

  private def document_build_attempt(): Boolean = {
    val document_session = PIDE.editor.document_session()
    if (document_session.is_vacuous) true
    else if (document_session.is_pending) false
    else {
      run_process(reset_pending = true) { progress =>
        output_process(progress)
        show_page(output_page)

        val result = Exn.capture { document_build(document_session, progress) }
        val msgs =
          result match {
            case Exn.Res(_) =>
              List(Protocol.writeln_message("OK"))
            case Exn.Exn(exn: Document_Build.Build_Error) =>
              exn.log_errors.map(s => Protocol.error_message(YXML.parse_body(s)))
            case Exn.Exn(exn) =>
              List(Protocol.error_message(YXML.parse_body(Exn.print(exn))))
          }

        progress.stop_program()
        output_process(progress)
        progress.stop()
        finish_process(Pretty.separate(msgs))

        show_page(output_page)
      }
      true
    }
  }

  private lazy val delay_build: Delay =
    Delay.first(PIDE.session.output_delay, gui = true) {
      if (!document_build_attempt()) delay_build.invoke()
    }

  private lazy val delay_auto_build: Delay =
    Delay.last(PIDE.session.document_delay, gui = true) {
      pending_process()
    }

  private def document_pending() = current_state.value.pending

  private val document_auto = new JEdit_Options.Bool_Access("editor_document_auto")



  /* controls */

  private val document_session =
    JEdit_Sessions.document_selector(PIDE.options, standalone = true)

  private lazy val delay_load: Delay =
    Delay.last(PIDE.session.load_delay, gui = true) {
      for (session <- document_session.selection_value) {
        current_state.change(_.reset())
        if (!load_document(session)) delay_load.invoke()
        else if (document_auto()) delay_auto_build.invoke()
      }
    }

  document_session.reactions += {
    case SelectionChanged(_) =>
      delay_load.invoke()
      delay_build.revoke()
      delay_auto_build.revoke()
  }

  private val auto_build_button =
    new JEdit_Options.Bool_GUI(document_auto, "Auto") {
      tooltip = Word.capitalize(document_auto.description)
      override def clicked(state: Boolean): Unit = {
        super.clicked(state)

        if (state) delay_auto_build.invoke()
        else delay_auto_build.revoke()
      }
    }

  private val build_button =
    new GUI.Button("<html><b>Build</b></html>") {
      tooltip = "Build document"
      override def clicked(): Unit = pending_process()
    }

  private val view_button =
    new GUI.Button("View") {
      tooltip = "View document"
      override def clicked(): Unit = Document_Editor.view_document()
    }

  private val controls =
    Wrap_Panel(List(document_session, process_indicator.component,
      auto_build_button, build_button, view_button))

  add(controls.peer, BorderLayout.NORTH)

  override def focusOnDefaultComponent(): Unit = build_button.requestFocus()


  /* message pane with pages */

  private val all_button =
    new GUI.Button("All") {
      tooltip = "Select all document theories"
      override def clicked(): Unit = PIDE.editor.document_select_all(set = true)
    }

  private val none_button =
    new GUI.Button("None") {
      tooltip = "Deselect all document theories"
      override def clicked(): Unit = PIDE.editor.document_select_all(set = false)
    }

  private val purge_button = new GUI.Button("Purge") {
    tooltip = "Remove theories that are no longer required"
    override def clicked(): Unit = PIDE.editor.purge()
  }

  private val input_controls =
    Wrap_Panel(List(all_button, none_button, purge_button))

  private val theories = new Theories_Status(view, document = true)
  private val theories_pane = new ScrollPane(theories.gui)

  private def refresh_theories(): Unit = {
    val domain = PIDE.editor.document_theories().toSet
    theories.update(domain = Some(domain), trim = true, force = true)
    theories.refresh()
  }

  private val input_page =
    new TabbedPane.Page("Input", new BorderPanel {
      layout(input_controls) = BorderPanel.Position.North
      layout(theories_pane) = BorderPanel.Position.Center
    }, "Selection and status of document theories")


  private val cancel_button =
    new GUI.Button("Cancel") {
      tooltip = "Cancel build process"
      override def clicked(): Unit = cancel_process()
    }

  private val output_controls = Wrap_Panel(List(cancel_button, zoom))

  private val output_page =
    new TabbedPane.Page("Output", new BorderPanel {
      layout(output_controls) = BorderPanel.Position.North
      layout(Component.wrap(pretty_text_area)) = BorderPanel.Position.Center
    }, "Results from document build process")

  message_pane.pages ++= List(input_page, output_page)

  set_content(message_pane)


  /* main */

  private val main =
    Session.Consumer[Any](getClass.getName) {
      case _: Session.Global_Options =>
        GUI_Thread.later {
          document_session.load()
          handle_resize()
          refresh_theories()
        }
      case changed: Session.Commands_Changed =>
        GUI_Thread.later {
          val domain = PIDE.editor.document_theories().filter(changed.nodes).toSet
          if (domain.nonEmpty) {
            theories.update(domain = Some(domain))

            val pending = document_pending()
            val auto = document_auto()
            if ((pending || auto) && PIDE.editor.document_session().is_ready) {
              if (pending) {
                delay_auto_build.revoke()
                delay_build.invoke()
              }
              else if (auto) {
                delay_auto_build.invoke()
              }
            }
          }
        }
    }

  override def init(): Unit = {
    PIDE.editor.document_init(dockable)
    init_state()
    PIDE.session.global_options += main
    PIDE.session.commands_changed += main
    handle_resize()
    delay_load.invoke()
  }

  override def exit(): Unit = {
    PIDE.session.global_options -= main
    PIDE.session.commands_changed -= main
    delay_resize.revoke()
    PIDE.editor.document_exit(dockable)
  }
}