Lightningbeam/GUI/ToDo/TextModels.py

185 lines
5.5 KiB
Python

#
# TextModels
#
import string
from array import array
from Models import Model
class TextModel(Model):
"""A TextModel is a Model holding a character string
suitable for viewing and editing with a TextView."""
_text = None
_sel_start = (0, 0)
_sel_end = (0, 0)
#
# The text is kept in a list of strings. There is an
# implicit newline at the end of every string except the
# last one. There is always at least one string in the
# list; a completely empty text is represented by a
# list containing one empty string.
#
# Positions in the text are represented by a tuple
# (line, col), where line is the line number (0-based)
# and col is the position within the line (0 to the
# number of characters in the line).
#
# Pieces of text to be inserted or extracted are
# represented as simple strings, with lines separated
# by newlines.
#
# Views may be sent the messages:
#
# text_changed(start_pos, multiline)
# where start_pos is the start of the affected text and
# multiline is true if more than one line is affected.
#
# selection_changed(pos1, pos2)
# where pos1 and pos2 are the extremes of the range
# of text affected.
#
def __init__(self, *args, **kw):
apply(Model.__init__, (self,)+args, kw)
self._text = [""]
def text(self):
"Return the whole text as a string."
return string.join(self._text, '\n')
def set_text(self, s):
"Replace the whole text with a string."
return self.set_chars((0, 0), self.end(), s)
def num_lines(self):
"Return the number of lines."
return len(self._text)
def line(self, i):
"""Return the specified line as a string, without any trailing newline"""
return self._text[i]
def end(self):
"Return the ending position of the text."
lines = self._text
return (len(lines) - 1, len(lines[-1]))
def char(self, (line, col)):
"Return the character at (line, col)."
return self._text[line][col]
def chars(self, (line1, col1), (line2, col2)):
"Return the text between (line1, col1) and (line2, col2)."
text = self._text
if line1 == line2:
return text[line1][col1:col2]
else:
a = text[line1][col1:]
b = text[line1 + 1 : line2 - 1]
c = text[line2][:col2]
return string.joinfields([a] + b + [c], '\n')
## def set_char(self, (line, col), c):
## "Replace the character at (line, col)."
## self._text[line][col] = c
## self.notify_views('char_changed', (line, col))
def set_chars(self, (line1, col1), (line2, col2), s):
"""Replace the text between (line1, col1) and (line2, col2).
Returns the line and column of the new endpoint of the
inserted text."""
#print "TextModel.set_chars", (line1, col1), (line2, col2), repr(s) ###
text = self._text
#print "...old text =", repr(text) ###
# Step 1: Delete old text
if line1 == line2:
line = text[line1]
text[line1] = line[:col1] + line[col2:]
else:
text[line1 : line2 + 1] = [text[line1][:col1] + text[line2][col2:]]
#a = text[line1][:col1]
#b = text[line2][col2:]
#text[line1 : line2 + 1] = [a + b]
# Step 2: Insert new text
ss = string.splitfields(s, '\n')
line = text[line1]
if len(ss) == 1:
text[line1] = line[:col1] + ss[0] + line[col1:]
else:
text[line1 : line1 + 1] = (
[line[:col1] + ss[0]] +
ss[1:-1] +
[ss[-1] + line[col1:]])
# Step 3: Calculate new endpoint
#print "...ss =", repr(ss) ###
if len(ss) == 1:
line3 = line1
col3 = col1 + len(s)
else:
line3 = line1 + len(ss) - 1
col3 = len(ss[-1])
new_end = (line3, col3)
#print "...new_end =", new_end
#print "...new text:", self._text
multiline = line2 <> line1 and line3 <> line1
self.notify_views('text_changed', (line1, col1), multiline)
self.set_selection(self._sel_start,
self.min_pos(self._sel_end, new_end))
return new_end
def selection(self):
"Return the endpoints of the current selection."
return (self._sel_start, self._sel_end)
def set_selection(self, new_start, new_end):
"Set the endpoints of the current selection."
new_start, new_end = self.normalise_range(new_start, new_end)
old_start = self._sel_start
old_end = self._sel_end
if old_start <> new_start or old_end <> new_end:
self._sel_start = new_start
self._sel_end = new_end
self.notify_views('selection_changed',
self.min_pos(old_start, new_start),
self.max_pos(old_end, new_end))
def replace_selection(self, new_text):
"Replace the selected text with the given string."
new_end = self.set_chars(self._sel_start, self._sel_end, new_text)
self.set_selection(self._sel_start, new_end)
#print "TextModels.replace_selection: selection =", ( ###
# self._sel_start, self._sel_end) ###
def max_line_length(self):
"Return the number of characters in the longest line."
return max(map(len, self._text))
#
# Internal
#
def min_pos(self, pos1, pos2):
line1, col1 = pos1
line2, col2 = pos2
if line1 < line2 or (line1 == line2 and col1 < col2):
return pos1
else:
return pos2
def max_pos(self, pos1, pos2):
line1, col1 = pos1
line2, col2 = pos2
if line1 > line2 or (line1 == line2 and col1 > col2):
return pos1
else:
return pos2
def normalise_range(self, pos1, pos2):
"Order two positions so that pos1 <= pos2."
return self.min_pos(pos1, pos2), self.max_pos(pos1, pos2)