"""
    attempt to add a prompt / button to input text or numbers to a
    matplotlib axes instance using key_press_event's
    (including 'backspace', 'enter' and numpad numbers
     - as in actual mpl-svn)

    creating a new class 'InputButton' inherited from class 'Widget',
    but in fact very similar to class 'Button' (from matplotlib/widgets.py)

    and a new class 'NumInputButton' inherited from class 'InputButton'

    usage of the example programm:
    ===============================
    (1) Click in the axes (click the button) to activate the input option.
    (2) You can input new characters / numbers using the keyboard.
        Deleting input is possible using '\' or '#' like backspace.
        [wrong input is reported as returned string!]
    (3) To get the displayed value into the corresponding variable, you have
        to deactivate the button pressing <return> / Enter or click once more
        the button.

issuses to be discussed:
 - wrong input: return a string holding the error message 
   -> Any other kind of warning like highlightning ?
 - possibility to click to a certain position to choose input place
 - usetex support

problems :
  - key_press_event 'l', 'g' makes the axes do it's job but that's
    not what I want in this widget!
"""

# ----- additional to widgets.py ---------------------------------------------
from math import pi                           # for evaluation of 'pi' in the
## from numerix import pi                     #   NumInputButton
                                              # useful expressions should be
                                              #   added here

from matplotlib.widgets import Widget

class InputButton(Widget):
    """ Textbox widget to input characters to a matplotlib axes instance

        class 'InputButton' inherited from class 'Widget', but in fact
        very similar to class 'Button' (from matplotlib/widgets.py)
        to input characters using 'key_press_event's
        and the method 'self.input_to_button' to redirect input
        from external program to here

        Call 'after_typing' to connect to the button
    """
    def __init__(self, ax, str_val, wr_inputs=[], dels=[],
                 color_1='0.85', color_2='w', hovercolor='0.95'):
        """
            ax         - the Axes the button renders into
            str_val    - list with one string representing the value
                         (list replaces missing pointer to the string),
                         this string will be changed with this button
                         and becomes the label of the button
            wr_inputs  - list of characters that are not allowed to
                         use for changing the string self.str_val
            dels       - list of characters to use as backspace to delete
                         input (e.g. '\' or '#')
            color_1    - color for inactive button -> no input
            color_2    - color for active button -> input characters
            hovercolor - color of the button when the mouse is over it
        """
        self.str_val = str_val
                                              # disabled input characters
        self.wr_inputs = ['None', 'shift', 'alt', 'ctrl'] + wr_inputs
                                              # deleting commands
        self.dels = ['backspace'] + dels 
        self.typing = False                   # allow input of numbers
        self.color_1 = color_1
        self.color_2 = color_2
        self.color = color_1              # actual color of the button
        self._lastcolor = color_1         # last color before actual
        self.hovercolor = hovercolor
        self.canvas = ax.figure.canvas
        self.region = self.canvas.copy_from_bbox(ax.bbox)
        self.label =  ax.text(0.025, 0.2, self.str_val[0],
                        fontsize=14,
                        #verticalalignment='baseline',
                        horizontalalignment='left',
                        transform=ax.transAxes)
        self.ax = ax
        ax.set_yticks([])
        ax.set_xticks([])
        ax.set_axis_bgcolor(color_1)
        ax.set_navigate(False)
        self.canvas.mpl_connect('motion_notify_event', self._motion)
        self.canvas.mpl_connect('button_press_event', self._change_mode)
        self.canvas.draw()
        
        r = self._get_text_right()
        self.cursor, = ax.plot([r,r], [0.2, 0.8], transform=ax.transAxes)
        self.redraw()

        self.cnt = 0
        self.observers = {}

    def redraw(self):
        self.ax.redraw_in_frame()
        self.canvas.blit(self.ax.bbox)

    def _motion(self, event):
        if event.inaxes==self.ax:
            c = self.hovercolor
        else:
            c = self.color
        if c != self._lastcolor:
            self.ax.set_axis_bgcolor(c)
            self._lastcolor = c
            ##if self.drawon: self.ax.figure.canvas.draw()
            self.redraw()

    def _change_mode(self, event):
        """ to handle presses of the button itself and key 'enter'

            Switching typing mode on and off after the button was
            pressed or event.key was 'enter'. During that background
            color is changed (between 'self.color_1' and 'self.color_2')
            Switching typing mode off causes: run of all functions
            in 'self.observers_2' 
        """
        if event.button is not None:
            if event.inaxes != self.ax: return
        self.typing = not self.typing         # change mode
        if self.typing:
            self.color = self.color_2
            self.redraw()
            ##if self.drawon: self.ax.figure.canvas.draw()
        else:
            self.color = self.color_1
            ##if self.drawon: self.ax.figure.canvas.draw()
            self.redraw()
                                              # return text to outer scope
            self.str_val[0] = self.label.get_text()
            print " intern : actual value = ", self.str_val[0]
            if not self.eventson: return
            for cid, func in self.observers.items():
                func(event)

    def input_to_button(self, event):
        """ to add new character to button-label-string

            input succeeds only if 'self.typing==True' and 'event.key'
            is not in list 'self.wr_inputs'

            Only the string displayed on the button is updated.
            The string represented is updated after leaving active mode.
        """
        if self.typing and not (event.key in self.wr_inputs):
            if event.key in ['enter']:
                self._change_mode(event)
                return
            if event.key == 'dec':            # replace numpad decimal 
                event.key = '.'
            text = self.label.get_text()      # get actual text
            if event.key in self.dels:
                print " # delete last character"
                text = text[:-1]
            else:
                print " # add new character : %s " % (event.key)
                text = text+event.key
            self.label.set_text(text)         # set new text
            ##if self.drawon: self.ax.figure.canvas.draw()
            r = self._get_text_right()
            self.cursor.set_xdata([r,r])
            self.redraw()
            return " succesfully"
        else:
            return " input error: disabled input character "

    def after_typing(self, func):
        """
        After the prompt / input-button was diactivated, call this
        func with event

        A connection id is returned which 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

    def _get_text_right(self):
        l,b,w,h = self.label.get_window_extent().get_bounds()
        r = l+w+2
        t = b+h
        s = self.label.get_text()
        # adjust cursor position for trailing space
        numtrail = len(s) - len(s.rstrip())
        en = self.ax.get_renderer_cache().points_to_pixels(\
            self.label.get_fontsize())/2.

        r += numtrail*en
        l,b = self.ax.transAxes.inverse_xy_tup((l,b))
        r,t = self.ax.transAxes.inverse_xy_tup((r,t))
        return r

class NumInputButton(InputButton):
    """ widget to input numbers to a matplotlib axes instance

        creating a new class 'NumInputButton' inherited from class
        'InputButton' (above)
        to input characters using 'key_press_event's
        and the method 'self.input_to_button' to redirect input
        from external program to here
        After input has finished the string is converted in an
        number using eval and formating according to 'self.valfmt'

        Call 'on_clicked' or 'after_typing' to connect to the button
    """
    def __init__(self, ax, value, valfmt='%f', 
                 inputs=[], wr_inputs=[], dels=[], color_1='0.85',
                 color_2='w', hovercolor='0.95'):
        """
            ax         - the Axes the button renders into
            value      - list with one value (list replaces missing pointer
                         to the value), will be changed with this button
            valfmt     - special value format for button label and value
            inputs     - list of characters as the allowed inputs for value
                         (evaluation of whole input using: eval(input),
                          additional to initial values in self.inputs,
                          e.g. 'p', 'i' for math.pi)
            wr_inputs  - list of characters that are not allowed to
                         use for changing the value during string
                         self.label resetting ('self.input_to_button')
            dels       - list of characters to use as backspace to delete
                         input (e.g. '\' or '#')
            color_1    - color for inactive button -> no input
            color_2    - color for active button -> input characters
            hovercolor - color of the button when the mouse is over it
        """
        self.value = value
        self.valfmt = valfmt      
        init_label = self.valfmt % (self.value[0])
        InputButton.__init__(self, ax, [init_label], wr_inputs=wr_inputs,
                             dels=dels, color_1=color_1, color_2=color_2,
                             hovercolor=hovercolor)
        del self.str_val
                                              # allowed input characters
        self.inputs = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
                       '.', 'dec', '+', '-', '*', '/'] + inputs

    def _change_mode(self, event):
        """ to handle presses of the button itself and key 'enter'

            Switching typing mode on and off after the button was
            pressed or event.key was 'enter'. During that background
            color is changed (between 'self.color_1' and 'self.color_2')
            Switching typing mode off causes: run of all functions
            in 'self.observers_2' 
        """
        if event.button is not None:
            if event.inaxes != self.ax: return
        self.typing = not self.typing         # change mode
        if self.typing:
            self.color = self.color_2
            if self.drawon: self.ax.figure.canvas.draw()
        else:
            self.color = self.color_1
            try:
                self.value[0] = eval(self.label.get_text())
            except (SyntaxError, NameError):    # no new value
                pass
            self.label.set_text(self.valfmt % self.value[0])
            ##if self.drawon: self.ax.figure.canvas.draw()
            r = self._get_text_right()
            self.cursor.set_xdata([r,r])
            self.redraw()
                                              # return value to outer scope
            self.value[0] = eval(self.label.get_text())
            print " intern : actual value = ", self.value[0]
            if not self.eventson: return
            for cid, func in self.observers.items():
                func(event)

    def input_to_button(self, event):
        """ to set new value in a window button

            input succeeds only if 'self.typing==True' and 'event.key'
            is not in list 'self.wr_inputs'

            to avoid wrong input only chars from 'self.inputs' and
            'self.dels' are allowed

            Only the value displayed on the button is updated.
            The value represented is updated after leaving typing mode.
        """
        if self.typing and not (event.key in self.wr_inputs):
            if event.key in (self.inputs+self.dels):
                if event.key == 'dec':            # replace numpad decimal 
                    event.key = '.'
                text = self.label.get_text()      # get actual text
                if event.key in self.dels:
                    print " # delete last character"
                    text = text[:-1]
                else:
                    print " # add new character : %s " % (event.key)
                    text = text+event.key
                self.label.set_text(text)         # set new text
                ##if self.drawon: self.ax.figure.canvas.draw()
                r = self._get_text_right()
                self.cursor.set_xdata([r,r])
                self.redraw()
                return " succesfully"
            elif event.key in ['enter']:
                self._change_mode(event)
                return " succesfully"
            else:
                return " input error: char not in 'self.inputs'+'self.dels'"
        else:
            return " input error: disabled input character "


# ----- example of usage -----------------------------------------------------

def main():
    """ testing new buttons """
    import matplotlib
    matplotlib.use("WxAgg")
    #matplotlib.use("TkAgg")
    #matplotlib.use("GTkAgg")
    ## GTkAgg gets an error during text.get_window_extent() in text.py
    ## -> message: 'RuntimeError: Cannot get window extent w/o renderer'
    import pylab

    print __doc__

    def event_handling(event):
        """ handle the events ... """
        if event.name == 'button_press_event':
            #print " mouse button >%s< was pressed." % (event.button)
            pass
        elif event.name == 'key_press_event':
            #print " key >%s< was pressed " % (event.key)
            if event_handling.new_button_A.typing:
                message = event_handling.new_button_A.input_to_button(event)
                print " ! input message: %s " % (message)
                print " extern : actual value = ", event_handling.A[0]
            elif event_handling.new_button_B.typing:
                message = event_handling.new_button_B.input_to_button(event)
                print " ! input message: %s " % (message)
                print " extern : actual value = ", event_handling.B[0]


    pylab.figure(figsize=(3, 2))
    ax_input_A = pylab.axes([0.1, 0.5, 0.6, 0.3])
    ax_input_B = pylab.axes([0.1, 0.1, 0.6, 0.3])

    event_handling.A = ['matplotlib']             # some string to be changed
    event_handling.B = [3.14]                     # some float to be changed

    print " Numbers '1' and '2' aren't allowed in the upper box. "
    event_handling.new_button_A = InputButton(ax_input_A, event_handling.A,
                                              wr_inputs=['1', '2'],
                                              dels=['\\', '#'])

    print " Characters 'a' and 's' aren't allowed in the lower box. "
    event_handling.new_button_B = NumInputButton(ax_input_B, event_handling.B,
                                                 inputs=['p', 'i'],
                                                 wr_inputs=['a', 's'],
                                                 dels=['\\', '#'],
                                                 valfmt='%.9f')
    
    pylab.connect('key_press_event', event_handling)
    pylab.connect('button_press_event', event_handling)
    pylab.show()

if __name__ == '__main__':
    main()
