Lightningbeam/GUI/ToDo/TextViews.py

374 lines
10 KiB
Python

#
# 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()