src/Tools/jEdit/src/rich_text_area.scala
changeset 49411 1da54e9bda68
parent 49410 34acbcc33adf
child 49424 491363c6feb4
equal deleted inserted replaced
49410:34acbcc33adf 49411:1da54e9bda68
       
     1 /*  Title:      Tools/jEdit/src/rich_text_area.scala
       
     2     Author:     Makarius
       
     3 
       
     4 Enhanced version of jEdit text area, with rich text rendering,
       
     5 tooltips, hyperlinks etc.
       
     6 */
       
     7 
       
     8 package isabelle.jedit
       
     9 
       
    10 
       
    11 import isabelle._
       
    12 
       
    13 import java.awt.{Graphics2D, Shape, Window, Color}
       
    14 import java.awt.event.{MouseMotionAdapter, MouseAdapter, MouseEvent,
       
    15   FocusAdapter, FocusEvent, WindowEvent, WindowAdapter}
       
    16 import java.awt.font.TextAttribute
       
    17 import java.text.AttributedString
       
    18 import java.util.ArrayList
       
    19 
       
    20 import org.gjt.sp.util.Log
       
    21 import org.gjt.sp.jedit.{OperatingSystem, Debug, View}
       
    22 import org.gjt.sp.jedit.syntax.{DisplayTokenHandler, Chunk}
       
    23 import org.gjt.sp.jedit.textarea.{TextAreaExtension, TextAreaPainter, TextArea}
       
    24 
       
    25 
       
    26 class Rich_Text_Area(view: View, text_area: TextArea, get_rendering: () => Isabelle_Rendering)
       
    27 {
       
    28   private val buffer = text_area.getBuffer
       
    29 
       
    30 
       
    31   /* robust extension body */
       
    32 
       
    33   def robust_body[A](default: A)(body: => A): A =
       
    34   {
       
    35     try {
       
    36       Swing_Thread.require()
       
    37       if (buffer == text_area.getBuffer) body
       
    38       else {
       
    39         Log.log(Log.ERROR, this, ERROR("Implicit change of text area buffer"))
       
    40         default
       
    41       }
       
    42     }
       
    43     catch { case t: Throwable => Log.log(Log.ERROR, this, t); default }
       
    44   }
       
    45 
       
    46 
       
    47   /* original painters */
       
    48 
       
    49   private def pick_extension(name: String): TextAreaExtension =
       
    50   {
       
    51     text_area.getPainter.getExtensions.iterator.filter(x => x.getClass.getName == name).toList
       
    52     match {
       
    53       case List(x) => x
       
    54       case _ => error("Expected exactly one " + name)
       
    55     }
       
    56   }
       
    57 
       
    58   private val orig_text_painter =
       
    59     pick_extension("org.gjt.sp.jedit.textarea.TextAreaPainter$PaintText")
       
    60 
       
    61 
       
    62   /* common painter state */
       
    63 
       
    64   @volatile private var painter_rendering: Isabelle_Rendering = null
       
    65   @volatile private var painter_clip: Shape = null
       
    66 
       
    67   private val set_state = new TextAreaExtension
       
    68   {
       
    69     override def paintScreenLineRange(gfx: Graphics2D,
       
    70       first_line: Int, last_line: Int, physical_lines: Array[Int],
       
    71       start: Array[Int], end: Array[Int], y: Int, line_height: Int)
       
    72     {
       
    73       painter_rendering = get_rendering()
       
    74       painter_clip = gfx.getClip
       
    75     }
       
    76   }
       
    77 
       
    78   private val reset_state = new TextAreaExtension
       
    79   {
       
    80     override def paintScreenLineRange(gfx: Graphics2D,
       
    81       first_line: Int, last_line: Int, physical_lines: Array[Int],
       
    82       start: Array[Int], end: Array[Int], y: Int, line_height: Int)
       
    83     {
       
    84       painter_rendering = null
       
    85       painter_clip = null
       
    86     }
       
    87   }
       
    88 
       
    89   private def robust_rendering(body: Isabelle_Rendering => Unit)
       
    90   {
       
    91     robust_body(()) { body(painter_rendering) }
       
    92   }
       
    93 
       
    94 
       
    95   /* active areas within the text */
       
    96 
       
    97   private class Active_Area[A](
       
    98     rendering: Isabelle_Rendering => Text.Range => Option[Text.Info[A]])
       
    99   {
       
   100     private var the_info: Option[Text.Info[A]] = None
       
   101 
       
   102     def info: Option[Text.Info[A]] = the_info
       
   103 
       
   104     def update(new_info: Option[Text.Info[A]])
       
   105     {
       
   106       val old_info = the_info
       
   107       if (new_info != old_info) {
       
   108         for { opt <- List(old_info, new_info); Text.Info(range, _) <- opt }
       
   109           JEdit_Lib.invalidate_range(text_area, range)
       
   110         the_info = new_info
       
   111       }
       
   112     }
       
   113 
       
   114     def update_rendering(r: Isabelle_Rendering, range: Text.Range)
       
   115     { update(rendering(r)(range)) }
       
   116 
       
   117     def reset { update(None) }
       
   118   }
       
   119 
       
   120   // owned by Swing thread
       
   121 
       
   122   private var control: Boolean = false
       
   123 
       
   124   private val highlight_area = new Active_Area[Color]((r: Isabelle_Rendering) => r.highlight _)
       
   125   private val hyperlink_area = new Active_Area[Hyperlink]((r: Isabelle_Rendering) => r.hyperlink _)
       
   126   private val active_areas = List(highlight_area, hyperlink_area)
       
   127   private def active_reset(): Unit = active_areas.foreach(_.reset)
       
   128 
       
   129   private val focus_listener = new FocusAdapter {
       
   130     override def focusLost(e: FocusEvent) { active_reset() }
       
   131   }
       
   132 
       
   133   private val window_listener = new WindowAdapter {
       
   134     override def windowIconified(e: WindowEvent) { active_reset() }
       
   135     override def windowDeactivated(e: WindowEvent) { active_reset() }
       
   136   }
       
   137 
       
   138   private val mouse_listener = new MouseAdapter {
       
   139     override def mouseClicked(e: MouseEvent) {
       
   140       hyperlink_area.info match {
       
   141         case Some(Text.Info(range, link)) => link.follow(view)
       
   142         case None =>
       
   143       }
       
   144     }
       
   145   }
       
   146 
       
   147   private val mouse_motion_listener = new MouseMotionAdapter {
       
   148     override def mouseMoved(e: MouseEvent) {
       
   149       control = if (OperatingSystem.isMacOS()) e.isMetaDown else e.isControlDown
       
   150       if (control && !buffer.isLoading) {
       
   151         JEdit_Lib.buffer_lock(buffer) {
       
   152           val rendering = get_rendering()
       
   153           val mouse_offset = text_area.xyToOffset(e.getX(), e.getY())
       
   154           val mouse_range = JEdit_Lib.point_range(buffer, mouse_offset)
       
   155           active_areas.foreach(_.update_rendering(rendering, mouse_range))
       
   156         }
       
   157       }
       
   158       else active_reset()
       
   159     }
       
   160   }
       
   161 
       
   162 
       
   163   /* tooltips */
       
   164 
       
   165   private val tooltip_painter = new TextAreaExtension
       
   166   {
       
   167     override def getToolTipText(x: Int, y: Int): String =
       
   168     {
       
   169       robust_body(null: String) {
       
   170         val rendering = get_rendering()
       
   171         val offset = text_area.xyToOffset(x, y)
       
   172         val range = Text.Range(offset, offset + 1)
       
   173         val tip =
       
   174           if (control) rendering.tooltip(range)
       
   175           else rendering.tooltip_message(range)
       
   176         tip.map(Isabelle.tooltip(_)) getOrElse null
       
   177       }
       
   178     }
       
   179   }
       
   180 
       
   181 
       
   182   /* text background */
       
   183 
       
   184   private val background_painter = new TextAreaExtension
       
   185   {
       
   186     override def paintScreenLineRange(gfx: Graphics2D,
       
   187       first_line: Int, last_line: Int, physical_lines: Array[Int],
       
   188       start: Array[Int], end: Array[Int], y: Int, line_height: Int)
       
   189     {
       
   190       robust_rendering { rendering =>
       
   191         val ascent = text_area.getPainter.getFontMetrics.getAscent
       
   192 
       
   193         for (i <- 0 until physical_lines.length) {
       
   194           if (physical_lines(i) != -1) {
       
   195             val line_range = JEdit_Lib.proper_line_range(buffer, start(i), end(i))
       
   196 
       
   197             // background color (1)
       
   198             for {
       
   199               Text.Info(range, color) <- rendering.background1(line_range)
       
   200               r <- JEdit_Lib.gfx_range(text_area, range)
       
   201             } {
       
   202               gfx.setColor(color)
       
   203               gfx.fillRect(r.x, y + i * line_height, r.length, line_height)
       
   204             }
       
   205 
       
   206             // background color (2)
       
   207             for {
       
   208               Text.Info(range, color) <- rendering.background2(line_range)
       
   209               r <- JEdit_Lib.gfx_range(text_area, range)
       
   210             } {
       
   211               gfx.setColor(color)
       
   212               gfx.fillRect(r.x + 2, y + i * line_height + 2, r.length - 4, line_height - 4)
       
   213             }
       
   214 
       
   215             // squiggly underline
       
   216             for {
       
   217               Text.Info(range, color) <- rendering.squiggly_underline(line_range)
       
   218               r <- JEdit_Lib.gfx_range(text_area, range)
       
   219             } {
       
   220               gfx.setColor(color)
       
   221               val x0 = (r.x / 2) * 2
       
   222               val y0 = r.y + ascent + 1
       
   223               for (x1 <- Range(x0, x0 + r.length, 2)) {
       
   224                 val y1 = if (x1 % 4 < 2) y0 else y0 + 1
       
   225                 gfx.drawLine(x1, y1, x1 + 1, y1)
       
   226               }
       
   227             }
       
   228           }
       
   229         }
       
   230       }
       
   231     }
       
   232   }
       
   233 
       
   234 
       
   235   /* text */
       
   236 
       
   237   private def paint_chunk_list(rendering: Isabelle_Rendering,
       
   238     gfx: Graphics2D, line_start: Text.Offset, head: Chunk, x: Float, y: Float): Float =
       
   239   {
       
   240     val clip_rect = gfx.getClipBounds
       
   241     val painter = text_area.getPainter
       
   242     val font_context = painter.getFontRenderContext
       
   243 
       
   244     var w = 0.0f
       
   245     var chunk = head
       
   246     while (chunk != null) {
       
   247       val chunk_offset = line_start + chunk.offset
       
   248       if (x + w + chunk.width > clip_rect.x &&
       
   249           x + w < clip_rect.x + clip_rect.width && chunk.accessable)
       
   250       {
       
   251         val chunk_range = Text.Range(chunk_offset, chunk_offset + chunk.length)
       
   252         val chunk_str = if (chunk.str == null) " " * chunk.length else chunk.str
       
   253         val chunk_font = chunk.style.getFont
       
   254         val chunk_color = chunk.style.getForegroundColor
       
   255 
       
   256         def string_width(s: String): Float =
       
   257           if (s.isEmpty) 0.0f
       
   258           else chunk_font.getStringBounds(s, font_context).getWidth.toFloat
       
   259 
       
   260         val caret_range =
       
   261           if (text_area.isCaretVisible)
       
   262             JEdit_Lib.point_range(buffer, text_area.getCaretPosition)
       
   263           else Text.Range(-1)
       
   264 
       
   265         val markup =
       
   266           for {
       
   267             r1 <- rendering.text_color(chunk_range, chunk_color)
       
   268             r2 <- r1.try_restrict(chunk_range)
       
   269           } yield r2
       
   270 
       
   271         val padded_markup =
       
   272           if (markup.isEmpty)
       
   273             Iterator(Text.Info(chunk_range, chunk_color))
       
   274           else
       
   275             Iterator(
       
   276               Text.Info(Text.Range(chunk_range.start, markup.head.range.start), chunk_color)) ++
       
   277             markup.iterator ++
       
   278             Iterator(Text.Info(Text.Range(markup.last.range.stop, chunk_range.stop), chunk_color))
       
   279 
       
   280         var x1 = x + w
       
   281         gfx.setFont(chunk_font)
       
   282         for (Text.Info(range, color) <- padded_markup if !range.is_singularity) {
       
   283           val str = chunk_str.substring(range.start - chunk_offset, range.stop - chunk_offset)
       
   284           gfx.setColor(color)
       
   285 
       
   286           range.try_restrict(caret_range) match {
       
   287             case Some(r) if !r.is_singularity =>
       
   288               val i = r.start - range.start
       
   289               val j = r.stop - range.start
       
   290               val s1 = str.substring(0, i)
       
   291               val s2 = str.substring(i, j)
       
   292               val s3 = str.substring(j)
       
   293 
       
   294               if (!s1.isEmpty) gfx.drawString(s1, x1, y)
       
   295 
       
   296               val astr = new AttributedString(s2)
       
   297               astr.addAttribute(TextAttribute.FONT, chunk_font)
       
   298               astr.addAttribute(TextAttribute.FOREGROUND, painter.getCaretColor)
       
   299               astr.addAttribute(TextAttribute.SWAP_COLORS, TextAttribute.SWAP_COLORS_ON)
       
   300               gfx.drawString(astr.getIterator, x1 + string_width(s1), y)
       
   301 
       
   302               if (!s3.isEmpty)
       
   303                 gfx.drawString(s3, x1 + string_width(str.substring(0, j)), y)
       
   304 
       
   305             case _ =>
       
   306               gfx.drawString(str, x1, y)
       
   307           }
       
   308           x1 += string_width(str)
       
   309         }
       
   310       }
       
   311       w += chunk.width
       
   312       chunk = chunk.next.asInstanceOf[Chunk]
       
   313     }
       
   314     w
       
   315   }
       
   316 
       
   317   private val text_painter = new TextAreaExtension
       
   318   {
       
   319     override def paintScreenLineRange(gfx: Graphics2D,
       
   320       first_line: Int, last_line: Int, physical_lines: Array[Int],
       
   321       start: Array[Int], end: Array[Int], y: Int, line_height: Int)
       
   322     {
       
   323       robust_rendering { rendering =>
       
   324         val clip = gfx.getClip
       
   325         val x0 = text_area.getHorizontalOffset
       
   326         val fm = text_area.getPainter.getFontMetrics
       
   327         var y0 = y + fm.getHeight - (fm.getLeading + 1) - fm.getDescent
       
   328 
       
   329         for (i <- 0 until physical_lines.length) {
       
   330           val line = physical_lines(i)
       
   331           if (line != -1) {
       
   332             val screen_line = first_line + i
       
   333             val chunks = text_area.getChunksOfScreenLine(screen_line)
       
   334             if (chunks != null) {
       
   335               val line_start = buffer.getLineStartOffset(line)
       
   336               gfx.clipRect(x0, y + line_height * i, Integer.MAX_VALUE, line_height)
       
   337               val w = paint_chunk_list(rendering, gfx, line_start, chunks, x0, y0).toInt
       
   338               gfx.clipRect(x0 + w.toInt, 0, Integer.MAX_VALUE, Integer.MAX_VALUE)
       
   339               orig_text_painter.paintValidLine(gfx,
       
   340                 screen_line, line, start(i), end(i), y + line_height * i)
       
   341               gfx.setClip(clip)
       
   342             }
       
   343           }
       
   344           y0 += line_height
       
   345         }
       
   346       }
       
   347     }
       
   348   }
       
   349 
       
   350 
       
   351   /* foreground */
       
   352 
       
   353   private val foreground_painter = new TextAreaExtension
       
   354   {
       
   355     override def paintScreenLineRange(gfx: Graphics2D,
       
   356       first_line: Int, last_line: Int, physical_lines: Array[Int],
       
   357       start: Array[Int], end: Array[Int], y: Int, line_height: Int)
       
   358     {
       
   359       robust_rendering { rendering =>
       
   360         for (i <- 0 until physical_lines.length) {
       
   361           if (physical_lines(i) != -1) {
       
   362             val line_range = JEdit_Lib.proper_line_range(buffer, start(i), end(i))
       
   363 
       
   364             // foreground color
       
   365             for {
       
   366               Text.Info(range, color) <- rendering.foreground(line_range)
       
   367               r <- JEdit_Lib.gfx_range(text_area, range)
       
   368             } {
       
   369               gfx.setColor(color)
       
   370               gfx.fillRect(r.x, y + i * line_height, r.length, line_height)
       
   371             }
       
   372 
       
   373             // highlight range -- potentially from other snapshot
       
   374             for {
       
   375               info <- highlight_area.info
       
   376               Text.Info(range, color) <- info.try_restrict(line_range)
       
   377               r <- JEdit_Lib.gfx_range(text_area, range)
       
   378             } {
       
   379               gfx.setColor(color)
       
   380               gfx.fillRect(r.x, y + i * line_height, r.length, line_height)
       
   381             }
       
   382 
       
   383             // hyperlink range -- potentially from other snapshot
       
   384             for {
       
   385               info <- hyperlink_area.info
       
   386               Text.Info(range, _) <- info.try_restrict(line_range)
       
   387               r <- JEdit_Lib.gfx_range(text_area, range)
       
   388             } {
       
   389               gfx.setColor(rendering.hyperlink_color)
       
   390               gfx.drawRect(r.x, y + i * line_height, r.length - 1, line_height - 1)
       
   391             }
       
   392           }
       
   393         }
       
   394       }
       
   395     }
       
   396   }
       
   397 
       
   398 
       
   399   /* caret -- outside of text range */
       
   400 
       
   401   private class Caret_Painter(before: Boolean) extends TextAreaExtension
       
   402   {
       
   403     override def paintValidLine(gfx: Graphics2D,
       
   404       screen_line: Int, physical_line: Int, start: Int, end: Int, y: Int)
       
   405     {
       
   406       robust_rendering { _ =>
       
   407         if (before) gfx.clipRect(0, 0, 0, 0)
       
   408         else gfx.setClip(painter_clip)
       
   409       }
       
   410     }
       
   411   }
       
   412 
       
   413   private val before_caret_painter1 = new Caret_Painter(true)
       
   414   private val after_caret_painter1 = new Caret_Painter(false)
       
   415   private val before_caret_painter2 = new Caret_Painter(true)
       
   416   private val after_caret_painter2 = new Caret_Painter(false)
       
   417 
       
   418   private val caret_painter = new TextAreaExtension
       
   419   {
       
   420     override def paintValidLine(gfx: Graphics2D,
       
   421       screen_line: Int, physical_line: Int, start: Int, end: Int, y: Int)
       
   422     {
       
   423       robust_rendering { _ =>
       
   424         if (text_area.isCaretVisible) {
       
   425           val caret = text_area.getCaretPosition
       
   426           if (start <= caret && caret == end - 1) {
       
   427             val painter = text_area.getPainter
       
   428             val fm = painter.getFontMetrics
       
   429 
       
   430             val offset = caret - text_area.getLineStartOffset(physical_line)
       
   431             val x = text_area.offsetToXY(physical_line, offset).x
       
   432             gfx.setColor(painter.getCaretColor)
       
   433             gfx.drawRect(x, y, JEdit_Lib.char_width(text_area) - 1, fm.getHeight - 1)
       
   434           }
       
   435         }
       
   436       }
       
   437     }
       
   438   }
       
   439 
       
   440 
       
   441   /* activation */
       
   442 
       
   443   def activate()
       
   444   {
       
   445     val painter = text_area.getPainter
       
   446     painter.addExtension(TextAreaPainter.LOWEST_LAYER, set_state)
       
   447     painter.addExtension(TextAreaPainter.LINE_BACKGROUND_LAYER + 1, tooltip_painter)
       
   448     painter.addExtension(TextAreaPainter.LINE_BACKGROUND_LAYER + 1, background_painter)
       
   449     painter.addExtension(TextAreaPainter.TEXT_LAYER, text_painter)
       
   450     painter.addExtension(TextAreaPainter.CARET_LAYER - 1, before_caret_painter1)
       
   451     painter.addExtension(TextAreaPainter.CARET_LAYER + 1, after_caret_painter1)
       
   452     painter.addExtension(TextAreaPainter.BLOCK_CARET_LAYER - 1, before_caret_painter2)
       
   453     painter.addExtension(TextAreaPainter.BLOCK_CARET_LAYER + 1, after_caret_painter2)
       
   454     painter.addExtension(TextAreaPainter.BLOCK_CARET_LAYER + 2, caret_painter)
       
   455     painter.addExtension(500, foreground_painter)
       
   456     painter.addExtension(TextAreaPainter.HIGHEST_LAYER, reset_state)
       
   457     painter.removeExtension(orig_text_painter)
       
   458     painter.addMouseListener(mouse_listener)
       
   459     painter.addMouseMotionListener(mouse_motion_listener)
       
   460     text_area.addFocusListener(focus_listener)
       
   461     view.addWindowListener(window_listener)
       
   462   }
       
   463 
       
   464   def deactivate()
       
   465   {
       
   466     val painter = text_area.getPainter
       
   467     view.removeWindowListener(window_listener)
       
   468     text_area.removeFocusListener(focus_listener)
       
   469     painter.removeMouseMotionListener(mouse_motion_listener)
       
   470     painter.removeMouseListener(mouse_listener)
       
   471     painter.addExtension(TextAreaPainter.TEXT_LAYER, orig_text_painter)
       
   472     painter.removeExtension(reset_state)
       
   473     painter.removeExtension(foreground_painter)
       
   474     painter.removeExtension(caret_painter)
       
   475     painter.removeExtension(after_caret_painter2)
       
   476     painter.removeExtension(before_caret_painter2)
       
   477     painter.removeExtension(after_caret_painter1)
       
   478     painter.removeExtension(before_caret_painter1)
       
   479     painter.removeExtension(text_painter)
       
   480     painter.removeExtension(background_painter)
       
   481     painter.removeExtension(tooltip_painter)
       
   482     painter.removeExtension(set_state)
       
   483   }
       
   484 }
       
   485