374 lines
10 KiB
Python
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()
|
|
|