|
17 | 17 | from six.moves import zip
|
18 | 18 |
|
19 | 19 | import numpy as np
|
| 20 | +from matplotlib import rcParams |
20 | 21 |
|
21 | 22 | from .mlab import dist
|
22 | 23 | from .patches import Circle, Rectangle, Ellipse
|
@@ -628,6 +629,314 @@ def disconnect(self, cid):
|
628 | 629 | pass
|
629 | 630 |
|
630 | 631 |
|
| 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 | + |
631 | 940 | class RadioButtons(AxesWidget):
|
632 | 941 | """
|
633 | 942 | A GUI neutral radio button.
|
|
0 commit comments