From 487c15a17d336e150029870cabf8de6b3156d75b Mon Sep 17 00:00:00 2001 From: Adam Ginsburg Date: Tue, 7 May 2013 07:47:29 -0600 Subject: [PATCH 01/24] added TextBox Widget --- lib/matplotlib/widgets.py | 194 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index eabff7dd6536..0a58c7d9991c 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1661,3 +1661,197 @@ def onmove(self, event): self.canvas.blit(self.ax.bbox) else: self.canvas.draw_idle() + + +class TextBox(Widget): + def __init__(self, ax, s='', horizontalalignment='left', + enter_callback=None, fontsize=12): + """ + Editable text box + + Creates a mouse-click callback such that clicking on the text box will + activate the cursor. + + *WARNING* Activating a textbox will permanently disable all other + key-press bindings! They'll be stored in TextBox.old_callbacks and + restored when TextBox.deactivate is called. + + The default widget assumes only numerical (float) data and will not + allow text entry besides numerical characters and ('e','-','.') + + Parameters + ---------- + ax : axis + Parent axis to turn into text box + s : str + Initial string contents of text box + horizontalalignment : left | center | right + Passed to self.text + enter_callback : function + A function of one argument that will be called with TextBox.value + passed in as the only argument when enter is pressed + fontsize : int + Font size for text box + """ + + self.value = float(s) + + self.canvas = ax.figure.canvas + self.text = ax.text(0.025, 0.2, s, + fontsize=fontsize, + verticalalignment='baseline', + horizontalalignment=horizontalalignment, + transform=ax.transAxes) + self.ax = ax + ax.set_yticks([]) + ax.set_xticks([]) + + ax.set_navigate(False) + self.canvas.draw() + + self.region = self.canvas.copy_from_bbox(ax.bbox) + + self._cursorpos = len(self.text.get_text()) + r = self._get_text_right() + + self.cursor, = ax.plot([r,r], [0.2, 0.8], transform=ax.transAxes) + self.active = False + + self.redraw() + self._cid = None + + self.enter_callback = enter_callback + + self.canvas.mpl_connect('button_press_event',self._mouse_activate) + + def redraw(self): + # blitting doesn't clear old text + #self.ax.redraw_in_frame() + #self.canvas.blit(self.ax.bbox) + self.canvas.draw() + + def _mouse_activate(self, event): + if self.ax == event.inaxes: + self.activate() + else: + self.deactivate() + + @property + def active(self): + return self._active + + @active.setter + def active(self, isactive): + self._active = bool(isactive) + self.cursor.set_visible(self._active) + self.redraw() + + def activate(self): + if self._cid not in self.canvas.callbacks.callbacks['key_press_event']: + + if not hasattr(self,'old_callbacks'): + self.old_callbacks = {} + + # remove all other key bindings + keys = self.canvas.callbacks.callbacks['key_press_event'].keys() + for k in keys: + self.old_callbacks[k] = self.canvas.callbacks.callbacks['key_press_event'].pop(k) + + self._cid = self.canvas.mpl_connect('key_press_event', self.keypress) + self.active = True + + def deactivate(self): + if self._cid in self.canvas.callbacks.callbacks['key_press_event']: + self.canvas.mpl_disconnect(self._cid) + if hasattr(self,'old_callbacks'): + for k in self.old_callbacks: + self.canvas.callbacks.callbacks['key_press_event'][k] = self.old_callbacks[k] + + self.active = False + + def keypress(self, event): + """ + Parse a keypress - only allow #'s! + """ + #print "event.key: '%s'" % event.key + #if event.key is not None and len(event.key)>1: return + + newt = t = self.text.get_text() + if event.key == 'backspace': # simulate backspace + if self._cursorpos == 0: return + if len(t) > 0: + newt = t[:self._cursorpos-1] + t[self._cursorpos:] + if self._cursorpos > 0: + self._cursorpos -= 1 + elif event.key == 'left' and self._cursorpos > 0: + self._cursorpos -= 1 + elif event.key == 'right' and self._cursorpos < len(t): + self._cursorpos += 1 + elif event.key == 'enter': + if self.enter_callback is not None: + try: + self.enter_callback(self.value) + self.deactivate() + except Exception as ex: + print(ex) + + elif len(event.key) > 1: + # ignore... + pass + elif event.key in '0123456789': + newt = t[:self._cursorpos] + event.key + t[self._cursorpos:] + self._cursorpos += 1 + elif event.key == 'e': + if 'e' not in t and '.' not in t[self._cursorpos:] and self._cursorpos != 0: + newt = t[:self._cursorpos] + event.key + t[self._cursorpos:] + self._cursorpos += 1 + elif event.key == '-': + # only allow negative at front or after e + if self._cursorpos == 0: + newt = event.key + t + self._cursorpos += 1 + elif (t[self._cursorpos-1]=='e' and not + (len(t) > self._cursorpos+1 and t[self._cursorpos+1] == '-')): + newt = t[:self._cursorpos] + event.key + t[self._cursorpos:] + self._cursorpos += 1 + elif event.key == '.': + # do nothing if extra decimals... + if '.' not in t: + newt = t[:self._cursorpos] + event.key + t[self._cursorpos:] + self._cursorpos += 1 + else: + pass # do not allow abcdef... + + self.set_text(newt, redraw=True) + + r = self._get_text_right() + self.cursor.set_xdata([r,r]) + self.redraw() + + def set_text(self, text, redraw=False): + try: + # only try to update if there's a real value + if (not(text.strip() in ('-.','.','-','')) + and not text[-1] in ('e','-')): + self.value = float(text) + # but always change the text + self.text.set_text(text) + except ValueError: + pass + + if redraw: + self.text.draw(self.text._renderer) + self.redraw() + + def _get_text_right(self): + l,b,w,h = self.text.get_window_extent().bounds + r = l+w+2 + t = b+h + s = self.text.get_text() + # adjust cursor position for trailing space + numtrail = len(s)-len(s.rstrip()) + en = self.ax.get_renderer_cache().points_to_pixels(self.text.get_fontsize())/2. + + r = l + self._cursorpos*np.ceil(en) + r,t = self.ax.transAxes.inverted().transform((r,t)) + return r From 557adaa70bef25cc46387dd01083fec334243d9e Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Tue, 4 Jun 2013 09:31:05 -0700 Subject: [PATCH 02/24] lazy cursor --- lib/matplotlib/widgets.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 0a58c7d9991c..eb38e41c7d25 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1711,10 +1711,9 @@ def __init__(self, ax, s='', horizontalalignment='left', self.region = self.canvas.copy_from_bbox(ax.bbox) + self._cursor = None self._cursorpos = len(self.text.get_text()) - r = self._get_text_right() - self.cursor, = ax.plot([r,r], [0.2, 0.8], transform=ax.transAxes) self.active = False self.redraw() @@ -1724,6 +1723,17 @@ def __init__(self, ax, s='', horizontalalignment='left', self.canvas.mpl_connect('button_press_event',self._mouse_activate) + @property + def cursor(self): + # Macos has issues with render objects. Lazily generating the cursor + # solve some of the problems associated + if self._cursor is None: + r = self.get_text_right() # needs a renderer + self._cursor, = self.ax.plot([r,r], [0.2, 0.8], + transform=self.ax.transAxes) + self._cursor.set_visible(False) + return self._cursor + def redraw(self): # blitting doesn't clear old text #self.ax.redraw_in_frame() From 4110887165739ee6aba97d794bc35f6458222cab Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Tue, 4 Jun 2013 09:37:30 -0700 Subject: [PATCH 03/24] rename (de)activate, remove inconsistent active properties --- lib/matplotlib/widgets.py | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index eb38e41c7d25..5b41506a0cd9 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1746,17 +1746,7 @@ def _mouse_activate(self, event): else: self.deactivate() - @property - def active(self): - return self._active - - @active.setter - def active(self, isactive): - self._active = bool(isactive) - self.cursor.set_visible(self._active) - self.redraw() - - def activate(self): + def begin_text_entry(self): if self._cid not in self.canvas.callbacks.callbacks['key_press_event']: if not hasattr(self,'old_callbacks'): @@ -1768,16 +1758,18 @@ def activate(self): self.old_callbacks[k] = self.canvas.callbacks.callbacks['key_press_event'].pop(k) self._cid = self.canvas.mpl_connect('key_press_event', self.keypress) - self.active = True + self.cursor.set_visible(True) + self.redraw() - def deactivate(self): + def end_text_entry(self): if self._cid in self.canvas.callbacks.callbacks['key_press_event']: self.canvas.mpl_disconnect(self._cid) if hasattr(self,'old_callbacks'): for k in self.old_callbacks: self.canvas.callbacks.callbacks['key_press_event'][k] = self.old_callbacks[k] - self.active = False + self.cursor.set_visible(False) + self.redraw() def keypress(self, event): """ @@ -1801,7 +1793,7 @@ def keypress(self, event): if self.enter_callback is not None: try: self.enter_callback(self.value) - self.deactivate() + self.end_text_entry() except Exception as ex: print(ex) From 82e3f9b4bd181263684c287abe2a260df04b2050 Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Tue, 4 Jun 2013 09:40:10 -0700 Subject: [PATCH 04/24] remove unused self.region --- lib/matplotlib/widgets.py | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 5b41506a0cd9..9ceb12e27ba7 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1709,8 +1709,6 @@ def __init__(self, ax, s='', horizontalalignment='left', ax.set_navigate(False) self.canvas.draw() - self.region = self.canvas.copy_from_bbox(ax.bbox) - self._cursor = None self._cursorpos = len(self.text.get_text()) @@ -1747,26 +1745,22 @@ def _mouse_activate(self, event): self.deactivate() def begin_text_entry(self): - if self._cid not in self.canvas.callbacks.callbacks['key_press_event']: - - if not hasattr(self,'old_callbacks'): - self.old_callbacks = {} - + keypress_cbs = self.canvas.callbacks.callbacks['key_press_event'] + if self._cid not in keypress_cbs: # remove all other key bindings - keys = self.canvas.callbacks.callbacks['key_press_event'].keys() - for k in keys: - self.old_callbacks[k] = self.canvas.callbacks.callbacks['key_press_event'].pop(k) + for k in keypress_cbs.keys(): + self.old_callbacks[k] = keypress_cbs.pop(k) self._cid = self.canvas.mpl_connect('key_press_event', self.keypress) self.cursor.set_visible(True) self.redraw() def end_text_entry(self): - if self._cid in self.canvas.callbacks.callbacks['key_press_event']: + keypress_cbs = self.canvas.callbacks.callbacks['key_press_event'] + if self._cid in keypress_cbs: self.canvas.mpl_disconnect(self._cid) - if hasattr(self,'old_callbacks'): - for k in self.old_callbacks: - self.canvas.callbacks.callbacks['key_press_event'][k] = self.old_callbacks[k] + for k in self.old_callbacks.keys(): + keypress_cbs[k] = self.old_callbacks.pop(k) self.cursor.set_visible(False) self.redraw() @@ -1823,9 +1817,9 @@ def keypress(self, event): self._cursorpos += 1 else: pass # do not allow abcdef... - + self.set_text(newt, redraw=True) - + r = self._get_text_right() self.cursor.set_xdata([r,r]) self.redraw() From 8ed729be8e3ad297d519a62362e3834887335042 Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Tue, 4 Jun 2013 09:41:26 -0700 Subject: [PATCH 05/24] inherit/use AxesWidget --- lib/matplotlib/widgets.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 9ceb12e27ba7..d8dfedd15170 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1693,27 +1693,24 @@ def __init__(self, ax, s='', horizontalalignment='left', fontsize : int Font size for text box """ + AxesWidget.__init__(self, ax) self.value = float(s) - self.canvas = ax.figure.canvas - self.text = ax.text(0.025, 0.2, s, + self.text = self.ax.text(0.025, 0.2, s, fontsize=fontsize, verticalalignment='baseline', horizontalalignment=horizontalalignment, - transform=ax.transAxes) - self.ax = ax - ax.set_yticks([]) - ax.set_xticks([]) + transform=self.ax.transAxes) + self.ax.set_yticks([]) + self.ax.set_xticks([]) - ax.set_navigate(False) + self.ax.set_navigate(False) self.canvas.draw() self._cursor = None self._cursorpos = len(self.text.get_text()) - self.active = False - self.redraw() self._cid = None From 19fd1608232309b112c3c058299abc4cca8a8323 Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Tue, 4 Jun 2013 09:44:08 -0700 Subject: [PATCH 06/24] cleanup __init__ --- lib/matplotlib/widgets.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index d8dfedd15170..ead36d37f02c 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1694,6 +1694,9 @@ def __init__(self, ax, s='', horizontalalignment='left', Font size for text box """ AxesWidget.__init__(self, ax) + self.ax.set_navigate(False) + self.ax.set_yticks([]) + self.ax.set_xticks([]) self.value = float(s) @@ -1702,21 +1705,15 @@ def __init__(self, ax, s='', horizontalalignment='left', verticalalignment='baseline', horizontalalignment=horizontalalignment, transform=self.ax.transAxes) - self.ax.set_yticks([]) - self.ax.set_xticks([]) - - self.ax.set_navigate(False) - self.canvas.draw() + self.enter_callback = enter_callback + self._cid = None self._cursor = None self._cursorpos = len(self.text.get_text()) - - self.redraw() - self._cid = None - - self.enter_callback = enter_callback + self.old_callbacks = {} - self.canvas.mpl_connect('button_press_event',self._mouse_activate) + self.redraw() + self.connect_event('button_press_event', self.mouse_activate) @property def cursor(self): From dc4b04c9486985c9301fb3894bdd8938414c2d26 Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Tue, 4 Jun 2013 09:45:51 -0700 Subject: [PATCH 07/24] actually inherit from AxesWidget --- lib/matplotlib/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index ead36d37f02c..6d6f6d7e8c95 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1663,7 +1663,7 @@ def onmove(self, event): self.canvas.draw_idle() -class TextBox(Widget): +class TextBox(AxesWidget): def __init__(self, ax, s='', horizontalalignment='left', enter_callback=None, fontsize=12): """ From 32d9bb068a122fe71ada501b0c40f907d717b2c9 Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Tue, 4 Jun 2013 09:47:08 -0700 Subject: [PATCH 08/24] rely on default Text properties --- lib/matplotlib/widgets.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 6d6f6d7e8c95..6d19419ffe37 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1664,8 +1664,7 @@ def onmove(self, event): class TextBox(AxesWidget): - def __init__(self, ax, s='', horizontalalignment='left', - enter_callback=None, fontsize=12): + def __init__(self, ax, s='', enter_callback=None, **text_kwargs): """ Editable text box @@ -1699,13 +1698,9 @@ def __init__(self, ax, s='', horizontalalignment='left', self.ax.set_xticks([]) self.value = float(s) + self.text = self.ax.text(0.025, 0.2, s, transform=self.ax.transAxes, + **text_kwargs) - self.text = self.ax.text(0.025, 0.2, s, - fontsize=fontsize, - verticalalignment='baseline', - horizontalalignment=horizontalalignment, - transform=self.ax.transAxes) - self.enter_callback = enter_callback self._cid = None self._cursor = None From dc8ef76045af8fcfec736345976c881b491a8595 Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Tue, 4 Jun 2013 09:47:27 -0700 Subject: [PATCH 09/24] docs --- lib/matplotlib/widgets.py | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 6d19419ffe37..8a0161129a76 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1671,26 +1671,29 @@ def __init__(self, ax, s='', enter_callback=None, **text_kwargs): Creates a mouse-click callback such that clicking on the text box will activate the cursor. - *WARNING* Activating a textbox will permanently disable all other - key-press bindings! They'll be stored in TextBox.old_callbacks and - restored when TextBox.deactivate is called. + *WARNING* Activating a textbox will remove all other key-press + bindings! They'll be stored in FloatTextBox.old_callbacks and restored + when FloatTextBox.end_text_entry() is called. - The default widget assumes only numerical (float) data and will not + The default widget assumes only numerical data and will not allow text entry besides numerical characters and ('e','-','.') Parameters ---------- - ax : axis - Parent axis to turn into text box - s : str - Initial string contents of text box - horizontalalignment : left | center | right - Passed to self.text - enter_callback : function - A function of one argument that will be called with TextBox.value - passed in as the only argument when enter is pressed - fontsize : int - Font size for text box + *ax* : :class:`matplotlib.axes.Axes` + The parent axes for the widget + + *s* : str + The initial text of the FloatTextBox. Should be able to be coerced + to a float. + + *enter_callback* : function + A function of one argument that will be called with + FloatTextBox.value passed in as the only argument when enter is + pressed + + *text_kwargs* : + Additional keywork arguments are passed on to self.ax.text() """ AxesWidget.__init__(self, ax) self.ax.set_navigate(False) From 003c93189318d6d67c50d17a2399963195891273 Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Tue, 4 Jun 2013 09:48:33 -0700 Subject: [PATCH 10/24] ignore callbacks when active=False --- lib/matplotlib/widgets.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 8a0161129a76..9696ae9c6b08 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1761,13 +1761,13 @@ def keypress(self, event): """ Parse a keypress - only allow #'s! """ - #print "event.key: '%s'" % event.key - #if event.key is not None and len(event.key)>1: return - + if self.ignore(event): + return + newt = t = self.text.get_text() if event.key == 'backspace': # simulate backspace if self._cursorpos == 0: return - if len(t) > 0: + if len(t) > 0: newt = t[:self._cursorpos-1] + t[self._cursorpos:] if self._cursorpos > 0: self._cursorpos -= 1 @@ -1798,7 +1798,7 @@ def keypress(self, event): if self._cursorpos == 0: newt = event.key + t self._cursorpos += 1 - elif (t[self._cursorpos-1]=='e' and not + elif (t[self._cursorpos-1]=='e' and not (len(t) > self._cursorpos+1 and t[self._cursorpos+1] == '-')): newt = t[:self._cursorpos] + event.key + t[self._cursorpos:] self._cursorpos += 1 From e77e5e51f20849d06cc8c339743aed1b92f43c33 Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Tue, 4 Jun 2013 09:49:15 -0700 Subject: [PATCH 11/24] fix spelling of _get_text_right --- lib/matplotlib/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 9696ae9c6b08..41a71ef7710e 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1718,7 +1718,7 @@ def cursor(self): # Macos has issues with render objects. Lazily generating the cursor # solve some of the problems associated if self._cursor is None: - r = self.get_text_right() # needs a renderer + r = self._get_text_right() # needs a renderer self._cursor, = self.ax.plot([r,r], [0.2, 0.8], transform=self.ax.transAxes) self._cursor.set_visible(False) From 952e122f2ea03e192d83f8c24d41f21889f00160 Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Tue, 4 Jun 2013 09:50:46 -0700 Subject: [PATCH 12/24] fix spelling mouse_activate, ignore when apropriate --- lib/matplotlib/widgets.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 41a71ef7710e..e77857627d3a 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1731,10 +1731,12 @@ def redraw(self): self.canvas.draw() def _mouse_activate(self, event): + if self.ignore(event): + return if self.ax == event.inaxes: - self.activate() - else: - self.deactivate() + self.begin_text_entry() + else: + self.end_text_entry() def begin_text_entry(self): keypress_cbs = self.canvas.callbacks.callbacks['key_press_event'] From e307788de6b9a626f1d3d6b24adfaba1c737d77b Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Tue, 4 Jun 2013 09:52:53 -0700 Subject: [PATCH 13/24] remove unused vars --- lib/matplotlib/widgets.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index e77857627d3a..07a651ca6b90 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1834,14 +1834,12 @@ def set_text(self, text, redraw=False): self.redraw() def _get_text_right(self): - l,b,w,h = self.text.get_window_extent().bounds - r = l+w+2 - t = b+h - s = self.text.get_text() - # adjust cursor position for trailing space - numtrail = len(s)-len(s.rstrip()) - en = self.ax.get_renderer_cache().points_to_pixels(self.text.get_fontsize())/2. + bbox = self.text.get_window_extent() + l,b,w,h = bbox.bounds + + renderer = self.ax.get_renderer_cache() + en = renderer.points_to_pixels(self.text.get_fontsize()) / 2. r = l + self._cursorpos*np.ceil(en) - r,t = self.ax.transAxes.inverted().transform((r,t)) + r,t = self.ax.transAxes.inverted().transform((r,b+h)) return r From a83b0961e6f035325f95e323d1d5cb28fdc32eba Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Tue, 4 Jun 2013 09:53:34 -0700 Subject: [PATCH 14/24] tab todo --- lib/matplotlib/widgets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 07a651ca6b90..a687a1795706 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1767,6 +1767,7 @@ def keypress(self, event): return newt = t = self.text.get_text() + # TODO tab raises exceptions if event.key == 'backspace': # simulate backspace if self._cursorpos == 0: return if len(t) > 0: From c11973c10f91f06763ec5cd00709702bf5ac4dfa Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Tue, 4 Jun 2013 09:54:15 -0700 Subject: [PATCH 15/24] spell _mouse_activate correctly --- lib/matplotlib/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index a687a1795706..8add65415ad4 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1711,7 +1711,7 @@ def __init__(self, ax, s='', enter_callback=None, **text_kwargs): self.old_callbacks = {} self.redraw() - self.connect_event('button_press_event', self.mouse_activate) + self.connect_event('button_press_event', self._mouse_activate) @property def cursor(self): From 5239d7e162da232429cf49d68bea6177599a52dc Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Wed, 5 Jun 2013 13:34:07 -0700 Subject: [PATCH 16/24] spelling and formatting --- lib/matplotlib/widgets.py | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 8add65415ad4..1317c856f354 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1672,8 +1672,8 @@ def __init__(self, ax, s='', enter_callback=None, **text_kwargs): activate the cursor. *WARNING* Activating a textbox will remove all other key-press - bindings! They'll be stored in FloatTextBox.old_callbacks and restored - when FloatTextBox.end_text_entry() is called. + bindings! They'll be stored in TextBox.old_callbacks and restored + when TextBox.end_text_entry() is called. The default widget assumes only numerical data and will not allow text entry besides numerical characters and ('e','-','.') @@ -1684,15 +1684,15 @@ def __init__(self, ax, s='', enter_callback=None, **text_kwargs): The parent axes for the widget *s* : str - The initial text of the FloatTextBox. Should be able to be coerced + The initial text of the TextBox. Should be able to be coerced to a float. *enter_callback* : function - A function of one argument that will be called with - FloatTextBox.value passed in as the only argument when enter is + A function of one argument that will be called with + TextBox.value passed in as the only argument when enter is pressed - *text_kwargs* : + *text_kwargs* : Additional keywork arguments are passed on to self.ax.text() """ AxesWidget.__init__(self, ax) @@ -1701,8 +1701,8 @@ def __init__(self, ax, s='', enter_callback=None, **text_kwargs): self.ax.set_xticks([]) self.value = float(s) - self.text = self.ax.text(0.025, 0.2, s, transform=self.ax.transAxes, - **text_kwargs) + self.text = self.ax.text(0.025, 0.2, s, + transform=self.ax.transAxes, **text_kwargs) self.enter_callback = enter_callback self._cid = None @@ -1719,8 +1719,10 @@ def cursor(self): # solve some of the problems associated if self._cursor is None: r = self._get_text_right() # needs a renderer - self._cursor, = self.ax.plot([r,r], [0.2, 0.8], - transform=self.ax.transAxes) + self._cursor = self.ax.plot([r, r], + [0.2, 0.8], + transform=self.ax.transAxes, + )[0] self._cursor.set_visible(False) return self._cursor @@ -1745,7 +1747,8 @@ def begin_text_entry(self): for k in keypress_cbs.keys(): self.old_callbacks[k] = keypress_cbs.pop(k) - self._cid = self.canvas.mpl_connect('key_press_event', self.keypress) + self._cid = self.canvas.mpl_connect('key_press_event', + self.keypress) self.cursor.set_visible(True) self.redraw() @@ -1816,7 +1819,7 @@ def keypress(self, event): self.set_text(newt, redraw=True) r = self._get_text_right() - self.cursor.set_xdata([r,r]) + self.cursor.set_xdata([r, r]) self.redraw() def set_text(self, text, redraw=False): @@ -1836,11 +1839,11 @@ def set_text(self, text, redraw=False): def _get_text_right(self): bbox = self.text.get_window_extent() - l,b,w,h = bbox.bounds + l, b, w, h = bbox.bounds renderer = self.ax.get_renderer_cache() en = renderer.points_to_pixels(self.text.get_fontsize()) / 2. - r = l + self._cursorpos*np.ceil(en) - r,t = self.ax.transAxes.inverted().transform((r,b+h)) + r = l + self._cursorpos * np.ceil(en) + r, t = self.ax.transAxes.inverted().transform((r, b + h)) return r From f523505323ac66cf93d1d07963c20d12e5734540 Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Wed, 5 Jun 2013 13:35:35 -0700 Subject: [PATCH 17/24] drop float-specific formatting rules --- lib/matplotlib/widgets.py | 66 ++++++++++++++------------------------- 1 file changed, 24 insertions(+), 42 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 1317c856f354..f3a0b46579be 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1770,54 +1770,36 @@ def keypress(self, event): return newt = t = self.text.get_text() - # TODO tab raises exceptions - if event.key == 'backspace': # simulate backspace - if self._cursorpos == 0: return - if len(t) > 0: - newt = t[:self._cursorpos-1] + t[self._cursorpos:] + assert self._cursorpos >= 0 + assert self._cursorpos <= len(t) + # TODO numeric keypad + + if not isinstance(event.key, str): + # event.key may be None + return + elif event.key in '0123456789.eE-+': + newt = t[:self._cursorpos] + event.key + t[self._cursorpos:] + self._cursorpos += 1 + elif event.key == 'backspace': # simulate backspace if self._cursorpos > 0: + newt = t[:self._cursorpos - 1] + t[self._cursorpos:] self._cursorpos -= 1 - elif event.key == 'left' and self._cursorpos > 0: - self._cursorpos -= 1 - elif event.key == 'right' and self._cursorpos < len(t): - self._cursorpos += 1 + elif event.key == 'delete': # forward delete + newt = t[:self._cursorpos] + t[self._cursorpos + 1:] + elif event.key == 'left': + if self._cursorpos > 0: + self._cursorpos -= 1 + elif event.key == 'right': + if self._cursorpos < len(t): + self._cursorpos += 1 elif event.key == 'enter': if self.enter_callback is not None: - try: - self.enter_callback(self.value) - self.end_text_entry() - except Exception as ex: - print(ex) - - elif len(event.key) > 1: - # ignore... - pass - elif event.key in '0123456789': - newt = t[:self._cursorpos] + event.key + t[self._cursorpos:] - self._cursorpos += 1 - elif event.key == 'e': - if 'e' not in t and '.' not in t[self._cursorpos:] and self._cursorpos != 0: - newt = t[:self._cursorpos] + event.key + t[self._cursorpos:] - self._cursorpos += 1 - elif event.key == '-': - # only allow negative at front or after e - if self._cursorpos == 0: - newt = event.key + t - self._cursorpos += 1 - elif (t[self._cursorpos-1]=='e' and not - (len(t) > self._cursorpos+1 and t[self._cursorpos+1] == '-')): - newt = t[:self._cursorpos] + event.key + t[self._cursorpos:] - self._cursorpos += 1 - elif event.key == '.': - # do nothing if extra decimals... - if '.' not in t: - newt = t[:self._cursorpos] + event.key + t[self._cursorpos:] - self._cursorpos += 1 + self.enter_callback(self.value) + self.end_text_entry() else: - pass # do not allow abcdef... - - self.set_text(newt, redraw=True) + return # do not allow abcdef... + self.set_text(newt) r = self._get_text_right() self.cursor.set_xdata([r, r]) self.redraw() From 42b8489beac961cafa2a10a4165d7a7d4a90b00f Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Wed, 5 Jun 2013 13:36:28 -0700 Subject: [PATCH 18/24] remove redraw option from set_text --- lib/matplotlib/widgets.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index f3a0b46579be..e6af00fb9fae 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1804,7 +1804,7 @@ def keypress(self, event): self.cursor.set_xdata([r, r]) self.redraw() - def set_text(self, text, redraw=False): + def set_text(self, text): try: # only try to update if there's a real value if (not(text.strip() in ('-.','.','-','')) @@ -1815,10 +1815,6 @@ def set_text(self, text, redraw=False): except ValueError: pass - if redraw: - self.text.draw(self.text._renderer) - self.redraw() - def _get_text_right(self): bbox = self.text.get_window_extent() l, b, w, h = bbox.bounds From d80083b968bc2d554009ae7d39ec242d8c108421 Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Wed, 5 Jun 2013 13:37:20 -0700 Subject: [PATCH 19/24] drop more float-specific logic --- lib/matplotlib/widgets.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index e6af00fb9fae..fe357972d25f 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1807,13 +1807,11 @@ def keypress(self, event): def set_text(self, text): try: # only try to update if there's a real value - if (not(text.strip() in ('-.','.','-','')) - and not text[-1] in ('e','-')): - self.value = float(text) - # but always change the text - self.text.set_text(text) + self.value = float(text) except ValueError: pass + # but always change the text + self.text.set_text(text) def _get_text_right(self): bbox = self.text.get_window_extent() From 4b963a58d8fc9c5b80c7e1c672fa5ba0873cf768 Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Wed, 5 Jun 2013 15:31:50 -0700 Subject: [PATCH 20/24] TextBox produces str values --- lib/matplotlib/widgets.py | 74 ++++++++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 29 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index fe357972d25f..32af1376b419 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1664,7 +1664,8 @@ def onmove(self, event): class TextBox(AxesWidget): - def __init__(self, ax, s='', enter_callback=None, **text_kwargs): + def __init__(self, ax, s='', allowed_chars=None, type=str, + enter_callback=None, **text_kwargs): """ Editable text box @@ -1684,8 +1685,15 @@ def __init__(self, ax, s='', enter_callback=None, **text_kwargs): The parent axes for the widget *s* : str - The initial text of the TextBox. Should be able to be coerced - to a float. + The initial text of the TextBox. + + *allowed_chars* : seq + TextBox will only respond if event.key in allowed_chars. Defaults + to None, which accepts anything. + + *type* : type + Construct self.value using this type. self.value is only updated + if self.type() succeeds. *enter_callback* : function A function of one argument that will be called with @@ -1700,7 +1708,9 @@ def __init__(self, ax, s='', enter_callback=None, **text_kwargs): self.ax.set_yticks([]) self.ax.set_xticks([]) - self.value = float(s) + self.type = type + self.allowed_chars = allowed_chars + self.value = self.type(s) self.text = self.ax.text(0.025, 0.2, s, transform=self.ax.transAxes, **text_kwargs) @@ -1710,26 +1720,23 @@ def __init__(self, ax, s='', enter_callback=None, **text_kwargs): self._cursorpos = len(self.text.get_text()) self.old_callbacks = {} - self.redraw() self.connect_event('button_press_event', self._mouse_activate) + self.redraw_callbacks = [] + self.redraw() @property def cursor(self): - # Macos has issues with render objects. Lazily generating the cursor - # solve some of the problems associated + # macosx does not provide render objects until the first fram is done. + # Lazily generating the cursor avoids issues if self._cursor is None: - r = self._get_text_right() # needs a renderer - self._cursor = self.ax.plot([r, r], - [0.2, 0.8], - transform=self.ax.transAxes, - )[0] + x, y = self._get_cursor_endpoints() # needs a renderer + self._cursor, = self.ax.plot(x, y, transform=self.ax.transAxes) self._cursor.set_visible(False) return self._cursor def redraw(self): - # blitting doesn't clear old text - #self.ax.redraw_in_frame() - #self.canvas.blit(self.ax.bbox) + for f in self.redraw_callbacks: + f() self.canvas.draw() def _mouse_activate(self, event): @@ -1777,9 +1784,6 @@ def keypress(self, event): if not isinstance(event.key, str): # event.key may be None return - elif event.key in '0123456789.eE-+': - newt = t[:self._cursorpos] + event.key + t[self._cursorpos:] - self._cursorpos += 1 elif event.key == 'backspace': # simulate backspace if self._cursorpos > 0: newt = t[:self._cursorpos - 1] + t[self._cursorpos:] @@ -1796,30 +1800,42 @@ def keypress(self, event): if self.enter_callback is not None: self.enter_callback(self.value) self.end_text_entry() + elif self.allowed_chars is None: + newt = t[:self._cursorpos] + event.key + t[self._cursorpos:] + self._cursorpos += len(event.key) + elif event.key in self.allowed_chars: + newt = t[:self._cursorpos] + event.key + t[self._cursorpos:] + self._cursorpos += 1 else: return # do not allow abcdef... self.set_text(newt) - r = self._get_text_right() - self.cursor.set_xdata([r, r]) + x, y = self._get_cursor_endpoints() + self.cursor.set_xdata(x) self.redraw() def set_text(self, text): try: # only try to update if there's a real value - self.value = float(text) + self.value = self.type(text) except ValueError: pass # but always change the text self.text.set_text(text) - def _get_text_right(self): + def _get_cursor_endpoints(self): + # to get cursor position + # change text to chars left of the cursor + text = self.text.get_text() + self.text.set_text(text[:self._cursorpos]) bbox = self.text.get_window_extent() - l, b, w, h = bbox.bounds - - renderer = self.ax.get_renderer_cache() - en = renderer.points_to_pixels(self.text.get_fontsize()) / 2. + l, b, w, h = bbox.bounds # in pixels + r = l + w + # now restore correct text + self.text.set_text(text) - r = l + self._cursorpos * np.ceil(en) - r, t = self.ax.transAxes.inverted().transform((r, b + h)) - return r + # cursor line in data coordinates + bx, by = self.ax.transAxes.inverted().transform((r, b)) + tx, ty = self.ax.transAxes.inverted().transform((r, b + h)) + dy = 0.5 * (ty - by) + return [bx, tx], [by - dy, ty + dy] From aba387a0adad73d1e74aa068f495431c25f49951 Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Wed, 5 Jun 2013 15:32:11 -0700 Subject: [PATCH 21/24] convenience function for producing TextBoxes with float values --- lib/matplotlib/widgets.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 32af1376b419..f277feb6fd53 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1839,3 +1839,12 @@ def _get_cursor_endpoints(self): tx, ty = self.ax.transAxes.inverted().transform((r, b + h)) dy = 0.5 * (ty - by) return [bx, tx], [by - dy, ty + dy] + + +def TextBoxFloat(*args, **kwargs): + """ + TextBox that produces float values + """ + kwargs['allowed_chars'] = '0123456789.eE-+' + kwargs['type'] = float + return TextBox(*args, **kwargs) From a1e0207e478589208bad6a0e622e7f0262c8a0d9 Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Sun, 9 Jun 2013 17:27:31 -0700 Subject: [PATCH 22/24] use on_changed() convention. use drawon, too --- lib/matplotlib/widgets.py | 48 +++++++++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index f277feb6fd53..dd4190c705c4 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1702,6 +1702,8 @@ def __init__(self, ax, s='', allowed_chars=None, type=str, *text_kwargs* : Additional keywork arguments are passed on to self.ax.text() + + Call :meth:`on_onchanged` to connect to TextBox updates """ AxesWidget.__init__(self, ax) self.ax.set_navigate(False) @@ -1720,9 +1722,28 @@ def __init__(self, ax, s='', allowed_chars=None, type=str, self._cursorpos = len(self.text.get_text()) self.old_callbacks = {} + self.cnt = 0 + self.observers = {} + self.connect_event('button_press_event', self._mouse_activate) - self.redraw_callbacks = [] - self.redraw() + + def on_changed(self, func): + """ + When the textbox changes self.value, call *func* with the new value. + + A connection id is returned with can be used to disconnect. + """ + cid = self.cnt + self.observers[cid] = func + self.cnt += 1 + return cid + + def disconnect(self, cid): + """remove the observer with connection id *cid*""" + try: + del self.observers[cid] + except KeyError: + pass @property def cursor(self): @@ -1734,13 +1755,8 @@ def cursor(self): self._cursor.set_visible(False) return self._cursor - def redraw(self): - for f in self.redraw_callbacks: - f() - self.canvas.draw() - def _mouse_activate(self, event): - if self.ignore(event): + if self.ignore(event) or not self.eventson: return if self.ax == event.inaxes: self.begin_text_entry() @@ -1757,7 +1773,8 @@ def begin_text_entry(self): self._cid = self.canvas.mpl_connect('key_press_event', self.keypress) self.cursor.set_visible(True) - self.redraw() + if self.drawon: + self.canvas.draw() def end_text_entry(self): keypress_cbs = self.canvas.callbacks.callbacks['key_press_event'] @@ -1767,13 +1784,14 @@ def end_text_entry(self): keypress_cbs[k] = self.old_callbacks.pop(k) self.cursor.set_visible(False) - self.redraw() + if self.drawon: + self.canvas.draw() def keypress(self, event): """ Parse a keypress - only allow #'s! """ - if self.ignore(event): + if self.ignore(event) or not self.eventson: return newt = t = self.text.get_text() @@ -1812,16 +1830,22 @@ def keypress(self, event): self.set_text(newt) x, y = self._get_cursor_endpoints() self.cursor.set_xdata(x) - self.redraw() + if self.drawon: + self.canvas.draw() def set_text(self, text): + success = False try: # only try to update if there's a real value self.value = self.type(text) + success = True except ValueError: pass # but always change the text self.text.set_text(text) + if success and self.eventson: + for func in self.observers.itervalues(): + func(self.value) def _get_cursor_endpoints(self): # to get cursor position From a81e0b80ea7c955a4f75f9379eb0b210e04d7b4e Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Sun, 9 Jun 2013 17:27:41 -0700 Subject: [PATCH 23/24] TextBoxFloat is a class --- lib/matplotlib/widgets.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index dd4190c705c4..3ea9e91844c0 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1865,10 +1865,8 @@ def _get_cursor_endpoints(self): return [bx, tx], [by - dy, ty + dy] -def TextBoxFloat(*args, **kwargs): - """ - TextBox that produces float values - """ - kwargs['allowed_chars'] = '0123456789.eE-+' - kwargs['type'] = float - return TextBox(*args, **kwargs) +class TextBoxFloat(TextBox): + def __init__(self, *args, **kwargs): + kwargs['allowed_chars'] = '0123456789.eE-+' + kwargs['type'] = float + TextBox.__init__(self, *args, **kwargs) From 8f753d013178a2fb5696762f268ef4378ef0ae8b Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Sun, 9 Jun 2013 18:04:23 -0700 Subject: [PATCH 24/24] spelling --- lib/matplotlib/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 3ea9e91844c0..dced7ffdb21e 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1747,7 +1747,7 @@ def disconnect(self, cid): @property def cursor(self): - # macosx does not provide render objects until the first fram is done. + # macosx does not provide render objects until the first frame is done. # Lazily generating the cursor avoids issues if self._cursor is None: x, y = self._get_cursor_endpoints() # needs a renderer