4949DBG_LABELS  =  2 
5050DBG_OUTLINE  =  4 
5151
52+ DBL_CLICK_TIMER  =  pygame .USEREVENT 
53+ DBL_CLICK_TIMEOUT  =  200 
54+ 
5255class  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
657664class  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+ 
715858class  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""" 
12911437class Num(nparray) - add more functions 
12921438class Graph - points, links (Mill) 
0 commit comments