8000 Text box widget by HastingsGreer · Pull Request #5375 · matplotlib/matplotlib · GitHub
[go: up one dir, main page]

Skip to content

Text box widget #5375

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
e0440cc
Added a text entry widget, that allows usere to register to be notifi…
HastingsGreer Oct 27, 2015
5b06d3f
resolved merge conflict in CHANGELOG
HastingsGreer Jan 25, 2016
23c5a74
Added description of textbox widget and example
HastingsGreer Oct 31, 2015
1f37c22
Added a text box example: evaluates any string inputs as y(x).
HastingsGreer Oct 31, 2015
507b6a4
Whitespace adjustments for PEP8 compliance
HastingsGreer Oct 31, 2015
ff8afc3
Removed a newline that was accidentally commited
HastingsGreer Oct 31, 2015
f1df42f
removed w accidentally added to the first line
HastingsGreer Oct 31, 2015
606d4a3
fixed PEP8 formatting
HastingsGreer Oct 31, 2015
02b9ffd
Fixed formatting of textbox widget entry in whats_new.rst
HastingsGreer Oct 31, 2015
497452e
formatting changes for Pep8 compliance
HastingsGreer Nov 3, 2015
382057d
removed "time the" from end of document, added last commit by mistake
HastingsGreer Nov 3, 2015
06ab4ce
refactored "start typing" and "stop typing" into their own functions …
HastingsGreer Jan 20, 2016
9826a3c
fixed typos from last commit
HastingsGreer Jan 20, 2016
bf4bccd
made textbox lose focus when window is resized: this prevents cursor …
HastingsGreer Jan 20, 2016
954dfd0
added adjustable padding between label and text box
HastingsGreer Jan 20, 2016
b26ec58
removed trailing whitespace
HastingsGreer Jan 25, 2016
0025a26
enabled moving the cursor by clicking
HastingsGreer Feb 13, 2016
2734051
Document feature: caret can be moved by clicking
HastingsGreer Feb 26, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@

2015-11-16 Levels passed to contour(f) and tricontour(f) must be in increasing
order.

2015-10-21 Added TextBox widget


2015-10-21 Added get_ticks_direction()

2015-02-27 Added the rcParam 'image.composite_image' to permit users
Expand Down
7 changes: 7 additions & 0 deletions doc/users/whats_new.rst
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,13 @@ Some parameters have been added, others have been improved.
Widgets
-------

Added TextBox Widget
````````````````````

Added a widget that allows text entry by reading key events when it is active.
Text caret in text box is visible when it is active, can be moved using arrow keys and mouse


Active state of Selectors
`````````````````````````

Expand Down
23 changes: 23 additions & 0 deletions examples/widgets/textbox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import TextBox
fig, ax = plt.subplots()
plt.subplots_adjust(bottom=0.2)
t = np.arange(-2.0, 2.0, 0.001)
s = t ** 2
initial_text = "t ** 2"
l, = plt.plot(t, s, lw=2)


def submit(text):
ydata = eval(text)
l.set_ydata(ydata)
ax.set_ylim(np.min(ydata), np.max(ydata))
plt.draw()

axbox = plt.axes([0.1, 0.05, 0.8, 0.075])
text_box = TextBox(axbox, 'Evaluate', initial=initial_text)
text_box.on_submit(submit)

plt.show()
294 changes: 294 additions & 0 deletions lib/matplotlib/widgets.py
9E88
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from matplotlib.externals.six.moves import zip

import numpy as np
from matplotlib import rcParams

from .mlab import dist
from .patches import Circle, Rectangle, Ellipse
Expand Down Expand Up @@ -622,6 +623,299 @@ def disconnect(self, cid):
pass


class TextBox(AxesWidget):
"""
A GUI neutral text input box.

For the text box to remain responsive you must keep a reference to it.

The following attributes are accessible:

*ax*
The :class:`matplotlib.axes.Axes` the button renders into.

*label*
A :class:`matplotlib.text.Text` instance.

*color*
The color of the text box when not hovering.

*hovercolor*
The color of the text box when hovering.

Call :meth:`on_text_change` to be updated whenever the text changes.

Call :meth:`on_submit` to be updated whenever the user hits enter or
leaves the text entry field.
"""

def __init__(self, ax, label, initial='',
color='.95', hovercolor='1', label_pad=.01):
"""
Parameters
----------
ax : matplotlib.axes.Axes
The :class:`matplotlib.axes.Axes` instance the button
will be placed into.

label : str
Label for this text box. Accepts string.

initial : str
Initial value in the text box

color : color
The color of the box

hovercolor : color
The color of the box when the mouse is over it

label_pad : float
the distance between the label and the right side of the textbox
"""
AxesWidget.__init__(self, ax)

self.DIST_FROM_LEFT = .05

self.params_to_disable = []
for key in rcParams.keys():
if u'keymap' in key:
self.params_to_disable += [key]

self.text = initial
self.label = ax.text(-label_pad, 0.5, label,
verticalalignment='center',
horizontalalignment='right',
transform=ax.transAxes)
self.text_disp = self._make_text_disp(self.text)

self.cnt = 0
self.change_observers = {}
self.submit_observers = {}

# If these lines are removed, the cursor won't appear the first
# time the box is clicked:
self.ax.set_xlim(0, 1)
self.ax.set_ylim(0, 1)

self.cursor_index = 0

# Because this is initialized, _render_cursor
# can assume that cursor exists.
self.cursor = self.ax.vlines(0, 0, 0)
self.cursor.set_visible(False)

self.connect_event('button_press_event', self._click)
self.connect_event('button_release_event', self._release)
self.connect_event('motion_notify_event', self._motion)
self.connect_event('key_press_event', self._keypress)
self.connect_event('resize_event', self._resize)
ax.set_navigate(False)
ax.set_facecolor(color)
ax.set_xticks([])
ax.set_yticks([])
self.color = color
self.hovercolor = hovercolor

self._lastcolor = color

self.capturekeystrokes = False

def _make_text_disp(self, string):
return self.ax.text(self.DIST_FROM_LEFT, 0.5, string,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This spacing should be in display, not data coordinates. That will be tricky and may require the use of the inverse transform, but will result in spacing that doesn't change with the size of the figure.

verticalalignment='center',
horizontalalignment='left',
transform=self.ax.transAxes)

def _rendercursor(self):
# this is a hack to figure out where the cursor should go.
# we draw the text up to where the cursor should go, measure
# and save its dimensions, draw the real text, then put the cursor
# at the saved dimensions

widthtext = self.text[:self.cursor_index]
no_text = False
if(widthtext == "" or widthtext == " " or widthtext == " "):
no_text = widthtext == ""
widthtext = ","

wt_disp = self._make_text_disp(widthtext)

self.ax.figure.canvas.draw()
bb = wt_disp.get_window_extent()
inv = self.ax.transData.inverted()
bb = inv.transform(bb)
wt_disp.set_visible(False)
if no_text:
bb[1, 0] = bb[0, 0]
# hack done
self.cursor.set_visible(False)

self.cursor = self.ax.vlines(bb[1, 0], bb[0, 1], bb[1, 1])
self.ax.figure.canvas.draw()

def _notify_submit_observers(self):
for cid, func in six.iteritems(self.submit_observers):
func(self.text)

def _release(self, event):
if self.ignore(event):
return
if event.canvas.mouse_grabber != self.ax:
return
event.canvas.release_mouse(self.ax)

def _keypress(self, event):
if self.ignore(event):
return
if self.capturekeystrokes:
key = event.key

if(len(key) == 1):
self.text = (self.text[:self.cursor_index] + key +
self.text[self.cursor_index:])
self.cursor_index += 1
elif key == "right":
if self.cursor_index != len(self.text):
self.cursor_index += 1
elif key == "left":
if self.cursor_index != 0:
self.cursor_index -= 1
elif key == "home":
self.cursor_index = 0
elif key == "end":
self.cursor_index = len(self.text)
elif(key == "backspace"):
if self.cursor_index != 0:
self.text = (self.text[:self.cursor_index - 1] +
self.text[self.cursor_index:])
self.cursor_index -= 1
elif(key == "delete"):
if self.cursor_index != len(self.text):
self.text = (self.text[:self.cursor_index] +
self.text[self.cursor_index + 1:])

self.text_disp.remove()
self.text_disp = self._make_text_disp(self.text)
self._rendercursor()
for cid, func in six.iteritems(self.change_observers):
func(self.text)
if key == "enter":
self._notify_submit_observers()

def begin_typing(self, x):
self.capturekeystrokes = True
#disable command keys so that the user can type without
#command keys causing figure to be saved, etc
self.reset_params = {}
for key in self.params_to_disable:
self.reset_params[key] = rcParams[key]
rcParams[key] = []

def stop_typing(self):
notifysubmit = False
# because _notify_submit_users might throw an error in the
# user's code, we only want to call it once we've already done
# our cleanup.
if self.capturekeystrokes:
#since the user is no longer typing,
#reactivate the standard command keys
for key in self.params_to_disable:
rcParams[key] = self.reset_params[key]
notifysubmit = True
self.capturekeystrokes = False
self.cursor.set_visible(False)
self.ax.figure.canvas.draw()
if notifysubmit:
self._notify_submit_observers()

def position_cursor(self, x):
#now, we have to figure out where the cursor goes.
#approximate it based on assuming all characters the same length
if len(self.text) == 0:
self.cursor_index = 0
else:
bb = self.text_disp.get_window_extent()

trans = self.ax.transData
inv = self.ax.transData.inverted()
bb = trans.transform(inv.transform(bb))

text_start = bb[0, 0]
text_end = bb[1, 0]

ratio = (x - text_start) / (text_end - text_start)

if ratio < 0:
ratio = 0
if ratio > 1:
ratio = 1

self.cursor_index = int(len(self.text) * ratio)

self._rendercursor()

def _click(self, event):
if self.ignore(event):
return
if event.inaxes != self.ax:
self.stop_typing()
return
if not self.eventson:
return
if event.canvas.mouse_grabber != self.ax:
event.canvas.grab_mouse(self.ax)
if not(self.capturekeystrokes):
self.begin_typing(event.x)
self.position_cursor(event.x)

def _resize(self, event):
self.stop_typing()

def _motion(self, event):
if self.ignore(event):
return
if event.inaxes == self.ax:
c = self.hovercolor
else:
c = self.color
if c != self._lastcolor:
self.ax.set_facecolor(c)
self._lastcolor = c
if self.drawon:
self.ax.figure.canvas.draw()

def on_text_change(self, func):
"""
When the text changes, call this *func* with event.

A connection id is returned which can be used to disconnect.
"""
cid = self.cnt
self.change_observers[cid] = func
self.cnt += 1
return cid

def on_submit(self, func):
"""
When the user hits enter or leaves the submision box, call this
*func* with event.

A connection id is returned which can be used to disconnect.
"""
cid = self.cnt
self.submit_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


class RadioButtons(AxesWidget):
"""
A GUI neutral radio button.
Expand Down
0