src/Tools/jEdit/src/jedit/document_view.scala
author wenzelm
Sun, 10 Jan 2010 20:14:21 +0100
changeset 34858 81d0410dc3ac
parent 34837 df9af932e418
child 34859 aa9e22d9f9a7
permissions -rw-r--r--
iterators for ranges of commands/starts -- avoid extra array per document;
try/finally for saved_color;
misc tuning;
     1 /*
     2  * Document view connected to jEdit text area
     3  *
     4  * @author Fabian Immler, TU Munich
     5  * @author Makarius
     6  */
     7 
     8 package isabelle.jedit
     9 
    10 
    11 import isabelle.proofdocument.{Command, Document, Session}
    12 
    13 import scala.actors.Actor._
    14 
    15 import java.awt.event.{MouseAdapter, MouseEvent}
    16 import java.awt.{BorderLayout, Graphics, Dimension, Color, Graphics2D}
    17 import javax.swing.{JPanel, ToolTipManager}
    18 import javax.swing.event.{CaretListener, CaretEvent}
    19 
    20 import org.gjt.sp.jedit.gui.RolloverButton
    21 import org.gjt.sp.jedit.textarea.{JEditTextArea, TextArea, TextAreaExtension, TextAreaPainter}
    22 
    23 
    24 object Document_View
    25 {
    26   def choose_color(command: Command, doc: Document): Color =
    27   {
    28     doc.current_state(command).status match {
    29       case Command.Status.UNPROCESSED => new Color(255, 228, 225)
    30       case Command.Status.FINISHED => new Color(234, 248, 255)
    31       case Command.Status.FAILED => new Color(255, 193, 193)
    32       case _ => Color.red
    33     }
    34   }
    35 
    36 
    37   /* document view of text area */
    38 
    39   private val key = new Object
    40 
    41   def init(model: Document_Model, text_area: TextArea): Document_View =
    42   {
    43     Swing_Thread.assert()
    44     val doc_view = new Document_View(model, text_area)
    45     text_area.putClientProperty(key, doc_view)
    46     doc_view.activate()
    47     doc_view
    48   }
    49 
    50   def apply(text_area: TextArea): Option[Document_View] =
    51   {
    52     Swing_Thread.assert()
    53     text_area.getClientProperty(key) match {
    54       case doc_view: Document_View => Some(doc_view)
    55       case _ => None
    56     }
    57   }
    58 
    59   def exit(text_area: TextArea)
    60   {
    61     Swing_Thread.assert()
    62     apply(text_area) match {
    63       case None => error("No document view for text area: " + text_area)
    64       case Some(doc_view) =>
    65         doc_view.deactivate()
    66         text_area.putClientProperty(key, null)
    67     }
    68   }
    69 }
    70 
    71 
    72 class Document_View(model: Document_Model, text_area: TextArea)
    73 {
    74   private val session = model.session
    75 
    76 
    77   /* visible document -- owned by Swing thread */
    78 
    79   @volatile private var document = model.recent_document()
    80 
    81 
    82   /* buffer of changed commands -- owned by Swing thread */
    83 
    84   @volatile private var changed_commands: Set[Command] = Set()
    85 
    86   private val changed_delay = Swing_Thread.delay_first(100) {
    87     if (!changed_commands.isEmpty) {
    88       document = model.recent_document()
    89       for (cmd <- changed_commands if document.commands.contains(cmd)) { // FIXME cover doc states as well!!?
    90         update_syntax(cmd)
    91         invalidate_line(cmd)
    92         overview.repaint()
    93       }
    94       changed_commands = Set()
    95     }
    96   }
    97 
    98 
    99   /* command change actor */
   100 
   101   private case object Exit
   102 
   103   private val command_change_actor = actor {
   104     loop {
   105       react {
   106         case command: Command =>  // FIXME rough filtering according to document family!?
   107           Swing_Thread.now {
   108             changed_commands += command
   109             changed_delay()
   110           }
   111 
   112         case Exit => reply(()); exit()
   113 
   114         case bad => System.err.println("command_change_actor: ignoring bad message " + bad)
   115       }
   116     }
   117   }
   118 
   119 
   120   /* text_area_extension */
   121 
   122   private val text_area_extension = new TextAreaExtension
   123   {
   124     override def paintValidLine(gfx: Graphics2D,
   125       screen_line: Int, physical_line: Int, start: Int, end: Int, y: Int)
   126     {
   127       def from_current(pos: Int) = model.from_current(document, pos)
   128       def to_current(pos: Int) = model.to_current(document, pos)
   129       val metrics = text_area.getPainter.getFontMetrics
   130       val saved_color = gfx.getColor
   131       try {
   132         for ((command, command_start) <-
   133           document.command_range(from_current(start), from_current(end)))
   134         {
   135           val begin = start max to_current(command_start)
   136           val finish = (end - 1) min to_current(command_start + command.length)
   137           encolor(gfx, y, metrics.getHeight, begin, finish,
   138             Document_View.choose_color(command, document), true)
   139         }
   140       }
   141       finally { gfx.setColor(saved_color) }
   142     }
   143 
   144     override def getToolTipText(x: Int, y: Int): String =
   145     {
   146       val offset = model.from_current(document, text_area.xyToOffset(x, y))
   147       document.command_at(offset) match {
   148         case Some((command, command_start)) =>
   149           document.current_state(command).type_at(offset - command_start).getOrElse(null)
   150         case None => null
   151       }
   152     }
   153   }
   154 
   155 
   156   /* caret_listener */
   157 
   158   private var _selected_command: Command = null
   159   private def selected_command = _selected_command
   160   private def selected_command_=(cmd: Command)
   161   {
   162     _selected_command = cmd
   163     session.results.event(cmd)
   164   }
   165 
   166   private val caret_listener = new CaretListener
   167   {
   168     override def caretUpdate(e: CaretEvent)
   169     {
   170       document.command_at(e.getDot) match {
   171         case Some((command, command_start)) if (selected_command != command) =>
   172           selected_command = command
   173         case _ =>
   174       }
   175     }
   176   }
   177 
   178 
   179   /* (re)painting */
   180 
   181   private val update_delay = Swing_Thread.delay_first(500) { model.buffer.propertiesChanged() }
   182 
   183   private def update_syntax(cmd: Command)
   184   {
   185     val (line1, line2) = model.lines_of_command(document, cmd)
   186     if (line2 >= text_area.getFirstLine &&
   187       line1 <= text_area.getFirstLine + text_area.getVisibleLines)
   188         update_delay()
   189   }
   190 
   191   private def invalidate_line(cmd: Command) =
   192   {
   193     val (start, stop) = model.lines_of_command(document, cmd)
   194     text_area.invalidateLineRange(start, stop)
   195 
   196     if (selected_command == cmd)
   197       session.results.event(cmd)
   198   }
   199 
   200   private def invalidate_all() =
   201     text_area.invalidateLineRange(text_area.getFirstPhysicalLine,
   202       text_area.getLastPhysicalLine)
   203 
   204   private def encolor(gfx: Graphics2D,
   205     y: Int, height: Int, begin: Int, finish: Int, color: Color, fill: Boolean)
   206   {
   207     val start = text_area.offsetToXY(begin)
   208     val stop =
   209       if (finish < model.buffer.getLength) text_area.offsetToXY(finish)
   210       else {
   211         val p = text_area.offsetToXY(finish - 1)
   212         val metrics = text_area.getPainter.getFontMetrics
   213         p.x = p.x + (metrics.charWidth(' ') max metrics.getMaxAdvance)
   214         p
   215       }
   216 
   217     if (start != null && stop != null) {
   218       gfx.setColor(color)
   219       if (fill) gfx.fillRect(start.x, y, stop.x - start.x, height)
   220       else gfx.drawRect(start.x, y, stop.x - start.x, height)
   221     }
   222   }
   223 
   224 
   225   /* overview of command status left of scrollbar */
   226 
   227   private val overview = new JPanel(new BorderLayout)
   228   {
   229     private val WIDTH = 10
   230     private val HEIGHT = 2
   231 
   232     setPreferredSize(new Dimension(WIDTH, 0))
   233 
   234     setRequestFocusEnabled(false)
   235 
   236     addMouseListener(new MouseAdapter {
   237       override def mousePressed(event: MouseEvent) {
   238         val line = y_to_line(event.getY)
   239         if (line >= 0 && line < text_area.getLineCount)
   240           text_area.setCaretPosition(text_area.getLineStartOffset(line))
   241       }
   242     })
   243 
   244     override def addNotify() {
   245       super.addNotify()
   246       ToolTipManager.sharedInstance.registerComponent(this)
   247     }
   248 
   249     override def removeNotify() {
   250       ToolTipManager.sharedInstance.unregisterComponent(this)
   251       super.removeNotify
   252     }
   253 
   254     override def getToolTipText(event: MouseEvent): String =
   255     {
   256       val line = y_to_line(event.getY())
   257       if (line >= 0 && line < text_area.getLineCount) "<html><b>TODO:</b><br>Tooltip</html>"
   258       else ""
   259     }
   260 
   261     override def paintComponent(gfx: Graphics)
   262     {
   263       super.paintComponent(gfx)
   264       val buffer = model.buffer
   265 
   266       for (command <- document.commands) {
   267         val line1 = buffer.getLineOfOffset(model.to_current(document, command.start(document)))
   268         val line2 = buffer.getLineOfOffset(model.to_current(document, command.stop(document))) + 1
   269         val y = line_to_y(line1)
   270         val height = HEIGHT * (line2 - line1)
   271         gfx.setColor(Document_View.choose_color(command, document))
   272         gfx.fillRect(0, y, getWidth - 1, height)
   273       }
   274     }
   275 
   276     private def line_to_y(line: Int): Int =
   277       (line * getHeight) / (text_area.getBuffer.getLineCount max text_area.getVisibleLines)
   278 
   279     private def y_to_line(y: Int): Int =
   280       (y * (text_area.getBuffer.getLineCount max text_area.getVisibleLines)) / getHeight
   281   }
   282 
   283 
   284   /* activation */
   285 
   286   private def activate()
   287   {
   288     text_area.getPainter.
   289       addExtension(TextAreaPainter.LINE_BACKGROUND_LAYER + 1, text_area_extension)
   290     text_area.addCaretListener(caret_listener)
   291     text_area.addLeftOfScrollBar(overview)
   292     session.command_change += command_change_actor
   293   }
   294 
   295   private def deactivate()
   296   {
   297     session.command_change -= command_change_actor
   298     command_change_actor !? Exit
   299     text_area.removeLeftOfScrollBar(overview)
   300     text_area.removeCaretListener(caret_listener)
   301     text_area.getPainter.removeExtension(text_area_extension)
   302   }
   303 }