Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 5f553bc

Browse files
authored
Merge pull request #6988 from fariza/text-box-widget
ENH: Text box widget, take over of PR5375
2 parents 09a78d5 + bde1040 commit 5f553bc

File tree

5 files changed

+345
-2
lines changed

5 files changed

+345
-2
lines changed

CHANGELOG

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
12
2015-11-16 Levels passed to contour(f) and tricontour(f) must be in increasing
23
order.
34

5+
2015-10-21 Added TextBox widget
6+
7+
48
2015-10-21 Added get_ticks_direction()
59

610
2015-02-27 Added the rcParam 'image.composite_image' to permit users

doc/users/whats_new.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,13 @@ Some parameters have been added, others have been improved.
244244
Widgets
245245
-------
246246

247+
Added TextBox Widget
248+
````````````````````
249+
250+
Added a widget that allows text entry by reading key events when it is active.
251+
Text caret in text box is visible when it is active, can be moved using arrow keys and mouse
252+
253+
247254
Active state of Selectors
248255
`````````````````````````
249256

examples/widgets/textbox.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
2+
import numpy as np
3+
import matplotlib.pyplot as plt
4+
from matplotlib.widgets import TextBox
5+
fig, ax = plt.subplots()
6+
plt.subplots_adjust(bottom=0.2)
7+
t = np.arange(-2.0, 2.0, 0.001)
8+
s = t ** 2
9+
initial_text = "t ** 2"
10+
l, = plt.plot(t, s, lw=2)
11+
12+
13+
def submit(text):
14+
ydata = eval(text)
15+
l.set_ydata(ydata)
16+
ax.set_ylim(np.min(ydata), np.max(ydata))
17+
plt.draw()
18+
19+
axbox = plt.axes([0.1, 0.05, 0.8, 0.075])
20+
text_box = TextBox(axbox, 'Evaluate', initial=initial_text)
21+
text_box.on_submit(submit)
22+
23+
plt.show()

lib/matplotlib/backend_bases.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2606,14 +2606,14 @@ def _get_uniform_gridstate(ticks):
26062606
# keys in list 'all' enables all axes (default key 'a'),
26072607
# otherwise if key is a number only enable this particular axes
26082608
# if it was the axes, where the event was raised
2609-
if not (event.key in all):
2609+
if not (event.key in all_keys):
26102610
n = int(event.key) - 1
26112611
for i, a in enumerate(canvas.figure.get_axes()):
26122612
# consider axes, in which the event was raised
26132613
# FIXME: Why only this axes?
26142614
if event.x is not None and event.y is not None \
26152615
and a.in_axes(event):
2616-
if event.key in all:
2616+
if event.key in all_keys:
26172617
a.set_navigate(True)
26182618
else:
26192619
a.set_navigate(i == n)

lib/matplotlib/widgets.py

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from six.moves import zip
1818

1919
import numpy as np
20+
from matplotlib import rcParams
2021

2122
from .mlab import dist
2223
from .patches import Circle, Rectangle, Ellipse
@@ -628,6 +629,314 @@ def disconnect(self, cid):
628629
pass
629630

630631

632+
class TextBox(AxesWidget):
633+
"""
634+
A GUI neutral text input box.
635+
636+
For the text box to remain responsive you must keep a reference to it.
637+
638+
The following attributes are accessible:
639+
640+
*ax*
641+
The :class:`matplotlib.axes.Axes` the button renders into.
642+
643+
*label*
644+
A :class:`matplotlib.text.Text` instance.
645+
646+
*color*
647+
The color of the text box when not hovering.
648+
649+
*hovercolor*
650+
The color of the text box when hovering.
651+
652+
Call :meth:`on_text_change` to be updated whenever the text changes.
653+
654+
Call :meth:`on_submit` to be updated whenever the user hits enter or
655+
leaves the text entry field.
656+
"""
657+
658+
def __init__(self, ax, label, initial='',
659+
color='.95', hovercolor='1', label_pad=.01):
660+
"""
661+
Parameters
662+
----------
663+
ax : matplotlib.axes.Axes
664+
The :class:`matplotlib.axes.Axes` instance the button
665+
will be placed into.
666+
667+
label : str
668+
Label for this text box. Accepts string.
669+
670+
initial : str
671+
Initial value in the text box
672+
673+
color : color
674+
The color of the box
675+
676+
hovercolor : color
677+
The color of the box when the mouse is over it
678+
679+
label_pad : float
680+
the distance between the label and the right side of the textbox
681+
"""
682+
AxesWidget.__init__(self, ax)
683+
684+
self.DIST_FROM_LEFT = .05
685+
686+
self.params_to_disable = []
687+
for key in rcParams.keys():
688+
if u'keymap' in key:
689+
self.params_to_disable += [key]
690+
691+
self.text = initial
692+
self.label = ax.text(-label_pad, 0.5, label,
693+
verticalalignment='center',
694+
horizontalalignment='right',
695+
transform=ax.transAxes)
696+
self.text_disp = self._make_text_disp(self.text)
697+
698+
self.cnt = 0
699+
self.change_observers = {}
700+
self.submit_observers = {}
701+
702+
# If these lines are removed, the cursor won't appear the first
703+
# time the box is clicked:
704+
self.ax.set_xlim(0, 1)
705+
self.ax.set_ylim(0, 1)
706+
707+
self.cursor_index = 0
708+
709+
# Because this is initialized, _render_cursor
710+
# can assume that cursor exists.
711+
self.cursor = self.ax.vlines(0, 0, 0)
712+
self.cursor.set_visible(False)
713+
714+
self.connect_event('button_press_event', self._click)
715+
self.connect_event('button_release_event', self._release)
716+
self.connect_event('motion_notify_event', self._motion)
717+
self.connect_event('key_press_event', self._keypress)
718+
self.connect_event('resize_event', self._resize)
719+
ax.set_navigate(False)
720+
ax.set_facecolor(color)
721+
ax.set_xticks([])
722+
ax.set_yticks([])
723+
self.color = color
724+
self.hovercolor = hovercolor
725+
726+
self._lastcolor = color
727+
728+
self.capturekeystrokes = False
729+
730+
def _make_text_disp(self, string):
731+
return self.ax.text(self.DIST_FROM_LEFT, 0.5, string,
732+
verticalalignment='center',
733+
horizontalalignment='left',
734+
transform=self.ax.transAxes)
735+
736+
def _rendercursor(self):
737+
# this is a hack to figure out where the cursor should go.
738+
# we draw the text up to where the cursor should go, measure
739+
# and save its dimensions, draw the real text, then put the cursor
740+
# at the saved dimensions
741+
742+
widthtext = self.text[:self.cursor_index]
743+
no_text = False
744+
if(widthtext == "" or widthtext == " " or widthtext == " "):
745+
no_text = widthtext == ""
746+
widthtext = ","
747+
748+
wt_disp = self._make_text_disp(widthtext)
749+
750+
self.ax.figure.canvas.draw()
751+
bb = wt_disp.get_window_extent()
752+
inv = self.ax.transData.inverted()
753+
bb = inv.transform(bb)
754+
wt_disp.set_visible(False)
755+
if no_text:
756+
bb[1, 0] = bb[0, 0]
757+
# hack done
758+
self.cursor.set_visible(False)
759+
760+
self.cursor = self.ax.vlines(bb[1, 0], bb[0, 1], bb[1, 1])
761+
self.ax.figure.canvas.draw()
762+
763+
def _notify_submit_observers(self):
764+
for cid, func in six.iteritems(self.submit_observers):
765+
func(self.text)
766+
767+
def _release(self, event):
768+
if self.ignore(event):
769+
return
770+
if event.canvas.mouse_grabber != self.ax:
771+
return
772+
event.canvas.release_mouse(self.ax)
773+
774+
def _keypress(self, event):
775+
if self.ignore(event):
776+
return
777+
if self.capturekeystrokes:
778+
key = event.key
779+
780+
if(len(key) == 1):
781+
self.text = (self.text[:self.cursor_index] + key +
782+
self.text[self.cursor_index:])
783+
self.cursor_index += 1
784+
elif key == "right":
785+
if self.cursor_index != len(self.text):
786+
self.cursor_index += 1
787+
elif key == "left":
788+
if self.cursor_index != 0:
789+
self.cursor_index -= 1
790+
elif key == "home":
791+
self.cursor_index = 0
792+
elif key == "end":
793+
self.cursor_index = len(self.text)
794+
elif(key == "backspace"):
795+
if self.cursor_index != 0:
796+
self.text = (self.text[:self.cursor_index - 1] +
797+
self.text[self.cursor_index:])
798+
self.cursor_index -= 1
799+
elif(key == "delete"):
800+
if self.cursor_index != len(self.text):
801+
self.text = (self.text[:self.cursor_index] +
802+
self.text[self.cursor_index + 1:])
803+
804+
self.text_disp.remove()
805+
self.text_disp = self._make_text_disp(self.text)
806+
self._rendercursor()
807+
self._notify_change_observers()
808+
if key == "enter":
809+
self._notify_submit_observers()
810+
811+
def set_val(self, val):
812+
newval = str(val)
813+
if self.text == newval:
814+
return
815+
self.text = newval
816+
self.text_disp.remove()
817+
self.text_disp = self._make_text_disp(self.text)
818+
self._rendercursor()
819+
self._notify_change_observers()
820+
self._notify_submit_observers()
821+
822+
def _notify_change_observers(self):
823+
for cid, func in six.iteritems(self.change_observers):
824+
func(self.text)
825+
826+
def begin_typing(self, x):
827+
self.capturekeystrokes = True
828+
# disable command keys so that the user can type without
829+
# command keys causing figure to be saved, etc
830+
self.reset_params = {}
831+
for key in self.params_to_disable:
832+
self.reset_params[key] = rcParams[key]
833+
rcParams[key] = []
834+
835+
def stop_typing(self):
836+
notifysubmit = False
837+
# because _notify_submit_users might throw an error in the
838+
# user's code, we only want to call it once we've already done
839+
# our cleanup.
840+
if self.capturekeystrokes:
841+
# since the user is no longer typing,
842+
# reactivate the standard command keys
843+
for key in self.params_to_disable:
844+
rcParams[key] = self.reset_params[key]
845+
notifysubmit = True
846+
self.capturekeystrokes = False
847+
self.cursor.set_visible(False)
848+
self.ax.figure.canvas.draw()
849+
if notifysubmit:
850+
self._notify_submit_observers()
851+
852+
def position_cursor(self, x):
853+
# now, we have to figure out where the cursor goes.
854+
# approximate it based on assuming all characters the same length
855+
if len(self.text) == 0:
856+
self.cursor_index = 0
857+
else:
858+
bb = self.text_disp.get_window_extent()
859+
860+
trans = self.ax.transData
861+
inv = self.ax.transData.inverted()
862+
bb = trans.transform(inv.transform(bb))
863+
864+
text_start = bb[0, 0]
865+
text_end = bb[1, 0]
866+
867+
ratio = (x - text_start) / (text_end - text_start)
868+
869+
if ratio < 0:
870+
ratio = 0
871+
if ratio > 1:
872+
ratio = 1
873+
874+
self.cursor_index = int(len(self.text) * ratio)
875+
876+
self._rendercursor()
877+
878+
def _click(self, event):
879+
if self.ignore(event):
880+
return
881+
if event.inaxes != self.ax:
882+
self.capturekeystrokes = False
883+
self.stop_typing()
884+
return
885+
if not self.eventson:
886+
return
887+
if event.canvas.mouse_grabber != self.ax:
888+
event.canvas.grab_mouse(self.ax)
889+
if not(self.capturekeystrokes):
890+
self.begin_typing(event.x)
891+
self.position_cursor(event.x)
892+
893+
def _resize(self, event):
894+
self.stop_typing()
895+
896+
def _motion(self, event):
897+
if self.ignore(event):
898+
return
899+
if event.inaxes == self.ax:
900+
c = self.hovercolor
901+
else:
902+
c = self.color
903+
if c != self._lastcolor:
904+
self.ax.set_facecolor(c)
905+
self._lastcolor = c
906+
if self.drawon:
907+
self.ax.figure.canvas.draw()
908+
909+
def on_text_change(self, func):
910+
"""
911+
When the text changes, call this *func* with event.
912+
913+
A connection id is returned which can be used to disconnect.
914+
"""
915+
cid = self.cnt
916+
self.change_observers[cid] = func
917+
self.cnt += 1
918+
return cid
919+
920+
def on_submit(self, func):
921+
"""
922+
When the user hits enter or leaves the submision box, call this
923+
*func* with event.
924+
925+
A connection id is returned which can be used to disconnect.
926+
"""
927+
cid = self.cnt
928+
self.submit_observers[cid] = func
929+
self.cnt += 1
930+
return cid
931+
932+
def disconnect(self, cid):
933+
"""remove the observer with connection id *cid*"""
934+
try:
935+
del self.observers[cid]
936+
except KeyError:
937+
pass
938+
939+
631940
class RadioButtons(AxesWidget):
632941
"""
633942
A GUI neutral radio button.

0 commit comments

Comments
 (0)