src/Tools/jEdit/src/jedit/document_view.scala
author wenzelm
Tue, 07 Sep 2010 14:08:21 +0200
changeset 39449 e3ac771235f7
parent 39391 ba17ca3acdd3
child 39450 18cdf2833371
permissions -rw-r--r--
report token range after inner parse error -- often provides important clues about misunderstanding concerning lexical phase;
tuned color;
     1 /*  Title:      Tools/jEdit/src/jedit/document_view.scala
     2     Author:     Fabian Immler, TU Munich
     3     Author:     Makarius
     4 
     5 Document view connected to jEdit text area.
     6 */
     7 
     8 package isabelle.jedit
     9 
    10 
    11 import isabelle._
    12 
    13 import scala.actors.Actor._
    14 
    15 import java.awt.event.{MouseAdapter, MouseMotionAdapter, MouseEvent, FocusAdapter, FocusEvent}
    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.OperatingSystem
    21 import org.gjt.sp.jedit.gui.RolloverButton
    22 import org.gjt.sp.jedit.textarea.{JEditTextArea, TextArea, TextAreaExtension, TextAreaPainter}
    23 import org.gjt.sp.jedit.syntax.SyntaxStyle
    24 
    25 
    26 object Document_View
    27 {
    28   /* physical rendering */
    29 
    30   def status_color(snapshot: Document.Snapshot, command: Command): Color =
    31   {
    32     val state = snapshot.state(command)
    33     if (snapshot.is_outdated) new Color(240, 240, 240)
    34     else
    35       Isar_Document.command_status(state.status) match {
    36         case Isar_Document.Forked(i) if i > 0 => new Color(255, 228, 225)
    37         case Isar_Document.Finished => new Color(234, 248, 255)
    38         case Isar_Document.Failed => new Color(255, 193, 193)
    39         case Isar_Document.Unprocessed => new Color(255, 228, 225)
    40         case _ => Color.red
    41       }
    42   }
    43 
    44   val message_markup: PartialFunction[Text.Info[Any], Color] =
    45   {
    46     case Text.Info(_, XML.Elem(Markup(Markup.WRITELN, _), _)) => new Color(192, 192, 192)
    47     case Text.Info(_, XML.Elem(Markup(Markup.WARNING, _), _)) => new Color(255, 165, 0)
    48     case Text.Info(_, XML.Elem(Markup(Markup.ERROR, _), _)) => new Color(255, 106, 106)
    49   }
    50 
    51   val box_markup: PartialFunction[Text.Info[Any], Color] =
    52   {
    53     case Text.Info(_, XML.Elem(Markup(Markup.TOKEN_RANGE, _), _)) => new Color(192, 192, 192)
    54   }
    55 
    56 
    57   /* document view of text area */
    58 
    59   private val key = new Object
    60 
    61   def init(model: Document_Model, text_area: TextArea): Document_View =
    62   {
    63     Swing_Thread.require()
    64     val doc_view = new Document_View(model, text_area)
    65     text_area.putClientProperty(key, doc_view)
    66     doc_view.activate()
    67     doc_view
    68   }
    69 
    70   def apply(text_area: TextArea): Option[Document_View] =
    71   {
    72     Swing_Thread.require()
    73     text_area.getClientProperty(key) match {
    74       case doc_view: Document_View => Some(doc_view)
    75       case _ => None
    76     }
    77   }
    78 
    79   def exit(text_area: TextArea)
    80   {
    81     Swing_Thread.require()
    82     apply(text_area) match {
    83       case None => error("No document view for text area: " + text_area)
    84       case Some(doc_view) =>
    85         doc_view.deactivate()
    86         text_area.putClientProperty(key, null)
    87     }
    88   }
    89 }
    90 
    91 
    92 class Document_View(val model: Document_Model, text_area: TextArea)
    93 {
    94   private val session = model.session
    95 
    96 
    97   /* extended token styles */
    98 
    99   private var styles: Array[SyntaxStyle] = null  // owned by Swing thread
   100 
   101   def extend_styles()
   102   {
   103     Swing_Thread.require()
   104     styles = Document_Model.Token_Markup.extend_styles(text_area.getPainter.getStyles)
   105   }
   106   extend_styles()
   107 
   108   def set_styles()
   109   {
   110     Swing_Thread.require()
   111     text_area.getPainter.setStyles(styles)
   112   }
   113 
   114 
   115   /* visible line ranges */
   116 
   117   // simplify slightly odd result of TextArea.getScreenLineEndOffset etc.
   118   // NB: jEdit already normalizes \r\n and \r to \n
   119   def proper_line_range(start: Text.Offset, end: Text.Offset): Text.Range =
   120   {
   121     val stop = if (start < end) end - 1 else end min model.buffer.getLength
   122     Text.Range(start, stop)
   123   }
   124 
   125   def screen_lines_range(): Text.Range =
   126   {
   127     val start = text_area.getScreenLineStartOffset(0)
   128     val raw_end = text_area.getScreenLineEndOffset(text_area.getVisibleLines - 1 max 0)
   129     proper_line_range(start, if (raw_end >= 0) raw_end else model.buffer.getLength)
   130   }
   131 
   132   def invalidate_line_range(range: Text.Range)
   133   {
   134     text_area.invalidateLineRange(
   135       model.buffer.getLineOfOffset(range.start),
   136       model.buffer.getLineOfOffset(range.stop))
   137   }
   138 
   139 
   140   /* commands_changed_actor */
   141 
   142   private val commands_changed_actor = actor {
   143     loop {
   144       react {
   145         case Session.Commands_Changed(changed) =>
   146           val buffer = model.buffer
   147           Isabelle.swing_buffer_lock(buffer) {
   148             val snapshot = model.snapshot()
   149 
   150             if (changed.exists(snapshot.node.commands.contains))
   151               overview.repaint()
   152 
   153             val visible_range = screen_lines_range()
   154             val visible_cmds = snapshot.node.command_range(snapshot.revert(visible_range)).map(_._1)
   155             if (visible_cmds.exists(changed)) {
   156               for {
   157                 line <- 0 until text_area.getVisibleLines
   158                 val start = text_area.getScreenLineStartOffset(line) if start >= 0
   159                 val end = text_area.getScreenLineEndOffset(line) if end >= 0
   160                 val range = proper_line_range(start, end)
   161                 val line_cmds = snapshot.node.command_range(snapshot.revert(range)).map(_._1)
   162                 if line_cmds.exists(changed)
   163               } text_area.invalidateScreenLineRange(line, line)
   164 
   165               // FIXME danger of deadlock!?
   166               // FIXME potentially slow!?
   167               model.buffer.propertiesChanged()
   168             }
   169           }
   170 
   171         case bad => System.err.println("command_change_actor: ignoring bad message " + bad)
   172       }
   173     }
   174   }
   175 
   176 
   177   /* subexpression highlighting */
   178 
   179   private def subexp_range(snapshot: Document.Snapshot, x: Int, y: Int): Option[Text.Range] =
   180   {
   181     val subexp_markup: PartialFunction[Text.Info[Any], Option[Text.Range]] =
   182     {
   183       case Text.Info(range, XML.Elem(Markup(Markup.ML_TYPING, _), _)) =>
   184         Some(snapshot.convert(range))
   185     }
   186     val offset = text_area.xyToOffset(x, y)
   187     val markup = snapshot.select_markup(Text.Range(offset, offset + 1))(subexp_markup)(None)
   188     if (markup.hasNext) markup.next.info else None
   189   }
   190 
   191   private var highlight_range: Option[Text.Range] = None
   192 
   193   private val focus_listener = new FocusAdapter {
   194     override def focusLost(e: FocusEvent) { highlight_range = None }
   195   }
   196 
   197   private val mouse_motion_listener = new MouseMotionAdapter {
   198     override def mouseMoved(e: MouseEvent) {
   199       val control = if (OperatingSystem.isMacOS()) e.isMetaDown else e.isControlDown
   200       if (!model.buffer.isLoaded) highlight_range = None
   201       else
   202         Isabelle.swing_buffer_lock(model.buffer) {
   203           highlight_range.map(invalidate_line_range(_))
   204           highlight_range =
   205             if (control) subexp_range(model.snapshot(), e.getX(), e.getY()) else None
   206           highlight_range.map(invalidate_line_range(_))
   207         }
   208     }
   209   }
   210 
   211 
   212   /* text_area_extension */
   213 
   214   private val text_area_extension = new TextAreaExtension
   215   {
   216     override def paintScreenLineRange(gfx: Graphics2D,
   217       first_line: Int, last_line: Int, physical_lines: Array[Int],
   218       start: Array[Int], end: Array[Int], y: Int, line_height: Int)
   219     {
   220       Isabelle.swing_buffer_lock(model.buffer) {
   221         val snapshot = model.snapshot()
   222         val saved_color = gfx.getColor
   223         val ascent = text_area.getPainter.getFontMetrics.getAscent
   224 
   225         try {
   226           for (i <- 0 until physical_lines.length) {
   227             if (physical_lines(i) != -1) {
   228               val line_range = proper_line_range(start(i), end(i))
   229 
   230               // background color
   231               val cmds = snapshot.node.command_range(snapshot.revert(line_range))
   232               for {
   233                 (command, command_start) <- cmds if !command.is_ignored
   234                 val range = line_range.restrict(snapshot.convert(command.range + command_start))
   235                 r <- Isabelle.gfx_range(text_area, range)
   236               } {
   237                 gfx.setColor(Document_View.status_color(snapshot, command))
   238                 gfx.fillRect(r.x, y + i * line_height, r.length, line_height)
   239               }
   240 
   241               // subexpression highlighting -- potentially from other snapshot
   242               if (highlight_range.isDefined) {
   243                 if (line_range.overlaps(highlight_range.get)) {
   244                   Isabelle.gfx_range(text_area, line_range.restrict(highlight_range.get)) match {
   245                     case None =>
   246                     case Some(r) =>
   247                       gfx.setColor(Color.black)
   248                       gfx.drawRect(r.x, y + i * line_height, r.length, line_height - 1)
   249                   }
   250                 }
   251               }
   252 
   253               // boxed text
   254               for {
   255                 Text.Info(range, color) <-
   256                   snapshot.select_markup(line_range)(Document_View.box_markup)(null)
   257                 if color != null
   258                 r <- Isabelle.gfx_range(text_area, range)
   259               } {
   260                 gfx.setColor(color)
   261                 gfx.drawRect(r.x + 1, y + i * line_height + 1, r.length - 2, line_height - 3)
   262               }
   263 
   264               // squiggly underline
   265               for {
   266                 Text.Info(range, color) <-
   267                   snapshot.select_markup(line_range)(Document_View.message_markup)(null)
   268                 if color != null
   269                 r <- Isabelle.gfx_range(text_area, range)
   270               } {
   271                 gfx.setColor(color)
   272                 val x0 = (r.x / 2) * 2
   273                 val y0 = r.y + ascent + 1
   274                 for (x1 <- Range(x0, x0 + r.length, 2)) {
   275                   val y1 = if (x1 % 4 < 2) y0 else y0 + 1
   276                   gfx.drawLine(x1, y1, x1 + 1, y1)
   277                 }
   278               }
   279             }
   280           }
   281         }
   282         finally { gfx.setColor(saved_color) }
   283       }
   284     }
   285 
   286     override def getToolTipText(x: Int, y: Int): String =
   287     {
   288       Isabelle.swing_buffer_lock(model.buffer) {
   289         val snapshot = model.snapshot()
   290         val offset = text_area.xyToOffset(x, y)
   291         val markup =
   292           snapshot.select_markup(Text.Range(offset, offset + 1)) {
   293             case Text.Info(range, XML.Elem(Markup(Markup.ML_TYPING, _), body)) =>
   294               Isabelle.tooltip(Pretty.string_of(List(Pretty.block(body)), margin = 40))
   295           } { null }
   296         if (markup.hasNext) markup.next.info else null
   297       }
   298     }
   299   }
   300 
   301 
   302   /* caret handling */
   303 
   304   def selected_command(): Option[Command] =
   305   {
   306     Swing_Thread.require()
   307     model.snapshot().node.proper_command_at(text_area.getCaretPosition)
   308   }
   309 
   310   private val caret_listener = new CaretListener {
   311     private val delay = Swing_Thread.delay_last(session.input_delay) {
   312       session.perspective.event(Session.Perspective)
   313     }
   314     override def caretUpdate(e: CaretEvent) { delay() }
   315   }
   316 
   317 
   318   /* overview of command status left of scrollbar */
   319 
   320   private val overview = new JPanel(new BorderLayout)
   321   {
   322     private val WIDTH = 10
   323     private val HEIGHT = 2
   324 
   325     setPreferredSize(new Dimension(WIDTH, 0))
   326 
   327     setRequestFocusEnabled(false)
   328 
   329     addMouseListener(new MouseAdapter {
   330       override def mousePressed(event: MouseEvent) {
   331         val line = y_to_line(event.getY)
   332         if (line >= 0 && line < text_area.getLineCount)
   333           text_area.setCaretPosition(text_area.getLineStartOffset(line))
   334       }
   335     })
   336 
   337     override def addNotify() {
   338       super.addNotify()
   339       ToolTipManager.sharedInstance.registerComponent(this)
   340     }
   341 
   342     override def removeNotify() {
   343       ToolTipManager.sharedInstance.unregisterComponent(this)
   344       super.removeNotify
   345     }
   346 
   347     override def getToolTipText(event: MouseEvent): String =
   348     {
   349       val line = y_to_line(event.getY())
   350       if (line >= 0 && line < text_area.getLineCount) "<html><b>TODO:</b><br>Tooltip</html>"
   351       else ""
   352     }
   353 
   354     override def paintComponent(gfx: Graphics)
   355     {
   356       super.paintComponent(gfx)
   357       Swing_Thread.assert()
   358       val buffer = model.buffer
   359       Isabelle.buffer_lock(buffer) {
   360         val snapshot = model.snapshot()
   361         val saved_color = gfx.getColor  // FIXME needed!?
   362         try {
   363           for ((command, start) <- snapshot.node.command_starts if !command.is_ignored) {
   364             val line1 = buffer.getLineOfOffset(snapshot.convert(start))
   365             val line2 = buffer.getLineOfOffset(snapshot.convert(start + command.length)) + 1
   366             val y = line_to_y(line1)
   367             val height = HEIGHT * (line2 - line1)
   368             gfx.setColor(Document_View.status_color(snapshot, command))
   369             gfx.fillRect(0, y, getWidth - 1, height)
   370           }
   371         }
   372         finally { gfx.setColor(saved_color) }
   373       }
   374     }
   375 
   376     private def line_to_y(line: Int): Int =
   377       (line * getHeight) / (text_area.getBuffer.getLineCount max text_area.getVisibleLines)
   378 
   379     private def y_to_line(y: Int): Int =
   380       (y * (text_area.getBuffer.getLineCount max text_area.getVisibleLines)) / getHeight
   381   }
   382 
   383 
   384   /* activation */
   385 
   386   private def activate()
   387   {
   388     text_area.getPainter.
   389       addExtension(TextAreaPainter.LINE_BACKGROUND_LAYER + 1, text_area_extension)
   390     text_area.addFocusListener(focus_listener)
   391     text_area.getPainter.addMouseMotionListener(mouse_motion_listener)
   392     text_area.addCaretListener(caret_listener)
   393     text_area.addLeftOfScrollBar(overview)
   394     session.commands_changed += commands_changed_actor
   395   }
   396 
   397   private def deactivate()
   398   {
   399     session.commands_changed -= commands_changed_actor
   400     text_area.removeFocusListener(focus_listener)
   401     text_area.getPainter.removeMouseMotionListener(mouse_motion_listener)
   402     text_area.removeCaretListener(caret_listener)
   403     text_area.removeLeftOfScrollBar(overview)
   404     text_area.getPainter.removeExtension(text_area_extension)
   405   }
   406 }