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)
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)
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
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
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
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)
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
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