Division of plots -> view/plots
This commit is contained in:
parent
bf1ad1be87
commit
059cddbc16
984
fluents/view.py
Normal file
984
fluents/view.py
Normal file
@ -0,0 +1,984 @@
|
||||
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.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._view_frames = []
|
||||
self._views = ObjectTable(2, 2, lambda : ViewFrame(self._view_frames))
|
||||
self._large_view = ViewFrame(list())
|
||||
self.update_small_views()
|
||||
|
||||
for vf in self._view_frames:
|
||||
vf.connect('focus-changed', self._on_view_focus_changed)
|
||||
|
||||
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"""
|
||||
|
||||
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()
|
||||
|
||||
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,))
|
Reference in New Issue
Block a user