Skip to content

Commit aedd402

Browse files
committed
improve TextEdit class
1 parent 7cdb613 commit aedd402

File tree

3 files changed

+249
-54
lines changed

3 files changed

+249
-54
lines changed

docs/1_intro/intro3.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
if event.type == pygame.QUIT:
2020
running = False
2121

22+
print(event)
23+
2224
screen.fill(YELLOW)
2325
pygame.display.update()
2426

docs/5_app/app.py

Lines changed: 200 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@
4949
DBG_LABELS = 2
5050
DBG_OUTLINE = 4
5151

52+
DBL_CLICK_TIMER = pygame.USEREVENT
53+
DBL_CLICK_TIMEOUT = 200
54+
5255
class App:
5356
"""Create a single-window app with multiple scenes having multiple objects."""
5457
scenes = [] # scene list
@@ -124,9 +127,6 @@ def run(self):
124127

125128
pygame.quit()
126129

127-
def multi_click(self):
128-
"""Detect double and triple clicks"""
129-
130130
def next_scene(self, d=1):
131131
"""Switch to the next scene."""
132132
i = App.scenes.index(App.scene)
@@ -197,6 +197,9 @@ def __init__(self, remember=True, **options):
197197
App.scene = self
198198
self.nodes = []
199199

200+
self.clicks = 0 # for double-clicks
201+
self.text = '' # for copy/paste
202+
200203
# Reset Node options to default
201204
Node.options = Node.options0.copy()
202205

@@ -287,6 +290,9 @@ def do_event(self, event):
287290
exec(self.shortcuts[k, m])
288291

289292
if event.type == MOUSEBUTTONDOWN:
293+
pygame.time.set_timer(DBL_CLICK_TIMER, DBL_CLICK_TIMEOUT)
294+
self.clicks += 1
295+
290296
if self.selection_rect.collidepoint(event.pos):
291297
self.moving = True
292298
else:
@@ -333,6 +339,18 @@ def do_event(self, event):
333339
for x in self.selection[1:]:
334340
self.selection_rect.union_ip(x.rect)
335341
self.selection_rect.inflate_ip((4, 4))
342+
343+
elif event.type == DBL_CLICK_TIMER:
344+
pygame.time.set_timer(DBL_CLICK_TIMER, 0)
345+
print(self.clicks, 'clicks in', self.focus)
346+
347+
if self.focus:
348+
if self.clicks == 2:
349+
self.focus.double_click()
350+
elif self.clicks == 3:
351+
self.focus.triple_click()
352+
353+
self.clicks = 0
336354

337355
if self.focus != None:
338356
self.focus.do_event(event)
@@ -349,24 +367,25 @@ def next_focus(self, d=1):
349367

350368
def cut(self):
351369
"""Cuts the selected objects and places them in App.selection."""
352-
App.focus = self.focus
353-
self.nodes.remove(self.focus)
354-
self.focus = None
370+
App.selection = self.selection
371+
for x in self.selection:
372+
self.nodes.remove(x)
373+
self.selection = []
355374

356375
def copy(self):
357376
"""Copies the selected objects and places them in App.selection."""
358-
App.focus = self.focus
377+
App.selection = self.selection
359378

360379
def paste(self):
361380
"""Pastes the objects from App.selection."""
362381
print('paste')
363-
obj = App.focus
364-
obj2 = eval(type(obj).__name__+'()')
365-
obj2.rect = obj.rect.copy()
366-
#®obj2.__dict__.update(obj.__dict__)
367-
obj2.rect.topleft = pygame.mouse.get_pos()
368-
obj2.label_rect.bottomleft = pygame.mouse.get_pos()
369-
self.nodes.append(obj2)
382+
# obj = App.focus
383+
# obj2 = eval(type(obj).__name__+'()')
384+
# obj2.rect = obj.rect.copy()
385+
# #®obj2.__dict__.update(obj.__dict__)
386+
# obj2.rect.topleft = pygame.mouse.get_pos()
387+
# obj2.label_rect.bottomleft = pygame.mouse.get_pos()
388+
# self.nodes.append(obj2)
370389

371390
def debug(self):
372391
"""Print all scene/node options."""
@@ -408,7 +427,6 @@ class Node:
408427
outline = Color('red'), 1
409428
focus = Color('blue'), 1
410429
selection = Color('green'), 2
411-
dbl_click_time = 300
412430

413431
# key direction vectors
414432
dirs = {K_LEFT:(-1, 0), K_RIGHT:(1, 0), K_UP:(0, -1), K_DOWN:(0, 1)}
@@ -422,8 +440,6 @@ def __init__(self, **options):
422440
# create instance attributes from current class options
423441
self.__dict__ = Node.options.copy()
424442
Node.options['id'] += 1
425-
self.t0 = 0
426-
self.t1 = 0
427443

428444
self.calculate_pos(options)
429445
self.rect = Rect(*self.pos, *self.size)
@@ -491,15 +507,6 @@ def do_event(self, event):
491507
mods = pygame.key.get_mods()
492508

493509
if event.type == MOUSEBUTTONDOWN:
494-
# detect double click
495-
t = pygame.time.get_ticks()
496-
if t - self.t1 < Node.dbl_click_time:
497-
self.triple_click()
498-
elif t- self.t0 < Node.dbl_click_time:
499-
self.double_click()
500-
self.t1 = self.t0
501-
self.t0 = t
502-
503510
# click in resize button
504511
r = Rect(0, 0, 7, 7)
505512
r.bottomright = self.rect.bottomright
@@ -656,62 +663,198 @@ def render(self):
656663

657664
class TextEdit(Text):
658665
"""Text with movable cursor to edit the text."""
666+
667+
cursor = Color('red'), 2 # cursor color and width
668+
cursor_blink = 600, 400 # interval, on_time
669+
text_selection = Color('pink') # selection color
670+
659671
def __init__(self, text='TextEdit', cmd='', **options):
660672
super().__init__(text=text, cmd=cmd, **options)
661673

662-
self.cursor_pos = len(self.text)
663-
self.cursor_img = pygame.Surface((2, self.rect.height))
664-
self.cursor_img.fill(Color('red'))
674+
col, d = TextEdit.cursor
675+
self.cursor = len(self.text)
676+
self.cursor_img = pygame.Surface((d, self.rect.height))
677+
self.cursor_img.fill(col)
665678
self.cursor_rect = self.cursor_img.get_rect()
666679
self.cursor_rect.topleft = self.rect.topright
680+
self.cursor2 = self.cursor
681+
682+
self.set_char_positions()
683+
self.render_cursor()
684+
685+
def set_char_positions(self):
686+
"""Get a list of all character positions."""
687+
self.char_positions = [0]
688+
for i in range(len(self.text)):
689+
w, h = self.font.size(self.text[:i+1])
690+
self.char_positions.append(w)
691+
692+
def get_char_index(self, position):
693+
"""Return the character index for a given position."""
694+
for i, pos in enumerate(self.char_positions):
695+
if position <= pos:
696+
return i
697+
# if not found return the highest index
698+
return i
699+
700+
def move_cursor(self, d):
701+
"""Move the cursor by d charactors, and limit to text length."""
702+
mod = pygame.key.get_mods()
703+
n = len(self.text)
704+
i = min(max(0, self.cursor+d), n)
705+
706+
if mod & KMOD_META:
707+
if d == 1:
708+
i = n
709+
else:
710+
i = 0
711+
712+
if mod & KMOD_ALT:
713+
while (0 < i < n) and self.text[i] != ' ':
714+
i += d
715+
716+
if not mod & KMOD_SHIFT:
717+
self.cursor2 = i
718+
719+
self.cursor = i
720+
721+
def get_selection_indices(self):
722+
"""Get ordered tuple of selection indicies."""
723+
i = self.cursor
724+
i2 = self.cursor2
725+
726+
if i < i2:
727+
return i, i2
728+
else:
729+
return i2, i
730+
731+
def copy_text(self):
732+
"""Copy text to Scene.text buffer."""
733+
i, i2 = self.get_selection_indices()
734+
text = self.text[i:i2]
735+
App.scene.text = text
736+
print('copy', text)
737+
738+
def cut_text(self):
739+
"""Cut text and place copy in Scene.text buffer."""
740+
self.copy_text()
741+
self.insert_text('')
742+
743+
def insert_text(self, text):
744+
"""Insert text at the cursor position or replace selection."""
745+
i, i2 = self.get_selection_indices()
746+
text1 = self.text[:i]
747+
text2 = self.text[i2:]
748+
self.text = text1 + text + text2
749+
self.cursor = i + len(text)
750+
self.cursor2 = self.cursor
667751

668752
def do_event(self, event):
669-
"""Move cursor left/right, add/backspace text."""
670-
#Node.do_event(self, event)
753+
"""Move cursor, handle selection, add/backspace text, copy/paste."""
671754
if event.type == KEYDOWN:
672755
if event.key == K_RETURN:
673756
App.scene.focus = None
674757
exec(self.cmd)
758+
675759
elif event.key == K_BACKSPACE:
676-
t0 = self.text[:self.cursor_pos-1]
677-
t1 = self.text[self.cursor_pos:]
678-
self.text = t0 + t1
679-
self.cursor_pos = max(0, self.cursor_pos-1)
680-
elif event.key in (K_TAB, K_UP, K_DOWN):
760+
if self.cursor == self.cursor2:
761+
self.cursor = max(0, self.cursor-1)
762+
self.insert_text('')
763+
764+
elif event.key in (K_TAB, K_UP, K_DOWN, K_LCTRL, K_LMETA):
681765
pass
766+
682767
elif event.key == K_LEFT:
683-
self.cursor_pos = max(0, self.cursor_pos-1)
684-
768+
self.move_cursor(-1)
769+
685770
elif event.key == K_RIGHT:
686-
self.cursor_pos = min(len(self.text), self.cursor_pos+1)
771+
self.move_cursor(1)
687772

688773
elif not (event.mod & KMOD_META + KMOD_CTRL):
689-
t0 = self.text[:self.cursor_pos]
690-
t1 = self.text[self.cursor_pos:]
691-
self.text = t0 + event.unicode + t1
692-
self.cursor_pos += 1
774+
self.insert_text(event.unicode)
775+
776+
elif event.key == K_x and event.mod & KMOD_META:
777+
self.cut_text()
778+
779+
elif event.key == K_c and event.mod & KMOD_META:
780+
self.copy_text()
781+
782+
elif event.key == K_v and event.mod & KMOD_META:
783+
self.insert_text(App.scene.text)
693784

694785
self.render()
695786

696-
if event.type == MOUSEBUTTONDOWN:
697-
for i in range(len(self.text+' ')):
698-
txt = self.text[:i]
699-
w, h = self.font.size(self.text[:i])
700-
if w+3 > (event.pos[0] - self.rect.left):
701-
break
702-
self.cursor_pos = i
787+
elif event.type == MOUSEBUTTONDOWN:
788+
pos = event.pos[0] - self.rect.left
789+
if pos < 3:
790+
pos = 0
791+
792+
i = self.get_char_index(pos)
793+
self.cursor = i
794+
if not pygame.key.get_mods() & KMOD_SHIFT:
795+
self.cursor2 = i
796+
797+
elif event.type == MOUSEMOTION and event.buttons[0]:
798+
pos = event.pos[0] - self.rect.left
799+
if pos < 3:
800+
pos = 0
801+
802+
i = self.get_char_index(pos)
803+
self.cursor = i
804+
805+
self.render_cursor()
806+
807+
def render_cursor(self):
808+
"""Render the cursor and selection image."""
809+
i = self.cursor
810+
i2 = self.cursor2
811+
812+
self.set_char_positions()
813+
p = self.char_positions[i]
814+
p2 = self.char_positions[i2]
703815

704-
w, h = self.font.size(self.text[:self.cursor_pos])
705-
self.cursor_rect = self.rect.move((w, 0))
816+
self.cursor_rect.left = self.rect.left + p
706817

818+
if p2 < p:
819+
p, p2 = p2, p
820+
821+
self.selection_rect = self.rect.copy()
822+
self.selection_rect.left = self.rect.left + p
823+
self.selection_rect.width = p2-p
824+
825+
col = TextEdit.text_selection
826+
self.selection_img = pygame.Surface((p2-p+1, self.rect.height))
827+
self.selection_img.fill(col)
707828

829+
708830
def draw(self):
831+
App.screen.blit(self.selection_img, self.selection_rect)
709832
Node.draw(self)
710833
if self == App.scene.focus:
711834
t = pygame.time.get_ticks()
712-
if (t % 600) > 300:
835+
interval, on_time = TextEdit.cursor_blink
836+
if (t % interval) < on_time:
713837
App.screen.blit(self.cursor_img, self.cursor_rect)
714838

839+
def double_click(self):
840+
"""Select the current word."""
841+
i = i2 = self.cursor
842+
n = len(self.text)
843+
844+
while (0 < i < n) and self.text[i] != ' ':
845+
i -= 1
846+
847+
while (0 < i2 < n) and self.text[i2] != ' ':
848+
i2 += 1
849+
850+
self.cursor = i2
851+
self.cursor2 = i+1 if self.text[i]==' ' else i
852+
853+
def triple_click(self):
854+
"""Select the whole text."""
855+
self.cursor = len(self.text)
856+
self.cursor2 = 0
857+
715858
class TextList(Node):
716859

717860
def __init__(self, items, i=0, **options):
@@ -1285,8 +1428,11 @@ def divide_img(self, m, n):
12851428
b.Num = np.arange(16).reshape((4, 4))
12861429
b.render()
12871430

1288-
app.run()
1431+
Scene(caption='TextEdit - editable text')
1432+
TextEdit('Elle', autosize=True)
1433+
TextEdit('Edit this text with the cursor')
12891434

1435+
app.run()
12901436
"""
12911437
class Num(nparray) - add more functions
12921438
class Graph - points, links (Mill)

0 commit comments

Comments
 (0)