Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/pgu/gui/textarea.py
blob: 667076ae27e6c04c10a12bed125f717faa61a0b6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
"""
"""
import pygame
from pygame.locals import *

from const import *
import widget

class TextArea(widget.Widget):
	"""A multi-line text input.
	
	<pre>TextArea(value="",width = 120, height = 30, size=20)</pre>
	
	<dl>
	<dt>value<dd>initial text
	<dt>size<dd>size for the text box, in characters
	</dl>
	
	<strong>Example</strong>
	<code>
	w = TextArea(value="Cuzco the Goat",size=20)
	
	w = TextArea("Marbles")
	
	w = TextArea("Groucho\nHarpo\nChico\nGummo\nZeppo\n\nMarx", 200, 400, 12)
	</code>
	
	"""
	def __init__(self,value="",width = 120, height = 30, size=20,**params):
		params.setdefault('cls','input')
		params.setdefault('width', width)
		params.setdefault('height', height)
		
		widget.Widget.__init__(self,**params)
		self.value = value				# The value of the TextArea
		self.pos = len(str(value))		# The position of the cursor
		self.vscroll = 0				# The number of lines that the TextArea is currently scrolled
		self.font = self.style.font		# The font used for rendering the text
		self.cursor_w = 2 				# Cursor width (NOTE: should be in a style)
		w,h = self.font.size("e"*size)	
		if not self.style.height: self.style.height = h
		if not self.style.width: self.style.width = w
	
	def resize(self,width=None,height=None):
		if (width != None) and (height != None):
			self.rect = pygame.Rect(self.rect.x, self.rect.y, width, height)
		return self.rect.w, self.rect.h
		
	def paint(self,s):
		
		# 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.
		max_line_w = self.rect.w - 20
				
		# Update the line allocation for the box's value
		self.doLines(max_line_w)
		
		# Make sure that the vpos and hpos of the cursor is set properly
		self.updateCursorPos()

		# Make sure that we're scrolled vertically such that the cursor is visible
		if (self.vscroll < 0):
			self.vscroll = 0
		if (self.vpos < self.vscroll):
			self.vscroll = self.vpos
		elif ((self.vpos - self.vscroll + 1) * self.line_h > self.rect.h):
			self.vscroll = - (self.rect.h / self.line_h - self.vpos - 1)

		# Blit each of the lines in turn
		cnt = 0
		for line in self.lines:
			line_pos = (0, (cnt - self.vscroll) * self.line_h)
			if (line_pos[1] >= 0) and (line_pos[1] < self.rect.h):
				s.blit( self.font.render(line, 1, self.style.color), line_pos )
			cnt += 1
		
		# If the textarea is focused, then also show the cursor
		if self.container.myfocus is self:
			r = self.getCursorRect()
			s.fill(self.style.color,r)
	
	# This function updates self.vpos and self.hpos based on self.pos
	def updateCursorPos(self):
		self.vpos = 0 # Reset the current line that the cursor is on
		self.hpos = 0
		
		line_cnt = 0
		char_cnt = 0

		for line in self.lines:
			line_char_start = char_cnt # The number of characters at the start of the line
			
			# Keep track of the character count for words
			char_cnt += len(line)
			
			# 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
			if (char_cnt > self.pos):
				self.vpos = line_cnt
				self.hpos = self.pos - line_char_start

				break # Now that we know where our cursor is, we exit the loop

			line_cnt += 1
		
		if (char_cnt <= self.pos) and (len(self.lines) > 0):
			self.vpos = len(self.lines) - 1
			self.hpos = len(self.lines[ self.vpos ] )

	# Returns a rectangle that is of the size and position of where the cursor is drawn	
	def getCursorRect(self):
		lw = 0
		if (len(self.lines) > 0):
			lw, lh = self.font.size( self.lines[ self.vpos ][ 0:self.hpos ] )
			
		r = pygame.Rect(lw, (self.vpos - self.vscroll) * self.line_h, self.cursor_w, self.line_h)
		return r
	
	# This function sets the cursor position according to an x/y value (such as by from a mouse click)
	def setCursorByXY(self, (x, y)):
		self.vpos = ((int) (y / self.line_h)) + self.vscroll
		if (self.vpos >= len(self.lines)):
			self.vpos = len(self.lines) - 1
			
		currentLine = self.lines[ self.vpos ]
		
		for cnt in range(0, len(currentLine) ):
			self.hpos = cnt
			lw, lh = self.font.size( currentLine[ 0:self.hpos + 1 ] )
			if (lw > x):
				break
		
		lw, lh = self.font.size( currentLine )
		if (lw < x):
			self.hpos = len(currentLine)
			
		self.setCursorByHVPos()
		
	# This function sets the cursor position by the horizontal/vertical cursor position.	
	def setCursorByHVPos(self):
		line_cnt = 0
		char_cnt = 0
		
		for line in self.lines:
			line_char_start = char_cnt # The number of characters at the start of the line
			
			# Keep track of the character count for words
			char_cnt += len(line)

			# If we're on the proper line
			if (line_cnt == self.vpos):
				# Make sure that we're not trying to go over the edge of the current line
				if ( self.hpos >= len(line) ):
					self.hpos = len(line) - 1
				# Set the cursor position
				self.pos = line_char_start + self.hpos
				break	# Now that we've set our cursor position, we exit the loop
				
			line_cnt += 1		
	
	# Splits up the text found in the control's value, and assigns it into the lines array
	def doLines(self, max_line_w):
		self.line_h = 10
		self.lines = [] # Create an empty starter list to start things out.
		
		inx = 0
		line_start = 0
		while inx >= 0:
			# Find the next breakable whitespace
			# HACK: Find a better way to do this to include tabs and system characters and whatnot.
			prev_word_start = inx # Store the previous whitespace
			spc_inx = self.value.find(' ', inx+1)
			nl_inx = self.value.find('\n', inx+1)
			
			if (min(spc_inx, nl_inx) == -1):
				inx = max(spc_inx, nl_inx)
			else:
				inx = min(spc_inx, nl_inx)
				
			# Measure the current line
			lw, self.line_h = self.font.size( self.value[ line_start : inx ] )
			
			# If we exceeded the max line width, then create a new line
			if (lw > max_line_w):
				#Fall back to the previous word start
				self.lines.append(self.value[ line_start : prev_word_start + 1 ])
				line_start = prev_word_start + 1
				# TODO: Check for extra-long words here that exceed the length of a line, to wrap mid-word
				
			# If we reached the end of our text
			if (inx < 0):
				# Then make sure we added the last of the line
				if (line_start < len( self.value ) ):
					self.lines.append( self.value[ line_start : len( self.value ) ] )
			# If we reached a hard line break
			elif (self.value[inx] == "\n"):
				# Then make a line break here as well.
				newline = self.value[ line_start : inx + 1 ]
				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.
				self.lines.append( newline )
				
				line_start = inx + 1
			else:
				# Otherwise, we just continue progressing to the next space
				pass
		
	def _setvalue(self,v):
		self.__dict__['value'] = v
		self.send(CHANGE)
	
	def event(self,e):
		used = None
		if e.type == KEYDOWN:	
			if e.key == K_BACKSPACE:
				if self.pos:
					self._setvalue(self.value[:self.pos-1] + self.value[self.pos:])
					self.pos -= 1
			elif e.key == K_DELETE:
				if len(self.value) > self.pos:
					self._setvalue(self.value[:self.pos] + self.value[self.pos+1:])
			elif e.key == K_HOME: 
				# Find the previous newline
				newPos = self.value.rfind('\n', 0, self.pos)
				if (newPos >= 0):
					self.pos = newPos
			elif e.key == K_END:
				# Find the previous newline
				newPos = self.value.find('\n', self.pos, len(self.value) )
				if (newPos >= 0):
					self.pos = newPos
			elif e.key == K_LEFT:
				if self.pos > 0: self.pos -= 1
				used = True
			elif e.key == K_RIGHT:
				if self.pos < len(self.value): self.pos += 1
				used = True
			elif e.key == K_UP:
				self.vpos -= 1
				self.setCursorByHVPos()
			elif e.key == K_DOWN:
				self.vpos += 1
				self.setCursorByHVPos()
			# The following return/tab keys are standard for PGU widgets, but I took them out here to facilitate multi-line text editing
#			elif e.key == K_RETURN:
#				self.next()
#			elif e.key == K_TAB:
#				pass				
			else:
				#c = str(e.unicode)
				try:
					if (e.key == K_RETURN):
						c = "\n"
					elif (e.key == K_TAB):
						c = "  "
					else:
						c = (e.unicode).encode('latin-1')
					if c:
						self._setvalue(self.value[:self.pos] + c + self.value[self.pos:])
						self.pos += len(c)
				except: #ignore weird characters
					pass
			self.repaint()
		elif e.type == MOUSEBUTTONDOWN:
			self.setCursorByXY(e.pos)
			self.repaint()
			
		elif e.type == FOCUS:
			self.repaint()
		elif e.type == BLUR:
			self.repaint()
		
		self.pcls = ""
		if self.container.myfocus is self: self.pcls = "focus"
		
		return used
	
	def __setattr__(self,k,v):
		if k == 'value':
			if v == None: v = ''
			v = str(v)
			self.pos = len(v)
		_v = self.__dict__.get(k,NOATTR)
		self.__dict__[k]=v
		if k == 'value' and _v != NOATTR and _v != v: 
			self.send(CHANGE)
			self.repaint()
			
# The first version of this code was done by Clint Herron, and is a modified version of input.py (by Phil Hassey).
# It is under the same license as the rest of the PGU library.