From a6a7eaaa9740593f990b7af133603ea025ea7691 Mon Sep 17 00:00:00 2001 From: Qua-non Date: Fri, 31 Aug 2012 02:30:06 +0530 Subject: [PATCH 1/3] UIX: TextInput, attempt to make handeling of large data smoother than before --- kivy/uix/textinput.py | 230 ++++++++++++++++++++++++++++++------------ 1 file changed, 164 insertions(+), 66 deletions(-) diff --git a/kivy/uix/textinput.py b/kivy/uix/textinput.py index cc9d9fa79..ff51ae0db 100644 --- a/kivy/uix/textinput.py +++ b/kivy/uix/textinput.py @@ -1,3 +1,4 @@ +# -*- encoding: utf8 -*- ''' Text Input ========== @@ -121,7 +122,12 @@ from kivy.properties import StringProperty, NumericProperty, \ ReferenceListProperty, BooleanProperty, AliasProperty, \ ListProperty, ObjectProperty -Cache.register('textinput.label', timeout=60.) +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_NEWLINE = 0x01 @@ -136,7 +142,8 @@ _textinput_list = [] if 'KIVY_DOC' not in environ: def _textinput_clear_cache(*l): - Cache.remove('textinput.label') + Cache_remove('textinput.label') + Cache_remove('textinput.width') for wr in _textinput_list[:]: textinput = wr() if textinput is None: @@ -278,7 +285,8 @@ class TextInput(Widget): offset = 0 if self.cursor_col: offset = self._get_text_width( - self._lines[self.cursor_row][:self.cursor_col]) + self._lines[self.cursor_row][:self.cursor_col], self.tab_width, + self._label_cached) return offset def get_cursor_from_index(self, index): @@ -329,20 +337,26 @@ class TextInput(Widget): ci = sci() text = self._lines[cr] len_str = len(substring) + insert_at_end = True if text[cc:] == '' else False new_text = text[:cc] + substring + text[cc:] self._set_line_text(cr, new_text) if len_str > 1 or substring == '\n': # Avoid refreshing text on every keystroke. # Allows for faster typing of text when the amount of text in # TextInput gets large. - self._trigger_refresh_text() - #reset cursor + start = cr + lines, lineflags = self._split_smart(new_text) + len_lines = len(lines) + finish = cr + (len_lines - 1) + self._trigger_refresh_text('insert', start, finish, lines, + lineflags, len_lines) + # reset cursor self.cursor = cursor = self.get_cursor_from_index(ci + len_str) - #handle undo and redo + # handle undo and redo self._set_unredo_insert(cc, cr, ci, sci, substring, cursor, from_undo) def _set_unredo_insert(self, cc, cr, ci, sci, substring, cursor, from_undo): - #handle undo and redo + # handle undo and redo if from_undo: return count = substring.count('\n') @@ -353,7 +367,7 @@ class TextInput(Widget): self._undo.append({'undo_command': ('insert', cursor, ci, sci()), 'redo_command': (cc, cr, substring)}) - #reset redo when undo is appended to + # reset redo when undo is appended to self._redo = [] def reset_undo(self): @@ -380,7 +394,6 @@ class TextInput(Widget): cc, cr, substring = x_item['redo_command'] self.cursor = cc, cr self.insert_text(substring, True) - #substring.replace('\n', '\\n').replace('\'', '\\\'') elif undo_type == 'bkspc': cc, cr = x_item['redo_command'] self.cursor = cc, cr @@ -419,7 +432,7 @@ class TextInput(Widget): self._selection = True self.delete_selection(True) elif undo_type == 'bkspc': - substring = x_item['undo_command'][2:] + substring = x_item['undo_command'][2:][0] self.insert_text(substring, True) else: # delsel @@ -462,11 +475,11 @@ class TextInput(Widget): # where large..ish text is involved. #self._refresh_text_from_property() self.cursor = cursor = self.get_cursor_from_index(cursor_index - 1) - #handle undo and redo + # handle undo and redo self._set_undo_redo_bkspc(cc, cr, cursor, substring, from_undo) def _set_undo_redo_bkspc(self, cc, cr, cursor, substring, from_undo): - #handle undo and redo for backspace + # handle undo and redo for backspace if from_undo: return @@ -530,8 +543,11 @@ class TextInput(Widget): cy = (self.top - self.padding_y + scrl_y * dy) - y cy = int(boundary(round(cy / dy), 0, len(l) - 1)) dcx = 0 + _get_text_width = self._get_text_width + _tab_width = self.tab_width + _label_cached = self._label_cached for i in xrange(1, len(l[cy]) + 1): - if self._get_text_width(l[cy][:i]) >= cx: + if _get_text_width(l[cy][:i], _tab_width, _label_cached) >= cx: break dcx = i cx = dcx @@ -556,33 +572,36 @@ class TextInput(Widget): scrl_x = self.scroll_x scrl_y = self.scroll_y cc, cr = self.cursor - #sci = self.cursor_index - #ci = sci() if not self._selection: return v = self.text a, b = self._selection_from, self._selection_to if a > b: a, b = b, a - text = v[:a] + v[b:] - self.text = text - text = v[a:b] self.cursor = cursor = self.get_cursor_from_index(a) + start = cursor + finish = self.get_cursor_from_index(b) + cur_line = self._lines[start[1]][:start[0]] +\ + self._lines[finish[1]][finish[0]:] + lines, lineflags = self._split_smart(cur_line) + len_lines = len(lines) + self._refresh_text(self.text, 'del', start[1], finish[1], lines, + lineflags, len_lines) self.scroll_x = scrl_x self.scroll_y = scrl_y - #handle undo and redo - self._set_unredo_delsel(cc, cr, a, b, cursor, text, from_undo) + # handle undo and redo for delete selecttion + self._set_unredo_delsel(cc, cr, a, b, cursor, v[a:b], from_undo) self.cancel_selection() def _set_unredo_delsel(self, cc, cr, ci, sci, cursor, substring, from_undo): - #handle undo and redo for backspace + # handle undo and redo for backspace if from_undo: return self._undo.append({ 'undo_command': ('delsel', cursor, substring), 'redo_command': (ci, sci, cc, cr)}) - #reset redo when undo is appended to + # reset redo when undo is appended to self._redo = [] def _update_selection(self, finished=False): @@ -603,7 +622,8 @@ class TextInput(Widget): # update graphics only on new line # allows smoother scrolling, noticeably # faster when dealing with large text. - self._trigger_update_graphics() + self._update_graphics_selection() + #self._trigger_update_graphics() # # Touch control @@ -773,11 +793,10 @@ class TextInput(Widget): return if Clipboard is None: from kivy.core.clipboard import Clipboard - Clipboard _platform = platform() if _platform == 'win': self._clip_mime_type = 'text/plain;charset=utf-8' - #windows clipboard uses a utf-16 encoding + # windows clipboard uses a utf-16 encoding self._encoding = 'utf-16' elif _platform == 'linux': self._clip_mime_type = 'UTF8_STRING' @@ -815,6 +834,7 @@ class TextInput(Widget): data = data.replace('\x00', '') self.delete_selection() self.insert_text(data) + data = None def _keyboard_released(self): # Callback called when the real keyboard is taken by someone else @@ -822,14 +842,22 @@ class TextInput(Widget): # FIXME: handle virtual keyboard. self.focus = False - def _get_text_width(self, text): + def _get_text_width(self, text, tab_width, _label_cached): # Return the width of a text, according to the current line options - if not self._label_cached: + width = Cache_get('textinput.width', text) + if width: + return width + if not _label_cached: self._get_line_options() - text = text.replace('\t', ' ' * self.tab_width) + _label_cached = self._label_cached + orig_text = text + text = text.replace('\t', ' ' * tab_width) if not self.password: - return self._label_cached.get_extents(text)[0] - return self._label_cached.get_extents('*' * len(text))[0] + width = _label_cached.get_extents(text)[0] + else: + width = _label_cached.get_extents('*' * len(text))[0] + Cache_append('textinput.width', orig_text, width) + return width def _do_blink_cursor(self, dt): # Callback called by the timer to blink the cursor, according to the @@ -845,6 +873,7 @@ class TextInput(Widget): def _delete_line(self, idx): # Delete current line, and fix cursor position + print 'delete lines' assert(idx < len(self._lines)) self._lines.pop(idx) self._lines_flags.pop(idx) @@ -867,23 +896,46 @@ class TextInput(Widget): self.cursor = self.get_cursor_from_index(len(self.text)) def _trigger_refresh_text(self, *largs): - Clock.unschedule(self._refresh_text_from_property) - Clock.schedule_once(self._refresh_text_from_property) + if len(largs) and largs[0] == self: + largs = () + Clock.unschedule( + lambda *args: self._refresh_text_from_property(*largs)) + Clock.schedule_once( + lambda *args: self._refresh_text_from_property(*largs)) def _refresh_text_from_property(self, *largs): - self._refresh_text(self.text) + self._refresh_text(self.text, *largs) - def _refresh_text(self, text): + def _refresh_text(self, text, *largs): # Refresh all the lines from a new text. # By using cache in internal functions, this method should be fast. - _lines, self._lines_flags = self._split_smart(text) - self._lines = _lines + mode = 'all' + if len(largs): + mode, start, finish, _lines, _lines_flags, len_lines = largs + else: + _lines, self._lines_flags = self._split_smart(text) + _lines_labels = [] + _line_rects = [] _create_label = self._create_line_label - _lines_labels = self._lines_labels =\ - [_create_label(x) for x in _lines] - self._lines_rects = [Rectangle(texture=x, size=( - x.size if x else (0, 0))) - for x in _lines_labels] + + for x in _lines: + lbl = _create_label(x) + _lines_labels.append(lbl) + _line_rects.append( + Rectangle(size=(lbl.size if lbl else (0, 0)))) + lbl = None + + if mode == 'all': + self._lines = _lines + self._lines_labels = _lines_labels + self._lines_rects = _line_rects + elif mode == 'del': + self._insert_lines(start, finish + 1, len_lines, _lines_flags, + _lines, _lines_labels, _line_rects) + elif mode == 'insert': + self._insert_lines(start, start + 1, len_lines, _lines_flags, + _lines, _lines_labels, _line_rects) + line_label = _lines_labels[0] if line_label is None: self.line_height = max(1, self.font_size + self.padding_y) @@ -901,6 +953,41 @@ class TextInput(Widget): # with the new text don't forget to update graphics again self._trigger_update_graphics() + def _insert_lines(self, start, finish, len_lines, _lines_flags, _lines, + _lines_labels, _line_rects): + _lins_flags = [] + _lins_flags.extend(self._lines_flags[:start]) + if len_lines: + # if not inserting at first line then + if start: + # make sure new line is set in line flags cause + # _split_smart assumes first line to be not a new line + _lines_flags[0] = 1 + _lins_flags.extend(_lines_flags) + _lins_flags.extend(self._lines_flags[finish:]) + self._lines_flags = _lins_flags + + _lins = [] + _lins.extend(self._lines[:start]) + if len_lines: + _lins.extend(_lines) + _lins.extend(self._lines[finish:]) + self._lines = _lins + + _lins_lbls = [] + _lins_lbls.extend(self._lines_labels[:start]) + if len_lines: + _lins_lbls.extend(_lines_labels) + _lins_lbls.extend(self._lines_labels[finish:]) + self._lines_labels = _lins_lbls + + _lins_rcts = [] + _lins_rcts.extend(self._lines_rects[:start]) + if len_lines: + _lins_rcts.extend(_line_rects) + _lins_rcts.extend(self._lines_rects[finish:]) + self._lines_rects = _lins_rcts + def _trigger_update_graphics(self, *largs): Clock.unschedule(self._update_graphics) Clock.schedule_once(self._update_graphics, -1) @@ -1018,23 +1105,32 @@ class TextInput(Widget): s1c, s1r = get_cursor_from_index(a) s2c, s2r = get_cursor_from_index(b) s2r += 1 - # pass only the selection lines + # pass only the selection lines[] # passing all the lines can get slow when dealing with a lot of text y -= s1r * dy - for line_num, value in enumerate(self._lines[s1r:s2r], start=s1r): + _lines = self._lines + _get_text_width = self._get_text_width + tab_width = self.tab_width + _label_cached = self._label_cached + width = self.width + padding_x = self.padding_x + x = self.x + canvas_add = self.canvas.add + selection_color = self.selection_color + for line_num, value in enumerate(_lines[s1r:s2r], start=s1r): if miny <= y <= maxy + dy: r = rects[line_num] - draw_selection(r.pos, r.size, line_num) + draw_selection(r.pos, r.size, line_num, (s1c, s1r), + (s2c, s2r - 1), _lines, _get_text_width, tab_width, + _label_cached, width, padding_x, x, canvas_add, + selection_color) y -= dy - def _draw_selection(self, pos, size, line_num): + def _draw_selection(self, *largs): + pos, size, line_num, (s1c, s1r), (s2c, s2r),\ + _lines, _get_text_width, tab_width, _label_cached, width,\ + padding_x, x, canvas_add, selection_color = largs # Draw the current selection on the widget. - a, b = self._selection_from, self._selection_to - if a > b: - a, b = b, a - get_cursor_from_index = self.get_cursor_from_index - s1c, s1r = get_cursor_from_index(a) - s2c, s2r = get_cursor_from_index(b) if line_num < s1r or line_num > s2r: return x, y = pos @@ -1042,18 +1138,17 @@ class TextInput(Widget): x1 = x x2 = x + w if line_num == s1r: - lines = self._lines[line_num] - x1 += self._get_text_width(lines[:s1c]) + lines = _lines[line_num] + x1 += _get_text_width(lines[:s1c], tab_width, _label_cached) if line_num == s2r: - lines = self._lines[line_num] - x2 = x + self._get_text_width(lines[:s2c]) - width_minus_padding_x = self.width - self.padding_x + lines = _lines[line_num] + x2 = x + _get_text_width(lines[:s2c], tab_width, _label_cached) + width_minus_padding_x = width - padding_x maxx = x + width_minus_padding_x if x1 > maxx: return - x2 = min(x2, self.x + width_minus_padding_x) - canvas_add = self.canvas.add - canvas_add(Color(*self.selection_color, group='selection')) + x2 = min(x2, x + width_minus_padding_x) + canvas_add(Color(*selection_color, group='selection')) canvas_add(Rectangle( pos=(x1, pos[1]), size=(x2 - x1, size[1]), group='selection')) @@ -1093,7 +1188,7 @@ class TextInput(Widget): ntext = '*' * len(ntext) kw = self._get_line_options() cid = '%s\0%s' % (ntext, str(kw)) - texture = Cache.get('textinput.label', cid) + texture = Cache_get('textinput.label', cid) if not texture: # FIXME right now, we can't render very long line... @@ -1124,7 +1219,7 @@ class TextInput(Widget): # ok, we found it. texture = label.texture - Cache.append('textinput.label', cid, texture) + Cache_append('textinput.label', cid, texture) return texture def _tokenize(self, text): @@ -1159,18 +1254,21 @@ class TextInput(Widget): line = [] lines = [] lines_flags = [] + _join = ''.join + lines_append, lines_flags_append = lines.append, lines_flags.append width = self.width - self.padding_x * 2 text_width = self._get_text_width + _tab_width, _label_cached = self.tab_width, self._label_cached # try to add each word on current line. for word in self._tokenize(text): is_newline = (word == '\n') - w = text_width(word) + w = text_width(word, _tab_width, _label_cached) # if we have more than the width, or if it's a newline, # push the current line, and create a new one if (x + w > width and line) or is_newline: - lines.append(''.join(line)) - lines_flags.append(flags) + lines_append(_join(line)) + lines_flags_append(flags) flags = 0 line = [] x = 0 @@ -1180,8 +1278,8 @@ class TextInput(Widget): x += w line.append(word) if line or flags & FL_IS_NEWLINE: - lines.append(''.join(line)) - lines_flags.append(flags) + lines_append(_join(line)) + lines_flags_append(flags) return lines, lines_flags From 8dbedc4141953594b75470ca8dc6f3d906a52961 Mon Sep 17 00:00:00 2001 From: Qua-non Date: Sun, 2 Sep 2012 05:17:43 +0530 Subject: [PATCH 2/3] remove prints --- kivy/uix/textinput.py | 1 - 1 file changed, 1 deletion(-) diff --git a/kivy/uix/textinput.py b/kivy/uix/textinput.py index ff51ae0db..83d625b9d 100644 --- a/kivy/uix/textinput.py +++ b/kivy/uix/textinput.py @@ -873,7 +873,6 @@ class TextInput(Widget): def _delete_line(self, idx): # Delete current line, and fix cursor position - print 'delete lines' assert(idx < len(self._lines)) self._lines.pop(idx) self._lines_flags.pop(idx) From 903609946cf7578b15ca78f2d814ba6b4ef78a31 Mon Sep 17 00:00:00 2001 From: Qua-non Date: Tue, 4 Sep 2012 03:47:27 +0530 Subject: [PATCH 3/3] UIX: textinput reset width cache on font_size/name/padding_x/... --- kivy/uix/textinput.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/kivy/uix/textinput.py b/kivy/uix/textinput.py index d2f1a6ac0..7f8231935 100644 --- a/kivy/uix/textinput.py +++ b/kivy/uix/textinput.py @@ -240,12 +240,12 @@ class TextInput(Widget): self.bind(font_size=self._trigger_refresh_line_options, font_name=self._trigger_refresh_line_options) - self.bind(padding_x=self._trigger_refresh_text, - padding_y=self._trigger_refresh_text, - tab_width=self._trigger_refresh_text, - font_size=self._trigger_refresh_text, - font_name=self._trigger_refresh_text, - size=self._trigger_refresh_text) + self.bind(padding_x=self._update_text_options, + padding_y=self._update_text_options, + tab_width=self._update_text_options, + font_size=self._update_text_options, + font_name=self._update_text_options, + size=self._update_text_options) self.bind(pos=self._trigger_update_graphics) @@ -902,6 +902,10 @@ class TextInput(Widget): Clock.schedule_once( lambda *args: self._refresh_text_from_property(*largs)) + def _update_text_options(self, *largs): + Cache_remove('textinput.width') + self._trigger_refresh_text() + def _refresh_text_from_property(self, *largs): self._refresh_text(self.text, *largs) @@ -1096,7 +1100,6 @@ class TextInput(Widget): miny = self.y + _padding_y maxy = _top - _padding_y draw_selection = self._draw_selection - #scroll_y = self.scroll_y a, b = self._selection_from, self._selection_to if a > b: a, b = b, a