Ejemplo n.º 1
0
class DeviceViewOptions(SlaveView):
    gsignal('connections-toggled', bool)
    gsignal('connections-alpha-changed', float)

    def create_ui(self):
        super(DeviceViewOptions, self).create_ui()
        self.widget.set_orientation(gtk.ORIENTATION_HORIZONTAL)

        self.connections_button = gtk.CheckButton('Connections')
        self.connections_alpha_label = gtk.Label('Opacity:')

        # Note that the page_size value only makes a difference for scrollbar
        # widgets, and the highest value you'll get is actually
        # (upper - page_size).
        # value, lower, upper, step_increment, page_increment, page_size
        self.connections_alpha_adjustment = gtk.Adjustment(100, 0, 110, 1, 10,
                                                           10)

        self.connections_alpha_scale = \
            gtk.HScale(self.connections_alpha_adjustment)
        self.connections_alpha_scale.set_size_request(100, 40)
        self.connections_alpha_scale.set_update_policy(gtk.UPDATE_DELAYED)
        self.connections_alpha_scale.set_digits(0)
        self.connections_alpha_scale.set_value_pos(gtk.POS_TOP)
        self.connections_alpha_scale.set_draw_value(True)

        widgets = [self.connections_button, self.connections_alpha_label,
                   self.connections_alpha_scale]
        for w in widgets:
            self.widget.pack_start(w, False, False, 5)

    def on_connections_button__toggled(self, button):
        self.emit('connections-toggled', button.get_property('active'))

    def on_connections_alpha_adjustment__value_changed(self, adjustment):
        self.emit('connections-alpha-changed', adjustment.value / 100.)

    @property
    def connections_alpha(self):
        return self.connections_button.get_property('active')

    @connections_alpha.setter
    def connections_alpha(self, value):
        self.connections_alpha_adjustment.value = value * 100

    @property
    def connections(self):
        return self.connections_button.get_property('active')

    @connections.setter
    def connections(self, active):
        self.connections_button.set_property('active', active)
Ejemplo n.º 2
0
class PluginConnection(SlaveView):
    gsignal('plugin-connected', object)

    def __init__(self, hub_uri='tcp://localhost:31000', plugin_name=None):
        self._hub_uri = hub_uri
        self._plugin_name = (generate_plugin_name()
                             if plugin_name is None else plugin_name)
        super(PluginConnection, self).__init__()

    def create_ui(self):
        super(PluginConnection, self).create_ui()
        self.widget.set_orientation(gtk.ORIENTATION_VERTICAL)
        self.top_row = gtk.HBox()

        self.plugin_uri_label = gtk.Label('Plugin hub URI:')
        self.plugin_uri = gtk.Entry()
        self.plugin_uri.set_text(self._hub_uri)
        self.plugin_uri.set_width_chars(len(self.plugin_uri.get_text()))
        self.ui_plugin_name_label = gtk.Label('UI plugin name:')
        self.ui_plugin_name = gtk.Entry()
        self.ui_plugin_name.set_text(self._plugin_name)
        self.ui_plugin_name.set_width_chars(len(
            self.ui_plugin_name.get_text()))
        self.connect_button = gtk.Button('Connect')

        top_widgets = [
            self.plugin_uri_label, self.plugin_uri, self.ui_plugin_name_label,
            self.ui_plugin_name, self.connect_button
        ]
        for w in top_widgets:
            self.top_row.pack_start(w, False, False, 5)
        for w in (self.top_row, ):
            self.widget.pack_start(w, False, False, 5)

    def create_plugin(self, plugin_name, hub_uri):
        return Plugin(plugin_name,
                      hub_uri,
                      subscribe_options={zmq.SUBSCRIBE: ''})

    def init_plugin(self, plugin):
        # Initialize sockets.
        plugin.reset()
        return plugin

    def on_connect_button__clicked(self, event):
        '''
        Connect to Zero MQ plugin hub (`zmq_plugin.hub.Hub`) using the settings
        from the text entry fields (e.g., hub URI, plugin name).

        Emit `plugin-connected` signal with the new plugin instance after hub
        connection has been established.
        '''
        hub_uri = self.plugin_uri.get_text()
        ui_plugin_name = self.ui_plugin_name.get_text()

        plugin = self.create_plugin(ui_plugin_name, hub_uri)
        self.init_plugin(plugin)

        self.connect_button.set_sensitive(False)
        self.emit('plugin-connected', plugin)
Ejemplo n.º 3
0
class BarcodeScanner(gobject.GObject):
    '''
    GObject barcode scanner class, which can scan frames from a GStreamer
    pipeline for barcodes.

    Usage
    -----

        scanner = BarcodeScanner(<`gst-launch` pipeline command>)

        # Start GStreamer pipeline.
        scanner.start()  # Emits `frame-update` signal for every video frame
        # Start scanning each frame for barcode(s).
        scanner.enable_scan()  # Emits `symbols-found` signal if symbols found

        # Stop scanning frames for barcode(s).
        scanner.disable_scan()
        # Pause GStreamer pipeline, but do not release video source.
        scanner.pause()
        # Stop GStreamer pipeline (e.g., free webcam).
        scanner.stop()

    Signals
    -------

     - `frame-update`: `(scanner, np_img)`
         * `scanner` (`BarcodeScanner`): Scanner object.
         * `np_img` (`numpy.ndarray`): Video frame, with shape of
           `(height, width, channels)`.
     - `symbols-found`: `(scanner, np_img, symbols)`
         * `scanner` (`BarcodeScanner`): Scanner object.
         * `np_img` (`numpy.ndarray`): Video frame containing found symbols,
           with shape of `(height, width, channels)`.
         * `symbols` (`list`): List of symbol record dictionaries.  Each
           record contains the following:
             - `type` (`str`): Type of `zbar` code (e.g., `QRCODE`).
             - `data` (`str`): Data from `zbar` symbol.
             - `symbol` (`zbar.Symbol`): Symbol object.
             - `timestamp` (`str`): UTC timestamp in ISO 8601 format.
    '''
    gsignal('frame-update', object)  # Args: `(scanner, np_img)`
    gsignal('symbols-found', object,
            object)  # Args: `(scanner, np_img, symbols)`

    def __init__(self, pipeline_command=None):
        super(BarcodeScanner, self).__init__()
        self.pipeline_command = pipeline_command
        self.connect('frame-update', self.process_frame)
        self.scanner = zbar.ImageScanner()
        self.scanner.parse_config('enable=0')
        self.scanner.parse_config('ean8.enable=1')
        self.scanner.parse_config('ean13.enable=1')
        self.scanner.parse_config('upce.enable=1')
        self.scanner.parse_config('isbn10.enable=1')
        self.scanner.parse_config('isbn13.enable=1')
        self.scanner.parse_config('i25.enable=1')
        self.scanner.parse_config('upca.enable=1')
        self.scanner.parse_config('code39.enable=1')
        self.scanner.parse_config('qrcode.enable=1')
        self.scanner.parse_config('code128.enable=1')
        self.scanner.parse_config('code128.ascii=1')
        self.scanner.parse_config('code128.min=3')
        self.scanner.parse_config('code128.max=8')
        self.scan_id = None
        self.pipeline = None

    def __dealloc__(self):
        self.stop()

    ###########################################################################
    # Callback methods
    def process_frame(self, obj, np_img):
        import PIL.Image
        import zbar

        if self.status.get('processing_scan'):
            return True
        self.status['processing_scan'] = True
        pil_image = PIL.Image.fromarray(np_img)
        raw = pil_image.convert(mode='L').tobytes()
        height, width, channels = np_img.shape
        zbar_image = zbar.Image(width, height, 'Y800', raw)
        self.scanner.scan(zbar_image)

        symbols = [{
            'timestamp': datetime.utcnow().isoformat(),
            'type': str(s.type),
            'data': str(s.data),
            'symbol': s
        } for s in zbar_image]

        def symbols_equal(a, b):
            key = lambda v: (v['type'], v['data'])
            if len(a) == len(b):
                return all([
                    a_i[k] == b_i[k] for k in ('type', 'data')
                    for a_i, b_i in zip(sorted(a, key=key), sorted(b, key=key))
                ])
            return False

        if symbols and not symbols_equal(symbols, self.status.get(
                'symbols', [])):
            self.emit('symbols-found', np_img, symbols)
            self.status['symbols'] = symbols
            self.status['np_img'] = np_img

        self.status['np_img'] = np_img
        self.status['processing_scan'] = False

    ###########################################################################
    # Control methods
    def disable_scan(self):
        '''
        Stop scanning frames for barcode(s).
        '''
        if self.scan_id is not None:
            self.disconnect(self.scan_id)
            self.scan_id = None
        self.reset()

    def enable_scan(self):
        '''
        Start scanning each frame for barcode(s).
        '''
        self.scan_id = self.connect('frame-update', self.process_frame)

    def pause(self):
        '''
        Pause GStreamer pipeline, but do not release video source.
        '''
        import gst

        if self.pipeline is not None:
            self.pipeline.set_state(gst.STATE_PAUSED)

    def reset(self):
        self.status = {'processing_frame': False, 'processing_scan': False}

    def start(self, pipeline_command=None, enable_scan=False):
        '''
        Start GStreamer pipeline and configure pipeline to trigger
        `frame-update` for every new video frame.
        '''
        import gst

        self.reset()
        if pipeline_command is None:
            if self.pipeline_command is None:
                raise ValueError('No default pipeline command available.  Must'
                                 ' provide `pipeline_command` argument.')
            else:
                pipeline_command = self.pipeline_command

        if self.pipeline is not None:
            self.stop()

        pipeline = gst.parse_launch(unicode(pipeline_command).encode('utf-8'))
        self.pipeline = pipeline
        app = pipeline.get_by_name('app-video')
        self.reset()

        def on_new_buffer(appsink):
            import numpy as np

            self.status['processing_frame'] = True
            buf = appsink.emit('pull-buffer')
            caps = buf.caps[0]
            np_img = (np.frombuffer(buf.data, dtype='uint8',
                                    count=buf.size).reshape(
                                        caps['height'], caps['width'], -1))
            self.emit('frame-update', np_img)
            self.status['processing_frame'] = False

        app.connect('new-buffer', on_new_buffer)

        pipeline.set_state(gst.STATE_PAUSED)
        pipeline.set_state(gst.STATE_PLAYING)
        self.pipeline = pipeline
        self.pipeline_command = pipeline_command
        if enable_scan:
            self.enable_scan()
        return pipeline, self.status

    def stop(self):
        '''
        Stop GStreamer pipeline (e.g., free webcam).
        '''
        import gst

        self.pause()
        if self.pipeline is not None:
            self.pipeline.set_state(gst.STATE_NULL)
            del self.pipeline
            self.pipeline = None
Ejemplo n.º 4
0
class PidaPaned(BigPaned):

    gsignal('pane-attachment-changed', gobject.TYPE_PYOBJECT, bool)

    def __init__(self):
        BigPaned.__init__(self)
        self._fullscreen = False
        self._fullscreen_vis = {}
        self.set_property('enable-detaching', True)
        self.set_name('PidaBigPaned')
        for paned in self.get_all_paneds(True):
            paned.connect('notify::active-pane', self._active_pane_change)

    @staticmethod
    def _active_pane_change(paned, dummy):
        """
        Remembers the last active pane
        """
        if paned and paned.props.active_pane:
            paned.last_pane = paned.props.active_pane.weak_ref()

    def get_all_pos(self, every=False):
        if every:
            return [
                PANE_POS_TOP, PANE_POS_BOTTOM, PANE_POS_LEFT, PANE_POS_RIGHT
            ]
        return [PANE_POS_TOP, PANE_POS_LEFT, PANE_POS_RIGHT]

    def get_all_paneds(self, every=False):
        for pos in self.get_all_pos(every):
            yield self.get_paned(pos)

    def _on_pane_param_changed(self, pane, props):
        """
        we save the detached param and send a clean signal so
        services can do stuff when windows are de/attached
        """
        if pane._was_detached != pane.get_params().detached:
            self.emit('pane-attachment-changed', pane,
                      pane.get_params().detached)
            pane._was_detached = pane.get_params().detached
            if pane.get_params().detached:
                pane.get_child().get_toplevel().connect(
                    'focus-in-event', self._on_focus_detached, pane)

    def _on_focus_detached(self, widget, direction, pane):
        paned = self.get_paned_of_pane(pane)
        if paned:
            paned.last_pane = pane.weak_ref()

    def get_paned_of_pane(self, pane):
        """
        Returns the paned of a pane. There is no dirct api :(
        """
        for paned in self.get_all_paneds(True):
            if pane in paned.list_panes():
                return paned

    def add_view(self,
                 name,
                 view,
                 removable=True,
                 present=True,
                 detachable=True):
        if name == PANE_EDITOR:
            self.add_child(view.get_toplevel())
        else:
            POS = POS_MAP[name]
            lab = PaneLabel(view.icon_name, None, view.label_text)
            if use_old:
                pane = self.insert_pane(view.get_toplevel(), lab, POS, POS)
            else:
                pane = view.key and self.lookup_pane(view.key) or None
                if pane:
                    # we will get a key collission if we dont remove first
                    self.remove_view(pane.view)

                pane = self.insert_pane(view.get_toplevel(), view.key, lab,
                                        POS, POS)

            pane.props.detachable = detachable
            #self.set_params(pane, keep_on_top=True)
            view.pane = pane
            pane._was_detached = False
            pane.connect('notify::params', self._on_pane_param_changed)
            pane.view = view
            if not removable:
                pane.set_property('removable', False)
            view._on_remove_attempt_id = pane.connect('remove',
                                                      view.on_remove_attempt)
            view.toplevel.parent.set_name('PidaWindow')
            if present:
                gcall(self.present_pane, view.get_toplevel())
            self.show_all()

    def __contains__(self, item):
        if not isinstance(item, Pane):
            item = item.pane
        for paned in self.get_all_paneds(True):
            for pane in paned.list_panes():
                if pane == item:
                    return True
        return False

    def remove_view(self, view):
        # remove the default handler and fire the remove handler
        # this ensures the remove event is fired at least once
        if view.pane:
            view.pane.disconnect(view._on_remove_attempt_id)
            view.pane.emit('remove')
        self.remove_pane(view.get_toplevel())
        view.pane = None

    def detach_view(self, view, size=(400, 300)):
        paned, pos = self.find_pane(view.get_toplevel())
        dparam = PaneParams(keep_on_top=True, detached=True)
        paned.set_params(dparam)
        paned.detach()
        self._center_on_parent(view, size)

    def present_view(self, view):
        pane, pos = self.find_pane(view.get_toplevel())
        pane.present()

    def list_panes(self, every=False):
        for paned in self.get_all_paneds(every):
            for pane in paned.list_panes():
                yield pane.view

    def get_open_pane(self, name):
        POS = POS_MAP[name]
        paned = self.get_paned(POS)
        pane = None
        if self.get_toplevel().is_active():
            pane = paned.get_open_pane()
        else:
            # we don't have the focus which means that a detached window
            # may have it.
            for pane2 in paned.list_panes():
                if pane2.get_params().detached and \
                   pane2.get_child().get_toplevel().is_active():
                    return paned, pane2
        return paned, pane

    def present_pane_if_not_focused(self, pane):
        """
        Present a pane if it (means any child) does not have the focus

        Returns True if the pane was presented
        """
        # test if it is detached
        if pane.get_params().detached:
            if not pane.view.toplevel.get_toplevel().is_active():
                pane.view.toplevel.get_toplevel().present()
                return True
            else:
                return False

        # most top focus candidate
        if getattr(pane.view, 'focus_ignore', False):
            return False
        focus = pane.view.toplevel.get_focus_child()
        while hasattr(focus, 'get_focus_child'):
            # we dive into the children until we find a child that has focus
            # or does not have a child
            if focus.is_focus():
                break
            focus = focus.get_focus_child()
        if not focus or not focus.is_focus():
            pane.present()
            return True
        return False

    @staticmethod
    def set_params(pane, **kwargs):
        """
        Updates the parameters on a pane.
        Keyword arguments can be one of the following:

        @keep_on_top: sets the sticky flag
        @detached: sets if the window is detached from the main window
        @window_position: position of the pane in detached mode
        @maximized: ???
        """
        oparam = pane.get_params()
        #OMFG don't look at this,
        # but changing the params does not work for keep_on_top
        try:
            mbuttons = pane.get_child().get_parent().get_parent().\
                            get_children()[0].get_children()
            if len(mbuttons) == 5 and isinstance(mbuttons[2],
                                                 gtk.ToggleButton):
                # only click works...
                is_top = mbuttons[2].get_active()
            elif len(mbuttons) == 3 and isinstance(mbuttons[1],
                                                   gtk.ToggleButton):
                # only click works...
                is_top = mbuttons[1].get_active()
            else:
                is_top = oparam.keep_on_top

            if kwargs.get('keep_on_top', None) is not None and \
               is_top != kwargs['keep_on_top']:
                if len(mbuttons) == 5 and isinstance(mbuttons[2],
                                                     gtk.ToggleButton):
                    # only click works...
                    mbuttons[2].clicked()
                elif len(mbuttons) == 3 and isinstance(mbuttons[1],
                                                       gtk.ToggleButton):
                    # only click works...
                    mbuttons[1].clicked()
                is_top = not is_top

        except Exception:
            # who knows...
            #import traceback
            #traceback.print_exc()
            mbuttons = None
            is_top = oparam.keep_on_top

        nparam = PaneParams(keep_on_top=is_top,
                            detached=kwargs.get('detached', oparam.detached),
                            window_position=kwargs.get('window_position',
                                                       oparam.window_position),
                            maximized=kwargs.get('maximized',
                                                 oparam.maximized))
        pane.set_params(nparam)

    def get_focus_pane(self):
        if self.get_toplevel().is_active():
            last_pane = getattr(self, 'focus_child', None)
            if not isinstance(last_pane, Paned):
                return
            while True:
                child_pane = getattr(last_pane, 'focus_child', None)
                if isinstance(child_pane, Paned):
                    last_pane = child_pane
                else:
                    return last_pane.get_open_pane()
        else:
            # we don't have the focus which means that a detached window
            # may have it.
            for view in self.list_panes(True):
                if view.pane.get_params().detached and \
                   view.pane.get_child().get_toplevel().is_active():
                    return view.pane

    def switch_next_pane(self, name, needs_focus=True):
        return self._switch_pane(name, 1, needs_focus)

    def switch_prev_pane(self, name, needs_focus=True):
        return self._switch_pane(name, -1, needs_focus)

    def _switch_pane(self, name, direction, needs_focus):
        paned, pane = self.get_open_pane(name)

        if not paned.n_panes():
            # return on empty panes
            return

        def ensure_focus(pane):
            # make sure the pane is in a window which is active
            if pane and not pane.get_child().get_toplevel().is_active():
                pane.get_child().get_toplevel().present()
            return pane

        if hasattr(paned, 'last_pane'):
            last_pane = paned.last_pane()  #it's a weak ref
            if last_pane and last_pane.get_params().detached:
                if not last_pane.get_child().get_toplevel().is_active(
                ) and needs_focus:
                    last_pane.present()
                    return ensure_focus(last_pane)
            elif last_pane and not pane:
                last_pane.present()
                return ensure_focus(last_pane)

        if needs_focus and pane and self.present_pane_if_not_focused(pane):
            return ensure_focus(pane)

        num = 0
        if pane:
            num = pane.get_index()
        newnum = num + direction

        if newnum == paned.n_panes():
            newnum = 0
        elif newnum < 0:
            newnum = paned.n_panes() - 1

        newpane = paned.get_nth_pane(newnum)
        if newpane is None:
            # no pane exists
            return
        paned.last_pane = newpane.weak_ref()
        newpane.present()
        return ensure_focus(newpane)

    def present_paned(self, name):
        paned, pane = self.get_open_pane(name)
        if pane is None:
            num = 0
        else:
            num = pane.get_index()
        pane = paned.get_nth_pane(num)
        if pane is not None:
            pane.present()

    def _center_on_parent(self, view, size):
        gdkwindow = view.get_parent_window()
        try:
            px, py, pw, ph, pbd = view.svc.window.window.get_geometry()
        except AttributeError:
            # this can fail if the window is not yet realized, so skip the
            # the renice stuff :-(
            return
        w, h = size
        cx = (pw - w) / 2
        cy = (ph - h) / 2
        gdkwindow.move_resize(cx, cy, w, h)
        #gdkwindow.resize(w, h)

    def set_fullscreen(self, fullscreen):
        if self._fullscreen == fullscreen:
            return
        if fullscreen:
            for pos in self.get_all_pos():
                paned = self.get_paned(pos)
                self._fullscreen_vis[pos] = {
                    'pane': paned.get_open_pane(),
                    'sticky': paned.props.sticky_pane
                }
                paned.set_sticky_pane(False)
                paned.props.sticky_pane = False
                paned.hide_pane()
        else:
            for pos in self.get_all_pos(True):
                paned = self.get_paned(pos)
                if pos in self._fullscreen_vis and \
                    self._fullscreen_vis[pos]['pane']:
                    paned.open_pane(self._fullscreen_vis[pos]['pane'])
                    paned.set_sticky_pane(self._fullscreen_vis[pos]['sticky'])
        self._fullscreen = fullscreen

    def get_fullscreen(self):
        return self._fullscreen

    def is_visible_pane(self, pane):
        """
        Test if a pane is visible to the user or not
        """
        # detached are always visible
        if not pane:
            return False
        if pane.get_params().detached:
            return True
        # this is kinda tricky because the widgets think they are visible
        # even when they are in a non top pane
        for paned in self.get_all_paneds(True):
            if pane == paned.get_open_pane():
                return True
        return False
Ejemplo n.º 5
0
class JsonschemaEditor(SlaveView):
    '''
    Slave widget that can added to another parent view.
    '''
    gsignal('changed', object)  # Emit when validated change applied to data.

    # TODO In validation code, add something like the following:
    #
    #     self.emit('changed', {'row': <index of changed row>,
    #                           'column_name': <column name>,
    #                           'original_value': <original value>,
    #                           'new_value': <new value>})
    #
    # Other code can then listen and react to this signal.
    #
    # For example:
    #
    #     schema_editor = JsonschemaEditor(...)
    #     ...
    #     schema_editor.connect('changed', on_changed)
    #
    # where `on_changed` is a function of the form:
    #
    #     def on_changed(jsonschema_editor, changed_info):
    #         # First argument is the `JsonschemaEditor` instance that emitted
    #         # the signal.
    #         print changed_info['row']
    #         print changed_info['column_name']
    #         print changed_info['original_value']
    #         print changed_info['new_value']
    #         print jsonschema_editor.to_frame()

    def __init__(self, schema, data=None):
        '''
        Args
        ----

            schema (dict) : jsonschema definition.
            data (pandas.DataFrame) : Initial data (optional).
        '''
        self.schema = schema
        self.data = data
        super(JsonschemaEditor, self).__init__()

    def create_ui(self):
        '''
        Called automatically during construction.  Prior to this call, a parent
        `gtk.VBox` widget named `self.widget` is automatically created.

        In general:

         1. Add all widgets to `self.widget` as needed.
         2. Show widgets as needed (e.g., `self.widget.show_all()`).

        Specifically for the jsonschema editor:

         1. Create `gtk.TreeView`.
         2. Create list store.
         3. Fill list store with data from `self.data` data frame.
         4. Create/add columns corresponding to properties from `self.schema`.
         5. Add callbacks to cell renderers to validate changes to the tree
            view.
        '''
        # Create `gtk.TreeView`.
        self.tree_view = gtk.TreeView()

        # TODO Create list store matching property data types from
        # `self.schema`.
        # ## Iterate through schema fields in a defined order ##
        #  - Order fields by `index` property (where it exists).
        #  - Note that `OrderedDict` maintains order that items are *inserted*.
        #  - Use:
        #      * `ordered_properties.iteritems()` to iterate through `(key,
        #        value)` pairs in order.
        #      * `ordered_properties.keys()` and `ordered_properties.values()`
        #        are in the order of insertion.
        ordered_properties = \
            OrderedDict(sorted(self.schema['properties'].items(),
                               key=lambda v: v[1].get('index', -1)))

        # TODO Fill list store with data from `self.data` data frame (if
        # necessary).
        if self.data is not None:
            pass

        # TODO Create/add columns corresponding to properties from
        # `self.schema`.
        pass  # TODO Use existing code...

        # TODO Add callbacks to cell renderers to validate changes to the tree
        # view.
        pass  # TODO Use existing code...

        # Add all widgets to `self.widget` as needed.
        self.widget.pack_start(child=self.tree_view,
                               expand=True,
                               fill=True,
                               padding=0)

        # Show widgets as needed.
        self.widget.show_all()

    def to_frame(self):
        '''
        Returns `pandas.DataFrame` with contents of `ListStore`.
        '''
        # TODO Create `pandas.DataFrame` containing contents of
        # `self.list_store`.
        pass
Ejemplo n.º 6
0
class PidaTerminal(Terminal):

    __gtype_name__ = 'PidaTerminal'

    gsignal('match-right-clicked', gtk.gdk.Event, int, str)

    def __init__(self, **kw):
        Terminal.__init__(self)
        self._fix_size()
        self._fix_events()
        self._connect_internal()
        self._init_matches()
        self.set_properties(**kw)

    def set_properties(self, **kw):
        """
        Set properties on the widget
        """
        for key, val in kw.items():
            getattr(self, 'set_%s' % key)(val)

    def _fix_size(self):
        """
        Fix the size of the terminal. Initially the widget starts very large,
        and is unable to be resized by conventional means.
        """
        self.set_size_request(50, 50)

    def _fix_events(self):
        self.add_events(gtk.gdk.BUTTON_PRESS_MASK)

    def _connect_internal(self):
        """
        Connect the internal signals
        """
        self.connect('button-press-event', self._on_button_press)
        self.connect('match-right-clicked', self._on_match_right_clicked)

    def _init_matches(self):
        """
        Initialize the matching system
        """
        self._matches = defaultdict(list)
        self._matches_res = {}

    def _get_position_from_pointer(self, x, y):
        """
        Get the row/column position for a pointer position
        """
        cw = self.get_char_width()
        ch = self.get_char_height()
        return int(x / cw), int(y / ch)

    def _on_button_press(self, term, event):
        """
        Called on a button press
        """
        if event.button == 3:
            col, row = self._get_position_from_pointer(event.x, event.y)
            match = self.match_check(col, row)
            if match is not None:
                match_str, match_num = match
                self.emit('match-right-clicked', event, match_num, match_str)
        elif event.button in [1, 2] and event.state & gtk.gdk.CONTROL_MASK:
            col, row = self._get_position_from_pointer(event.x, event.y)
            match = self.match_check(col, row)
            if match is not None:
                match_str, match_num = match
                for call in self._matches[match_num]:
                    if not isinstance(call, TerminalMatch):
                        continue
                    match_str, match_num = match
                    match_val = [match_str]
                    rematch = call.match_groups_re.match(match_str)
                    if rematch is not None:
                        groups = rematch.groups()
                        if groups:
                            match_val = groups
                    if call.callback(term,
                                     event,
                                     match_str,
                                     usr=call.usr,
                                     *match_val):
                        break

    def _on_match_right_clicked(self, term, event, match_num, match_str):
        """
        Called when there is a right click on the terminal. Internally, this
        checks whether there has been a match, and fires the required call
        back or menu.
        """
        if match_num in self._matches:
            match_val = [match_str]
            menu = gtk.Menu()

            for call in self._matches[match_num]:
                rematch = call.match_groups_re.match(match_str)
                if rematch is not None:
                    groups = rematch.groups()
                    if groups:
                        match_val = groups
                print match_val

                if not isinstance(
                        call, (TerminalMenuMatch, TerminalMenuCallbackMatch)):
                    continue

                first = True
                for action in call.callback(term,
                                            event,
                                            match_str,
                                            usr=call.usr,
                                            *match_val):
                    action.match_args = match_val
                    if isinstance(action, gtk.Action):
                        menu_item = action.create_menu_item()
                    else:
                        menu_item = action
                    if len(menu) and first:
                        menu.add(gtk.SeparatorMenuItem())
                    menu.add(menu_item)
                    first = False

            if len(menu):
                menu.show_all()
                menu.popup(None, None, None, event.button, event.time)

    def get_named_match(self, name):
        """
        Get a match object for the name
        
        :param name: the name of the match object
        :raises KeyError: If the named match does not exist
        """
        for match in self._matches.values():
            if match.name == name:
                return match
        raise KeyError(_('No match named "%s" was found') % name)

    def match_add_match(self, match):
        """
        Add a match object.
        """
        # adding more then one match that does the same is not going to work
        # very well :(
        # instead we register it once and dispatch it later
        if not match.match_re in self._matches_res:
            match_num = self.match_add(match.match_re)
            self._matches_res[match.match_re] = match_num
        else:
            match_num = self._matches_res[match.match_re]

        self._matches[match_num].append(match)

        return match_num

    def match_add_callback(self,
                           name,
                           match_str,
                           match_groups,
                           callback,
                           usr=None):
        """
        Add a match with a callback.

        :param name:            the name of the match
        :param match_str:       the regular expression to match
        :param match_groups:    a regular expression of groups which wil be
                                passed as parameters to the callback function
        :param callback:        the callback function to be called with the result of
                                the match
        """
        match = TerminalMatch(name, match_str, match_groups, callback, usr=usr)
        return self.match_add_match(match)

    def match_add_menu(self,
                       name,
                       match_str,
                       match_groups,
                       menu=None,
                       usr=None):
        """
        Add a menu match object.
        """
        match = TerminalMenuMatch(name, match_str, match_groups, menu, usr=usr)
        return self.match_add_match(match)

    def match_add_menu_callback(self,
                                name,
                                match_str,
                                match_groups,
                                callback,
                                usr=None):
        """
        Add a match that will result in a menu item for right click
        """
        match = TerminalMenuCallbackMatch(name,
                                          match_str,
                                          match_groups,
                                          callback,
                                          usr=usr)
        return self.match_add_match(match)

    def match_menu_register_action(self, name, action):
        """
        Register an action with the named match

        :param name: The name of the match
        :param action: A gtk.Action to use in the menu
        """
        self.get_named_match(name).register_action(action)

    def feed_text(self, text, color=None):
        """
        Feed text to the terminal, optionally coloured.
        """
        if color is not None:
            text = '\x1b[%sm%s\x1b[0m' % (color, text)
        self.feed(text)

    def get_all_text(self):
        col, row = self.get_cursor_position()
        return self.get_text_range(0, 0, row, col, lambda *a: True)
Ejemplo n.º 7
0
class SchemaView(FormView):
    gsignal('invalid', object)
    gsignal('valid', object)

    def __init__(self, schema, values=None, **kwargs):
        self.validator = jsonschema.Draft4Validator(schema)
        self.df_fields = get_fields_frame(schema)
        self.df_fields.sort_values('field', inplace=True)
        self.schema_type = fields_frame_to_flatland_form_class(self.df_fields)
        if values is None:
            self.values = {}
        super(SchemaView, self).__init__()

    def create_ui(self):
        super(SchemaView, self).create_ui()
        self.label_error = gtk.Label()
        self.label_event_box = gtk.EventBox()
        self.label_event_box.add(self.label_error)
        self.vbox_errors = gtk.VBox()
        self.vbox_errors.add(self.label_event_box)
        self.widget.show_all()
        self.widget.add(self.vbox_errors)
        self.vbox_errors.show_all()
        self.vbox_errors.hide()
        self.connect('changed', self.on_changed)
        self.validate()

        for field_i in self.form.schema.field_schema:
            name_i = field_i.name
            form_field_i = self.form.fields[name_i]
            value = self.values.get(name_i, form_field_i.element.default_value)

            if not form_field_i.element.set(value):
                raise ValueError('"%s" is not a valid value for field "%s"' %
                                 (value, name_i))
            form_field_i.proxy.set_widget_value(value)
            if hasattr(form_field_i.widget, 'set_activates_default'):
                form_field_i.widget.set_activates_default(True)
            form_field_i.label_widget.set_use_markup(True)

    def on_changed(self, form_view, proxy_group, proxy, field_name, new_value):
        self.validate()

    def as_dict(self):
        return expand_items(flatten_form(self.form.schema).items())

    def validate(self):
        data_dict = self.as_dict()
        errors = OrderedDict([('.'.join(e.path), e)
                              for e in self.validator.iter_errors(data_dict)])

        # Light red color.
        light_red = gtk.gdk.Color(240 / 255., 126 / 255., 110 / 255.)

        for name_i, field_i in self.form.fields.iteritems():
            color_i = light_red if name_i in errors else None
            label_widget_i = (
                field_i.widget.get_data('pygtkhelpers::label_widget'))
            label_widget_i.get_parent().modify_bg(gtk.STATE_NORMAL, color_i)

        if errors:
            message = '\n'.join([
                '[{}] {}'.format(name, error.message)
                for name, error in errors.iteritems()
            ])
            self.label_event_box.modify_bg(gtk.STATE_NORMAL, light_red)
            self.label_error.set_markup(message)
            self.vbox_errors.show()
            self.emit('invalid', errors)
            return False
        else:
            self.label_error.set_markup('')
            self.vbox_errors.hide()
            self.emit('valid', data_dict)
            return True
Ejemplo n.º 8
0
class DmfDeviceCanvas(GtkShapesCanvasView):
    '''
    Draw device layout from SVG file.

    Mouse events are handled as follows:

     - Click and release on the same electrode emits electrode selected signal.
     - Click on one electrode, drag, and release on another electrode emits
       electrode *pair* selected signal, with *source* electrode and *target*
       electrode.
     - Moving mouse cursor over electrode emits electrode mouse over signal.
     - Moving mouse cursor out of electrode emits electrode mouse out signal.

    Signals are emitted as gobject signals.  See `emit` calls for payload
    formats.
    '''
    gsignal('clear-electrode-states')
    gsignal('clear-routes', object)
    gsignal('device-set', object)
    gsignal('electrode-command', str, str, object)
    gsignal('electrode-mouseout', object)
    gsignal('electrode-mouseover', object)
    gsignal('electrode-pair-selected', object)
    gsignal('electrode-selected', object)
    gsignal('execute-routes', object)
    gsignal('key-press', object)
    gsignal('key-release', object)
    gsignal('route-command', str, str, object)
    gsignal('route-electrode-added', object)
    gsignal('route-selected', object)
    gsignal('set-electrode-channels', str, object) # electrode_id, channels
    gsignal('surface-rendered', str, object)
    gsignal('surfaces-reset', object)

    # Video signals
    gsignal('point-pair-selected', object)
    gsignal('video-enabled')
    gsignal('video-disabled')

    def __init__(self, connections_alpha=1., connections_color=1.,
                 transport='tcp', target_host='*', port=None, **kwargs):
        # Video sink socket info.
        self.socket_info = {'transport': transport,
                            'host': target_host,
                            'port': port}
        # Identifier for video incoming socket check.
        self.callback_id = None
        self._enabled = False  # Video enable
        self.start_event = None  # Video modify start click event
        # Matched corner points between canvas and video frame.  Used to
        # generate map between coordinate spaces.
        self.df_canvas_corners = pd.DataFrame(None, columns=['x', 'y'],
                                              dtype=float)
        self.df_frame_corners = pd.DataFrame(None, columns=['x', 'y'],
                                             dtype=float)
        # Matrix map from frame coordinates to canvas coordinates.
        self.frame_to_canvas_map = None
        # Matrix map from canvas coordinates to frame coordinates.
        self.canvas_to_frame_map = None
        # Shape of canvas (i.e., drawing area widget).
        self.shape = None

        self.mode = 'control'

        # Read SVG polygons into dataframe, one row per polygon vertex.
        df_shapes = pd.DataFrame(None, columns=['id', 'vertex_i', 'x', 'y'])
        self.device = None
        self.shape_i_column = 'id'

        # Save alpha for drawing connections.
        self.connections_alpha = connections_alpha

        # Save color for drawing connections.
        self.connections_color = connections_color

        self.reset_states()
        self.reset_routes()

        self.connections_attrs = {}
        self.last_pressed = None
        self.last_hovered = None
        self._route = None
        self.connections_enabled = (self.connections_alpha > 0)

        self.default_corners = {}  # {'canvas': None, 'frame': None}

        # Registered electrode commands
        self.electrode_commands = OrderedDict()
        # Register test command
        #self.register_electrode_command('ping',
                                        #group='microdrop.device_info_plugin')
        # Registered route commands
        self.route_commands = OrderedDict()
        super(DmfDeviceCanvas, self).__init__(df_shapes, self.shape_i_column,
                                              **kwargs)

    def reset_canvas_corners(self):
        self.df_canvas_corners = (self.default_corners
                                  .get('canvas',
                                       self.default_shapes_corners()))

    def reset_frame_corners(self):
        self.df_frame_corners = (self.default_corners
                                 .get('frame', self.default_frame_corners()))

    def default_shapes_corners(self):
        if self.canvas is None:
            return self.df_canvas_corners
        width, height = self.canvas.source_shape
        return pd.DataFrame([[0, 0], [width, 0], [width, height], [0, height]],
                            columns=['x', 'y'], dtype=float)

    def default_frame_corners(self):
        if self.video_sink.frame_shape is None:
            return self.df_frame_corners
        width, height = self.video_sink.frame_shape
        return pd.DataFrame([[0, 0], [width, 0], [width, height], [0, height]],
                            columns=['x', 'y'], dtype=float)

    def update_transforms(self):
        from opencv_helpers.safe_cv import cv2

        if (self.df_canvas_corners.shape[0] == 0 or
            self.df_frame_corners.shape[0] == 0):
            return

        self.canvas_to_frame_map = cv2.findHomography(self.df_canvas_corners
                                                      .values,
                                                      self.df_frame_corners
                                                      .values)[0]
        self.frame_to_canvas_map = cv2.findHomography(self.df_frame_corners
                                                      .values,
                                                      self.df_canvas_corners
                                                      .values)[0]

        # Translate transform shape coordinate space to drawing area coordinate
        # space.
        transform = self.frame_to_canvas_map
        if self.canvas is not None:
            transform = (self.canvas.shapes_to_canvas_transform.values
                         .dot(transform))
        self.video_sink.transform = transform
        self.set_surface('registration', self.render_registration())

    def create_ui(self):
        super(DmfDeviceCanvas, self).create_ui()
        self.video_sink = VideoSink(*[self.socket_info[k]
                                      for k in ['transport', 'host', 'port']])
        # Initialize video sink socket.
        self.video_sink.reset()
        # Required to have key-press and key-release events trigger.
        self.widget.set_flags(gtk.CAN_FOCUS)
        self.widget.add_events(gtk.gdk.KEY_PRESS_MASK |
                               gtk.gdk.KEY_RELEASE_MASK)
        # Create initial (empty) cairo surfaces.
        surface_names = ('background', 'shapes', 'connections', 'routes',
                         'channel_labels', 'actuated_shapes', 'registration')
        self.df_surfaces = pd.DataFrame([[self.get_surface(), 1.]
                                         for i in xrange(len(surface_names))],
                                        columns=['surface', 'alpha'],
                                        index=pd.Index(surface_names,
                                                       name='name'))

    def reset_canvas(self, width, height):
        super(DmfDeviceCanvas, self).reset_canvas(width, height)
        if self.device is None or self.canvas.df_canvas_shapes.shape[0] == 0:
            return

        self.canvas.df_canvas_shapes =\
            compute_shape_centers(self.canvas.df_canvas_shapes
                                  [[self.shape_i_column, 'vertex_i', 'x',
                                    'y']], self.shape_i_column)
        self.canvas.df_shape_centers = (self.canvas.df_canvas_shapes
                                        [[self.shape_i_column, 'x_center',
                                          'y_center']].drop_duplicates()
                                        .set_index(self.shape_i_column))
        df_shape_connections = self.device.df_shape_connections
        self.canvas.df_connection_centers =\
            (df_shape_connections.join(self.canvas.df_shape_centers
                                       .loc[df_shape_connections.source]
                                       .reset_index(drop=True))
             .join(self.canvas.df_shape_centers.loc[df_shape_connections
                                                    .target]
                   .reset_index(drop=True), lsuffix='_source',
                   rsuffix='_target'))

    def reset_states(self):
        self.electrode_states = pd.Series(name='electrode_states')
        self.electrode_states.index.name = 'electrode_id'

    def reset_routes(self):
        self.df_routes = pd.DataFrame(None, columns=['route_i', 'electrode_i',
                                                     'transition_i'])

    def set_device(self, dmf_device):
        self.device = dmf_device
        # Index channels by electrode ID for fast look up.
        self.electrode_channels = (self.device.df_electrode_channels
                                   .set_index('electrode_id'))
        self.df_shapes = self.device.df_shapes
        self.reset_routes()
        self.reset_states()
        x, y, width, height = self.widget.get_allocation()
        if width > 0 and height > 0:
            self.canvas = None
            self._dirty_size = width, height
        self.emit('device-set', dmf_device)

    def get_labels(self):
        if self.device is None:
            return pd.Series(None, index=pd.Index([], name='channel'))
        return (self.electrode_channels.astype(str)
                .groupby(level='electrode_id', axis=0)
                .agg(lambda v: ', '.join(v))['channel'])

    ###########################################################################
    # Properties
    @property
    def connection_count(self):
        return self.device.df_shape_connections.shape[0] if self.device else 0

    @property
    def shape_count(self):
        return self.df_shapes[self.shape_i_column].unique().shape[0]

    @property
    def enabled(self):
        return self._enabled

    @property
    def mode(self):
        return self._mode

    @mode.setter
    def mode(self, value):
        if value in ('register_video', 'control'):
            self._mode = value
    ###########################################################################
    # ## Mutators ##
    def insert_surface(self, position, name, surface, alpha=1.):
        '''
        Insert Cairo surface as new layer.


        Args
        ----

            position (int) : Index position to insert layer at.
            name (str) : Name of layer.
            surface (cairo.Context) : Surface to render.
            alpha (float) : Alpha/transparency level in the range `[0, 1]`.
        '''
        if name in self.df_surfaces.index:
            raise NameError('Surface already exists with `name="{}"`.'
                            .format(name))
        self.df_surfaces.loc[name] = surface, alpha

        # Reorder layers such that the new surface is placed at the specified
        # layer position (relative to the background surface).
        surfaces_order = self.df_surfaces.index.values.tolist()
        surfaces_order.remove(name)
        base_index = surfaces_order.index('background') + 1
        if position < 0:
            position = len(surfaces_order) + position
        surfaces_order.insert(base_index + position, name)
        self.reorder_surfaces(surfaces_order)

    def append_surface(self, name, surface, alpha=1.):
        '''
        Append Cairo surface as new layer on top of existing layers.

        Args
        ----

            name (str) : Name of layer.
            surface (cairo.ImageSurface) : Surface to render.
            alpha (float) : Alpha/transparency level in the range `[0, 1]`.
        '''
        self.insert_surface(position=self.df_surfaces.index.shape[0],
                            name=name, surface=surface, alpha=alpha)

    def remove_surface(self, name):
        '''
        Remove layer from rendering stack and flatten remaining layers.

        Args
        ----

            name (str) : Name of layer.
        '''
        self.df_surfaces.drop(name, axis=0, inplace=True)

        # Order of layers may have changed after removing a layer. Trigger
        # refresh of surfaces.
        self.reorder_surfaces(self.df_surfaces.index)

    def clone_surface(self, source_name, target_name, target_position=-1,
                      alpha=1.):
        '''
        Clone surface from existing layer to a new name, inserting new surface
        at specified position.

        By default, new surface is appended as the top surface layer.

        Args
        ----

            source_name (str) : Name of layer to clone.
            target_name (str) : Name of new layer.
        '''
        source_surface = self.df_surfaces.surface.ix[source_name]
        source_width = source_surface.get_width()
        source_height = source_surface.get_height()
        source_format = source_surface.get_format()

        target_surface = cairo.ImageSurface(source_format, source_width,
                                            source_height)
        target_cairo_context = cairo.Context(target_surface)
        target_cairo_context.set_source_surface(source_surface, 0, 0)
        target_cairo_context.paint()
        self.insert_surface(target_position, target_name, target_surface,
                            alpha)

    def enable(self):
        if self.callback_id is None:
            self._enabled = True
            self.set_surface('shapes', self.render_shapes())

            # Add layer to which video frames will be rendered.
            if 'video' in self.df_surfaces.index:
                self.set_surface('video', self.render_shapes())
            else:
                self.df_surfaces.loc['video'] = self.render_shapes(), 1.

            # Reorder layers such that the video layer is directly on top of
            # the background layer.
            surfaces_order = self.df_surfaces.index.values.tolist()
            surfaces_order.remove('video')
            surfaces_order.insert(surfaces_order.index('background') + 1,
                                  'video')
            self.reorder_surfaces(surfaces_order)

            self.render()
            self.callback_id = self.video_sink.connect('frame-update',
                                                       self.on_frame_update)
            self.emit('video-enabled')

    def disable(self):
        if self.callback_id is not None:
            self._enabled = False
            self.set_surface('shapes', self.render_shapes())
            self.video_sink.disconnect(self.callback_id)
            self.callback_id = None
            if 'video' in self.df_surfaces.index:
                self.df_surfaces.drop('video', axis=0, inplace=True)
                self.reorder_surfaces(self.df_surfaces.index)
            self.emit('video-disabled')
        self.on_frame_update(None, None)

    ###########################################################################
    # ## Drawing area event handling ##
    def check_dirty(self):
        if self._dirty_size is not None:
            width, height = self._dirty_size
            self.set_shape(width, height)
            transform_update_required = True
        else:
            transform_update_required = False
        result = super(DmfDeviceCanvas, self).check_dirty()
        if transform_update_required:
            gtk.idle_add(self.update_transforms)
        return result

    def set_shape(self, width, height):
        logger.debug('[set_shape]: Set drawing area shape to %sx%s', width,
                     height)
        self.shape = width, height
        # Set new target size for scaled frames from video sink.
        self.video_sink.shape = width, height
        self.update_transforms()
        if not self._enabled:
            gtk.idle_add(self.on_frame_update, None, None)

    ###########################################################################
    # ## Drawing methods ##
    def get_surfaces(self):
        surface1 = cairo.ImageSurface(cairo.FORMAT_ARGB32, 320, 240)
        surface1_context = cairo.Context(surface1)
        surface1_context.set_source_rgba(0, 0, 1, .5)
        surface1_context.rectangle(0, 0, surface1.get_width(), surface1.get_height())
        surface1_context.fill()

        surface2 = cairo.ImageSurface(cairo.FORMAT_ARGB32, 800, 600)
        surface2_context = cairo.Context(surface2)
        surface2_context.save()
        surface2_context.translate(100, 200)
        surface2_context.set_source_rgba(0, 1, .5, .5)
        surface2_context.rectangle(0, 0, surface1.get_width(), surface1.get_height())
        surface2_context.fill()
        surface2_context.restore()

        return [surface1, surface2]

    def draw_surface(self, surface, operator=cairo.OPERATOR_OVER):
        x, y, width, height = self.widget.get_allocation()
        if width <= 0 and height <= 0 or self.widget.window is None:
            return
        cairo_context = self.widget.window.cairo_create()
        cairo_context.set_operator(operator)
        cairo_context.set_source_surface(surface)
        cairo_context.rectangle(0, 0, width, height)
        cairo_context.fill()

    ###########################################################################
    # Render methods
    def render_actuated_shapes(self, df_shapes=None, shape_scale=0.8):
        '''
        Render actuated electrode shapes.

        Draw each electrode shape filled white.

        See also `render_shapes(...)`.
        '''
        surface = self.get_surface()
        if df_shapes is None:
            if hasattr(self.canvas, 'df_canvas_shapes'):
                df_shapes = self.canvas.df_canvas_shapes
            else:
                return surface
        if not self.electrode_states.shape[0]:
            # There are no actuated electrodes.  Nothing to draw.
            return surface
        if 'x_center' not in df_shapes or 'y_center' not in df_shapes:
            # No center points have been computed for shapes.
            return surface

        cairo_context = cairo.Context(surface)

        df_shapes = df_shapes.copy()
        # Scale shapes to leave shape edges uncovered.
        df_shapes[['x', 'y']] = (df_shapes[['x_center', 'y_center']] +
                                 df_shapes[['x_center_offset',
                                            'y_center_offset']].values *
                                 shape_scale)
        df_shapes['state'] = self.electrode_states.ix[df_shapes.id].values

        # Find actuated shapes.
        df_actuated_shapes = (df_shapes.loc[df_shapes.state > 0]
                              .dropna(subset=['state']))

        for path_id, df_path_i in (df_actuated_shapes
                                   .groupby(self.canvas.shape_i_columns)
                                   [['x', 'y']]):
            # Use attribute lookup for `x` and `y`, since it is considerably
            # faster than `get`-based lookup using columns name strings.
            vertices_x = df_path_i.x.values
            vertices_y = df_path_i.y.values
            cairo_context.move_to(vertices_x[0], vertices_y[0])
            for x, y in itertools.izip(vertices_x[1:], vertices_y[1:]):
                cairo_context.line_to(x, y)
            cairo_context.close_path()

            # Draw filled shape to indicate actuated electrode state.
            cairo_context.set_source_rgba(1, 1, 1)
            cairo_context.fill()
        return surface

    def render_background(self):
        surface = self.get_surface()
        context = cairo.Context(surface)
        context.set_source_rgb(0, 0, 0)
        context.paint()
        return surface

    def render_connections(self, indexes=None, hex_color='#fff', alpha=1.,
                           **kwargs):
        surface = self.get_surface()
        if not hasattr(self.canvas, 'df_connection_centers'):
            return surface
        cairo_context = cairo.Context(surface)
        coords_columns = ['source', 'target',
                          'x_center_source', 'y_center_source',
                          'x_center_target', 'y_center_target']
        df_connection_coords = (self.canvas.df_connection_centers
                                [coords_columns])
        if indexes is not None:
            df_connection_coords = df_connection_coords.loc[indexes].copy()

        rgba = hex_color_to_rgba(hex_color, normalize_to=1.)
        if rgba[-1] is None:
            rgba = rgba[:-1] + (alpha, )
        cairo_context.set_line_width(2.5)
        for i, (target, source, x1, y1, x2, y2) in (df_connection_coords
                                                    .iterrows()):
            cairo_context.move_to(x1, y1)
            cairo_context.set_source_rgba(*rgba)
            for k, v in kwargs.iteritems():
                getattr(cairo_context, 'set_' + k)(v)
            cairo_context.line_to(x2, y2)
            cairo_context.stroke()
        return surface

    def render_shapes(self, df_shapes=None, clip=False):
        '''
        Render static electrode shapes (independent of actuation state).

        If video is enabled, draw white outline for each electrode (no fill).

        If video is disabled, draw white outline for each electrode and fill
        blue.

        See also `render_actuated_shapes(...)`.
        '''
        surface = self.get_surface()
        if df_shapes is None:
            if hasattr(self.canvas, 'df_canvas_shapes'):
                df_shapes = self.canvas.df_canvas_shapes
            else:
                return surface

        cairo_context = cairo.Context(surface)

        for path_id, df_path_i in (df_shapes
                                   .groupby(self.canvas
                                            .shape_i_columns)[['x', 'y']]):
            # Use attribute lookup for `x` and `y`, since it is considerably
            # faster than `get`-based lookup using columns name strings.
            vertices_x = df_path_i.x.values
            vertices_y = df_path_i.y.values
            cairo_context.move_to(vertices_x[0], vertices_y[0])
            for x, y in itertools.izip(vertices_x[1:], vertices_y[1:]):
                cairo_context.line_to(x, y)
            cairo_context.close_path()

            if self.enabled:
                # Video is enabled.

                # Draw white border around electrode.
                line_width = 1
                if path_id not in self.electrode_channels.index:
                    #         on  off on  off
                    dashes = [10, 10]
                    color = (1, 0, 1)
                    line_width *= 2
                else:
                    dashes = []
                    color = (1, 1, 1)
                cairo_context.set_dash(dashes)
                cairo_context.set_line_width(line_width)
                cairo_context.set_source_rgb(*color)
                cairo_context.stroke()
            else:
                # Video is enabled.  Fill electrode blue.
                color = ((0, 0, 1) if path_id in self.electrode_channels.index
                         else (1, 0, 1))
                cairo_context.set_source_rgb(*color)
                cairo_context.fill_preserve()
                # Draw white border around electrode.
                cairo_context.set_line_width(1)
                cairo_context.set_source_rgba(1, 1, 1)
                cairo_context.stroke()
        return surface

    def render_routes(self):
        surface = self.get_surface()

        if (not hasattr(self.device, 'df_shape_connections') or
                not hasattr(self.canvas, 'df_shape_centers')):
            return surface

        cairo_context = cairo.Context(surface)
        connections = self.device.df_shape_connections
        for route_i, df_route in self.df_routes.groupby('route_i'):
            source_id = df_route.electrode_i.iloc[0]
            source_connections = connections.loc[(connections.source ==
                                                  source_id) |
                                                 (connections.target ==
                                                  source_id)]

            # Colors from ["Show me the numbers"][1].
            #
            # [1]: http://blog.axc.net/its-the-colors-you-have/
            # LiteOrange = rgb(251,178,88);
            # MedOrange = rgb(250,164,58);
            # LiteGreen = rgb(144,205,151);
            # MedGreen = rgb(96,189,104);
            if source_connections.shape[0] == 1:
                # Electrode only has one adjacent electrode, assume reservoir.
                color_rgb_255 = np.array([250, 164, 58, 255])
            else:
                color_rgb_255 = np.array([96, 189, 104, 255])
            color = (color_rgb_255 / 255.).tolist()
            self.draw_route(df_route, cairo_context, color=color,
                            line_width=.25)
        return surface

    def render_channel_labels(self, color_rgba=None):
        return self.render_labels(self.get_labels(), color_rgba=color_rgba)

    def render_registration(self):
        '''
        Render pinned points on video frame as red rectangle.
        '''
        surface = self.get_surface()
        if self.canvas is None or self.df_canvas_corners.shape[0] == 0:
            return surface

        corners = self.df_canvas_corners.copy()
        corners['w'] = 1

        transform = self.canvas.shapes_to_canvas_transform
        canvas_corners = corners.values.dot(transform.T.values).T

        points_x = canvas_corners[0]
        points_y = canvas_corners[1]

        cairo_context = cairo.Context(surface)
        cairo_context.move_to(points_x[0], points_y[0])
        for x, y in zip(points_x[1:], points_y[1:]):
            cairo_context.line_to(x, y)
        cairo_context.line_to(points_x[0], points_y[0])
        cairo_context.set_source_rgb(1, 0, 0)
        cairo_context.stroke()
        return surface

    def set_surface(self, name, surface):
        self.df_surfaces.loc[name, 'surface'] = surface
        self.emit('surface-rendered', name, surface)

    def set_surface_alpha(self, name, alpha):
        if 'alpha' not in self.df_surfaces:
            self.df_surfaces['alpha'] = 1.
        if name in self.df_surfaces.index:
            self.df_surfaces.loc[name, 'alpha'] = alpha

    def reorder_surfaces(self, surface_names):
        assert(len(surface_names) == self.df_surfaces.shape[0])
        self.df_surfaces = self.df_surfaces.ix[surface_names]
        self.emit('surfaces-reset', self.df_surfaces)
        self.cairo_surface = flatten_surfaces(self.df_surfaces)

    def render(self):
        # Render each layer and update data frame with new content for each
        # surface.
        surface_names = ('background', 'shapes', 'connections', 'routes',
                         'channel_labels', 'actuated_shapes', 'registration')
        for k in surface_names:
            self.set_surface(k, getattr(self, 'render_' + k)())
        self.emit('surfaces-reset', self.df_surfaces)
        self.cairo_surface = flatten_surfaces(self.df_surfaces)

    ###########################################################################
    # Drawing helper methods
    def draw_route(self, df_route, cr, color=None, line_width=None):
        '''
        Draw a line between electrodes listed in a route.

        Arguments
        ---------

         - `df_route`:
             * A `pandas.DataFrame` containing a column named `electrode_i`.
             * For each row, `electrode_i` corresponds to the integer index of
               the corresponding electrode.
         - `cr`: Cairo context.
         - `color`: Either a RGB or RGBA tuple, with each color channel in the
           range [0, 1].  If `color` is `None`, the electrode color is set to
           white.
        '''
        df_route_centers = (self.canvas.df_shape_centers
                            .ix[df_route.electrode_i][['x_center',
                                                       'y_center']])
        df_endpoint_marker = (.6 * self.get_endpoint_marker(df_route_centers)
                              + df_route_centers.iloc[-1].values)

        # Save cairo context to restore after drawing route.
        cr.save()
        if color is None:
            # Colors from ["Show me the numbers"][1].
            #
            # [1]: http://blog.axc.net/its-the-colors-you-have/
            # LiteOrange = rgb(251,178,88);
            # MedOrange = rgb(250,164,58);
            # LiteGreen = rgb(144,205,151);
            # MedGreen = rgb(96,189,104);
            color_rgb_255 = np.array([96,189,104, .8 * 255])
            color = (color_rgb_255 / 255.).tolist()
        if len(color) < 4:
            color += [1.] * (4 - len(color))
        cr.set_source_rgba(*color)
        cr.move_to(*df_route_centers.iloc[0])
        for electrode_i, center_i in df_route_centers.iloc[1:].iterrows():
            cr.line_to(*center_i)
        if line_width is None:
            line_width = np.sqrt((df_endpoint_marker.max().values -
                                  df_endpoint_marker.min().values).prod()) * .1
        cr.set_line_width(4)
        cr.stroke()

        cr.move_to(*df_endpoint_marker.iloc[0])
        for electrode_i, center_i in df_endpoint_marker.iloc[1:].iterrows():
            cr.line_to(*center_i)
        cr.close_path()
        cr.set_source_rgba(*color)
        cr.fill()
        # Restore cairo context after drawing route.
        cr.restore()

    def get_endpoint_marker(self, df_route_centers):
        df_shapes = self.canvas.df_canvas_shapes
        df_endpoint_electrode = df_shapes.loc[df_shapes.id ==
                                              df_route_centers.index[-1]]
        df_endpoint_bbox = (df_endpoint_electrode[['x_center_offset',
                                                   'y_center_offset']]
                            .describe().loc[['min', 'max']])
        return pd.DataFrame([[df_endpoint_bbox.x_center_offset['min'],
                              df_endpoint_bbox.y_center_offset['min']],
                             [df_endpoint_bbox.x_center_offset['min'],
                              df_endpoint_bbox.y_center_offset['max']],
                             [df_endpoint_bbox.x_center_offset['max'],
                              df_endpoint_bbox.y_center_offset['max']],
                             [df_endpoint_bbox.x_center_offset['max'],
                              df_endpoint_bbox.y_center_offset['min']]],
                            columns=['x_center_offset', 'y_center_offset'])

    ###########################################################################
    # ## Mouse event handling ##
    def on_widget__button_press_event(self, widget, event):
        '''
        Called when any mouse button is pressed.
        '''
        if self.mode == 'register_video' and event.button == 1:
            self.start_event = event.copy()
            return
        elif self.mode == 'control':
            shape = self.canvas.find_shape(event.x, event.y)

            if shape is None: return
            if event.button == 1:
                # `<Alt>` key is held down.
                # Start a new route.
                self._route = Route(self.device)
                self._route.append(shape)
                self.emit('route-electrode-added', shape)
                self.last_pressed = shape

    def on_widget__button_release_event(self, widget, event):
        '''
        Called when any mouse button is released.
        '''
        event = event.copy()
        if self.mode == 'register_video' and (event.button == 1 and
                                              self.start_event is not None):
            self.emit('point-pair-selected', {'start_event': self.start_event,
                                              'end_event': event.copy()})
            self.start_event = None
            return
        elif self.mode == 'control':
            shape = self.canvas.find_shape(event.x, event.y)

            if shape is None: return

            electrode_data = {'electrode_id': shape, 'event': event.copy()}
            if event.button == 1:
                if gtk.gdk.BUTTON1_MASK == event.get_state():
                    if self._route.append(shape):
                        self.emit('route-electrode-added', shape)
                    if len(self._route.electrode_ids) == 1:
                        # Single electrode, so select electrode.
                        self.emit('electrode-selected', electrode_data)
                    else:
                        # Multiple electrodes, so select route.
                        route = self._route
                        self.emit('route-selected', route)
                    # Clear route.
                    self._route = None
                elif (event.get_state() == (gtk.gdk.MOD1_MASK |
                                            gtk.gdk.BUTTON1_MASK) and
                    self.last_pressed != shape):
                    self.emit('electrode-pair-selected',
                              {'source_id': self.last_pressed, 'target_id': shape,
                               'event': event.copy()})
                self.last_pressed = None
            elif event.button == 3:
                # Create right-click pop-up menu.
                menu = self.create_context_menu(event, shape)

                # Display menu popup
                menu.popup(None, None, None, event.button, event.time)

    def create_context_menu(self, event, shape):
        '''
        Parameters
        ----------
        event : gtk.gdk.Event
            GTK mouse click event.
        shape : str
            Electrode shape identifier (e.g., `"electrode028"`).

        Returns
        -------
        gtk.Menu
            Context menu.
        '''
        if self.df_routes.electrode_i.count() != 0:
            routes = self.df_routes.loc[self.df_routes.electrode_i == shape,
                                        'route_i'].astype(int).unique().tolist()
        else:
            routes = []

        def clear_electrode_states(widget):
            self.emit('clear-electrode-states')

        def edit_electrode_channels(widget):
            # Create schema to only accept a well-formed comma-separated list
            # of integer channel numbers.  Default to list of channels
            # currently mapped to electrode.
            if shape in self.electrode_channels.index:
                # If there is a single channel mapped to the electrode,
                # the `...ix[shape]` lookup below returns a `pandas.Series`.
                # However, if multiple channels are mapped to the electrode
                # the `...ix[shape]` lookup returns a `pandas.DataFrame`.
                # Calling `.values.ravel()` returns data in the same form in
                # either situation.
                current_channels = (self.electrode_channels.ix[shape]
                                    .values.ravel().tolist())
            else:
                # Electrode has no channels currently mapped to it.
                current_channels = []
            schema = {'type': 'object',
                      'properties': {'channels':
                                     {'type': 'string', 'pattern':
                                      r'^(\d+\s*(,\s*\d+\s*)*)?$',
                                      'default':
                                      ','.join(map(str, current_channels))}}}

            try:
                # Prompt user to enter a list of channel numbers (or nothing).
                result = pgh.schema.schema_dialog(schema, device_name=False)
            except ValueError:
                pass
            else:
                # Well-formed (according to schema pattern) comma-separated
                # list of channels was provided.
                channels = sorted(set(map(int, filter(len, result['channels']
                                                      .split(',')))))
                self.emit('set-electrode-channels', shape, channels)

        def clear_routes(widget):
            self.emit('clear-routes', shape)

        def clear_all_routes(widget):
            self.emit('clear-routes', None)

        def execute_routes(widget):
            self.emit('execute-routes', shape)

        def execute_all_routes(widget):
            self.emit('execute-routes', None)

        menu = gtk.Menu()
        menu_separator = gtk.SeparatorMenuItem()
        menu_clear_electrode_states = gtk.MenuItem('Clear all electrode '
                                                   'states')
        menu_clear_electrode_states.connect('activate', clear_electrode_states)
        menu_edit_electrode_channels = gtk.MenuItem('Edit electrode '
                                                    'channels...')
        menu_edit_electrode_channels.connect('activate',
                                             edit_electrode_channels)
        menu_clear_routes = gtk.MenuItem('Clear electrode routes')
        menu_clear_routes.connect('activate', clear_routes)
        menu_clear_all_routes = gtk.MenuItem('Clear all electrode routes')
        menu_clear_all_routes.connect('activate', clear_all_routes)
        menu_execute_routes = gtk.MenuItem('Execute electrode routes')
        menu_execute_routes.connect('activate', execute_routes)
        menu_execute_all_routes = gtk.MenuItem('Execute all electrode '
                                               'routes')
        menu_execute_all_routes.connect('activate', execute_all_routes)

        for item in (menu_clear_electrode_states, menu_edit_electrode_channels,
                     menu_separator, menu_clear_routes, menu_clear_all_routes,
                     menu_execute_routes, menu_execute_all_routes):
            menu.append(item)
            item.show()

        # Add menu items/groups for registered electrode commands.
        if self.electrode_commands:
            separator = gtk.SeparatorMenuItem()
            menu.append(separator)

            # Add electrode sub-menu.
            menu_e = gtk.Menu()
            menu_head_e = gtk.MenuItem('Electrode')
            menu_head_e.set_submenu(menu_e)
            menu_head_e.set_use_underline(False)
            menu.append(menu_head_e)

            electrode_data = {'electrode_id': shape, 'event': event.copy()}
            for group, commands in self.electrode_commands.iteritems():
                if group is None:
                    menu_i = menu_e
                else:
                    # Add sub-menu for group.
                    menu_i = gtk.Menu()
                    menu_head_i = gtk.MenuItem(group)
                    menu_head_i.set_submenu(menu_i)
                    menu_head_i.set_use_underline(False)
                    menu_e.append(menu_head_i)
                for command, title in commands.iteritems():
                    menu_item_j = gtk.MenuItem(title)
                    menu_i.append(menu_item_j)

                    def callback(group, command, electrode_data):
                        # Closure for `callback` function to persist current
                        # values `group, command, title` in callback.
                        def wrapped(widget):
                            gtk.idle_add(self.emit, 'electrode-command', group,
                                         command, electrode_data)
                        return wrapped
                    menu_item_j.connect('activate', callback(group, command,
                                                             electrode_data))

        # Add menu items/groups for registered route commands.
        if routes and self.route_commands:
            # TODO: Refactor electrode/route command menu code to reduce code
            # duplication (i.e., DRY).
            separator = gtk.SeparatorMenuItem()
            menu.append(separator)

            # Add route sub-menu.
            menu_r = gtk.Menu()
            menu_head_r = gtk.MenuItem('Route(s)')
            menu_head_r.set_submenu(menu_r)
            menu_head_r.set_use_underline(False)
            menu.append(menu_head_r)

            route_data = {'route_ids': routes, 'event': event.copy()}
            for group, commands in self.route_commands.iteritems():
                if group is None:
                    menu_i = menu_r
                else:
                    # Add sub-menu for group.
                    menu_i = gtk.Menu()
                    menu_head_i = gtk.MenuItem(group)
                    menu_head_i.set_submenu(menu_i)
                    menu_head_i.set_use_underline(False)
                    menu_r.append(menu_head_i)
                for command, title in commands.iteritems():
                    menu_item_j = gtk.MenuItem(title)
                    menu_i.append(menu_item_j)

                    def callback(group, command, route_data):
                        # Closure for `callback` function to persist current
                        # values `group, command, title` in callback.
                        def wrapped(widget):
                            gtk.idle_add(self.emit, 'route-command', group,
                                         command, route_data)
                        return wrapped
                    menu_item_j.connect('activate', callback(group, command,
                                                             route_data))

        menu.show_all()
        return menu

    def on_widget__motion_notify_event(self, widget, event):
        '''
        Called when mouse pointer is moved within drawing area.
        '''
        if self.canvas is None:
            # Canvas has not been initialized.  Nothing to do.
            return
        elif event.is_hint:
            pointer = event.window.get_pointer()
            x, y, mod_type = pointer
        else:
            x = event.x
            y = event.y
        shape = self.canvas.find_shape(x, y)

        # Grab focus to [enable notification on key press/release events][1].
        #
        # [1]: http://mailman.daa.com.au/cgi-bin/pipermail/pygtk/2003-August/005770.html
        self.widget.grab_focus()

        if shape != self.last_hovered:
            if self.last_hovered is not None:
                # Leaving shape
                self.emit('electrode-mouseout', {'electrode_id':
                                                 self.last_hovered,
                                                 'event': event.copy()})
                self.last_hovered = None
            elif shape is not None:
                # Entering shape
                self.last_hovered = shape

                # `<Alt>` key was held down.
                if self._route is not None:
                    if self._route.append(shape):
                        self.emit('route-electrode-added', shape)

                self.emit('electrode-mouseover', {'electrode_id':
                                                  self.last_hovered,
                                                  'event': event.copy()})
    def on_widget__key_press_event(self, widget, event):
        '''
        Called when key is pressed when widget has focus.
        '''
        self.emit('key-press', {'event': event.copy()})

    def on_widget__key_release_event(self, widget, event):
        '''
        Called when key is released when widget has focus.
        '''
        self.emit('key-release', {'event': event.copy()})

    ###########################################################################
    # ## Slave signal handling ##
    def on_video_sink__frame_shape_changed(self, slave, old_shape, new_shape):
        # Video frame is a new shape.
        if old_shape is not None:
            # Switched video resolution, so scale existing corners to maintain
            # video registration.
            old_shape = pd.Series(old_shape, dtype=float, index=['width',
                                                                 'height'])
            new_shape = pd.Series(new_shape, dtype=float, index=['width',
                                                                 'height'])
            old_aspect_ratio = old_shape.width / old_shape.height
            new_aspect_ratio = new_shape.width / new_shape.height
            if old_aspect_ratio != new_aspect_ratio:
                # The aspect ratio has changed.  The registration will have the
                # proper rotational orientation, but the scale will be off and
                # will require manual adjustment.
                logger.warning('Aspect ratio does not match previous frame.  '
                               'Manual adjustment of registration is required.')

            corners_scale = new_shape / old_shape
            df_frame_corners = self.df_frame_corners.copy()
            df_frame_corners.y = old_shape.height - df_frame_corners.y
            df_frame_corners *= corners_scale.values
            df_frame_corners.y = new_shape.height - df_frame_corners.y
            self.df_frame_corners = df_frame_corners
        else:
            # No existing frame shape, so nothing to scale from.
            self.reset_frame_corners()
        self.update_transforms()

    def on_frame_update(self, slave, np_frame):
        if self.widget.window is None:
            return
        if np_frame is None or not self._enabled:
            if 'video' in self.df_surfaces.index:
                self.df_surfaces.drop('video', axis=0, inplace=True)
                self.reorder_surfaces(self.df_surfaces.index)
        else:
            cr_warped, np_warped_view = np_to_cairo(np_frame)
            self.set_surface('video', cr_warped)
        self.cairo_surface = flatten_surfaces(self.df_surfaces)
        # Execute a few gtk main loop iterations to improve responsiveness when
        # using high video frame rates.
        #
        # N.B., Without doing this, for example, some mouse over events may be
        # missed, leading to problems drawing routes, etc.
        for i in xrange(5):
            if not gtk.events_pending():
                break
            gtk.main_iteration_do()

        self.draw()
    ###########################################################################
    # ## Electrode operation registration ##
    def register_electrode_command(self, command, title=None, group=None):
        '''
        Register electrode command.

        Add electrode plugin command to context menu.
        '''
        commands = self.electrode_commands.setdefault(group, OrderedDict())
        if title is None:
            title = (command[:1].upper() + command[1:]).replace('_', ' ')
        commands[command] = title

    ###########################################################################
    # ## Route operation registration ##
    def register_route_command(self, command, title=None, group=None):
        '''
        Register route command.

        Add route plugin command to context menu.
        '''
        commands = self.route_commands.setdefault(group, OrderedDict())
        if title is None:
            title = (command[:1].upper() + command[1:]).replace('_', ' ')
        commands[command] = title