class TabBar(QCSWidget): tabSelected = QtCore.pyqtSignal(str) def __init__(self, spacing=10, parent=None): super().__init__(parent=parent) self.hbox = HBox(self, spacing=spacing, align=QtCore.Qt.AlignHCenter) self.tabs = dict() self.tab_group = QtWidgets.QButtonGroup() self.tab_group.setExclusive(True) def addTab(self, name, title): if name in self.tabs: return btn = QtWidgets.QPushButton(title) btn.setCheckable(True) self.hbox.addWidget(btn) self.tabs[name] = btn self.tab_group.addButton(btn) btn.toggled.connect(lambda t: t and self.tabSelected.emit(name)) def setSelected(self, name): if name in self.tabs: self.tabs[name].setChecked(True) def clear(self): for tab in self.tabs.values(): tab.setParent(None) self.tab_group.removeButton(tab) self.tabs.clear()
class AnnotationLabel(QCSWidget): __height__ = 30 removed = QtCore.pyqtSignal(int) def __init__(self, label, level_dataset, parent=None): super().__init__(parent=parent) self.level_dataset = level_dataset self.label_idx = label['idx'] self.label_color = label['color'] self.label_name = label['name'] self.label_visible = label['visible'] self.btn_del = DelIconButton(secondary=True) self.txt_label_name = LineEdit(label['name']) self.btn_label_color = ColorButton(label['color']) self.btn_select = Spacing(35) self.setMinimumHeight(self.__height__) self.setFixedHeight(self.__height__) self.txt_label_name.editingFinished.connect(self.update_label) self.btn_label_color.colorChanged.connect(self.update_label) self.btn_del.clicked.connect(self.delete) hbox = HBox(self) hbox.addWidget( HWidgets(self.btn_del, self.btn_label_color, self.txt_label_name, self.btn_select, stretch=2)) def update_label(self): label = dict(idx=self.label_idx, name=self.txt_label_name.text(), color=self.btn_label_color.color, visible=self.label_visible) params = dict(level=self.level_dataset, workspace=True) result = Launcher.g.run('annotations', 'update_label', **params, **label) if result: self.label_name = result['name'] self.label_color = result['color'] self.label_visible = result['visible'] _AnnotationNotifier.notify() else: self.txt_label_name.setText(self.label_name) self.btn_label_color.setColor(self.label_color) def delete(self): params = dict(level=self.level_dataset, workspace=True, idx=self.label_idx) result = Launcher.g.run('annotations', 'delete_label', **params) if result: _AnnotationNotifier.notify() self.removed.emit(self.label_idx)
class AnnotationLevel(Card): removed = QtCore.pyqtSignal(str) def __init__(self, level, parent=None): super().__init__(level['name'], editable=True, collapsible=True, removable=True, addbtn=True, parent=parent) self.level_id = level['id'] self.le_title = LineEdit(level['name']) self.le_title.setProperty('header', True) self.labels = {} self._populate_labels() def card_title_edited(self, title): params = dict(level=self.level_id, name=title, workspace=True) return Launcher.g.run('annotations', 'rename_level', **params) def card_add_item(self): params = dict(level=self.level_id, workspace=True) result = Launcher.g.run('annotations', 'add_label', **params) if result: self._add_label_widget(result) _AnnotationNotifier.notify() def card_deleted(self): params = dict(level=self.level_id, workspace=True) result = Launcher.g.run('annotations', 'delete_level', **params) if result: self.removed.emit(self.level_id) _AnnotationNotifier.notify() def remove_label(self, idx): if idx in self.labels: self.labels.pop(idx).setParent(None) self.update_height() def _add_label_widget(self, label): widget = AnnotationLabel(label, self.level_id) widget.removed.connect(self.remove_label) self.add_row(widget) self.labels[label['idx']] = widget self.expand() def _populate_labels(self): params = dict(level=self.level_id, workspace=True) result = Launcher.g.run('annotations', 'get_labels', **params) if result: for k, label in result.items(): if k not in self.labels: self._add_label_widget(label)
class ColorButton(QtWidgets.QPushButton): colorChanged = QtCore.pyqtSignal(str) def __init__(self, color='#000000', clickable=True, **kwargs): super().__init__(**kwargs) self.setColor(color) if clickable: self.clicked.connect(self.on_click) def setColor(self, color): color = str(QtGui.QColor(color).name()) if color is None: self.setStyleSheet(""" QPushButton, QPushButton:hover { background-color: qlineargradient( x1:0, y1:0, x2:1, y2:1, stop: 0 white, stop: 0.15 white, stop: 0.2 red, stop: 0.25 white, stop: 0.45 white, stop: 0.5 red, stop: 0.55 white, stop: 0.75 white, stop: 0.8 red, stop: 0.85 white ); } """) else: self.setStyleSheet(""" QPushButton { background-color: %s; } QPushButton:hover { background-color: qlineargradient( x1:0, y1:0, x2:0.5, y2:1, stop: 0 white, stop: 1 %s ); } """ % (color, color)) self.color = color def on_click(self): c = QtWidgets.QColorDialog.getColor(QtGui.QColor(self.color), self.parent()) if not c.isValid(): return self.setColor(str(c.name())) self.colorChanged.emit(self.color) def value(self): return self.color
class AcceptModal(_Modal): accepted = QtCore.pyqtSignal() def __init__(self, message, btn_text='Close', parent=None): super().__init__(message, parent=parent) self.btn = PushButton(btn_text) if parent: self.btn.clicked.connect(parent.hide) self.btn.clicked.connect(self._trigger) self(HWidgets(None, self.btn, None)) def _trigger(self): self.accepted.emit()
class YesNoModal(_Modal): accepted = QtCore.pyqtSignal(bool) def __init__(self, message, msg_yes='Accept', msg_no='Cancel', parent=None): super().__init__(message, parent=parent) self.yes = PushButton(msg_yes) self.no = PushButton(msg_no) self.yes.clicked.connect(self._yes) self.no.clicked.connect(self._no) self(HWidgets(None, self.no, None, self.yes, None)) def _yes(self): self.accepted.emit(True) def _no(self): self.accepted.emit(False)
class LayerManager(QtWidgets.QMenu): __all_layers__ = {} paramsUpdated = QtCore.pyqtSignal() def __init__(self, parent=None): super().__init__(parent=parent) vbox = VBox(self, spacing=5, margin=5) self.vmin = RealSlider(value=0, vmin=-1, vmax=1, auto_accept=False) self.vmax = RealSlider(value=1, vmin=0, vmax=1, auto_accept=False) vbox.addWidget(self.vmin) vbox.addWidget(self.vmax) self._layers = OrderedDict() for key, title, cls in self.__all_layers__: layer = cls(key) layer.updated.connect(self.on_layer_updated) vbox.addWidget(SubHeader(title)) vbox.addWidget(layer) self._layers[key] = layer self.vmin.valueChanged.connect(self.on_layer_updated) self.vmax.valueChanged.connect(self.on_layer_updated) def refresh(self): for layer in self._layers.values(): layer.update() return self def on_layer_updated(self): self.paramsUpdated.emit() def show_layer(self, layer, view): if layer in self._layers: self._layers[layer].select(view) def value(self): params = {k: v.value() for k, v in self._layers.items()} params['clim'] = (self.vmin.value(), self.vmax.value()) return params def accept(self): self.vmin.accept() self.vmax.accept() for layer in self._layers.values(): layer.accept()
def __init__(self, icon, text='', size=None, checkable=False, parent=None, **kwargs): super().__init__(parent) self.setStyleSheet(""" QToolButton { margin-left: 0px; } QToolButton::menu-indicator { width: 0px; border: none; image: none; } """) self.setText(text) self.setIcon(qta.icon(icon, **kwargs)) if size is not None: self.setIconSize(QtCore.QSize(*size)) self.setCheckable(checkable) self.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon);
class Layer(QCSWidget): updated = QtCore.pyqtSignal() def __init__(self, name='layer', source=None, cmap=None, parent=None): super().__init__(parent=parent) self.name = name self.source = source or ComboBox() self.cmap = cmap or CmapComboBox() self.slider = Slider(value=100, label=False, auto_accept=False) self.checkbox = CheckBox(checked=True) hbox = HBox(self, spacing=5, margin=(5, 0, 5, 0)) hbox.addWidget(self.source, 1) hbox.addWidget(self.cmap, 1) hbox.addWidget(self.slider) hbox.addWidget(self.checkbox) if hasattr(self.source, 'currentIndexChanged'): self.source.currentIndexChanged.connect(self._params_updated) elif hasattr(self.source, 'valueChanged'): self.source.valueChanged.connect(self._params_updated) if hasattr(self.cmap, 'currentIndexChanged'): self.cmap.currentIndexChanged.connect(self._params_updated) elif hasattr(self.cmap, 'colorChanged'): self.cmap.colorChanged.connect(self._params_updated) self.slider.setMinimumWidth(150) self.slider.valueChanged.connect(self._params_updated) self.checkbox.toggled.connect(self._params_updated) def value(self): return (self.source.value(), self.cmap.value(), self.slider.value(), self.checkbox.value()) def _params_updated(self): self.updated.emit() def accept(self): self.slider.accept() def select(self, view): self.source.select(view) self.updated.emit()
class QCSWidget(SWidget): resized = QtCore.pyqtSignal() def __init__(self, parent=None): super().__init__(self.__class__.__name__, parent=parent) def resizeEvent(self, event): super().resizeEvent(event) self.resized.emit() def __call__(self, widget, stretch=0, connect=None): if self.layout() is not None: self.layout().addWidget(widget, stretch=stretch) if connect and hasattr(widget, connect[0]): getattr(widget, connect[0]).connect(connect[1]) def value(self): return None
class PluginNotifier(QtCore.QObject): updated = QtCore.pyqtSignal() def listen(self, *args, **kwargs): self.updated.connect(*args, **kwargs) def notify(self): self.updated.emit()
class SliceViewer(QCSWidget): slice_updated = QtCore.pyqtSignal(int) def __init__(self, layer_manager=None, parent=None): super().__init__(parent=parent) self.slider = Slider(auto_accept=False, center=True) self.viewer = Viewer() self.menu_panel = QtWidgets.QToolBar() self.tool_container = VBox(margin=0, spacing=0) self.current_tool = None vbox = VBox(self, margin=15, spacing=10) vbox.addWidget(self.slider) vbox.addWidget(self.viewer, 1) vbox2 = VBox(margin=0, spacing=0) vbox2.addLayout(self.tool_container) vbox2.addWidget(self.menu_panel) vbox.addLayout(vbox2) if layer_manager is None: layer_manager = WorkspaceLayerManager self.tools = [] self.viz_params = None self.layer_manager = self.add_tool('Layer', 'fa.adjust', layer_manager) self.layer_manager.paramsUpdated.connect(self.params_updated) self.slider.valueChanged.connect(self.show_slice) self.slider.sliderReleased.connect(self._updated) self.viewer.mpl_connect('button_press_event', self.show_layer_manager) self.update() def eventFilter(self, source, event): if event.type() == QtCore.QEvent.Show: p = self.menu_panel point = QtCore.QPoint(0, -source.height()) source.move(p.mapToGlobal(point)) return source.event(event) def clear_tools(self): for tool in self.tools[1:]: if tool.menu(): tool.menu().removeEventFilter(self) tool.setParent(None) self.tools = [self.tools[0]] def add_tool(self, name, icon, menu=None, tool=None): btn = ToolIconButton(icon, name, size=(36, 36), color='white', checkable=tool is not None and menu is None) self.tools.append(btn) self.menu_panel.addWidget(btn) if tool: tool.setProperty('menu', True) tool.setVisible(False) tool.set_viewer(self) self.tool_container.addWidget(tool) btn.toggled.connect(lambda flag: self.toggle_tool(tool, flag)) if menu: menu = menu(btn) btn.setMenu(menu) btn.setPopupMode(QtWidgets.QToolButton.InstantPopup) btn.toggled.connect(btn.showMenu) menu.installEventFilter(self) return menu def toggle_tool(self, tool, flag): tool.setEnabled(flag) if flag: self.current_tool = tool for tool in layout_widgets(self.tool_container): tool.setVisible(False) self.current_tool.setVisible(True) self.current_tool.slice_updated(self.slider.value()) else: tool.setVisible(False) self.current_tool = None def update_viz_params(self): params = self.layer_manager.value() self.viz_params = {k: v for k, v in params.items() if v[0] is not None} def show_slice(self, idx): params = dict(slice_idx=idx, workspace=True, timeit=True) params.update(self.viz_params) result = Launcher.g.run('render', 'render_workspace', **params) if result: image = decode_numpy(result) self.viewer.update_image(image) self.slider.accept() def update(self): self.update_viz_params() self.show_slice(self.slider.value()) def _updated(self): idx = self.slider.value() self.slice_updated.emit(idx) def params_updated(self): self.update() self.layer_manager.accept() def setup(self): max_depth = DataModel.g.current_workspace_shape[0] self.slider.setMaximum(max_depth - 1) def show_layer_manager(self, event): if event.button != 3: return self.layer_manager.show() def triggerKeybinding(self, key, modifiers): if key == QtCore.Qt.Key_H: self.viewer.center() elif self.current_tool and hasattr(self.current_tool, 'triggerKeybinding'): self.current_tool.triggerKeybinding(key, modifiers) def install_extension(self, ext): self.viewer.install_extension(ext) def show_layer(self, plugin, view): self.layer_manager.show_layer(plugin, view)
def parent_resized(self): self.move(QtCore.QPoint(0, 0)) self.resize(self.parent().size())
class Annotator(ViewerExtension): annotated = QtCore.pyqtSignal(tuple) def __init__(self, **kwargs): super().__init__(**kwargs) self.color = (1, 0, 0, 1) self.color_overlay = (1, 0, 0, 0.5) self.annotating = False self.line = None self.line_width = 1 self.region = None self.region_mask = None self.cmap = None self.current = dict() self.all_regions = set() def install(self, fig, axes): super().install(fig, axes) self.connect('button_press_event', self.draw_press) self.connect('button_release_event', self.draw_release) self.connect('motion_notify_event', self.draw_motion) self.data_size = DataModel.g.current_workspace_shape[1:] self.line_mask = np.zeros(self.data_size, np.bool) cmap = ListedColormap([(0, 0, 0, 0)] * 2) self.line_mask_ax = self.axes.imshow(self.line_mask, cmap=cmap, vmin=0, vmax=1) if self.region is not None: self.region_mask = self.axes.imshow( np.empty(self.data_size, np.uint8)) self.initialize_region() def disable(self): super().disable() if self.line_mask_ax: self.line_mask_ax.remove() self.line_mask_ax = None self.line_mask = None if self.region_mask: self.region_mask.remove() self.region_mask = None def set_region(self, region): self.region = region if self.axes is None: return if self.region is None and self.region_mask is not None: self.region_mask.remove() self.region_mask = None if region is None: return if self.region_mask is None: self.region_mask = self.axes.imshow( np.empty(self.data_size, np.uint8)) if region is not None: self.initialize_region() self.redraw() def initialize_region(self): n = self.region.max() self.region_mask.set_data(self.region) self.region_mask.set_clim(0, n) self.region_mask.set_alpha(1) self.initialize_region_cmap() def initialize_region_cmap(self): n = self.region.max() + 1 cmap = [(0, 0, 0, 0)] * n self.cmap = ListedColormap(cmap) self.region_mask.set_cmap(self.cmap) def set_color(self, color): self.color = QtGui.QColor(color).getRgbF() self.color_overlay = self.color[:3] + (0.5, ) self.line_mask_ax.set_cmap(ListedColormap([(0, 0, 0, 0), self.color])) def set_linewidth(self, line_width): self.line_width = line_width def draw_press(self, evt): if not evt.inaxes == self.axes or evt.button != 1: return if self.line is not None: self.draw_release(evt) self.annotating = True y = int(evt.ydata) x = int(evt.xdata) self.current = dict(y=[y], x=[x]) self.line_mask[y, x] = True if self.region is not None and self.cmap is not None: if hasattr(self.cmap, '_lut'): sv = self.region[y, x] self.all_regions |= set([sv]) self.cmap._lut[sv] = self.color_overlay self.region_mask.set_cmap(self.cmap) self.fig.redraw() def draw_motion(self, evt): if not evt.inaxes == self.axes or not self.annotating: return y = int(evt.ydata) x = int(evt.xdata) py = self.current['y'][-1] px = self.current['x'][-1] yy, xx = line(py, px, y, x) self.current['y'].append(y) self.current['x'].append(x) if self.line_width > 1: yy, xx = self.dilate_annotations(yy, xx) self.line_mask[yy, xx] = True self.line_mask_ax.set_data(self.line_mask) if self.region is not None and self.cmap is not None: svs = set(self.region[yy, xx]) self.all_regions |= svs modified = False if hasattr(self.cmap, '_lut'): for sv in svs: if self.cmap._lut[sv][-1] == 0: self.cmap._lut[sv] = self.color_overlay modified = True if modified: self.region_mask.set_cmap(self.cmap) self.fig.redraw() def draw_release(self, evt): if not self.annotating: return self.annotating = False if self.region is None: annotations = np.where(self.line_mask) else: annotations = tuple(self.all_regions) # Clear Line self.current.clear() self.line_mask[:] = False self.line_mask_ax.set_data(self.line_mask) # Clear regions if self.region is not None: self.initialize_region_cmap() self.all_regions = set() # Render clean self.fig.redraw() # Send result self.annotated.emit(annotations) def dilate_annotations(self, yy, xx): data = np.zeros_like(self.line_mask) data[yy, xx] = True r = np.ceil(self.line_width / 2) ymin = int(max(0, yy.min() - r)) ymax = int(min(data.shape[0], yy.max() + 1 + r)) xmin = int(max(0, xx.min() - r)) xmax = int(min(data.shape[1], xx.max() + 1 + r)) mask = data[ymin:ymax, xmin:xmax] mask = binary_dilation(mask, disk(self.line_width / 2).astype(np.bool)) yy, xx = np.where(mask) yy += ymin xx += xmin return yy, xx
class MultiComboBox(QtWidgets.QPushButton, AbstractLazyWrapper): valueChanged = QtCore.pyqtSignal() def __init__(self, header=None, lazy=False, select=None, text='Select', groupby=None, parent=None): self._items = [] self._actions = [] self._header = header self._groupby = groupby self._text = text QtWidgets.QPushButton.__init__(self, parent=parent) self._toolmenu = QtWidgets.QMenu(self) self._toolmenu.installEventFilter(self) AbstractLazyWrapper.__init__(self, lazy) self.setMenu(self._toolmenu) #self.setPopupMode(QtWidgets.QToolButton.InstantPopup) self.setProperty('combo', True) self._toolmenu.setProperty('combo', True) self._toolmenu.aboutToHide.connect(self._update_text) self._update_text() # TODO: improve this file_path = resource('qcs', 'survos.qcs') if op.isfile(file_path): with open(file_path, 'r') as f: style = f.read() self.setStyleSheet(style) self._toolmenu.setStyleSheet(style) def _add_header(self): if self._header is not None: self.addItem(*self._header) def _update_text(self): names = self.names() if len(names) == 0: self.setText(self._text) else: self.setText('; '.join(names)) def eventFilter(self, target, evt): super().eventFilter(target, evt) if evt.type() == QtCore.QEvent.MouseButtonRelease: action = self._toolmenu.activeAction() if action: action.setChecked(not action.isChecked()) self.valueChanged.emit() return True return False def addCategory(self, name): item = self.addItem(name) item.setEnabled(False) def addItem(self, key, value=None, icon=None, data=None, checkable=True): self._items.append((key, value, data)) text = value if value else key if icon: action = self._toolmenu.addAction(icon, text) else: action = self._toolmenu.addAction(text) action.setCheckable(checkable) self._actions.append(action) return action def items(self): return (item for item in self._items) def removeItem(self, idx): self._items.pop(idx) self._toolmenu.removeAction(self._actions.pop(idx)) def update(self): prev = list(self.keys()) self.clear() self._add_header() self.fill() for i, (key, value, data) in enumerate(self.items()): if key in prev: prev.remove(key) self.setItemChecked(i, True) self._update_text() if len(prev) > 0: self.valueChanged.emit() def clear(self): self._items.clear() self._actions.clear() self._toolmenu.clear() def select(self, key): for i, (k, v, d) in enumerate(self._items): if not k: continue kname = k.split(op.sep) if k == key or (len(kname) > 1 and kname[1] == key): if not self.itemChecked(i): self.setItemChecked(i, True) self._update_text() self.valueChanged.emit() return True break return False def select_prefix(self, key): found = False for i, k in enumerate(self._items): if k and k[0].startswith(key): if not self.itemChecked(i): found = True self.setItemChecked(i, True) if found: self.valueChanged.emit() return found def key(self): return list(self.keys()) def value(self): return list(self.values()) def itemChecked(self, idx): return self._actions[idx].isChecked() def setItemChecked(self, idx, flag=True): self._actions[idx].setChecked(flag) def names(self): return list(item[1] for i, item in enumerate(self.items()) if self.itemChecked(i)) def keys(self): return self.values(keys=True) def values(self, keys=False): values = (item[0] if item[2] is None or keys else item[2] for i, item in enumerate(self.items()) if self.itemChecked(i)) if keys: return values if self._groupby: result = defaultdict(list) for val in values: if len(self._groupby) == 2: group, target = self._groupby if group in val: result[val[group]].append(val[target]) else: if self._groupby in val: result[val[self._groupby]].append(val) return [(k, v) for k,v in result.items()] return values
class Slider(QCSWidget): valueChanged = QtCore.pyqtSignal(int) def __init__(self, value=None, vmax=100, vmin=0, step=1, tracking=True, label=True, auto_accept=True, center=False, parent=None): super().__init__(parent=parent) if value is None: value = vmin self.setMinimumWidth(200) self.slider = QtWidgets.QSlider(QtCore.Qt.Horizontal) self.slider.setMinimum(vmin) self.slider.setMaximum(vmax) self.slider.setValue(value) self.slider.setTickInterval(step) self.slider.setSingleStep(step) self.slider.setTracking(tracking) self.step = step hbox = HBox(self, spacing=5) if label: self.label = Label(str(value)) self.label.setMinimumWidth(50) if center: hbox.addSpacing(50) hbox.addWidget(self.slider, 1) hbox.addWidget(self.label) self.valueChanged.connect(self.update_label) else: hbox.addWidget(self.slider, 1) self.slider.valueChanged.connect(self.value_changed) self.slider.wheelEvent = self.wheelEvent self.auto_accept = auto_accept self.locked_idx = None self.pending = None self.blockSignals = self.slider.blockSignals def value_changed(self, idx): if self.auto_accept: self.valueChanged.emit(idx) elif self.locked_idx is None: self.locked_idx = idx self.valueChanged.emit(idx) else: self.slider.blockSignals(True) self.slider.setValue(self.locked_idx) self.slider.blockSignals(False) self.pending = idx def accept(self): if self.pending is not None: val = self.pending self.pending = None self.slider.blockSignals(True) self.slider.setValue(val) self.slider.blockSignals(False) self.valueChanged.emit(val) self.locked_idx = None def update_label(self, idx): self.label.setText(str(idx)) def wheelEvent(self, e): if e.angleDelta().y() > 0 and self.value() < self.maximum(): self.setValue(self.value()+self.step) elif e.angleDelta().y() < 0 and self.value() > self.minimum(): self.setValue(self.value()-self.step) def value(self): return self.pending or self.slider.value() def setValue(self, value): return self.slider.setValue(value) def __getattr__(self, key): return self.slider.__getattribute__(key)
def eventFilter(self, source, event): if event.type() == QtCore.QEvent.Show: p = self.menu_panel point = QtCore.QPoint(0, -source.height()) source.move(p.mapToGlobal(point)) return source.event(event)