Source code for wbia.plottool.abstract_interaction

# -*- coding: utf-8 -*-
"""
Known Interactions that use AbstractInteraction:
    pt.MatchInteraction2
    pt.MultiImageInteraction
    wbia.NameInteraction
"""
import re
import utool as ut
import matplotlib as mpl

ut.noinject(__name__, '[abstract_iteract]')
from . import draw_func2 as df2  # NOQA
from wbia.plottool import fig_presenter  # NOQA
from wbia.plottool import plot_helpers as ph  # NOQA
from wbia.plottool import interact_helpers as ih  # NOQA

# (print, print_, printDBG, rrr, profile) = utool.inject(__name__,
# '[abstract_iteract]')

DEBUG = ut.get_argflag('--debug-interact')
VERBOSE = ut.VERBOSE or True


# for scoping
__REGISTERED_INTERACTIONS__ = []


[docs]def register_interaction(self): global __REGISTERED_INTERACTIONS__ if VERBOSE: print('[pt] Registering intearction: self=%r' % (self,)) __REGISTERED_INTERACTIONS__.append(self) if VERBOSE: print( '[pt] There are now %d registered interactions' % (len(__REGISTERED_INTERACTIONS__)) )
[docs]def unregister_interaction(self): global __REGISTERED_INTERACTIONS__ if VERBOSE: print('[pt] Unregistering intearction: self=%r' % (self,)) try: __REGISTERED_INTERACTIONS__.remove(self) except ValueError: pass if VERBOSE: print( '[pt] There are now %d registered interactions' % (len(__REGISTERED_INTERACTIONS__)) )
[docs]class AbstractInteraction(object): """ An interaction is meant to take up an entire figure overwrite either self.plot(fnum, pnum) or self.staic_plot(fnum, pnum) or show_page """ LEFT_BUTTON = 1 MIDDLE_BUTTON = 2 RIGHT_BUTTON = 3 MOUSE_BUTTONS = { LEFT_BUTTON: 'left', MIDDLE_BUTTON: 'middle', RIGHT_BUTTON: 'right', } def __init__(self, **kwargs): debug = kwargs.get('debug', None) self.debug = DEBUG if debug is None else debug if self.debug: print('[pt.a] create interaction') self.fnum = kwargs.get('fnum', None) if self.fnum is None: self.fnum = df2.next_fnum() self.interaction_name = kwargs.get('interaction_name', 'AbstractInteraction') # Careful this might cause memory leaks self.scope = [] # for keeping those widgets alive! self.is_down = {} self.is_drag = {} self.is_running = False self.pan_event_list = [] self.zoom_event_list = [] self.fig = getattr(self, 'fig', None) for button in self.MOUSE_BUTTONS.values(): self.is_down[button] = None self.is_drag[button] = None autostart = kwargs.get('autostart', False) if autostart: self.start()
[docs] def reset_mouse_state(self): for key in self.is_down.keys(): self.is_down[key] = False for key in self.is_drag.keys(): self.is_drag[key] = False
[docs] def enable_pan_and_zoom(self, ax): self.enable_zoom(ax) self.enable_pan(ax)
[docs] def enable_pan(self, ax): from wbia.plottool.interactions import PanEvents pan = PanEvents(ax) self.pan_event_list.append(pan)
[docs] def enable_zoom(self, ax): from wbia.plottool.interactions import zoom_factory self.zoom_event_list.append(zoom_factory(ax))
def _start_interaction(self): # self.fig = df2.figure(fnum=self.fnum, doclf=True, docla=True) self.fig = df2.figure(fnum=self.fnum, doclf=True) ih.connect_callback(self.fig, 'close_event', self.on_close) register_interaction(self) self.is_running = True def _ensure_running(self): if not self.is_running: self._start_interaction()
[docs] def start(self): self._ensure_running() self.show_page() self.show()
[docs] def print_status(self): print('is_down = ' + ut.repr2(self.is_down)) print('is_drag = ' + ut.repr2(self.is_drag))
def _preshow_page(self): self._ensure_running() if self.debug: print('[pt.a] show page') self.fig = ih.begin_interaction(self.interaction_name, fnum=self.fnum) def _postshow_page(self): self.connect_callbacks() def _show_page(self): if hasattr(self, 'plot'): self.plot(fnum=self.fnum, pnum=(1, 1, 1)) else: self.static_plot(fnum=self.fnum, pnum=(1, 1, 1))
[docs] def show_page(self, *args): """ Hack: this function should probably not be defined, but it is for convinience of a developer. Override this or create static plot function (preferably override) """ self._preshow_page() self._show_page() self._postshow_page()
[docs] def connect_callbacks(self): if self.debug: print('[pt.a] connect_callbacks') ih.connect_callback(self.fig, 'button_press_event', self.on_click) ih.connect_callback(self.fig, 'button_release_event', self.on_click_release) ih.connect_callback(self.fig, 'key_press_event', self.on_key_press) ih.connect_callback(self.fig, 'motion_notify_event', self.on_motion) ih.connect_callback(self.fig, 'draw_event', self.on_draw) ih.connect_callback(self.fig, 'scroll_event', self.on_scroll)
[docs] def bring_to_front(self): import utool with utool.embed_on_exception_context: fig_presenter.bring_to_front(self.fig)
[docs] def draw(self): if self.debug > 5: print('[pt.a] draw') self.fig.canvas.draw()
[docs] def on_draw(self, event=None): if self.debug > 5: print('[pt.a] on draw') pass
[docs] def show(self): if self.debug: print('[pt.a] show') self.fig.show()
[docs] def update(self): if self.debug: print('[pt.a] update') # fig_presenter.update() self.fig.canvas.update() self.fig.canvas.flush_events()
[docs] def on_scroll(self, event): if self.debug: print('[pt.a] on_scroll') print(ut.repr3(event.__dict__)) pass
[docs] def close(self): assert isinstance(self.fig, mpl.figure.Figure) fig_presenter.close_figure(self.fig)
[docs] def on_close(self, event=None): print('[pt] handling interaction close') unregister_interaction(self) self.is_running = False
[docs] def on_motion(self, event): if self.debug > 5: print('[pt.a] on_motion') for button in self.MOUSE_BUTTONS.values(): if self.is_down[button]: if not self.is_drag[button]: self.is_drag[button] = True self.on_drag_start(event) if any(self.is_drag.values()): self.on_drag(event) for pan in self.pan_event_list: pan.pan_on_motion(event) # print('event = %r' % (event.__dict__,)) pass
[docs] def on_drag(self, event=None): if self.debug > 1: print('[pt.a] on_drag') if ih.clicked_inside_axis(event): self.on_drag_inside(event) # Make sure BLIT (bit block transfer) is used for updates # self.fig.canvas.blit(self.fig.ax.bbox) pass
[docs] def on_drag_inside(self, event=None): if self.debug > 1: print('[pt.a] on_drag_inside')
[docs] def on_drag_stop(self, event=None): if self.debug > 0: print('[pt.a] on_drag_stop') pass
[docs] def on_drag_start(self, event=None): if self.debug > 0: print('[pt.a] on_drag_start') pass
[docs] def on_key_press(self, event): if self.debug > 0: print('[pt.a] on_key_press') # self.print_status() pass
[docs] def on_click(self, event): if self.debug > 0: print('[pt.a] on_click') # print('[pt.a] on_click. event=%r' % (ut.repr2(event.__dict__))) # raise NotImplementedError('implement yourself') if event.button is not None: for button in self.MOUSE_BUTTONS.values(): if self.MOUSE_BUTTONS[event.button] == button: self.is_down[button] = True # if event.button == self.LEFT_BUTTON: # self.is_down['left'] = True # if event.button == self.RIGHT_BUTTON: # self.is_down['right'] = True # if event.button == self.MIDDLE_BUTTON: # self.is_down['middle'] = True if ih.clicked_inside_axis(event): ax = event.inaxes self.on_click_inside(event, ax) else: self.on_click_outside(event) for pan in self.pan_event_list: pan.pan_on_press(event)
[docs] def on_click_release(self, event): if self.debug > 0: print('[pt.a] on_release') for button in self.MOUSE_BUTTONS.values(): flag = ( event is None or event.button is None or self.MOUSE_BUTTONS[event.button] == button ) if flag: self.is_down[button] = False if self.is_drag[button]: self.is_drag[button] = False self.on_drag_stop(event) # if event.button == self.LEFT_BUTTON: # self.is_down['left'] = False # self.is_drag['left'] = False # if event.button == self.RIGHT_BUTTON: # self.is_down['right'] = False # if event.button == self.MIDDLE_BUTTON: # self.is_down['middle'] = False for pan in self.pan_event_list: pan.pan_on_release(event)
[docs] def on_click_inside(self, event, ax): pass
[docs] def on_click_outside(self, event): pass
[docs] def show_popup_menu(self, options, event): """ context menu """ import wbia.guitool as gt height = self.fig.canvas.geometry().height() qpoint = gt.newQPoint(event.x, height - event.y) qwin = self.fig.canvas gt.popup_menu(qwin, qpoint, options)
[docs] def clear_parent_axes(self, ax): """for clearing axes that we appended anything to""" child_axes = ph.get_plotdat(ax, 'child_axes', []) ph.set_plotdat(ax, 'child_axes', []) for subax in child_axes: to_remove = None for tup in self.scope: if tup[1] is subax: to_remove = tup break if to_remove is not None: self.scope.remove(to_remove) subax.cla() self.fig.delaxes(subax) ph.del_plotdat(ax, df2.DF2_DIVIDER_KEY) ax.cla()
# def make_hud(self): # def prepare_page(self, pagenum): # self.clean_scope()
[docs] def clean_scope(self): """Removes any widgets saved in the interaction scope""" self.scope = []
[docs] def append_button( self, text, divider=None, rect=None, callback=None, size='9%', location='bottom', ax=None, **kwargs, ): """Adds a button to the current page""" if rect is not None: new_ax = df2.plt.axes(rect) if rect is None and divider is None: if ax is None: ax = df2.gca() divider = df2.ensure_divider(ax) if divider is not None: new_ax = divider.append_axes(location, size=size, pad=0.05) if callback is not None: color, hovercolor = '.85', '.95' else: color, hovercolor = '.88', '.88' # color, hovercolor = u'.45', u'.45' # if isinstance(text, str): new_but = mpl.widgets.Button(new_ax, text, color=color, hovercolor=hovercolor) # elif isinstance(text, (list, tuple)): # labels = [False] * len(text) # labels[0] = True # new_but = mpl.widgets.CheckButtons(new_ax, text, labels) # else: # raise ValueError('bad input') if callback is not None: new_but.on_clicked(callback) else: button_text = new_but.ax.texts[0] button_text.set_color('.6') # button_text.set_color('r') # ut.embed() # print('new_but.color = %r' % (new_but.color,)) # else: # TODO: figure ou how to gray out these buttons # new_but.color = u'.1' # new_but.hovercolor = u'.1' # new_but.active = False # print('new_but.color = %r' % (new_but.color,)) ph.set_plotdat(new_ax, 'viztype', 'button') ph.set_plotdat(new_ax, 'text', text) # ph.set_plotdat(new_ax, 'parent_axes', ax) if ax is not None: child_axes = ph.get_plotdat(ax, 'child_axes', []) child_axes.append(new_ax) ph.set_plotdat(ax, 'child_axes', child_axes) for key, val in kwargs.items(): ph.set_plotdat(new_ax, key, val) # Keep buttons from losing scrop tup = (new_but, new_ax) self.scope.append(tup) return tup
[docs]class AbstractPagedInteraction(AbstractInteraction): def __init__(self, nPages=None, draw_hud=True, **kwargs): self.current_pagenum = 0 assert nPages is not None self.nPages = nPages self.draw_hud = draw_hud self.NEXT_PAGE_HOTKEYS = ['right', 'pagedown'] self.PREV_PAGE_HOTKEYS = ['left', 'pageup'] super(AbstractPagedInteraction, self).__init__(**kwargs)
[docs] def next_page(self, event): # print('next') self.show_page(self.current_pagenum + 1) pass
[docs] def prev_page(self, event): if self.current_pagenum == 0: return # print('prev') self.show_page(self.current_pagenum - 1) pass
[docs] def make_hud(self): """Creates heads up display""" import wbia.plottool as pt if not self.draw_hud: return # Button positioning # w, h = .08, .04 # w, h = .14, .08 w, h = 0.14, 0.07 hl_slot, hr_slot = pt.make_bbox_positioners( y=0.02, w=w, h=h, xpad=0.05, startx=0, stopx=1 ) prev_rect = hl_slot(0) next_rect = hr_slot(0) # print('prev_rect = %r' % (prev_rect,)) # print('next_rect = %r' % (next_rect,)) # Create buttons prev_callback = None if self.current_pagenum == 0 else self.prev_page next_callback = ( None if self.current_pagenum == self.nPages - 1 else self.next_page ) prev_text = 'prev\n' + pretty_hotkey_map(self.PREV_PAGE_HOTKEYS) next_text = 'next\n' + pretty_hotkey_map(self.NEXT_PAGE_HOTKEYS) self.append_button(prev_text, callback=prev_callback, rect=prev_rect) self.append_button(next_text, callback=next_callback, rect=next_rect)
[docs] def prepare_page(self, fulldraw=True): import wbia.plottool as pt ih.disconnect_callback(self.fig, 'button_press_event') ih.disconnect_callback(self.fig, 'button_release_event') ih.disconnect_callback(self.fig, 'key_press_event') ih.disconnect_callback(self.fig, 'motion_notify_event') figkw = { 'fnum': self.fnum, 'doclf': fulldraw, 'docla': fulldraw, } if fulldraw: self.fig = pt.figure(**figkw) self.make_hud() ih.connect_callback(self.fig, 'button_press_event', self.on_click) ih.connect_callback(self.fig, 'button_release_event', self.on_click_release) ih.connect_callback(self.fig, 'key_press_event', self.on_key_press) ih.connect_callback(self.fig, 'motion_notify_event', self.on_motion)
[docs] def on_key_press(self, event): if matches_hotkey(event.key, self.PREV_PAGE_HOTKEYS): if self.current_pagenum != 0: self.prev_page(event) if matches_hotkey(event.key, self.NEXT_PAGE_HOTKEYS): if self.current_pagenum != self.nPages - 1: self.next_page(event) self.draw()
# moved to interactions.py # def zoom_factory(ax, zoomable_list, base_scale=1.1): # """ # TODO: make into interaction # References: # https://gist.github.com/tacaswell/3144287 # """ # def zoom_fun(event): # #print('zooming') # # get the current x and y limits # cur_xlim = ax.get_xlim() # cur_ylim = ax.get_ylim() # xdata = event.xdata # get event x location # ydata = event.ydata # get event y location # if xdata is None or ydata is None: # return # if event.button == 'up': # # deal with zoom in # scale_factor = 1 / base_scale # elif event.button == 'down': # # deal with zoom out # scale_factor = base_scale # else: # raise NotImplementedError('event.button=%r' % (event.button,)) # # deal with something that should never happen # scale_factor = 1 # print(event.button) # for zoomable in zoomable_list: # zoom = zoomable.get_zoom() # new_zoom = zoom / (scale_factor ** (1.2)) # zoomable.set_zoom(new_zoom) # # Get distance from the cursor to the edge of the figure frame # x_left = xdata - cur_xlim[0] # x_right = cur_xlim[1] - xdata # y_top = ydata - cur_ylim[0] # y_bottom = cur_ylim[1] - ydata # ax.set_xlim([xdata - x_left * scale_factor, xdata + x_right * scale_factor]) # ax.set_ylim([ydata - y_top * scale_factor, ydata + y_bottom * scale_factor]) # # ---- # ax.figure.canvas.draw() # force re-draw # fig = ax.get_figure() # get the figure of interest # # attach the call back # fig.canvas.mpl_connect('scroll_event', zoom_fun) # #return the function # return zoom_fun
[docs]def pretty_hotkey_map(hotkeys): if hotkeys is None: return '' hotkeys = [hotkeys] if not isinstance(hotkeys, list) else hotkeys mapping = { # 'right': 'right arrow', # 'left': 'left arrow', } mapped_hotkeys = [mapping.get(hk, hk) for hk in hotkeys] hotkey_str = '(' + ut.conj_phrase(mapped_hotkeys, 'or') + ')' return hotkey_str
[docs]def matches_hotkey(key, hotkeys): hotkeys = [hotkeys] if not isinstance(hotkeys, list) else hotkeys # flags = [re.match(hk, '^' + key + '$') for hk in hotkeys] flags = [re.match(hk, key) is not None for hk in hotkeys] return any(flags)