import pygtk
import gobject
import gtk

from matplotlib.patches import Polygon,Rectangle
from matplotlib.lines import Line2D

import fluents
import logger


# global active mode. Used by toolbars to communicate correct mode
active_mode = 'default'


class ObjectTable:
    """A 2D table of elements.
    An ObjectTable is a resizable two-dimensional array of objects.
    When resized, it will keep all the elements that fit in the new shape,
    and forget about other elements. When new elements are needed to fill
    out the new shape, they will be created.
    """

    def __init__(self, xsize=0, ysize=0, creator=None):
        """Creates a two-dimensional table of objects.
        @param xsize: Width of the object table.
        @param ysize: Height of the object table.
        @param creator: A function that will be used to instantiate new items.
        """
        self._elements = []
        self._creator = creator or (lambda : None)
        self.xsize = xsize
        self.ysize = ysize
        self.resize(xsize, ysize)

    def resize(self, xsize, ysize):
        """Resizes the table by removing and creating elements as required.
        Makes the ObjectTable the specified size by removing elements that
        will not fit in the new shape and appending elements to fill the
        table. New elements will be created with the creator function passed
        as an argument to __init__.

        @param xsize: The new width.
        @type xsize: int
        @param ysize: The new height.
        @type ysize: int
        """

        # Delete or append new rows
        del self._elements[ysize:]
        new_rows = ysize - len(self._elements)
        self._elements.extend([list() for i in range(new_rows)])

        # Delete or append new columns
        for row in self._elements:
            del row[xsize:]
            new_elems = xsize - len(row)
            row.extend([self._creator() for i in range(new_elems)])

        self.xsize = xsize
        self.ysize = ysize

    def __getitem__(self, index):
        x, y = index
        return self._elements[y][x]

    def __setitem__(self, index, value):
        x, y = index
        self._elements[y][x] = value


class ViewFrame (gtk.Frame):
    """
    A ViewFrame is a gtk container widget that contains a View.
    The ViewFrame is either active or inactive, and belongs to a group of 
    VeiwFrames of which only one can be active at any time.
    """

    def __init__(self, view_frames):
        """Initializes a new ViewFrame.
        @param view_frames: A list of view frames to which the new view
            frame shouls belong. Only one ViewFrame in this list can be
            active at any time.
        """
        gtk.Frame.__init__(self)
        self.focused = False
        self.view_frames = view_frames
        self.empty_view = EmptyView()
        self._button_event = None

        ## Set up a VBox with a label wrapped in an event box.
        label = gtk.Label()
        ebox = gtk.EventBox()
        ebox.add(label)
        vbox = gtk.VBox()
        vbox.pack_start(ebox, expand=False)
        vbox.pack_start(gtk.HSeparator(), expand=False)

        self._ebox_button_event = ebox.connect("button-press-event", 
                                               self._on_button_press_event)
        ## Keep the references for later use.
        self._vbox = vbox
        self._ebox = ebox
        self._view_title = label
        self.add(vbox)
                
        view_frames.append(self)
        if len(view_frames) == 1:
            self.focus()
        else:
            self.focused = True
            self.unfocus()

        # Get dropped views
        self.drag_dest_set(gtk.DEST_DEFAULT_ALL,
                           [("GTK_TREE_MODEL_ROW", gtk.TARGET_SAME_APP, 7)],
                           gtk.gdk.ACTION_LINK)
        self.connect("drag-data-received", self.on_drag_data_received)
       
        # Set view
        self._view = self.empty_view
        self._view.connect("button-press-event", self._on_button_press_event)
        self._vbox.add(self._view)
        self._view_title.set_text(self._view.title)
        self.show_all()
        self._view.show()

    def focus(self):
        """Gets focus and ensures that no other ViewFrame in the same set
        is in focus.
        """
        if self.focused:
            self.emit('focus-changed', self, True)
            return self

        for frame in self.view_frames:
            frame.unfocus()

        self.set_shadow_type(gtk.SHADOW_IN)
        self._ebox.set_state(gtk.STATE_ACTIVE)
        self.focused = True
        self.emit('focus-changed', self, True)
        return self

    def unfocus(self):
        """Removes focus from the ViewFrame. Does nothing if unfocused."""
        if not self.focused:
            return

        self.set_shadow_type(gtk.SHADOW_OUT)
        self._ebox.set_state(gtk.STATE_NORMAL)
        self.focused = False
        self.emit('focus-changed', self, False)

    def set_view(self, view):
        """Set view to view or to empty view if parameter is None
        @param view: An instance of View
        """

        # if None is passed, use empty view
        if view == None:
            view = self.empty_view

        # do nothing if the view is already there
        if view == self._view:
            return
        
        # detach view from current parent
        if view._view_frame:
            view._view_frame.set_view(None)

        # switch which widget we are listening to
        if self._button_event:
            self._view.disconnect(self._button_event)

        self._button_event = view.connect("button-press-event", 
                                          self._on_button_press_event)

        # remove old view, set new view
        if self._view:
            self._view.hide()
            self._vbox.remove(self._view)
            self._view._view_frame = None

        self._view_title.set_text(view.title)
        self._vbox.add(view)
        view.show()
        
        view._view_frame = self
        self._view = view

    def get_view(self):
        """Returns current view, or None if the empty view is set.
        @returns: None if the ViewFrame is currently displaying it's
            EmptyView. Otherwise it returns the currently displeyd View.
        """
        if self._view == self.empty_view:
            return None
        return self._view

    def _on_button_press_event(self, widget, event):
        if not self.focused:
            self.focus()
            
    def on_drag_data_received(self, widget, drag_context, x, y, 
                              selection, info, timestamp):
        treestore, path = selection.tree_get_row_drag_data()
        iter = treestore.get_iter(path)
        obj  = treestore.get_value(iter, 2)

        if isinstance(obj, View):
            self.set_view(obj)
            self.focus()
            
        elif isinstance(obj, fluents.dataset.Dataset):
            view = self.get_view()
            if view != None and view.is_mappable_with(obj):
                view._update_color_from_dataset(obj)

        elif isinstance(obj, fluents.dataset.Selection):
            view = self.get_view()
            if view.is_mappable_with(obj):
                view.selection_changed(self.current_dim, obj)
            
                

class MainView (gtk.Notebook):
    """The MainView class displays the Views in Fluents.

    MainView, of which there is normally only one instance, contains a 
    gtk.Table that holds all the visible Views, and a single ViewFrame
    that is used for maximizing plots.
    """
    def __init__(self):
        gtk.Notebook.__init__(self)
        self.set_show_tabs(False)
        self.set_show_border(False)

        self._focus_listeners = []
        self._view_frames = []
        self._views = ObjectTable(2, 2, lambda : ViewFrame(self._view_frames))
        self._large_view = ViewFrame(list())
        self.update_small_views()

        
        self.append_page(self._small_views)
        self.append_page(self._large_view)
        self.show()
        self.set_current_page(0)

    def __getitem__(self, x, y):
        return self._views[x, y]

    def update_small_views(self):
        """Creates a new gtk.Table to show the views. Called after changes to
        the _views property"""

        # Disconnect focus-changed signal for all ViewFrames
        for vf, fl in zip(self._view_frames, self._focus_listeners):
            vf.disconnect(fl)

        # Resize table
        self._small_views = gtk.Table(self._views.ysize, self._views.xsize, True)
        self._small_views.set_col_spacings(4)
        self._small_views.set_row_spacings(4)
        for x in range(self._views.xsize):
            for y in range(self._views.ysize):
                child = self._views[x,y]
                self._small_views.attach(child, x, x+1, y, y+1)
        self._small_views.show_all()

        # Create new focus-changed signal handlers
        fls = []
        for vf in self._view_frames:
            fls.append(vf.connect('focus-changed', self._on_view_focus_changed))
        self._focus_listeners = fls

    def get_active_small_view(self):
        """Returns the active ViewFrame in the small views table.
        If a view is maximized, the corresponding ViewFrame in the table
        will be returned, not the active maximized ViewFrame"""
        for vf in self._view_frames:
            if vf.focused:
                return vf
        return None

    def get_active_view_frame(self):
        """Returns the active view frame."""
        if self.get_current_page() == 0:
            return self.get_active_small_view()
        else:
            return self._large_view
            
    def goto_large(self):
        """Maximize the view in the current ViewFrame.
        
        Maximizes the View in the current ViewFrame. The ViewFrame itself is
        not resized or modified, except it's view will be set to None.
        This method will do nothing if a view is currently maximized.
        """
        if self.get_current_page() == 1:
            return

        vf = self.get_active_small_view()
        view = vf.get_view()
        vf.set_view(None)
        self._large_view.set_view(view)
        self.set_current_page(1)

    def goto_small(self):
        """Goes to the small views page.

        The maximized View will be given to the active ViewFrame in the view
        table.
        """
        if self.get_current_page() == 0:
            return

        vf = self.get_active_small_view()
        view = self._large_view.get_view()
        self._large_view.set_view(None)
        vf.set_view(view)
        self.set_current_page(0)

    def insert_view(self, view):
        """Set a view in the currently active ViewFrame.
        @param view: An instance of View.
        """
        if self.get_current_page() == 0:
            vf = self.get_active_small_view()
        else:
            vf = self._large_view
        vf.set_view(view)
       
    def set_all_plots(self, views):
        """Displays all the views in the list, and hides all other views.

        Loops through all ViewFrames from top left to bottom right, and sets
        their currently active View to the next view in the list. After the
        last view is placed in a ViewFrame, the remaining ViewFrames are
        emptied.

        @param views: A list of views to set.
        """
        for y in range(self._views.ysize):
            for x in range(self._views.xsize):
                if views:
                    self._views[x, y].set_view(views.pop())
                else:
                    self._views[x, y].set_view(None)

    def get_active_view_index(self):
        current = self.get_active_small_view()
        for i in range(self._views.xsize):
            for j in range(self._views.ysize):
                if self._views[i, j] == current:
                    return (i, j)
        return None

    def set_active_view_by_index(self, x, y):
        self._views[x, y].focus()

    def move_focus_left(self):
        x, y = self.get_active_view_index()
        if x > 0:
            self.set_active_view_by_index(x-1, y)

    def move_focus_right(self):
        x, y = self.get_active_view_index()
        if x < self._views.xsize-1:
            self.set_active_view_by_index(x+1, y)

    def move_focus_up(self):
        x, y = self.get_active_view_index()
        if y > 0:
            self.set_active_view_by_index(x, y-1)

    def move_focus_down(self):
        x, y = self.get_active_view_index()
        if y < self._views.ysize-1:
            self.set_active_view_by_index(x, y+1)

    def _on_view_focus_changed(self, widget, vf, focused):
        if focused:
            self.emit('view-changed', vf)
    
    def resize_table(self, width, height):
        """Resizes the list of small views.
        """ 
        ## Hide all plots that will be removed from the screen. 
        for x in range(self._views.xsize):
            for y in range(self._views.ysize):
                if x >= width or y >= height:
                    self._views[x, y].set_view(None)
                    self._view_frames.remove(self._views[x, y])

        for x in range(self._views.xsize):
            for y in range(self._views.ysize):
                self._small_views.remove(self._views[x, y])

        self._views.resize(width, height)
        self.update_small_views()
        self.insert_page(self._small_views, gtk.Label("small"), 0)
        self.remove_page(1)
        #self.set_current_page(0)
        self.goto_small()


class View (gtk.Frame):
    """The base class of everything that is shown in the center view of
    fluents.

    Most views should rather subclass Plot, which automatically handles 
    freezing, creates a toolbar, and sets up matplotlib Figure and Canvas 
    objects.
    """

    def __init__(self, title):
        gtk.Frame.__init__(self)
        self.title = title
        self.set_shadow_type(gtk.SHADOW_NONE)
        self._view_frame = None

    def get_toolbar(self):
        return None

    def is_mappable_with(self, dataset):
        """Override in individual plots."""
        return False


class EmptyView (View):
    """EmptyView is shown in ViewFrames that are unused."""
    def __init__(self):
        View.__init__(self, 'Empty view')
        self.set_label(None)
        label = gtk.Label('No view')
        ebox = gtk.EventBox()
        ebox.add(label)
        self.add(ebox)
        label.show()
        ebox.show()
        self.show()

class PlotMode:
    """A PlotMode object corresponds to a mouse mode in a plot.

    When a mode is selected in the toolbar, the PlotMode corresponding
    to the toolbar button is activated by calling setup(ax) for the axis
    system ax.
    """
    def __init__(self, plot, name, tooltip, image_file):
        self.name = name
        self.tooltip = tooltip
        self.image_file = image_file
        self.plot = plot
        self.canvas = plot.canvas

    def get_icon(self):
        """Returns the icon for the PlotMode"""
        image = gtk.Image()
        image.set_from_pixbuf(fluents.icon_factory.get(self.image_file))
        return image
        
    def activate(self):
        """Subclasses of PlotMode should do their initialization here.

        The activate method is called when a mode is activated, and is
        used primarily to set up callback functions on events in the
        canvas.
        """
        pass

    def deactivate(self):
        """Subclasses of PlotMode should do their cleanup here.

        The deactivate method is primarily by subclasses of PlotMode to
        remove any callbacks they might have on the matplotlib canvas.
        """
        pass


class DefaultPlotMode(PlotMode):
    def __init__(self, plot):
        PlotMode.__init__(self, plot, 'default', 'Default mode', 'cursor')


class PanPlotMode(PlotMode):
    def __init__(self, plot):
        PlotMode.__init__(self, plot, 'pan',
                          'Pan axes with left mouse, zoom with right',
                          'move')

        # Holds handler IDs for callbacks.
        self._button_press = None
        self._button_release = None
        self._motion_notify = None
        self._xypress = None
        self._button_pressed = None

    def activate(self):
        self._button_press = self.canvas.mpl_connect(
            'button_press_event', self._on_button_press)
        self._button_release = self.canvas.mpl_connect(
            'button_release_event', self._on_button_release)

    def deactivate(self):
        if self._button_press:
            self.canvas.mpl_disconnect(self._button_press)

        if self._button_release:
            self.canvas.mpl_disconnect(self._button_release)

    def _on_button_press(self, event):

        if event.button == 1:
            self._button_pressed = 1
        elif  event.button == 3:
            self._button_pressed = 3
        else:
            self._button_pressed=None
            return

        x, y = event.x, event.y

        # push the current view to define home if stack is empty
        # if self._views.empty(): self.push_current()

        self._xypress=[]
        for i, a in enumerate(self.canvas.figure.get_axes()):
            if x is not None and y is not None and a.in_axes(x, y) \
                   and a.get_navigate():
                xmin, xmax = a.get_xlim()
                ymin, ymax = a.get_ylim()
                lim = xmin, xmax, ymin, ymax
                self._xypress.append((x, y, a, i, lim,a.transData.deepcopy()))
                self.canvas.mpl_disconnect(self._motion_notify)
                
                cid = self.canvas.mpl_connect('motion_notify_event',
                                              self._on_motion_notify)
                self._motion_notify = cid

    def _on_motion_notify(self, event):
        """The drag callback in pan/zoom mode"""

        def format_deltas(event, dx, dy):
            """Returns the correct dx and dy based on the modifier keys"""
            if event.key=='control':
                if(abs(dx)>abs(dy)):
                    dy = dx
                else:
                    dx = dy
            elif event.key=='x':
                dy = 0
            elif event.key=='y':
                dx = 0
            elif event.key=='shift':
                if 2*abs(dx) < abs(dy):
                    dx=0
                elif 2*abs(dy) < abs(dx):
                    dy=0
                elif(abs(dx)>abs(dy)):
                    dy=dy/abs(dy)*abs(dx)
                else:
                    dx=dx/abs(dx)*abs(dy)
            return (dx,dy)

        for cur_xypress in self._xypress:
            lastx, lasty, a, ind, lim, trans = cur_xypress
            xmin, xmax, ymin, ymax = lim

            #safer to use the recorded button at the press than current button:
            #multiple button can get pressed during motion...
            if self._button_pressed==1:
                lastx, lasty = trans.inverse_xy_tup( (lastx, lasty) )
                x, y = trans.inverse_xy_tup( (event.x, event.y) )
                if a.get_xscale()=='log':
                    dx=1-lastx/x
                else:
                    dx=x-lastx
                if a.get_yscale()=='log':
                    dy=1-lasty/y
                else:
                    dy=y-lasty

                dx,dy=format_deltas(event,dx,dy)

                if a.get_xscale()=='log':
                    xmin *= 1-dx
                    xmax *= 1-dx
                else:
                    xmin -= dx
                    xmax -= dx
                if a.get_yscale()=='log':
                    ymin *= 1-dy
                    ymax *= 1-dy
                else:
                    ymin -= dy
                    ymax -= dy
                    
            elif self._button_pressed==3:
                try:
                    dx=(lastx-event.x)/float(a.bbox.width())
                    dy=(lasty-event.y)/float(a.bbox.height())
                    dx,dy=format_deltas(event,dx,dy)
                    if a.get_aspect() != 'auto':
                        dx = 0.5*(dx + dy)
                        dy = dx
                    alphax = pow(10.0,dx)
                    alphay = pow(10.0,dy)
                    lastx, lasty = trans.inverse_xy_tup( (lastx, lasty) )
                    if a.get_xscale()=='log':
                        xmin = lastx*(xmin/lastx)**alphax
                        xmax = lastx*(xmax/lastx)**alphax
                    else:
                        xmin = lastx+alphax*(xmin-lastx)
                        xmax = lastx+alphax*(xmax-lastx)
                    if a.get_yscale()=='log':
                        ymin = lasty*(ymin/lasty)**alphay
                        ymax = lasty*(ymax/lasty)**alphay
                    else:
                        ymin = lasty+alphay*(ymin-lasty)
                        ymax = lasty+alphay*(ymax-lasty)
                        
                except OverflowError:
                    warnings.warn('Overflow while panning')
                    return

            a.set_xlim(xmin, xmax)
            a.set_ylim(ymin, ymax)
            
        self.canvas.draw()

    def _on_button_release(self, event):
        'the release mouse button callback in pan/zoom mode'
        self.canvas.mpl_disconnect(self._motion_notify)
        if not self._xypress: return
        self._xypress = None
        self._button_pressed = None
        self.canvas.draw()
        self.plot.emit('pan-changed', self)


class ZoomPlotMode(PlotMode):
    def __init__(self, plot):
        PlotMode.__init__(self, plot, 'zoom',
                          'Zoom to rectangle','zoom_to_rect')        
        self._selectors = {}

    def activate(self):
        self.cids = []
        for ax in self.canvas.figure.get_axes():
            selector = Selector(ax, self._on_select,
                             select_type='rectangle')
            selector.line.set_linestyle('-')
            selector.line.set_linewidth(0.5)
            self.cids.append(self.canvas.mpl_connect('button_release_event',
                                                     selector._onrelease))
            self.cids.append(self.canvas.mpl_connect('motion_notify_event',
                                                     selector._onmove))
            self.cids.append(self.canvas.mpl_connect('button_press_event',
                                                     selector._onpress))
    def deactivate(self):
        for cid in self.cids:
            self.canvas.mpl_disconnect(cid)
    
    def _on_select(self, x1, y1, x2, y2, key):
        if abs(x1-x2)<0.0001 or abs(y1-y2)<0.0001:
            # too sensitive zoom crashes matplotlib
            return
        ax = self.canvas.figure.get_axes()[0]
        ax.set_xlim((min(x1, x2), max(x1, x2)))
        ax.set_ylim((min(y1,y2), max(y1, y2)))
        self.canvas.draw()
        self.plot.emit('zoom-changed', self)


class SelectLassoPlotMode(PlotMode):
    def __init__(self, plot):
        PlotMode.__init__(self, plot, 'lassoselect',
                          'Select within lasso', 'lasso')

        self._selectors = {}
        
    def activate(self):
        self.cids = []
        for ax in self.canvas.figure.get_axes():
            lasso = Selector(ax, self.plot.lasso_select_callback,
                             select_type='lasso')
            self.cids.append(self.canvas.mpl_connect('button_release_event',
                                                     lasso._onrelease))
            self.cids.append(self.canvas.mpl_connect('motion_notify_event',
                                                     lasso._onmove))
            self.cids.append(self.canvas.mpl_connect('button_press_event',
                                                     lasso._onpress))
    def deactivate(self):
        for cid in self.cids:
            self.canvas.mpl_disconnect(cid)


class SelectRectanglePlotMode(PlotMode):
    def __init__(self, plot):
        PlotMode.__init__(self, plot, 'select',
                          'Select within rectangle', 'select')
        self._selectors = {}
        
    def activate(self):
        self.cids = []
        for ax in self.canvas.figure.get_axes():
            lasso = Selector(ax, self.plot.rectangle_select_callback,
                             select_type='rectangle')
            self.cids.append(self.canvas.mpl_connect('button_release_event',
                                                     lasso._onrelease))
            self.cids.append(self.canvas.mpl_connect('motion_notify_event',
                                                     lasso._onmove))
            self.cids.append(self.canvas.mpl_connect('button_press_event',
                                                     lasso._onpress))
    def deactivate(self):
        for cid in self.cids:
            self.canvas.mpl_disconnect(cid)


class PlotToolbar(gtk.Toolbar):
    
    def __init__(self, plot):
        gtk.Toolbar.__init__(self)
        self.plot = plot
        self.canvas = plot.canvas
        self._current_mode = None
        self.tooltips = gtk.Tooltips()
        self._mode_sensitive = {}
        ## Maps toolbar buttons to PlotMode objects.
        self._mode_buttons = {}
        self.set_property('show-arrow', False)
        self.canvas.connect('enter-notify-event', self.on_enter_notify)
        self.show()
        
        self.add_mode(DefaultPlotMode(self.plot))
        self.add_mode(PanPlotMode(self.plot))
        self.add_mode(ZoomPlotMode(self.plot))
        self.add_mode(SelectRectanglePlotMode(self.plot))
        self.add_mode(SelectLassoPlotMode(self.plot))

        self.insert(gtk.SeparatorToolItem(), -1)
        self.set_style(gtk.TOOLBAR_ICONS)

         # Set up freeze button
        btn = gtk.ToolButton(gtk.STOCK_SAVE)
        
        #btn.set_icon_widget(image)
        btn.connect('clicked', self._on_save_clicked)
        self.insert(btn, -1)
        self.save_button = btn
        
        # Set up freeze button
        btn = gtk.ToggleToolButton()

        image = gtk.Image()
        image.set_from_pixbuf(fluents.icon_factory.get('freeze'))

        btn.set_icon_widget(image)
        btn.connect('toggled', self._on_freeze_toggle)
        self.insert(btn, -1)
        self.freeze_button = btn

        self.show_all()

    def add_mode(self, mode):
        """Adds a new mode to the toolbar."""

        self._mode_sensitive[mode] = True
        if len(self._mode_buttons) > 0:
            other = self._mode_buttons.keys()[0]
        else:
            other = None

        btn = gtk.RadioToolButton(other)
        btn.set_icon_widget(mode.get_icon())
        btn.set_tooltip(self.tooltips, mode.tooltip, 'Private')
        btn.connect('toggled', self._on_mode_toggle)

        self._mode_buttons[btn] = mode
        self.insert(btn, -1)

        if self._current_mode == None:
            self._current_mode = mode

    def get_mode(self):
        """Returns the active mode name."""
        if self._current_mode:
            return self._current_mode.name
        return None
    
    def get_mode_by_name(self, mode_name):
        """Returns the mode with the given name or None."""
        for m in self._mode_buttons.values():
            if m.name == mode_name:
                return m
        return None

    def get_button(self, mode_name):
        """Returns the button that corresponds to a mode name."""
        for b, m in self._mode_buttons.items():
            if m.name == mode_name:
                return b
        return None
    
    def set_mode(self, mode_name):
        """Sets a mode by name. Returns the mode or None"""
        if mode_name == self._current_mode.name:
            return None
        
        if self._current_mode:
            self._current_mode.deactivate()

        new_mode = self.get_mode_by_name(mode_name)
        if new_mode:
            new_mode.activate()
            self._current_mode = self.get_mode_by_name(mode_name)
        else:
            logger.log('warning', 'No such mode: %s' % mode_name)

        if self.get_button(mode_name) and \
               not self.get_button(mode_name).get_active():
            self.get_button(mode_name).set_active(True)
        globals()['active_mode'] = mode_name
        return self._current_mode

    def set_mode_sensitive(self, mode, bool):
        self._mode_sensitive[mode] = bool
        self.get_button(mode).set_sensitive(False)
        
    def _on_mode_toggle(self, button):
        mode = self._mode_buttons[button].name
        if self._mode_sensitive.get(mode) == False:
            logger.log('notice', 'Mode: %s is not sensitive in this plot' %mode)
        if button.get_active():
            self.set_mode(self._mode_buttons[button].name)

    def _on_save_clicked(self, button):
        """Pops up a file dialog saver, and saves current plot"""
        dialog = gtk.FileChooserDialog('Save plot')
        dialog.set_action(gtk.FILE_CHOOSER_ACTION_SAVE)
        dialog.add_buttons(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE, gtk.RESPONSE_OK)
        default_name = "fluents_image"
        dialog.set_current_name("%s.png" % default_name)
        retval = dialog.run()
        if retval in [gtk.RESPONSE_CANCEL, gtk.RESPONSE_DELETE_EVENT]:
            logger.log("debug", "Cancelled save plot")
        elif retval == gtk.RESPONSE_OK:
            logger.log("debug", "Saving plot as: %s" % dialog.get_filename())
            self.canvas.print_figure(dialog.get_filename())
        else:
            print "unknown; ", retval 
        dialog.destroy()
        
    def _on_freeze_toggle(self, button):
        self.plot.set_frozen(button.get_active())

    def on_enter_notify(self, widget, event):
        self.set_mode(active_mode)
        # need views (plots) to grab key-events
        widget.grab_focus()


class Selector:
    def __init__(self, ax, callback, select_type='lasso'):
        self.drawon = True
        self.eventson = True
        self.select_type = select_type
        self.eventpress = None
        self.eventrelease = None
        self._background = None
        self.callback = callback
        self.axes = ax
        self.figure = self.axes.figure
        self.canvas = self.figure.canvas
        self.verts = []
        # to draw
        patch_props = dict(facecolor='gray', linewidth = 0,
                                 alpha=0.3, fill=True)
        line_props  = dict(linestyle='--', lw=1.0, color='black')

        self.line = Line2D([0,0], [0,0], visible=False, **line_props)
        self.axes.add_line(self.line)
        if self.select_type == 'lasso':
            self.polygon = Polygon([(0, 0)], visible=False, **patch_props)
        elif self.select_type == 'rectangle':
            self.polygon = Rectangle((0,0), 0, 1, visible=False, **patch_props)
        self.axes.add_patch(self.polygon)
    
    def _onrelease(self, event):
        if self.eventpress is None or self._ignore(event):
            return
        
        self.eventrelease = event
        self._show_selector(False)
        if self.verts is not None:
            self.verts.append((event.xdata, event.ydata))
            if self.select_type == 'lasso':
                if len(self.verts)>2:
                    self.callback(self.verts, event.key)
            elif self.select_type == 'rectangle':
                    x1, y1 = self.eventpress.xdata, self.eventpress.ydata
                    x2, y2 = self.eventrelease.xdata, self.eventrelease.ydata
                    self.callback(x1, y1, x2, y2, event.key)

        self.verts = []
        self.eventpress = None
        self.eventrelease = None
        self._background = None
        return False
        
    def _onmove(self, event):
        if self.eventpress is None or self._ignore(event):return

        # add new data
        if self.select_type == 'lasso':
            self.verts.append((event.xdata, event.ydata))
            self.polygon.xy = self.verts
            self.line.set_data(zip(*self.verts))
        
        elif self.select_type == 'rectangle':
            x,y = event.xdata, event.ydata
            minx, maxx = self.eventpress.xdata, x 
            miny, maxy = self.eventpress.ydata, y 
            if minx>maxx: minx, maxx = maxx, minx 
            if miny>maxy: miny, maxy = maxy, miny
            self.polygon.xy[0] = minx             
            self.polygon.xy[1] = miny
            self.polygon.set_width(maxx-minx)     
            self.polygon.set_height(maxy-miny)
            xx =  [minx, maxx, maxx, minx, minx]
            yy = [miny, miny, maxy, maxy, miny]
            self.line.set_data(xx,yy)
            
        # draw
        self.canvas.restore_region(self._background)
        self.axes.draw_artist(self.line)
        self.axes.draw_artist(self.polygon)
        self.canvas.blit(self.axes.bbox)

    def _onpress(self, event):
        # Is the event to be ignored?
        if self._ignore(event): return
        self._show_selector(True)
        self.eventpress = event
        self.verts = [(event.xdata, event.ydata)]
        return False

    def _ignore(self, event):
        'return True if event should be ignored'
        ignore = False
        # If no button was pressed yet ignore the event if it was out
        # of the axes
        if self.eventpress == None:
            return event.inaxes!= self.axes

        # If a button was pressed, check if the release-button is the
        # same.
        ignore = event.inaxes!=self.axes or\
                 event.button != self.eventpress.button
        
        if ignore:
            # remove poly + line if they are visible
            if self.polygon._visible or self.line._visible:
                self._show_selector(False)
        else:
           if not self.polygon._visible or not self.line._visible: 
               # put poly + line back if we event is ok
               self._show_selector(True)
    
        return ignore

    def _show_selector(self, bool):
        if self.polygon and self.line:
            self.polygon.set_visible(bool)
            self.line.set_visible(bool)
            if self._background == None:
                self._background = self.canvas.copy_from_bbox(self.axes.bbox)
            self.canvas.restore_region(self._background)
            self.canvas.blit()


# Create a view-changed signal that should be emitted every time
# the active view changes.
gobject.signal_new('view-changed', MainView, gobject.SIGNAL_RUN_LAST,
                   gobject.TYPE_NONE, 
                   (gobject.TYPE_PYOBJECT,))

# Create focus-changed signal
gobject.signal_new('focus-changed', ViewFrame, gobject.SIGNAL_RUN_LAST,
                   gobject.TYPE_NONE, 
                   (gobject.TYPE_PYOBJECT, gobject.TYPE_BOOLEAN,))