init.
[minwii.git] / src / pgu / gui / textarea.py
1 """
2 """
3 import pygame
4 from pygame.locals import *
5
6 from const import *
7 import widget
8
9 class TextArea(widget.Widget):
10 """A multi-line text input.
11
12 <pre>TextArea(value="",width = 120, height = 30, size=20)</pre>
13
14 <dl>
15 <dt>value<dd>initial text
16 <dt>size<dd>size for the text box, in characters
17 </dl>
18
19 <strong>Example</strong>
20 <code>
21 w = TextArea(value="Cuzco the Goat",size=20)
22
23 w = TextArea("Marbles")
24
25 w = TextArea("Groucho\nHarpo\nChico\nGummo\nZeppo\n\nMarx", 200, 400, 12)
26 </code>
27
28 """
29 def __init__(self,value="",width = 120, height = 30, size=20,**params):
30 params.setdefault('cls','input')
31 params.setdefault('width', width)
32 params.setdefault('height', height)
33
34 widget.Widget.__init__(self,**params)
35 self.value = value # The value of the TextArea
36 self.pos = len(str(value)) # The position of the cursor
37 self.vscroll = 0 # The number of lines that the TextArea is currently scrolled
38 self.font = self.style.font # The font used for rendering the text
39 self.cursor_w = 2 # Cursor width (NOTE: should be in a style)
40 w,h = self.font.size("e"*size)
41 if not self.style.height: self.style.height = h
42 if not self.style.width: self.style.width = w
43
44 def resize(self,width=None,height=None):
45 if (width != None) and (height != None):
46 self.rect = pygame.Rect(self.rect.x, self.rect.y, width, height)
47 return self.rect.w, self.rect.h
48
49 def paint(self,s):
50
51 # 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.
52 max_line_w = self.rect.w - 20
53
54 # Update the line allocation for the box's value
55 self.doLines(max_line_w)
56
57 # Make sure that the vpos and hpos of the cursor is set properly
58 self.updateCursorPos()
59
60 # Make sure that we're scrolled vertically such that the cursor is visible
61 if (self.vscroll < 0):
62 self.vscroll = 0
63 if (self.vpos < self.vscroll):
64 self.vscroll = self.vpos
65 elif ((self.vpos - self.vscroll + 1) * self.line_h > self.rect.h):
66 self.vscroll = - (self.rect.h / self.line_h - self.vpos - 1)
67
68 # Blit each of the lines in turn
69 cnt = 0
70 for line in self.lines:
71 line_pos = (0, (cnt - self.vscroll) * self.line_h)
72 if (line_pos[1] >= 0) and (line_pos[1] < self.rect.h):
73 s.blit( self.font.render(line, 1, self.style.color), line_pos )
74 cnt += 1
75
76 # If the textarea is focused, then also show the cursor
77 if self.container.myfocus is self:
78 r = self.getCursorRect()
79 s.fill(self.style.color,r)
80
81 # This function updates self.vpos and self.hpos based on self.pos
82 def updateCursorPos(self):
83 self.vpos = 0 # Reset the current line that the cursor is on
84 self.hpos = 0
85
86 line_cnt = 0
87 char_cnt = 0
88
89 for line in self.lines:
90 line_char_start = char_cnt # The number of characters at the start of the line
91
92 # Keep track of the character count for words
93 char_cnt += len(line)
94
95 # 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
96 if (char_cnt > self.pos):
97 self.vpos = line_cnt
98 self.hpos = self.pos - line_char_start
99
100 break # Now that we know where our cursor is, we exit the loop
101
102 line_cnt += 1
103
104 if (char_cnt <= self.pos) and (len(self.lines) > 0):
105 self.vpos = len(self.lines) - 1
106 self.hpos = len(self.lines[ self.vpos ] )
107
108 # Returns a rectangle that is of the size and position of where the cursor is drawn
109 def getCursorRect(self):
110 lw = 0
111 if (len(self.lines) > 0):
112 lw, lh = self.font.size( self.lines[ self.vpos ][ 0:self.hpos ] )
113
114 r = pygame.Rect(lw, (self.vpos - self.vscroll) * self.line_h, self.cursor_w, self.line_h)
115 return r
116
117 # This function sets the cursor position according to an x/y value (such as by from a mouse click)
118 def setCursorByXY(self, (x, y)):
119 self.vpos = ((int) (y / self.line_h)) + self.vscroll
120 if (self.vpos >= len(self.lines)):
121 self.vpos = len(self.lines) - 1
122
123 currentLine = self.lines[ self.vpos ]
124
125 for cnt in range(0, len(currentLine) ):
126 self.hpos = cnt
127 lw, lh = self.font.size( currentLine[ 0:self.hpos + 1 ] )
128 if (lw > x):
129 break
130
131 lw, lh = self.font.size( currentLine )
132 if (lw < x):
133 self.hpos = len(currentLine)
134
135 self.setCursorByHVPos()
136
137 # This function sets the cursor position by the horizontal/vertical cursor position.
138 def setCursorByHVPos(self):
139 line_cnt = 0
140 char_cnt = 0
141
142 for line in self.lines:
143 line_char_start = char_cnt # The number of characters at the start of the line
144
145 # Keep track of the character count for words
146 char_cnt += len(line)
147
148 # If we're on the proper line
149 if (line_cnt == self.vpos):
150 # Make sure that we're not trying to go over the edge of the current line
151 if ( self.hpos >= len(line) ):
152 self.hpos = len(line) - 1
153 # Set the cursor position
154 self.pos = line_char_start + self.hpos
155 break # Now that we've set our cursor position, we exit the loop
156
157 line_cnt += 1
158
159 # Splits up the text found in the control's value, and assigns it into the lines array
160 def doLines(self, max_line_w):
161 self.line_h = 10
162 self.lines = [] # Create an empty starter list to start things out.
163
164 inx = 0
165 line_start = 0
166 while inx >= 0:
167 # Find the next breakable whitespace
168 # HACK: Find a better way to do this to include tabs and system characters and whatnot.
169 prev_word_start = inx # Store the previous whitespace
170 spc_inx = self.value.find(' ', inx+1)
171 nl_inx = self.value.find('\n', inx+1)
172
173 if (min(spc_inx, nl_inx) == -1):
174 inx = max(spc_inx, nl_inx)
175 else:
176 inx = min(spc_inx, nl_inx)
177
178 # Measure the current line
179 lw, self.line_h = self.font.size( self.value[ line_start : inx ] )
180
181 # If we exceeded the max line width, then create a new line
182 if (lw > max_line_w):
183 #Fall back to the previous word start
184 self.lines.append(self.value[ line_start : prev_word_start + 1 ])
185 line_start = prev_word_start + 1
186 # TODO: Check for extra-long words here that exceed the length of a line, to wrap mid-word
187
188 # If we reached the end of our text
189 if (inx < 0):
190 # Then make sure we added the last of the line
191 if (line_start < len( self.value ) ):
192 self.lines.append( self.value[ line_start : len( self.value ) ] )
193 # If we reached a hard line break
194 elif (self.value[inx] == "\n"):
195 # Then make a line break here as well.
196 newline = self.value[ line_start : inx + 1 ]
197 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.
198 self.lines.append( newline )
199
200 line_start = inx + 1
201 else:
202 # Otherwise, we just continue progressing to the next space
203 pass
204
205 def _setvalue(self,v):
206 self.__dict__['value'] = v
207 self.send(CHANGE)
208
209 def event(self,e):
210 used = None
211 if e.type == KEYDOWN:
212 if e.key == K_BACKSPACE:
213 if self.pos:
214 self._setvalue(self.value[:self.pos-1] + self.value[self.pos:])
215 self.pos -= 1
216 elif e.key == K_DELETE:
217 if len(self.value) > self.pos:
218 self._setvalue(self.value[:self.pos] + self.value[self.pos+1:])
219 elif e.key == K_HOME:
220 # Find the previous newline
221 newPos = self.value.rfind('\n', 0, self.pos)
222 if (newPos >= 0):
223 self.pos = newPos
224 elif e.key == K_END:
225 # Find the previous newline
226 newPos = self.value.find('\n', self.pos, len(self.value) )
227 if (newPos >= 0):
228 self.pos = newPos
229 elif e.key == K_LEFT:
230 if self.pos > 0: self.pos -= 1
231 used = True
232 elif e.key == K_RIGHT:
233 if self.pos < len(self.value): self.pos += 1
234 used = True
235 elif e.key == K_UP:
236 self.vpos -= 1
237 self.setCursorByHVPos()
238 elif e.key == K_DOWN:
239 self.vpos += 1
240 self.setCursorByHVPos()
241 # The following return/tab keys are standard for PGU widgets, but I took them out here to facilitate multi-line text editing
242 # elif e.key == K_RETURN:
243 # self.next()
244 # elif e.key == K_TAB:
245 # pass
246 else:
247 #c = str(e.unicode)
248 try:
249 if (e.key == K_RETURN):
250 c = "\n"
251 elif (e.key == K_TAB):
252 c = " "
253 else:
254 c = (e.unicode).encode('latin-1')
255 if c:
256 self._setvalue(self.value[:self.pos] + c + self.value[self.pos:])
257 self.pos += len(c)
258 except: #ignore weird characters
259 pass
260 self.repaint()
261 elif e.type == MOUSEBUTTONDOWN:
262 self.setCursorByXY(e.pos)
263 self.repaint()
264
265 elif e.type == FOCUS:
266 self.repaint()
267 elif e.type == BLUR:
268 self.repaint()
269
270 self.pcls = ""
271 if self.container.myfocus is self: self.pcls = "focus"
272
273 return used
274
275 def __setattr__(self,k,v):
276 if k == 'value':
277 if v == None: v = ''
278 v = str(v)
279 self.pos = len(v)
280 _v = self.__dict__.get(k,NOATTR)
281 self.__dict__[k]=v
282 if k == 'value' and _v != NOATTR and _v != v:
283 self.send(CHANGE)
284 self.repaint()
285
286 # The first version of this code was done by Clint Herron, and is a modified version of input.py (by Phil Hassey).
287 # It is under the same license as the rest of the PGU library.