4 from pygame.locals import *
9 class TextArea(widget.Widget):
10 """A multi-line text input.
13 w = TextArea(value="Cuzco the Goat",size=20)
14 w = TextArea("Marbles")
15 w = TextArea("Groucho\nHarpo\nChico\nGummo\nZeppo\n\nMarx", 200, 400, 12)
18 def __init__(self,value="",width = 120, height = 30, size=20,**params):
19 params.setdefault('cls','input')
20 params.setdefault('width', width)
21 params.setdefault('height', height)
23 widget.Widget.__init__(self,**params)
24 self.value = value # The value of the TextArea
25 self.pos = len(str(value)) # The position of the cursor
26 self.vscroll = 0 # The number of lines that the TextArea is currently scrolled
27 self.font = self.style.font # The font used for rendering the text
28 self.cursor_w = 2 # Cursor width (NOTE: should be in a style)
29 w,h = self.font.size("e"*size)
30 if not self.style.height: self.style.height = h
31 if not self.style.width: self.style.width = w
33 ## BUG: This causes textarea to grow every time table._Table_td calculates its
35 ## def resize(self,width=None,height=None):
36 ## if (width != None) and (height != None):
37 ## print 'TextArea RESIZE'
38 ## self.rect = pygame.Rect(self.rect.x, self.rect.y, width, height)
39 ## return self.rect.w, self.rect.h
43 # TODO: What's up with this 20 magic number? It's the margin of the left and right sides, but I'm not sure how this should be gotten other than by trial and error.
44 max_line_w = self.rect.w - 20
46 # Update the line allocation for the box's value
47 self.doLines(max_line_w)
49 # Make sure that the vpos and hpos of the cursor is set properly
50 self.updateCursorPos()
52 # Make sure that we're scrolled vertically such that the cursor is visible
53 if (self.vscroll < 0):
55 if (self.vpos < self.vscroll):
56 self.vscroll = self.vpos
57 elif ((self.vpos - self.vscroll + 1) * self.line_h > self.rect.h):
58 self.vscroll = - (self.rect.h / self.line_h - self.vpos - 1)
60 # Blit each of the lines in turn
62 for line in self.lines:
63 line_pos = (0, (cnt - self.vscroll) * self.line_h)
64 if (line_pos[1] >= 0) and (line_pos[1] < self.rect.h):
65 s.blit( self.font.render(line, 1, self.style.color), line_pos )
68 # If the textarea is focused, then also show the cursor
69 if self.container.myfocus is self:
70 r = self.getCursorRect()
71 s.fill(self.style.color,r)
73 # This function updates self.vpos and self.hpos based on self.pos
74 def updateCursorPos(self):
75 self.vpos = 0 # Reset the current line that the cursor is on
81 for line in self.lines:
82 line_char_start = char_cnt # The number of characters at the start of the line
84 # Keep track of the character count for words
87 # If our cursor count is still less than the cursor position, then we can update our cursor line to assume that it's at least on this line
88 if (char_cnt > self.pos):
90 self.hpos = self.pos - line_char_start
92 break # Now that we know where our cursor is, we exit the loop
96 if (char_cnt <= self.pos) and (len(self.lines) > 0):
97 self.vpos = len(self.lines) - 1
98 self.hpos = len(self.lines[ self.vpos ] )
100 # Returns a rectangle that is of the size and position of where the cursor is drawn
101 def getCursorRect(self):
103 if (len(self.lines) > 0):
104 lw, lh = self.font.size( self.lines[ self.vpos ][ 0:self.hpos ] )
106 r = pygame.Rect(lw, (self.vpos - self.vscroll) * self.line_h, self.cursor_w, self.line_h)
109 # This function sets the cursor position according to an x/y value (such as by from a mouse click)
110 def setCursorByXY(self, pos):
112 self.vpos = ((int) (y / self.line_h)) + self.vscroll
113 if (self.vpos >= len(self.lines)):
114 self.vpos = len(self.lines) - 1
116 currentLine = self.lines[ self.vpos ]
118 for cnt in range(0, len(currentLine) ):
120 lw, lh = self.font.size( currentLine[ 0:self.hpos + 1 ] )
124 lw, lh = self.font.size( currentLine )
126 self.hpos = len(currentLine)
128 self.setCursorByHVPos()
130 # This function sets the cursor position by the horizontal/vertical cursor position.
131 def setCursorByHVPos(self):
135 for line in self.lines:
136 line_char_start = char_cnt # The number of characters at the start of the line
138 # Keep track of the character count for words
139 char_cnt += len(line)
141 # If we're on the proper line
142 if (line_cnt == self.vpos):
143 # Make sure that we're not trying to go over the edge of the current line
144 if ( self.hpos > len(line) ):
145 self.hpos = len(line) - 1
146 # Set the cursor position
147 self.pos = line_char_start + self.hpos
148 break # Now that we've set our cursor position, we exit the loop
152 # Splits up the text found in the control's value, and assigns it into the lines array
153 def doLines(self, max_line_w):
155 self.lines = [] # Create an empty starter list to start things out.
160 # Find the next breakable whitespace
161 # HACK: Find a better way to do this to include tabs and system characters and whatnot.
162 prev_word_start = inx # Store the previous whitespace
163 spc_inx = self.value.find(' ', inx+1)
164 nl_inx = self.value.find('\n', inx+1)
166 if (min(spc_inx, nl_inx) == -1):
167 inx = max(spc_inx, nl_inx)
169 inx = min(spc_inx, nl_inx)
171 # Measure the current line
172 lw, self.line_h = self.font.size( self.value[ line_start : inx ] )
174 # If we exceeded the max line width, then create a new line
175 if (lw > max_line_w):
176 #Fall back to the previous word start
177 self.lines.append(self.value[ line_start : prev_word_start + 1 ])
178 line_start = prev_word_start + 1
179 # TODO: Check for extra-long words here that exceed the length of a line, to wrap mid-word
181 # If we reached the end of our text
183 # Then make sure we added the last of the line
184 if (line_start < len( self.value ) ):
185 self.lines.append( self.value[ line_start : len( self.value ) ] )
187 self.lines.append('')
188 # If we reached a hard line break
189 elif (self.value[inx] == "\n"):
190 # Then make a line break here as well.
191 newline = self.value[ line_start : inx + 1 ]
192 newline = newline.replace("\n", " ") # HACK: We know we have a newline character, which doesn't print nicely, so make it into a space. Comment this out to see what I mean.
193 self.lines.append( newline )
197 # Otherwise, we just continue progressing to the next space
200 def _setvalue(self,v):
201 self.__dict__['value'] = v
206 if e.type == KEYDOWN:
208 if e.key == K_BACKSPACE:
210 self._setvalue(self.value[:self.pos-1] + self.value[self.pos:])
212 elif e.key == K_DELETE:
213 if len(self.value) > self.pos:
214 self._setvalue(self.value[:self.pos] + self.value[self.pos+1:])
215 elif e.key == K_HOME:
216 # Find the previous newline
217 newPos = self.value.rfind('\n', 0, self.pos)
221 # Find the previous newline
222 newPos = self.value.find('\n', self.pos, len(self.value) )
225 elif e.key == K_LEFT:
226 if self.pos > 0: self.pos -= 1
228 elif e.key == K_RIGHT:
229 if self.pos < len(self.value): self.pos += 1
233 self.setCursorByHVPos()
234 elif e.key == K_DOWN:
236 self.setCursorByHVPos()
237 # The following return/tab keys are standard for PGU widgets, but I took them out here to facilitate multi-line text editing
238 # elif e.key == K_RETURN:
240 # elif e.key == K_TAB:
246 if (e.key == K_RETURN):
248 elif (e.key == K_TAB):
251 if (type(e.unicode) == str):
254 c = (e.unicode).encode('latin-1')
257 self._setvalue(self.value[:self.pos] + c + self.value[self.pos:])
259 except: #ignore weird characters
262 elif e.type == MOUSEBUTTONDOWN:
263 self.setCursorByXY(e.pos)
266 elif e.type == FOCUS:
272 if self.container.myfocus is self: self.pcls = "focus"
276 def __setattr__(self,k,v):
281 _v = self.__dict__.get(k,NOATTR)
283 if k == 'value' and _v != NOATTR and _v != v:
287 # The first version of this code was done by Clint Herron, and is a modified version of input.py (by Phil Hassey).
288 # It is under the same license as the rest of the PGU library.