#
#   TextViews
#

from math import floor
from Views import View
from Colors import Color
from Fonts import application_font
from Applications import application

class TextView(View):
  """A TextView provides an editable view of
  a TextModel."""

  _fg = Color(0, 0, 0)
  _bg = Color(1, 1, 1)
  _font = application_font()
  _line_height = _font.height()
  _ascent = _font.ascent()
  _left_margin = 5
  _right_margin = 5
  _top_margin = 5
  _bottom_margin = 5
  _caret_on = 1
  _caret_rect = None

  def __init__(self, *args, **kw):
    apply(View.__init__, (self,) + args, kw)
    self.update_caret_rect()

  #
  #   Properties
  #

  def font(self):
    "Return the font property."
    return self._font

  def set_font(self, f):
    "Set the font property."
    self._font = f
    self._line_height = f.height()
    self._ascent = f.ascent()
    self.update_extent()
    self.update_caret_rect()

  def margins(self):
    "Return the text margins."
    return (self._left_margin, self._top_margin,
        self._right_margin, self._bottom_margin)

  def set_margins(self, l, t, r, b):
    "Set the text margins."
    self._left_margin = l
    self._top_margin = t
    self._right_margin = r
    self._bottom_margin = b
    self.update_extent()
    self.update_caret_rect()
    self.invalidate()

  #
  #   Callbacks
  #

  def draw(self, c):
    debug = 0
    if debug:
      print "TextView.draw:" ###
    text = self.model()
    sel_start, sel_end = text.selection()
    sel_start_line, sel_start_col = sel_start
    sel_end_line, sel_end_col = sel_end
    text_end_line, text_end_col = text.end()
##     if sel_start == sel_end and self._caret_on:
##       caret_line = sel_start_line
##     else:
##       caret_line = None
    (minx, miny), (maxx, maxy) = self.viewed_rect()
    min_line = max(self.y_to_line(miny), 0)
    max_line = min(self.y_to_line(maxy) + 1, text_end_line)
    fg = self._fg
    bg = self._bg
    c.set_font(self._font)
    #ext_right = self.extent_right()
    #ext_bottom = self.extent_bottom()
    #hilite_right_edge = self.viewed_rect()[1][0]
    right, bottom = self.viewed_rect()[1]
    top_margin = self._top_margin
    left_margin = self._left_margin
    bottom_margin = self._bottom_margin
    line_height = self._line_height
    ascent = self._ascent
    if debug:
      print "...min_line =", min_line
      print "...max_line =", max_line
##       print "...caret_line =", caret_line
      print "...line_height =", line_height
    # Erase top and left margin area
    c.set_forecolor(bg)
    c.fill_rect(((0, 0), (right, top_margin)))
    c.fill_rect(((0, top_margin), (left_margin, bottom)))
    # Draw relevant lines
    y = top_margin
    for line in xrange(min_line, max_line + 1):
      if debug:
        print "...drawing line", line
      chars = text.line(line)
      line_end_col = len(chars)
      if line == sel_start_line:
        hilite_start_col = sel_start_col
      elif line > sel_start_line:
        hilite_start_col = 0
      else:
        hilite_start_col = line_end_col
      if line == sel_end_line:
        hilite_end_col = sel_end_col
      elif line < sel_end_line:
        hilite_end_col = line_end_col
      else:
        hilite_end_col = 0
      hilite_to_eol = line >= sel_start_line and line < sel_end_line
      if debug:
        print "......hilite_start_col =", hilite_start_col ###
        print "......hilite_end_col =", hilite_end_col ###
        print "......line_end_col =", line_end_col ###
      x = left_margin
      #base = y + ascent
      # Draw chars before hilite
      if hilite_start_col > 0:
        x = self.draw_chars(
          c, chars, x, y, 0, hilite_start_col, fg, bg)
##       if line == caret_line:
##         caret_x = x
##         if debug:
##           print "......caret_x =", caret_x
      # Draw hilited chars
      if hilite_start_col < hilite_end_col:
        x = self.draw_chars(
          c, chars, x, y, hilite_start_col, hilite_end_col, bg, fg)
      # Draw chars after hilite
      if hilite_end_col < line_end_col:
        x = self.draw_chars(
          c, chars, x, y, hilite_end_col, line_end_col, fg, bg)
##       # Draw caret
##       if line == caret_line:
##         if debug:
##           print "......drawing caret at", (caret_x, y)
##         c.set_forecolor(fg)
##         c.fill_rect(((caret_x, y), (caret_x + 1, y + line_height)))
      # Erase to end of line
      if x < right:
        if hilite_to_eol:
          c.set_forecolor(fg)
        else:
          c.set_forecolor(bg)
        r = ((x, y), (right, y + line_height))
        if debug:
          print "...erasing", r ###
        c.fill_rect(r)
      y = y + line_height
    # Erase to bottom of extent
    c.fill_rect(((0, y), (right, bottom)))
    # Draw caret
    if self._caret_on:
      r = self._caret_rect
      if r:
        c.set_forecolor(fg)
        c.fill_rect(r)

  def draw_chars(self, c, line, x, y, start_col, end_col, fg, bg):
    debug = 0
    if debug:
      print ".........TextView.draw_chars:", \
            repr(line), "at", (x, y), "col", start_col, "to", end_col ###
      print "............using", fg, "on", bg ###
    chars = line[start_col:end_col]
    w = self._font.width(chars)
    c.set_forecolor(bg)
    c.fill_rect(((x, y), (x + w, y + self._line_height)))
    c.set_forecolor(fg)
    c.set_backcolor(bg)
    c.moveto(x, y + self._ascent)
    c.show(chars)
    return x + w


  def line_to_y(self, line):
    return self._top_margin + line * self._line_height

  def col_to_x(self, line, col):
    return self._left_margin + self._font.width(self.model().line(line)[:col])

  def y_to_line(self, y):
    #print "TextView.y_to_line:", y
    #print "...self._top_margin =", self._top_margin
    #print "...self._line_height =", self._line_height
    return int((y - self._top_margin) / self._line_height)

  def x_to_col(self, line, x):
    chars = self.model().line(line)
    if chars is not None:
      return self._font.x_to_pos(chars, x - self._left_margin)
    else:
      return 0

  def pt_to_pos(self, (x, y)):
    text = self.model()
    line = self.y_to_line(y)
    if line < 0:
      return (0, 0)
    elif line < text.num_lines():
      col = self.x_to_col(line, x)
      return (line, col)
    else:
      return text.end()
    
  def selection_changed(self, (line1, col1), (line2, col2)):
    top = self.line_to_y(line1)
    if line1 == line2:
      left = self.col_to_x(line1, col1)
      right = self.col_to_x(line1, col2)
      bottom = top + self._line_height
    else:
      left = self.extent_left()
      right = self.extent_right()
      bottom = self.line_to_y(line2 + 1)
    self.invalidate_rect(((left, top), (right, bottom)))
    self.update_caret_rect()

  def text_changed(self, (line, col), multiline):
    self.update_extent()
    top = self.line_to_y(line)
    right = self.extent_right()
    if not multiline:
      left = self.col_to_x(line, col)
      bottom = top + self._line_height
    else:
      left = self.extent_left()
      bottom = self.viewed_rect()[1][1]
    if top < bottom:
      self.invalidate_rect(((left, top), (right, bottom)))

  def click(self, e):
    text = self.model()
    pos1 = self.pt_to_pos(e.where())
    #print "TextView.click: pos1=", pos1
    text.set_selection(pos1, pos1)
    while self.track_mouse():
      pos2 = self.pt_to_pos(self.get_mouse().where())
      #print "TextView.click: pos2=", pos2
      text.set_selection(pos1, pos2)

  def key(self, e):
    c = e.char
    if c == '\b' or c == '\177':
      self.backspace()
    else:
      if c == '\r':
        c = '\n'
      self.insert(c)

  def update_extent(self):
    text = self.model()
    max_line, _ = text.end()
    width = text.max_line_length() * self._font.width("m")
    height = (max_line + 1) * self._line_height
    self.set_extent(
      ((0, 0), (self._left_margin + width + self._right_margin,
                self._top_margin + height + self._bottom_margin)))

  #
  #   Caret
  #

  def blink(self):
    #print "TextView.blink" ###
    self.set_caret(not self._caret_on)

  def set_caret(self, on):
    # print "TextView.set_caret:", on ###
    self._caret_on = on
    r = self._caret_rect
    if r:
      #print "...invalidating", r ###
      self.invalidate_rect(r)

  def update_caret_rect(self):
    old_r = self._caret_rect
    text = self.model()
    sel_start, sel_end = text.selection()
    if sel_start == sel_end:
      line, col = sel_start
      #print "TextView.update_caret_rect:" ###
      #print "...line =", line ###
      #print "...line_height =", self._line_height ###
      y = self.line_to_y(line)
      x = self.col_to_x(line, col)
      new_r = ((x - 1, y), (x, y + self._line_height))
    else:
      new_r = None
    if old_r <> new_r:
      if old_r:
        self.invalidate_rect(old_r)
      if new_r:
        self.invalidate_rect(new_r)
      self._caret_rect = new_r
      self.set_caret(1)
      self.reset_blink()

  #
  #   Menu commands
  #

  def setup_menus(self, m):
    text = self.model()
    start, end = text.selection()
    if start <> end:
      m.enable('cut_cmd')
      m.enable('copy_cmd')
      m.enable('clear_cmd')
    if application().clipboard():
      m.enable('paste_cmd')
    View.setup_menus(self, m)

  def cut_cmd(self):
    self.copy_cmd()
    self.clear_cmd()

  def copy_cmd(self):
    text = self.model()
    start, end = text.selection()
    chars = text.chars(start, end)
    #print "TextView.copy_cmd: chars =", repr(chars) ###
    application().set_clipboard(chars)

  def paste_cmd(self):
    self.insert(str(application().clipboard()))

  def clear_cmd(self):
    self.insert("")

  #
  #   Text modification
  #

  def insert(self, chars):
    """Replace selection with the given chars and position insertion
    point just after the inserted chars."""
    text = self.model()
    text.replace_selection(chars)
    _, end = text.selection()
    text.set_selection(end, end)

  def backspace(self):
    """Delete the selection, or if the selection is empty, delete
    the character just before the selection, if any."""
    text = self.model()
    start, end = text.selection()
    if start == end:
      if start <> (0, 0):
        line, col = start
        if col > 0:
          col = col - 1
        else:
          line = line - 1
          col = len(text.line(line))
        start = (line, col)
        text.set_chars(start, end, "")
        text.set_selection(start, start)
    else:
      self.clear_cmd()

