-
-
Notifications
You must be signed in to change notification settings - Fork 3k
/
textinput.py
3802 lines (3166 loc) · 127 KB
/
textinput.py
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
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
'''
Text Input
==========
.. versionadded:: 1.0.4
.. image:: images/textinput-mono.jpg
.. image:: images/textinput-multi.jpg
The :class:`TextInput` widget provides a box for editable plain text.
Unicode, multiline, cursor navigation, selection and clipboard features
are supported.
The :class:`TextInput` uses two different coordinate systems:
* (x, y) - coordinates in pixels, mostly used for rendering on screen.
* (col, row) - cursor index in characters / lines, used for selection
and cursor movement.
Usage example
-------------
To create a multiline :class:`TextInput` (the 'enter' key adds a new line)::
from kivy.uix.textinput import TextInput
textinput = TextInput(text='Hello world')
To create a singleline :class:`TextInput`, set the :class:`TextInput.multiline`
property to False (the 'enter' key will defocus the TextInput and emit an
:meth:`TextInput.on_text_validate` event)::
def on_enter(instance, value):
print('User pressed enter in', instance)
textinput = TextInput(text='Hello world', multiline=False)
textinput.bind(on_text_validate=on_enter)
The textinput's text is stored in its :attr:`TextInput.text` property. To run a
callback when the text changes::
def on_text(instance, value):
print('The widget', instance, 'have:', value)
textinput = TextInput()
textinput.bind(text=on_text)
You can set the :class:`focus <kivy.uix.behaviors.FocusBehavior>` to a
Textinput, meaning that the input box will be highlighted and keyboard focus
will be requested::
textinput = TextInput(focus=True)
The textinput is defocused if the 'escape' key is pressed, or if another
widget requests the keyboard. You can bind a callback to the focus property to
get notified of focus changes::
def on_focus(instance, value):
if value:
print('User focused', instance)
else:
print('User defocused', instance)
textinput = TextInput()
textinput.bind(focus=on_focus)
See :class:`~kivy.uix.behaviors.FocusBehavior`, from which the
:class:`TextInput` inherits, for more details.
Selection
---------
The selection is automatically updated when the cursor position changes.
You can get the currently selected text from the
:attr:`TextInput.selection_text` property.
Filtering
---------
You can control which text can be added to the :class:`TextInput` by
overwriting :meth:`TextInput.insert_text`. Every string that is typed, pasted
or inserted by any other means into the :class:`TextInput` is passed through
this function. By overwriting it you can reject or change unwanted characters.
For example, to write only in capitalized characters::
class CapitalInput(TextInput):
def insert_text(self, substring, from_undo=False):
s = substring.upper()
return super().insert_text(s, from_undo=from_undo)
Or to only allow floats (0 - 9 and a single period)::
class FloatInput(TextInput):
pat = re.compile('[^0-9]')
def insert_text(self, substring, from_undo=False):
pat = self.pat
if '.' in self.text:
s = re.sub(pat, '', substring)
else:
s = '.'.join(
re.sub(pat, '', s)
for s in substring.split('.', 1)
)
return super().insert_text(s, from_undo=from_undo)
Default shortcuts
-----------------
=============== ========================================================
Shortcuts Description
--------------- --------------------------------------------------------
Left Move cursor to left
Right Move cursor to right
Up Move cursor to up
Down Move cursor to down
Home Move cursor at the beginning of the line
End Move cursor at the end of the line
PageUp Move cursor to 3 lines before
PageDown Move cursor to 3 lines after
Backspace Delete the selection or character before the cursor
Del Delete the selection of character after the cursor
Shift + <dir> Start a text selection. Dir can be Up, Down, Left or
Right
Control + c Copy selection
Control + x Cut selection
Control + v Paste clipboard content
Control + a Select all the content
Control + z undo
Control + r redo
=============== ========================================================
.. note::
To enable Emacs-style keyboard shortcuts, you can use
:class:`~kivy.uix.behaviors.emacs.EmacsBehavior`.
'''
import re
import sys
import math
from os import environ
from weakref import ref
from itertools import chain, islice
from kivy.animation import Animation
from kivy.base import EventLoop
from kivy.cache import Cache
from kivy.clock import Clock
from kivy.config import Config
from kivy.core.window import Window
from kivy.metrics import inch
from kivy.utils import boundary, platform
from kivy.uix.behaviors import FocusBehavior
from kivy.core.text import Label, DEFAULT_FONT
from kivy.graphics import Color, Rectangle, PushMatrix, PopMatrix, Callback
from kivy.graphics.context_instructions import Transform
from kivy.graphics.texture import Texture
from kivy.uix.widget import Widget
from kivy.uix.bubble import Bubble
from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.image import Image
from kivy.properties import StringProperty, NumericProperty, \
BooleanProperty, AliasProperty, OptionProperty, \
ListProperty, ObjectProperty, VariableListProperty, ColorProperty
__all__ = ('TextInput', )
if 'KIVY_DOC' in environ:
def triggered(*_, **__):
def decorator_func(func):
def decorated_func(*args, **kwargs):
return func(*args, **kwargs)
return decorated_func
return decorator_func
else:
from kivy.clock import triggered
Cache_register = Cache.register
Cache_append = Cache.append
Cache_get = Cache.get
Cache_remove = Cache.remove
Cache_register('textinput.label', timeout=60.)
Cache_register('textinput.width', timeout=60.)
FL_IS_LINEBREAK = 0x01
FL_IS_WORDBREAK = 0x02
FL_IS_NEWLINE = FL_IS_LINEBREAK | FL_IS_WORDBREAK
# late binding
Clipboard = None
CutBuffer = None
MarkupLabel = None
_platform = platform
# for reloading, we need to keep a list of textinput to retrigger the rendering
_textinput_list = []
# cache the result
_is_osx = sys.platform == 'darwin'
# When we are generating documentation, Config doesn't exist
_is_desktop = False
if Config:
_is_desktop = Config.getboolean('kivy', 'desktop')
# register an observer to clear the textinput cache when OpenGL will reload
if 'KIVY_DOC' not in environ:
def _textinput_clear_cache(*l):
Cache_remove('textinput.label')
Cache_remove('textinput.width')
for wr in _textinput_list[:]:
textinput = wr()
if textinput is None:
_textinput_list.remove(wr)
else:
textinput._trigger_refresh_text()
textinput._refresh_hint_text()
from kivy.graphics.context import get_context
get_context().add_reload_observer(_textinput_clear_cache, True)
class Selector(ButtonBehavior, Image):
# Internal class for managing the selection Handles.
window = ObjectProperty()
target = ObjectProperty()
matrix = ObjectProperty()
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.matrix = self.target.get_window_matrix()
with self.canvas.before:
Callback(self.update_transform)
PushMatrix()
self.transform = Transform()
with self.canvas.after:
PopMatrix()
def update_transform(self, cb):
matrix = self.target.get_window_matrix()
if self.matrix != matrix:
self.matrix = matrix
self.transform.identity()
self.transform.transform(self.matrix)
def transform_touch(self, touch):
matrix = self.matrix.inverse()
touch.apply_transform_2d(
lambda x, y: matrix.transform_point(x, y, 0)[:2]
)
def on_touch_down(self, touch):
if self.parent is not EventLoop.window:
return
try:
touch.push()
self.transform_touch(touch)
self._touch_diff = self.top - touch.y
if self.collide_point(*touch.pos):
FocusBehavior.ignored_touch.append(touch)
return super().on_touch_down(touch)
finally:
touch.pop()
class TextInputCutCopyPaste(Bubble):
# Internal class used for showing the little bubble popup when
# copy/cut/paste happen.
textinput = ObjectProperty(None)
''' Holds a reference to the TextInput this Bubble belongs to.
'''
but_cut = ObjectProperty(None)
but_copy = ObjectProperty(None)
but_paste = ObjectProperty(None)
but_selectall = ObjectProperty(None)
matrix = ObjectProperty(None)
_check_parent_ev = None
def __init__(self, **kwargs):
self.mode = 'normal'
super().__init__(**kwargs)
self._check_parent_ev = Clock.schedule_interval(self._check_parent, .5)
self.matrix = self.textinput.get_window_matrix()
with self.canvas.before:
Callback(self.update_transform)
PushMatrix()
self.transform = Transform()
with self.canvas.after:
PopMatrix()
def update_transform(self, cb):
m = self.textinput.get_window_matrix()
if self.matrix != m:
self.matrix = m
self.transform.identity()
self.transform.transform(self.matrix)
def transform_touch(self, touch):
matrix = self.matrix.inverse()
touch.apply_transform_2d(
lambda x, y: matrix.transform_point(x, y, 0)[:2])
def on_touch_down(self, touch):
try:
touch.push()
self.transform_touch(touch)
if self.collide_point(*touch.pos):
FocusBehavior.ignored_touch.append(touch)
return super().on_touch_down(touch)
finally:
touch.pop()
def on_touch_up(self, touch):
try:
touch.push()
self.transform_touch(touch)
for child in self.content.children:
if ref(child) in touch.grab_list:
touch.grab_current = child
break
return super().on_touch_up(touch)
finally:
touch.pop()
def on_textinput(self, instance, value):
global Clipboard
if value and not Clipboard and not _is_desktop:
value._ensure_clipboard()
def _check_parent(self, dt):
# this is a prevention to get the Bubble staying on the screen, if the
# attached textinput is not on the screen anymore.
parent = self.textinput
while parent is not None:
if parent == parent.parent:
break
parent = parent.parent
if parent is None:
self._check_parent_ev.cancel()
if self.textinput:
self.textinput._hide_cut_copy_paste()
def on_parent(self, instance, value):
parent = self.textinput
mode = self.mode
if parent:
self.clear_widgets()
if mode == 'paste':
# show only paste on long touch
self.but_selectall.opacity = 1
widget_list = [self.but_selectall, ]
if not parent.readonly:
widget_list.append(self.but_paste)
elif parent.readonly:
# show only copy for read only text input
widget_list = (self.but_copy, )
else:
# normal mode
widget_list = (self.but_cut, self.but_copy, self.but_paste)
for widget in widget_list:
self.add_widget(widget)
def do(self, action):
textinput = self.textinput
if action == 'cut':
textinput._cut(textinput.selection_text)
elif action == 'copy':
textinput.copy()
elif action == 'paste':
textinput.paste()
elif action == 'selectall':
textinput.select_all()
self.mode = ''
anim = Animation(opacity=0, d=.333)
anim.bind(on_complete=lambda *args:
self.on_parent(self, self.parent))
anim.start(self.but_selectall)
return
self.hide()
def hide(self):
parent = self.parent
if not parent:
return
anim = Animation(opacity=0, d=.225)
anim.bind(on_complete=lambda *args: parent.remove_widget(self))
anim.start(self)
class TextInput(FocusBehavior, Widget):
'''TextInput class. See module documentation for more information.
:Events:
`on_text_validate`
Fired only in multiline=False mode when the user hits 'enter'.
This will also unfocus the textinput.
`on_double_tap`
Fired when a double tap happens in the text input. The default
behavior selects the text around the cursor position. More info at
:meth:`on_double_tap`.
`on_triple_tap`
Fired when a triple tap happens in the text input. The default
behavior selects the line around the cursor position. More info at
:meth:`on_triple_tap`.
`on_quad_touch`
Fired when four fingers are touching the text input. The default
behavior selects the whole text. More info at
:meth:`on_quad_touch`.
.. warning::
When changing a :class:`TextInput` property that requires re-drawing,
e.g. modifying the :attr:`text`, the updates occur on the next
clock cycle and not instantly. This might cause any changes to the
:class:`TextInput` that occur between the modification and the next
cycle to be ignored, or to use previous values. For example, after
a update to the :attr:`text`, changing the cursor in the same clock
frame will move it using the previous text and will likely end up in an
incorrect position. The solution is to schedule any updates to occur
on the next clock cycle using
:meth:`~kivy.clock.ClockBase.schedule_once`.
.. Note::
Selection is cancelled when TextInput is focused. If you need to
show selection when TextInput is focused, you should delay
(use Clock.schedule) the call to the functions for selecting
text (select_all, select_text).
.. versionchanged:: 1.10.0
`background_disabled_active` has been removed.
.. versionchanged:: 1.9.0
:class:`TextInput` now inherits from
:class:`~kivy.uix.behaviors.FocusBehavior`.
:attr:`~kivy.uix.behaviors.FocusBehavior.keyboard_mode`,
:meth:`~kivy.uix.behaviors.FocusBehavior.show_keyboard`,
:meth:`~kivy.uix.behaviors.FocusBehavior.hide_keyboard`,
:meth:`~kivy.uix.behaviors.FocusBehavior.focus`,
and :attr:`~kivy.uix.behaviors.FocusBehavior.input_type`
have been removed since they are now inherited
from :class:`~kivy.uix.behaviors.FocusBehavior`.
.. versionchanged:: 1.7.0
`on_double_tap`, `on_triple_tap` and `on_quad_touch` events added.
.. versionchanged:: 2.1.0
:attr:`~kivy.uix.behaviors.FocusBehavior.keyboard_suggestions`
is now inherited from :class:`~kivy.uix.behaviors.FocusBehavior`.
'''
__events__ = ('on_text_validate', 'on_double_tap', 'on_triple_tap',
'on_quad_touch')
_resolved_base_dir = None
def __init__(self, **kwargs):
self._update_graphics_ev = Clock.create_trigger(
self._update_graphics, -1)
self.is_focusable = kwargs.get('is_focusable', True)
self._cursor = [0, 0]
self._selection = False
self._selection_finished = True
self._selection_touch = None
self.selection_text = u''
self._selection_from = None
self._selection_to = None
self._selection_callback = None
self._handle_left = None
self._handle_right = None
self._handle_middle = None
self._bubble = None
self._lines_flags = []
self._lines_labels = []
self._lines_rects = []
self._hint_text_flags = []
self._hint_text_labels = []
self._hint_text_rects = []
self._label_cached = None
self._line_options = None
self._keyboard_mode = Config.get('kivy', 'keyboard_mode')
self._command_mode = False
self._command = ''
self.reset_undo()
self._touch_count = 0
self._ctrl_l = False
self._ctrl_r = False
self._alt_l = False
self._alt_r = False
self._refresh_text_from_property_ev = None
self._long_touch_ev = None
self._do_blink_cursor_ev = Clock.create_trigger(
self._do_blink_cursor, .5, interval=True)
self._refresh_line_options_ev = None
# [from; to) range of lines being partially or fully rendered
# in TextInput's viewport
self._visible_lines_range = 0, 0
self.interesting_keys = {
8: 'backspace',
13: 'enter',
127: 'del',
271: 'enter',
273: 'cursor_up',
274: 'cursor_down',
275: 'cursor_right',
276: 'cursor_left',
278: 'cursor_home',
279: 'cursor_end',
280: 'cursor_pgup',
281: 'cursor_pgdown',
303: 'shift_L',
304: 'shift_R',
305: 'ctrl_L',
306: 'ctrl_R',
308: 'alt_L',
307: 'alt_R'
}
super().__init__(**kwargs)
fbind = self.fbind
refresh_line_options = self._trigger_refresh_line_options
update_text_options = self._update_text_options
trigger_update_graphics = self._trigger_update_graphics
fbind('font_size', refresh_line_options)
fbind('font_name', refresh_line_options)
fbind('font_context', refresh_line_options)
fbind('font_family', refresh_line_options)
fbind('base_direction', refresh_line_options)
fbind('text_language', refresh_line_options)
def handle_readonly(instance, value):
if value and (not _is_desktop or not self.allow_copy):
self.is_focusable = False
if (not (value or self.disabled) or _is_desktop and
self._keyboard_mode == 'system'):
self._editable = True
else:
self._editable = False
fbind('padding', update_text_options)
fbind('tab_width', update_text_options)
fbind('font_size', update_text_options)
fbind('font_name', update_text_options)
fbind('size', update_text_options)
fbind('password', update_text_options)
fbind('password_mask', update_text_options)
fbind('pos', trigger_update_graphics)
fbind('halign', trigger_update_graphics)
fbind('readonly', handle_readonly)
fbind('focus', self._on_textinput_focused)
handle_readonly(self, self.readonly)
handles = self._trigger_position_handles = Clock.create_trigger(
self._position_handles)
self._trigger_show_handles = Clock.create_trigger(
self._show_handles, .05)
self._trigger_cursor_reset = Clock.create_trigger(
self._reset_cursor_blink)
self._trigger_update_cutbuffer = Clock.create_trigger(
self._update_cutbuffer)
refresh_line_options()
self._trigger_refresh_text()
fbind('pos', handles)
fbind('size', handles)
# when the gl context is reloaded, trigger the text rendering again.
_textinput_list.append(ref(self, TextInput._reload_remove_observer))
if platform == 'linux':
self._ensure_clipboard()
def on_text_validate(self):
pass
def cursor_index(self, cursor=None):
'''Return the cursor index in the text/value.
'''
if not cursor:
cursor = self.cursor
try:
lines = self._lines
if not lines:
return 0
flags = self._lines_flags
index, cursor_row = cursor
for _, line, flag in zip(
range(min(cursor_row, len(lines))),
lines,
flags
):
index += len(line)
if flag & FL_IS_LINEBREAK:
index += 1
if flags[cursor_row] & FL_IS_LINEBREAK:
index += 1
return index
except IndexError:
return 0
def cursor_offset(self):
'''Get the cursor x offset on the current line.
'''
offset = 0
row = int(self.cursor_row)
col = int(self.cursor_col)
lines = self._lines
if col and row < len(lines):
offset = self._get_text_width(
lines[row][:col],
self.tab_width,
self._label_cached
)
return offset
def get_cursor_from_index(self, index):
'''Return the (col, row) of the cursor from text index.
'''
index = boundary(index, 0, len(self.text))
if index <= 0:
return 0, 0
flags = self._lines_flags
lines = self._lines
if not lines:
return 0, 0
i = 0
for row, line in enumerate(lines):
count = i + len(line)
if flags[row] & FL_IS_LINEBREAK:
count += 1
i += 1
if count >= index:
return index - i, row
i = count
return int(index), int(row)
def select_text(self, start, end):
''' Select a portion of text displayed in this TextInput.
.. versionadded:: 1.4.0
:Parameters:
`start`
Index of textinput.text from where to start selection
`end`
Index of textinput.text till which the selection should be
displayed
'''
if end < start:
raise Exception('end must be superior to start')
text_length = len(self.text)
self._selection_from = boundary(start, 0, text_length)
self._selection_to = boundary(end, 0, text_length)
self._selection_finished = True
self._update_selection(True)
self._update_graphics_selection()
def select_all(self):
''' Select all of the text displayed in this TextInput.
.. versionadded:: 1.4.0
'''
self.select_text(0, len(self.text))
re_indent = re.compile(r'^(\s*|)')
def _auto_indent(self, substring):
index = self.cursor_index()
if index > 0:
_text = self.text
line_start = _text.rfind('\n', 0, index)
if line_start > -1:
line = _text[line_start + 1:index]
indent = self.re_indent.match(line).group()
substring += indent
return substring
def insert_text(self, substring, from_undo=False):
'''Insert new text at the current cursor position. Override this
function in order to pre-process text for input validation.
'''
if self.readonly or not substring or not self._lines:
return
if isinstance(substring, bytes):
substring = substring.decode('utf8')
if self.replace_crlf:
substring = substring.replace(u'\r\n', u'\n')
self._hide_handles(EventLoop.window)
if not from_undo and self.multiline and self.auto_indent \
and substring == u'\n':
substring = self._auto_indent(substring)
mode = self.input_filter
if mode not in (None, 'int', 'float'):
substring = mode(substring, from_undo)
if not substring:
return
col, row = self.cursor
cindex = self.cursor_index()
text = self._lines[row]
len_str = len(substring)
new_text = text[:col] + substring + text[col:]
if mode is not None:
if mode == 'int':
if not re.match(self._insert_int_pat, new_text):
return
elif mode == 'float':
if not re.match(self._insert_float_pat, new_text):
return
self._set_line_text(row, new_text)
wrap = (self._get_text_width(
new_text,
self.tab_width,
self._label_cached) > (self.width - self.padding[0] -
self.padding[2]))
if len_str > 1 or substring == u'\n' or wrap:
# Avoid refreshing text on every keystroke.
# Allows for faster typing of text when the amount of text in
# TextInput gets large.
(
start, finish, lines, lineflags, len_lines
) = self._get_line_from_cursor(row, new_text)
# calling trigger here could lead to wrong cursor positioning
# and repeating of text when keys are added rapidly in a automated
# fashion. From Android Keyboard for example.
self._refresh_text_from_property(
'insert', start, finish, lines, lineflags, len_lines
)
self.cursor = self.get_cursor_from_index(cindex + len_str)
# handle undo and redo
self._set_unredo_insert(cindex, cindex + len_str, substring, from_undo)
def _get_line_from_cursor(self, start, new_text):
# get current paragraph from cursor position
finish = start
lines = self._lines
linesflags = self._lines_flags
if start and not linesflags[start]:
start -= 1
new_text = u''.join((lines[start], new_text))
try:
while not linesflags[finish + 1]:
new_text = u''.join((new_text, lines[finish + 1]))
finish += 1
except IndexError:
pass
lines, lineflags = self._split_smart(new_text)
len_lines = max(1, len(lines))
return start, finish, lines, lineflags, len_lines
def _set_unredo_insert(self, ci, sci, substring, from_undo):
# handle undo and redo
if from_undo:
return
self._undo.append({
'undo_command': ('insert', ci, sci),
'redo_command': (ci, substring)
})
# reset redo when undo is appended to
self._redo = []
def reset_undo(self):
'''Reset undo and redo lists from memory.
.. versionadded:: 1.3.0
'''
self._redo = self._undo = []
def do_redo(self):
'''Do redo operation.
.. versionadded:: 1.3.0
This action re-does any command that has been un-done by
do_undo/ctrl+z. This function is automatically called when
`ctrl+r` keys are pressed.
'''
try:
x_item = self._redo.pop()
undo_type = x_item['undo_command'][0]
_get_cusror_from_index = self.get_cursor_from_index
if undo_type == 'insert':
cindex, substring = x_item['redo_command']
self.cursor = _get_cusror_from_index(cindex)
self.insert_text(substring, True)
elif undo_type == 'bkspc':
self.cursor = _get_cusror_from_index(x_item['redo_command'])
self.do_backspace(from_undo=True)
elif undo_type == 'shiftln':
direction, rows, cursor = x_item['redo_command'][1:]
self._shift_lines(direction, rows, cursor, True)
else:
# delsel
cindex, scindex = x_item['redo_command']
self._selection_from = cindex
self._selection_to = scindex
self._selection = True
self.delete_selection(True)
self.cursor = _get_cusror_from_index(cindex)
self._undo.append(x_item)
except IndexError:
# reached at top of undo list
pass
def do_undo(self):
'''Do undo operation.
.. versionadded:: 1.3.0
This action un-does any edits that have been made since the last
call to reset_undo().
This function is automatically called when `ctrl+z` keys are pressed.
'''
try:
x_item = self._undo.pop()
undo_type = x_item['undo_command'][0]
self.cursor = self.get_cursor_from_index(x_item['undo_command'][1])
if undo_type == 'insert':
cindex, scindex = x_item['undo_command'][1:]
self._selection_from = cindex
self._selection_to = scindex
self._selection = True
self.delete_selection(True)
elif undo_type == 'bkspc':
substring = x_item['undo_command'][2:][0]
self.insert_text(substring, True)
elif undo_type == 'shiftln':
direction, rows, cursor = x_item['undo_command'][1:]
self._shift_lines(direction, rows, cursor, True)
else:
# delsel
substring = x_item['undo_command'][2:][0]
self.insert_text(substring, True)
self._redo.append(x_item)
except IndexError:
# reached at top of undo list
pass
def do_backspace(self, from_undo=False, mode='bkspc'):
'''Do backspace operation from the current cursor position.
This action might do several things:
- removing the current selection if available.
- removing the previous char and move the cursor back.
- do nothing, if we are at the start.
'''
# IME system handles its own backspaces
if self.readonly or self._ime_composition:
return
col, row = self.cursor
_lines = self._lines
text = _lines[row]
cursor_index = self.cursor_index()
text_last_line = _lines[row - 1]
if col == 0 and row == 0:
return
_lines_flags = self._lines_flags
start = row
if col == 0:
substring = u'\n' if _lines_flags[row] else u' '
new_text = text_last_line + text
self._set_line_text(row - 1, new_text)
self._delete_line(row)
start = row - 1
else:
# ch = text[col-1]
substring = text[col - 1]
new_text = text[:col - 1] + text[col:]
self._set_line_text(row, new_text)
# refresh just the current line instead of the whole text
start, finish, lines, lineflags, len_lines = (
self._get_line_from_cursor(start, new_text)
)
# avoid trigger refresh, leads to issue with
# keys/text send rapidly through code.
self._refresh_text_from_property(
'del', start, finish, lines, lineflags, len_lines
)
self.cursor = self.get_cursor_from_index(cursor_index - 1)
# handle undo and redo
self._set_undo_redo_bkspc(
cursor_index,
cursor_index - 1,
substring, from_undo)
def _set_undo_redo_bkspc(self, ol_index, new_index, substring, from_undo):
# handle undo and redo for backspace
if from_undo:
return
self._undo.append({
'undo_command': ('bkspc', new_index, substring),
'redo_command': ol_index})
# reset redo when undo is appended to
self._redo = []
_re_whitespace = re.compile(r'\s+')
def _move_cursor_word_left(self, index=None):
pos = index or self.cursor_index()
if pos == 0:
return self.cursor
lines = self._lines
col, row = self.get_cursor_from_index(pos)
if col == 0:
row -= 1
col = len(lines[row])
while True:
matches = list(self._re_whitespace.finditer(lines[row], 0, col))
if not matches:
if col == 0:
if row == 0:
return 0, 0
row -= 1
col = len(lines[row])
continue
return 0, row
match = matches[-1]
mpos = match.end()
if mpos == col:
if len(matches) > 1:
match = matches[-2]
mpos = match.end()
else:
if match.start() == 0:
if row == 0:
return 0, 0
row -= 1
col = len(lines[row])
continue
return 0, row
col = mpos
return col, row
def _move_cursor_word_right(self, index=None):
pos = index or self.cursor_index()
col, row = self.get_cursor_from_index(pos)
lines = self._lines
mrow = len(lines) - 1
if row == mrow and col == len(lines[row]):
return col, row
if col == len(lines[row]):
row += 1
col = 0
while True:
matches = list(self._re_whitespace.finditer(lines[row], col))