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