def test_widget_javascript_debounce(qtbot, event_name): phy.gui.qt.Debouncer.delay = 300 widget = HTMLWidget(debounce_events=('select', )) widget.build() widget.show() qtbot.addWidget(widget) qtbot.waitForWindowShown(widget) _block(lambda: widget.html is not None) event_code = lambda i: r''' var event = new CustomEvent("phy_event", {detail: {name: '%s', data: {'i': %s}}}); document.dispatchEvent(event); ''' % (event_name, i) _l = [] def f(sender, *args): _l.append(args) connect(f, sender=widget, event=event_name) for i in range(5): widget.eval_js(event_code(i)) qtbot.wait(10) qtbot.wait(500) assert len(_l) == (2 if event_name == 'select' else 5) # qtbot.stop() widget.close() phy.gui.qt.Debouncer.delay = 1
def attach(self, gui): """Attach the view to the GUI. Perform the following: - Add the view to the GUI. - Update the view's attribute from the GUI state - Add the default view actions (auto_update, screenshot) - Bind the on_select() method to the select event raised by the supervisor. """ # Add shortcuts only for the first view of any given type. shortcuts = self.shortcuts if not gui.list_views(self.__class__) else None gui.add_view(self, position=self._default_position) self.gui = gui # Set the view state. self.set_state(gui.state.get_view_state(self)) self.actions = Actions( gui, name=self.name, view=self, default_shortcuts=shortcuts, default_snippets=self.default_snippets) # Freeze and unfreeze the view when selecting clusters. self.actions.add( self.toggle_auto_update, checkable=True, checked=self.auto_update, show_shortcut=False) self.actions.add(self.screenshot, show_shortcut=False) self.actions.add(self.close, show_shortcut=False) self.actions.separator() on_select = partial(self.on_select_threaded, gui=gui) connect(on_select, event='select') # Save the view state in the GUI state. @connect def on_close_view(view_, gui): if view_ != self: return logger.debug("Close view %s.", self.name) self._closed = True gui.remove_menu(self.name) unconnect(on_select) gui.state.update_view_state(self, self.state) self.canvas.close() gc.collect(0) @connect(sender=gui) def on_close(sender): gui.state.update_view_state(self, self.state) # HACK: Fix bug on macOS where docked OpenGL widgets were not displayed at startup. self._set_floating = AsyncCaller(delay=5) @self._set_floating.set def _set_floating(): self.dock.setFloating(False) emit('view_attached', self, gui)
def _wait_until_table_ready(qtbot, table): b = Barrier() connect(b(1), event='ready', sender=table) table.show() qtbot.addWidget(table) qtbot.waitForWindowShown(table) b.wait()
def _init_table(self, columns=None, value_names=None, data=None, sort=None): """Build the table.""" columns = columns or ['id'] value_names = value_names or columns data = data or [] b = self.builder b.set_body_src('index.html') if is_high_dpi(): # pragma: no cover b.add_style(''' /* This is for high-dpi displays. */ body { transform: scale(2); transform-origin: 0 0; overflow-y: scroll; /*overflow-x: hidden;*/ } input.filter { width: 50% !important; } ''') b.add_style(_color_styles()) self.data = data self.columns = columns self.value_names = value_names emit('pre_build', self) data_json = dumps(self.data) columns_json = dumps(self.columns) value_names_json = dumps(self.value_names) sort_json = dumps(sort) b.body += ''' <script> var data = %s; var options = { valueNames: %s, columns: %s, sort: %s, }; var table = new Table('table', options, data); </script> ''' % (data_json, value_names_json, columns_json, sort_json) self.build(lambda html: emit('ready', self)) connect(event='select', sender=self, func=lambda *args: self.update(), last=True) connect(event='ready', sender=self, func=lambda *args: self._set_ready())
def attach(self, gui): """Attach to the GUI.""" # Make sure the selected field in cluster and similarity views are saved in the local # supervisor state, as this information is dataset-dependent. gui.state.add_local_keys(['ClusterView.selected']) # Create the cluster view and similarity view. self._create_views(gui=gui, sort=gui.state.get('ClusterView', {}).get('current_sort', None)) # Create the TaskLogger. self.task_logger = TaskLogger( cluster_view=self.cluster_view, similarity_view=self.similarity_view, supervisor=self, ) connect(self._save_gui_state, event='close', sender=gui) gui.add_view(self.cluster_view, position='left', closable=False) gui.add_view(self.similarity_view, position='left', closable=False) # Create all supervisor actions (edit and view menu). self.action_creator.attach(gui) self.actions = self.action_creator.edit_actions # clustering actions self.select_actions = self.action_creator.select_actions self.view_actions = gui.view_actions emit('attach_gui', self) # Call supervisor.save() when the save/ctrl+s action is triggered in the GUI. @connect(sender=gui) def on_request_save(sender): self.save() # Set the debouncer. self._busy = {} self._is_busy = False # Collect all busy events from the views, and sets the GUI as busy # if at least one view is busy. @connect def on_is_busy(sender, is_busy): self._busy[sender] = is_busy self._set_busy(any(self._busy.values())) @connect(sender=gui) def on_close(e): unconnect(on_is_busy, self) @connect(sender=self.cluster_view) def on_ready(sender): """Select the clusters from the cluster view state.""" selected = gui.state.get('ClusterView', {}).get('selected', []) if selected: # pragma: no cover self.cluster_view.select(selected)
def _create_gui(cls): cls._gui = cls._controller.create_gui(do_prompt_save=False) s = cls._controller.supervisor b = Barrier() connect(b('cluster_view'), event='ready', sender=s.cluster_view) connect(b('similarity_view'), event='ready', sender=s.similarity_view) cls._gui.show() # cls._qtbot.addWidget(cls._gui) cls._qtbot.waitForWindowShown(cls._gui) b.wait()
def attach(self, gui): """Attach the view to the GUI.""" super(FeatureView, self).attach(gui) self.actions.add(self.toggle_automatic_channel_selection, checked=not self.fixed_channels, checkable=True) self.actions.add(self.clear_channels) self.actions.separator() connect(self.on_channel_click) connect(self.on_request_split)
def test_supervisor_split_2(gui, similarity): spike_clusters = np.array([0, 0, 1]) supervisor = Supervisor(spike_clusters, similarity=similarity) supervisor.attach(gui) b = Barrier() connect(b('cluster_view'), event='ready', sender=supervisor.cluster_view) connect(b('similarity_view'), event='ready', sender=supervisor.similarity_view) b.wait() supervisor.actions.split([0]) supervisor.block() _assert_selected(supervisor, [2, 3])
def _create_views(self, gui=None, sort=None): """Create the cluster view and similarity view.""" sort = sort or self._sort # comes from either the GUI state or constructor # Create the cluster view. self.cluster_view = ClusterView(gui, data=self.cluster_info, columns=self.columns, sort=sort) # Update the action flow and similarity view when selection changes. connect(self._clusters_selected, event='select', sender=self.cluster_view) # Create the similarity view. self.similarity_view = SimilarityView(gui, columns=self.columns + ['similarity'], sort=('similarity', 'desc')) connect(self._get_similar_clusters, event='request_similar_clusters', sender=self.similarity_view) connect(self._similar_selected, event='select', sender=self.similarity_view) # Change the state after every clustering action, according to the action flow. connect(self._after_action, event='cluster', sender=self)
def _set_supervisor(self): """Create the Supervisor instance.""" # Load the new cluster id. new_cluster_id = self.context.load('new_cluster_id').get( 'new_cluster_id', None) # Cluster groups. cluster_groups = self.model.cluster_groups # Create the Supervisor instance. supervisor = Supervisor( spike_clusters=self.model.spike_clusters, cluster_groups=cluster_groups, cluster_metrics=self.cluster_metrics, similarity=self.similarity_functions[self.similarity], new_cluster_id=new_cluster_id, context=self.context, ) # Connect the `save_clustering` event raised by the supervisor when saving # to the model's saving functions. connect(self.on_save_clustering, sender=supervisor) @connect(sender=supervisor) def on_attach_gui(sender): @supervisor.actions.add(shortcut='shift+ctrl+k', set_busy=True) def recluster(cluster_ids=None): """Relaunch KlustaKwik on the selected clusters.""" # Selected clusters. cluster_ids = supervisor.selected spike_ids = self.selector.select_spikes(cluster_ids) logger.info("Running KlustaKwik on %d spikes.", len(spike_ids)) # Run KK2 in a temporary directory to avoid side effects. n = 10 with TemporaryDirectory() as tempdir: spike_clusters, metadata = cluster( self.model, spike_ids, num_starting_clusters=n, tempdir=tempdir, ) self.supervisor.split(spike_ids, spike_clusters) self.color_selector = supervisor.color_selector self.supervisor = supervisor
def _eval(self, task): """Evaluate a task and call a callback function.""" sender, name, args, kwargs = task logger.log(5, "Calling %s.%s(%s)", sender.__class__.__name__, name, args, kwargs) f = getattr(sender, name) callback = partial(self._callback, task) argspec = inspect.getfullargspec(f) argspec = argspec.args + argspec.kwonlyargs if 'callback' in argspec: f(*args, **kwargs, callback=callback) else: # HACK: use on_cluster event instead of callback. def _cluster_callback(tsender, up): self._callback(task, up) connect(_cluster_callback, event='cluster', sender=self.supervisor) f(*args, **kwargs) unconnect(_cluster_callback)
def supervisor(qtbot, gui, cluster_ids, cluster_groups, cluster_labels, similarity, tempdir): spike_clusters = np.repeat(cluster_ids, 2) s = Supervisor( spike_clusters, cluster_groups=cluster_groups, cluster_labels=cluster_labels, similarity=similarity, context=Context(tempdir), sort=('id', 'desc'), ) s.attach(gui) b = Barrier() connect(b('cluster_view'), event='ready', sender=s.cluster_view) connect(b('similarity_view'), event='ready', sender=s.similarity_view) b.wait() return s
def _init_table(self, columns=None, value_names=None, data=None, sort=None): """Build the table.""" columns = columns or ['id'] value_names = value_names or columns data = data or [] b = self.builder b.set_body_src('index.html') b.add_style(_color_styles()) self.data = data self.columns = columns self.value_names = value_names emit('pre_build', self) data_json = dumps(self.data) columns_json = dumps(self.columns) value_names_json = dumps(self.value_names) sort_json = dumps(sort) b.body += ''' <script> var data = %s; var options = { valueNames: %s, columns: %s, sort: %s, }; var table = new Table('table', options, data); </script> ''' % (data_json, value_names_json, columns_json, sort_json) self.build(lambda html: emit('ready', self)) connect(event='select', sender=self, func=lambda *args: self.update(), last=True) connect(event='ready', sender=self, func=lambda *args: self._set_ready())
def test_supervisor_cluster_metrics(qtbot, gui, cluster_ids, cluster_groups, similarity, tempdir): spike_clusters = np.repeat(cluster_ids, 2) def my_metrics(cluster_id): return cluster_id**2 cluster_metrics = {'my_metrics': my_metrics} mc = Supervisor( spike_clusters, cluster_groups=cluster_groups, cluster_metrics=cluster_metrics, similarity=similarity, context=Context(tempdir), ) mc.attach(gui) b = Barrier() connect(b('cluster_view'), event='ready', sender=mc.cluster_view) connect(b('similarity_view'), event='ready', sender=mc.similarity_view) b.wait() assert 'my_metrics' in mc.columns
def attach(self, gui): """Attach to the GUI.""" # Make sure the selected field in cluster and similarity views are saved in the local # supervisor state, as this information is dataset-dependent. gui.state.add_local_keys(['ClusterView.selected']) # Create the cluster view and similarity view. self._create_views( gui=gui, sort=gui.state.get('ClusterView', {}).get('current_sort', None)) # Create the TaskLogger. self.task_logger = TaskLogger( cluster_view=self.cluster_view, similarity_view=self.similarity_view, supervisor=self, ) connect(self._save_gui_state, event='close', sender=gui) gui.add_view(self.cluster_view, position='left', closable=False) gui.add_view(self.similarity_view, position='left', closable=False) # Create the ClusterColorSelector instance. # Pass the state variables: color_field, colormap, categorical, logarithmic self.color_selector = ClusterColorSelector( cluster_meta=self.cluster_meta, cluster_metrics=self.cluster_metrics, cluster_ids=self.clustering.cluster_ids, **gui.state.get('color_selector', Bunch()) ) # Create all supervisor actions (edit and view menu). self.action_creator.attach(gui) self.actions = self.action_creator.edit_actions # clustering actions self.select_actions = self.action_creator.select_actions self.view_actions = self.action_creator.view_actions emit('attach_gui', self) @connect(sender=self) def on_cluster(sender, up): # After a clustering action, get the cluster ids as shown # in the cluster view, and update the color selector accordingly. @self.cluster_view.get_ids def _update(cluster_ids): if cluster_ids is not None: self.color_selector.set_cluster_ids(cluster_ids) # Call supervisor.save() when the save/ctrl+s action is triggered in the GUI. @connect(sender=gui) def on_request_save(sender): self.save() # Set the debouncer. self._busy = {} self._is_busy = False # Collect all busy events from the views, and sets the GUI as busy # if at least one view is busy. @connect def on_is_busy(sender, is_busy): self._busy[sender] = is_busy self._set_busy(any(self._busy.values())) @connect(sender=gui) def on_close(e): gui.state['color_selector'] = self.color_selector.state unconnect(on_is_busy) @connect(sender=self.cluster_view) def on_ready(sender): """Select the clusters from the cluster view state.""" selected = gui.state.get('ClusterView', {}).get('selected', []) if selected: # pragma: no cover self.cluster_view.select(selected)
def __init__( self, spike_clusters=None, cluster_groups=None, cluster_metrics=None, cluster_labels=None, similarity=None, new_cluster_id=None, sort=None, context=None): super(Supervisor, self).__init__() self.context = context self.similarity = similarity # function cluster => [(cl, sim), ...] self.actions = None # will be set when attaching the GUI self._is_dirty = None self._sort = sort # Initial sort requested in the constructor # Cluster metrics. # This is a dict {name: func cluster_id => value}. self.cluster_metrics = cluster_metrics or {} self.cluster_metrics['n_spikes'] = self.n_spikes # Cluster labels. # This is a dict {name: {cl: value}} self.cluster_labels = cluster_labels or {} self.columns = ['id'] # n_spikes comes from cluster_metrics self.columns += list(self.cluster_metrics.keys()) self.columns += [ label for label in self.cluster_labels.keys() if label not in self.columns + ['group']] # Create Clustering and ClusterMeta. # Load the cached spikes_per_cluster array. spc = context.load('spikes_per_cluster') if context else None self.clustering = Clustering( spike_clusters, spikes_per_cluster=spc, new_cluster_id=new_cluster_id) # Cache the spikes_per_cluster array. self._save_spikes_per_cluster() # Create the ClusterMeta instance. self.cluster_meta = create_cluster_meta(cluster_groups or {}) # Add the labels. for label, values in self.cluster_labels.items(): if label == 'group': continue self.cluster_meta.add_field(label) for cl, v in values.items(): self.cluster_meta.set(label, [cl], v, add_to_stack=False) # Create the GlobalHistory instance. self._global_history = GlobalHistory(process_ups=_process_ups) # Create The Action Creator instance. self.action_creator = ActionCreator(self) connect(self._on_action, event='action', sender=self.action_creator) # Log the actions. connect(self._log_action, event='cluster', sender=self.clustering) connect(self._log_action_meta, event='cluster', sender=self.cluster_meta) # Raise supervisor.cluster @connect(sender=self.clustering) def on_cluster(sender, up): # NOTE: update the cluster meta of new clusters, depending on the values of the # ancestor clusters. In case of a conflict between the values of the old clusters, # the largest cluster wins and its value is set to its descendants. if up.added: self.cluster_meta.set_from_descendants( up.descendants, largest_old_cluster=up.largest_old_cluster) emit('cluster', self, up) @connect(sender=self.cluster_meta) # noqa def on_cluster(sender, up): # noqa emit('cluster', self, up) connect(self._save_new_cluster_id, event='cluster', sender=self) self._is_busy = False
def attach(self, gui): """Attach the GUI.""" super(ClusterScatterView, self).attach(gui) def _make_action(dim, name): def callback(): self.change_bindings(**{dim: name}) return callback def _make_log_toggle(dim): def callback(checked): self.toggle_log_scale(dim, checked) return callback # Change the bindings. for dim in self._dims: view_submenu = 'Change %s' % dim # Change to every cluster info. for name in self.fields: self.actions.add(_make_action(dim, name), show_shortcut=False, name='Change %s to %s' % (dim, name), view_submenu=view_submenu) # Toggle logarithmic scale. self.actions.separator(view_submenu=view_submenu) self.actions.add(_make_log_toggle(dim), checkable=True, view_submenu=view_submenu, name='Toggle log scale for %s' % dim, show_shortcut=False, checked=getattr(self, '%s_log_scale' % dim)) self.actions.separator() self.actions.add(self.set_x_axis, prompt=True, prompt_default=lambda: self.x_axis) self.actions.add(self.set_y_axis, prompt=True, prompt_default=lambda: self.y_axis) self.actions.add(self.set_size, prompt=True, prompt_default=lambda: self.size) connect(self.on_select) connect(self.on_cluster) @connect(sender=self.canvas) def on_lasso_updated(sender, polygon): if len(polygon) < 3: return pos = range_transform([self.data_bounds], [NDC], self.marker_positions) ind = self.canvas.lasso.in_polygon(pos) cluster_ids = self.all_cluster_ids[ind] emit("request_select", self, list(cluster_ids)) @connect(sender=self) def on_close_view(view_, gui): """Unconnect all events when closing the view.""" unconnect(self.on_select) unconnect(self.on_cluster) unconnect(on_lasso_updated) if self.all_cluster_ids is not None: self.set_cluster_ids(self.all_cluster_ids) self._update_labels()
def attach(self, gui): super(LassoMixin, self).attach(gui) connect(self.on_request_split)