class QtViewer(QSplitter): with open(os.path.join(resources_dir, 'stylesheet.qss'), 'r') as f: raw_stylesheet = f.read() def __init__(self, viewer): super().__init__() QCoreApplication.setAttribute( Qt.AA_UseStyleSheetPropagationInWidgetStyles, True ) self.viewer = viewer self.dims = QtDims(self.viewer.dims) self.controls = QtControls(self.viewer) self.layers = QtLayerList(self.viewer.layers) self.buttons = QtLayersButtons(self.viewer) self.console = QtConsole({'viewer': self.viewer}) if self.console.shell is not None: self.console.style().unpolish(self.console) self.console.style().polish(self.console) self.console.hide() self.buttons.consoleButton.clicked.connect( lambda: self._toggle_console() ) else: self.buttons.consoleButton.setEnabled(False) self.canvas = SceneCanvas(keys=None, vsync=True) self.canvas.native.setMinimumSize(QSize(200, 200)) self.canvas.connect(self.on_mouse_move) self.canvas.connect(self.on_mouse_press) self.canvas.connect(self.on_mouse_release) self.canvas.connect(self.on_key_press) self.canvas.connect(self.on_key_release) self.canvas.connect(self.on_draw) self.view = self.canvas.central_widget.add_view() self._update_camera() center = QWidget() center_layout = QVBoxLayout() center_layout.setContentsMargins(15, 20, 15, 10) center_layout.addWidget(self.canvas.native) center_layout.addWidget(self.dims) center.setLayout(center_layout) right = QWidget() right_layout = QVBoxLayout() right_layout.addWidget(self.layers) right_layout.addWidget(self.buttons) right.setLayout(right_layout) right.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding) left = self.controls top = QWidget() top_layout = QHBoxLayout() top_layout.addWidget(left) top_layout.addWidget(center) top_layout.addWidget(right) top.setLayout(top_layout) self.setOrientation(Qt.Vertical) self.addWidget(top) if self.console.shell is not None: self.addWidget(self.console) self._last_visited_dir = str(Path.home()) self._cursors = { 'disabled': QCursor( QPixmap(':/icons/cursor/cursor_disabled.png').scaled(20, 20) ), 'cross': Qt.CrossCursor, 'forbidden': Qt.ForbiddenCursor, 'pointing': Qt.PointingHandCursor, 'standard': QCursor(), } self._update_palette(viewer.palette) self._key_release_generators = {} self.viewer.events.interactive.connect(self._on_interactive) self.viewer.events.cursor.connect(self._on_cursor) self.viewer.events.reset_view.connect(self._on_reset_view) self.viewer.events.palette.connect( lambda event: self._update_palette(event.palette) ) self.viewer.layers.events.reordered.connect(self._update_canvas) self.viewer.dims.events.display.connect( lambda event: self._update_camera() ) self.setAcceptDrops(True) def _update_camera(self): if np.sum(self.viewer.dims.display) == 3: # Set a 3D camera self.view.camera = ArcballCamera(name="ArcballCamera") # flip y-axis to have correct alignment self.view.camera.flip = (0, 1, 0) min_shape, max_shape = self.viewer._calc_bbox() centroid = np.add(max_shape, min_shape) / 2 size = np.subtract(max_shape, min_shape) # Scale the camera to the contents in the scene if len(centroid) > 0: centroid = centroid[-3:] self.view.camera.center = centroid[::-1] self.view.camera.scale_factor = 1.5 * np.mean(size[-3:]) elif np.sum(self.viewer.dims.display) == 2: # Set 2D camera self.view.camera = PanZoomCamera(aspect=1, name="PanZoomCamera") # flip y-axis to have correct alignment self.view.camera.flip = (0, 1, 0) # Scale the camera to the contents in the scene self.view.camera.set_range() else: raise ValueError( "Invalid display flags set in dimensions {}".format( self.viewer.dims.display ) ) self.view.camera.viewbox_key_event = viewbox_key_event # TO DO: Remove self.viewer._view = self.view def screenshot(self): """Take currently displayed screen and convert to an image array. Returns ------- image : array Numpy array of type ubyte and shape (h, w, 4). Index [0, 0] is the upper-left corner of the rendered region. """ img = self.canvas.native.grabFramebuffer() b = img.constBits() h, w, c = img.height(), img.width(), 4 # As vispy doesn't use qtpy we need to reconcile the differences # between the `QImage` API for `PySide2` and `PyQt5` on how to convert # a QImage to a numpy array. if API_NAME == 'PySide2': arr = np.array(b).reshape(h, w, c) else: b.setsize(h * w * c) arr = np.frombuffer(b, np.uint8).reshape(h, w, c) # Format of QImage is ARGB32_Premultiplied, but color channels are # reversed. arr = arr[:, :, [2, 1, 0, 3]] return arr def _open_images(self): """Adds image files from the menubar.""" filenames, _ = QFileDialog.getOpenFileNames( parent=self, caption='Select image(s)...', directory=self._last_visited_dir, # home dir by default ) self._add_files(filenames) def _add_files(self, filenames): """Adds an image layer to the viewer. Whether the image is multichannel is determined by :func:`napari.util.misc.is_multichannel`. If multiple images are selected, they are stacked along the 0th axis. Parameters ------- filenames : list List of filenames to be opened """ if len(filenames) > 0: image = read(filenames) self.viewer.add_image( image, multichannel=is_multichannel(image.shape) ) self._last_visited_dir = os.path.dirname(filenames[0]) def _on_interactive(self, event): self.view.interactive = self.viewer.interactive def _on_cursor(self, event): cursor = self.viewer.cursor size = self.viewer.cursor_size if cursor == 'square': if size < 10 or size > 300: q_cursor = self._cursors['cross'] else: q_cursor = QCursor( QPixmap(':/icons/cursor/cursor_square.png').scaledToHeight( size ) ) else: q_cursor = self._cursors[cursor] self.canvas.native.setCursor(q_cursor) def _on_reset_view(self, event): self.view.camera.rect = event.viewbox def _update_canvas(self, event): """Clears draw order and refreshes canvas. Usefeul for when layers are reoredered. """ self.canvas._draw_order.clear() self.canvas.update() def _update_palette(self, palette): # template and apply the primary stylesheet themed_stylesheet = template(self.raw_stylesheet, **palette) self.console.style_sheet = themed_stylesheet self.console.syntax_style = palette['syntax_style'] self.setStyleSheet(themed_stylesheet) def _toggle_console(self): """Toggle console visible and not visible.""" self.console.setVisible(not self.console.isVisible()) self.buttons.consoleButton.setProperty( 'expanded', self.console.isVisible() ) self.buttons.consoleButton.style().unpolish(self.buttons.consoleButton) self.buttons.consoleButton.style().polish(self.buttons.consoleButton) def on_mouse_move(self, event): """Called whenever mouse moves over canvas. """ layer = self.viewer.active_layer if layer is not None: layer.on_mouse_move(event) def on_mouse_press(self, event): """Called whenever mouse pressed in canvas. """ layer = self.viewer.active_layer if layer is not None: layer.on_mouse_press(event) def on_mouse_release(self, event): """Called whenever mouse released in canvas. """ layer = self.viewer.active_layer if layer is not None: layer.on_mouse_release(event) def on_key_press(self, event): """Called whenever key pressed in canvas. """ if event.native.isAutoRepeat() or event.key is None: return comb = components_to_key_combo(event.key.name, event.modifiers) layer = self.viewer.active_layer if layer is not None and comb in layer.keymap: parent = layer elif comb in self.viewer.keymap: parent = self.viewer else: return func = parent.keymap[comb] gen = func(parent) if inspect.isgeneratorfunction(func): try: next(gen) except StopIteration: # only one statement pass else: self._key_release_generators[event.key] = gen def on_key_release(self, event): """Called whenever key released in canvas. """ try: next(self._key_release_generators[event.key]) except (KeyError, StopIteration): pass def on_draw(self, event): """Called whenever drawn in canvas. Called for all layers, not just top """ for layer in self.viewer.layers: layer.on_draw(event) def dragEnterEvent(self, event): if event.mimeData().hasUrls(): event.accept() else: event.ignore() def dropEvent(self, event): """Add local files and web URLS with drag and drop.""" filenames = [] for url in event.mimeData().urls(): path = url.toString() if os.path.isfile(path): filenames.append(path) elif os.path.isdir(path): filenames = filenames + list(glob(os.path.join(path, '*'))) else: filenames.append(path) self._add_files(filenames)
class QtViewer(QSplitter): with open(os.path.join(resources_dir, 'stylesheet.qss'), 'r') as f: raw_stylesheet = f.read() def __init__(self, viewer): super().__init__() self.pool = QThreadPool() QCoreApplication.setAttribute( Qt.AA_UseStyleSheetPropagationInWidgetStyles, True) self.viewer = viewer self.dims = QtDims(self.viewer.dims) self.controls = QtControls(self.viewer) self.layers = QtLayerList(self.viewer.layers) self.layerButtons = QtLayerButtons(self.viewer) self.viewerButtons = QtViewerButtons(self.viewer) self.console = QtConsole({'viewer': self.viewer}) layerList = QWidget() layerList.setObjectName('layerList') layerListLayout = QVBoxLayout() layerListLayout.addWidget(self.layerButtons) layerListLayout.addWidget(self.layers) layerListLayout.addWidget(self.viewerButtons) layerListLayout.setContentsMargins(8, 4, 8, 6) layerList.setLayout(layerListLayout) self.dockLayerList = QtViewerDockWidget( self, layerList, name='layer list', area='left', allowed_areas=['left', 'right'], ) self.dockLayerControls = QtViewerDockWidget( self, self.controls, name='layer controls', area='left', allowed_areas=['left', 'right'], ) self.dockConsole = QtViewerDockWidget( self, self.console, name='console', area='bottom', allowed_areas=['top', 'bottom'], shortcut='Ctrl+Shift+C', ) self.dockConsole.setVisible(False) self.dockLayerControls.visibilityChanged.connect(self._constrain_width) self.dockLayerList.setMaximumWidth(258) self.dockLayerList.setMinimumWidth(258) self.aboutKeybindings = QtAboutKeybindings(self.viewer) self.aboutKeybindings.hide() # This dictionary holds the corresponding vispy visual for each layer self.layer_to_visual = {} if self.console.shell is not None: self.viewerButtons.consoleButton.clicked.connect( lambda: self.toggle_console()) else: self.viewerButtons.consoleButton.setEnabled(False) self.canvas = SceneCanvas(keys=None, vsync=True) self.canvas.events.ignore_callback_errors = False self.canvas.events.draw.connect(self.dims.enable_play) self.canvas.native.setMinimumSize(QSize(200, 200)) self.canvas.context.set_depth_func('lequal') self.canvas.connect(self.on_mouse_move) self.canvas.connect(self.on_mouse_press) self.canvas.connect(self.on_mouse_release) self.canvas.connect(self.on_key_press) self.canvas.connect(self.on_key_release) self.canvas.connect(self.on_draw) self.view = self.canvas.central_widget.add_view() self._update_camera() main_widget = QWidget() main_layout = QVBoxLayout() main_layout.setContentsMargins(10, 22, 10, 2) main_layout.addWidget(self.canvas.native) main_layout.addWidget(self.dims) main_layout.setSpacing(10) main_widget.setLayout(main_layout) self.setOrientation(Qt.Vertical) self.addWidget(main_widget) self._last_visited_dir = str(Path.home()) self._cursors = { 'disabled': QCursor( QPixmap(':/icons/cursor/cursor_disabled.png').scaled(20, 20)), 'cross': Qt.CrossCursor, 'forbidden': Qt.ForbiddenCursor, 'pointing': Qt.PointingHandCursor, 'standard': QCursor(), } self._update_palette(viewer.palette) self._key_release_generators = {} self.viewer.events.interactive.connect(self._on_interactive) self.viewer.events.cursor.connect(self._on_cursor) self.viewer.events.reset_view.connect(self._on_reset_view) self.viewer.events.palette.connect( lambda event: self._update_palette(event.palette)) self.viewer.layers.events.reordered.connect(self._reorder_layers) self.viewer.layers.events.added.connect(self._add_layer) self.viewer.layers.events.removed.connect(self._remove_layer) self.viewer.dims.events.camera.connect( lambda event: self._update_camera()) # stop any animations whenever the layers change self.viewer.events.layers_change.connect(lambda x: self.dims.stop()) self.setAcceptDrops(True) def _constrain_width(self, event): # allow the layer controls to be wider, only if floated if self.dockLayerControls.isFloating(): self.controls.setMaximumWidth(700) else: self.controls.setMaximumWidth(220) def _add_layer(self, event): """When a layer is added, set its parent and order.""" layers = event.source layer = event.item vispy_layer = create_vispy_visual(layer) vispy_layer.camera = self.view.camera vispy_layer.node.parent = self.view.scene vispy_layer.order = len(layers) self.layer_to_visual[layer] = vispy_layer def _remove_layer(self, event): """When a layer is removed, remove its parent.""" layer = event.item vispy_layer = self.layer_to_visual[layer] vispy_layer.node.transforms = ChainTransform() vispy_layer.node.parent = None del self.layer_to_visual[layer] def _reorder_layers(self, event): """When the list is reordered, propagate changes to draw order.""" for i, layer in enumerate(self.viewer.layers): vispy_layer = self.layer_to_visual[layer] vispy_layer.order = i self.canvas._draw_order.clear() self.canvas.update() def _update_camera(self): if self.viewer.dims.ndisplay == 3: # Set a 3D camera if not isinstance(self.view.camera, ArcballCamera): self.view.camera = ArcballCamera(name="ArcballCamera", fov=0) # flip y-axis to have correct alignment # self.view.camera.flip = (0, 1, 0) self.view.camera.viewbox_key_event = viewbox_key_event self.viewer.reset_view() else: # Set 2D camera if not isinstance(self.view.camera, PanZoomCamera): self.view.camera = PanZoomCamera(aspect=1, name="PanZoomCamera") # flip y-axis to have correct alignment self.view.camera.flip = (0, 1, 0) self.view.camera.viewbox_key_event = viewbox_key_event self.viewer.reset_view() def screenshot(self): """Take currently displayed screen and convert to an image array. Returns ------- image : array Numpy array of type ubyte and shape (h, w, 4). Index [0, 0] is the upper-left corner of the rendered region. """ img = self.canvas.native.grabFramebuffer() return QImg2array(img) def _open_images(self): """Add image files from the menubar.""" filenames, _ = QFileDialog.getOpenFileNames( parent=self, caption='Select image(s)...', directory=self._last_visited_dir, # home dir by default ) if (filenames != []) and (filenames is not None): self._add_files(filenames) def _open_folder(self): """Add a folder of files from the menubar.""" folder = QFileDialog.getExistingDirectory( parent=self, caption='Select folder...', directory=self._last_visited_dir, # home dir by default ) if folder not in {'', None}: self._add_files([folder]) def _add_files(self, filenames): """Add an image layer to the viewer. If multiple images are selected, they are stacked along the 0th axis. Parameters ------- filenames : list List of filenames to be opened """ if len(filenames) > 0: self.viewer.add_image(path=filenames) self._last_visited_dir = os.path.dirname(filenames[0]) def _on_interactive(self, event): self.view.interactive = self.viewer.interactive def _on_cursor(self, event): cursor = self.viewer.cursor size = self.viewer.cursor_size if cursor == 'square': if size < 10 or size > 300: q_cursor = self._cursors['cross'] else: q_cursor = QCursor( QPixmap(':/icons/cursor/cursor_square.png').scaledToHeight( size)) else: q_cursor = self._cursors[cursor] self.canvas.native.setCursor(q_cursor) def _on_reset_view(self, event): if isinstance(self.view.camera, ArcballCamera): quat = self.view.camera._quaternion.create_from_axis_angle( *event.quaternion) self.view.camera._quaternion = quat self.view.camera.center = event.center self.view.camera.scale_factor = event.scale_factor else: # Assumes default camera has the same properties as PanZoomCamera self.view.camera.rect = event.rect def _update_palette(self, palette): # template and apply the primary stylesheet themed_stylesheet = template(self.raw_stylesheet, **palette) self.console.style_sheet = themed_stylesheet self.console.syntax_style = palette['syntax_style'] bracket_color = QtGui.QColor(*str_to_rgb(palette['highlight'])) self.console._bracket_matcher.format.setBackground(bracket_color) self.setStyleSheet(themed_stylesheet) self.aboutKeybindings.setStyleSheet(themed_stylesheet) self.canvas.bgcolor = palette['canvas'] def toggle_console(self): """Toggle console visible and not visible.""" viz = not self.dockConsole.isVisible() # modulate visibility at the dock widget level as console is docakable self.dockConsole.setVisible(viz) if self.dockConsole.isFloating(): self.dockConsole.setFloating(True) self.viewerButtons.consoleButton.setProperty( 'expanded', self.dockConsole.isVisible()) self.viewerButtons.consoleButton.style().unpolish( self.viewerButtons.consoleButton) self.viewerButtons.consoleButton.style().polish( self.viewerButtons.consoleButton) def on_mouse_press(self, event): """Called whenever mouse pressed in canvas. """ if event.pos is None: return event = ReadOnlyWrapper(event) mouse_press_callbacks(self.viewer, event) layer = self.viewer.active_layer if layer is not None: # Line bellow needed until layer mouse callbacks are refactored self.layer_to_visual[layer].on_mouse_press(event) mouse_press_callbacks(layer, event) def on_mouse_move(self, event): """Called whenever mouse moves over canvas. """ if event.pos is None: return mouse_move_callbacks(self.viewer, event) layer = self.viewer.active_layer if layer is not None: # Line bellow needed until layer mouse callbacks are refactored self.layer_to_visual[layer].on_mouse_move(event) mouse_move_callbacks(layer, event) def on_mouse_release(self, event): """Called whenever mouse released in canvas. """ mouse_release_callbacks(self.viewer, event) layer = self.viewer.active_layer if layer is not None: # Line bellow needed until layer mouse callbacks are refactored self.layer_to_visual[layer].on_mouse_release(event) mouse_release_callbacks(layer, event) def on_key_press(self, event): """Called whenever key pressed in canvas. """ if (event.native is not None and event.native.isAutoRepeat() and event.key.name not in ['Up', 'Down', 'Left', 'Right' ]) or event.key is None: # pass is no key is present or if key is held down, unless the # key being held down is one of the navigation keys return comb = components_to_key_combo(event.key.name, event.modifiers) layer = self.viewer.active_layer if layer is not None and comb in layer.keymap: parent = layer elif comb in self.viewer.keymap: parent = self.viewer else: return func = parent.keymap[comb] gen = func(parent) if inspect.isgeneratorfunction(func): try: next(gen) except StopIteration: # only one statement pass else: self._key_release_generators[event.key] = gen def on_key_release(self, event): """Called whenever key released in canvas. """ try: next(self._key_release_generators[event.key]) except (KeyError, StopIteration): pass def on_draw(self, event): """Called whenever drawn in canvas. Called for all layers, not just top """ for visual in self.layer_to_visual.values(): visual.on_draw(event) def keyPressEvent(self, event): self.canvas._backend._keyEvent(self.canvas.events.key_press, event) event.accept() def keyReleaseEvent(self, event): self.canvas._backend._keyEvent(self.canvas.events.key_release, event) event.accept() def dragEnterEvent(self, event): if event.mimeData().hasUrls(): event.accept() else: event.ignore() def dropEvent(self, event): """Add local files and web URLS with drag and drop.""" filenames = [] for url in event.mimeData().urls(): if url.isLocalFile(): filenames.append(url.toLocalFile()) else: filenames.append(url.toString()) self._add_files(filenames) def closeEvent(self, event): if self.pool.activeThreadCount() > 0: self.pool.clear() event.accept() def shutdown(self): self.pool.clear() self.canvas.close() self.console.shutdown()
class QtViewer(QSplitter): """Qt view for the napari Viewer model. Parameters ---------- viewer : napari.components.ViewerModel Napari viewer containing the rendered scene, layers, and controls. Attributes ---------- canvas : vispy.scene.SceneCanvas Canvas for rendering the current view. console : QtConsole iPython console terminal integrated into the napari GUI. controls : QtControls Qt view for GUI controls. dims : napari.qt_dims.QtDims Dimension sliders; Qt View for Dims model. dockConsole : QtViewerDockWidget QWidget wrapped in a QDockWidget with forwarded viewer events. aboutKeybindings : QtAboutKeybindings Key bindings for the 'About' Qt dialog. dockLayerControls : QtViewerDockWidget QWidget wrapped in a QDockWidget with forwarded viewer events. dockLayerList : QtViewerDockWidget QWidget wrapped in a QDockWidget with forwarded viewer events. layerButtons : QtLayerButtons Button controls for napari layers. layers : QtLayerList Qt view for LayerList controls. layer_to_visual : dict Dictionary mapping napari layers with their corresponding vispy_layers. pool : qtpy.QtCore.QThreadPool Pool of worker threads. view : vispy scene widget View displayed by vispy canvas. Adds a vispy ViewBox as a child widget. viewer : napari.components.ViewerModel Napari viewer containing the rendered scene, layers, and controls. viewerButtons : QtViewerButtons Button controls for the napari viewer. """ raw_stylesheet = combine_stylesheets() def __init__(self, viewer): super().__init__() self.setAttribute(Qt.WA_DeleteOnClose) self.pool = QThreadPool() QCoreApplication.setAttribute( Qt.AA_UseStyleSheetPropagationInWidgetStyles, True) self.viewer = viewer self.dims = QtDims(self.viewer.dims) self.controls = QtControls(self.viewer) self.layers = QtLayerList(self.viewer.layers) self.layerButtons = QtLayerButtons(self.viewer) self.viewerButtons = QtViewerButtons(self.viewer) self.console = QtConsole({'viewer': self.viewer}) layerList = QWidget() layerList.setObjectName('layerList') layerListLayout = QVBoxLayout() layerListLayout.addWidget(self.layerButtons) layerListLayout.addWidget(self.layers) layerListLayout.addWidget(self.viewerButtons) layerListLayout.setContentsMargins(8, 4, 8, 6) layerList.setLayout(layerListLayout) self.dockLayerList = QtViewerDockWidget( self, layerList, name='layer list', area='left', allowed_areas=['left', 'right'], ) self.dockLayerControls = QtViewerDockWidget( self, self.controls, name='layer controls', area='left', allowed_areas=['left', 'right'], ) self.dockConsole = QtViewerDockWidget( self, self.console, name='console', area='bottom', allowed_areas=['top', 'bottom'], shortcut='Ctrl+Shift+C', ) self.dockConsole.setVisible(False) self.dockLayerControls.visibilityChanged.connect(self._constrain_width) self.dockLayerList.setMaximumWidth(258) self.dockLayerList.setMinimumWidth(258) # This dictionary holds the corresponding vispy visual for each layer self.layer_to_visual = {} if self.console.shell is not None: self.viewerButtons.consoleButton.clicked.connect( lambda: self.toggle_console()) else: self.viewerButtons.consoleButton.setEnabled(False) self.canvas = SceneCanvas(keys=None, vsync=True, parent=self) self.canvas.events.ignore_callback_errors = False self.canvas.events.draw.connect(self.dims.enable_play) self.canvas.native.setMinimumSize(QSize(200, 200)) self.canvas.context.set_depth_func('lequal') self.canvas.connect(self.on_mouse_move) self.canvas.connect(self.on_mouse_press) self.canvas.connect(self.on_mouse_release) self.canvas.connect(self.on_key_press) self.canvas.connect(self.on_key_release) self.canvas.connect(self.on_draw) self.view = self.canvas.central_widget.add_view() self._update_camera() main_widget = QWidget() main_layout = QVBoxLayout() main_layout.setContentsMargins(10, 22, 10, 2) main_layout.addWidget(self.canvas.native) main_layout.addWidget(self.dims) main_layout.setSpacing(10) main_widget.setLayout(main_layout) self.setOrientation(Qt.Vertical) self.addWidget(main_widget) self._last_visited_dir = str(Path.home()) self._cursors = { 'disabled': QCursor( QPixmap(':/icons/cursor/cursor_disabled.png').scaled(20, 20)), 'cross': Qt.CrossCursor, 'forbidden': Qt.ForbiddenCursor, 'pointing': Qt.PointingHandCursor, 'standard': QCursor(), } self._update_palette(viewer.palette) self._key_release_generators = {} self.viewer.events.interactive.connect(self._on_interactive) self.viewer.events.cursor.connect(self._on_cursor) self.viewer.events.reset_view.connect(self._on_reset_view) self.viewer.events.palette.connect( lambda event: self._update_palette(event.palette)) self.viewer.layers.events.reordered.connect(self._reorder_layers) self.viewer.layers.events.added.connect(self._add_layer) self.viewer.layers.events.removed.connect(self._remove_layer) self.viewer.dims.events.camera.connect( lambda event: self._update_camera()) # stop any animations whenever the layers change self.viewer.events.layers_change.connect(lambda x: self.dims.stop()) self.setAcceptDrops(True) def _constrain_width(self, event): """Allow the layer controls to be wider, only if floated. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ if self.dockLayerControls.isFloating(): self.controls.setMaximumWidth(700) else: self.controls.setMaximumWidth(220) def _add_layer(self, event): """When a layer is added, set its parent and order. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ layers = event.source layer = event.item vispy_layer = create_vispy_visual(layer) vispy_layer.camera = self.view.camera vispy_layer.node.parent = self.view.scene vispy_layer.order = len(layers) self.layer_to_visual[layer] = vispy_layer def _remove_layer(self, event): """When a layer is removed, remove its parent. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ layer = event.item vispy_layer = self.layer_to_visual[layer] vispy_layer.node.transforms = ChainTransform() vispy_layer.node.parent = None del self.layer_to_visual[layer] def _reorder_layers(self, event): """When the list is reordered, propagate changes to draw order. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ for i, layer in enumerate(self.viewer.layers): vispy_layer = self.layer_to_visual[layer] vispy_layer.order = i self.canvas._draw_order.clear() self.canvas.update() def _update_camera(self): """Update the viewer camera.""" if self.viewer.dims.ndisplay == 3: # Set a 3D camera if not isinstance(self.view.camera, ArcballCamera): self.view.camera = ArcballCamera(name="ArcballCamera", fov=0) # flip y-axis to have correct alignment # self.view.camera.flip = (0, 1, 0) self.view.camera.viewbox_key_event = viewbox_key_event self.viewer.reset_view() else: # Set 2D camera if not isinstance(self.view.camera, PanZoomCamera): self.view.camera = PanZoomCamera(aspect=1, name="PanZoomCamera") # flip y-axis to have correct alignment self.view.camera.flip = (0, 1, 0) self.view.camera.viewbox_key_event = viewbox_key_event self.viewer.reset_view() def screenshot(self, path=None): """Take currently displayed screen and convert to an image array. Parmeters --------- path : str Filename for saving screenshot image. Returns ------- image : array Numpy array of type ubyte and shape (h, w, 4). Index [0, 0] is the upper-left corner of the rendered region. """ img = self.canvas.native.grabFramebuffer() if path is not None: imsave(path, QImg2array(img)) # scikit-image imsave method return QImg2array(img) def _save_screenshot(self): """Save screenshot of current display, default .png""" filename, _ = QFileDialog.getSaveFileName( parent=self, caption='', directory=self._last_visited_dir, # home dir by default filter= "Image files (*.png *.bmp *.gif *.tif *.tiff)", # first one used by default # jpg and jpeg not included as they don't support an alpha channel ) if (filename != '') and (filename is not None): # double check that an appropriate extension has been added as the filter # filter option does not always add an extension on linux and windows # see https://bugreports.qt.io/browse/QTBUG-27186 image_extensions = ('.bmp', '.gif', '.png', '.tif', '.tiff') if not filename.endswith(image_extensions): filename = filename + '.png' self.screenshot(path=filename) def _open_images(self): """Add image files from the menubar.""" filenames, _ = QFileDialog.getOpenFileNames( parent=self, caption='Select image(s)...', directory=self._last_visited_dir, # home dir by default ) if (filenames != []) and (filenames is not None): self._add_files(filenames) def _open_folder(self): """Add a folder of files from the menubar.""" folder = QFileDialog.getExistingDirectory( parent=self, caption='Select folder...', directory=self._last_visited_dir, # home dir by default ) if folder not in {'', None}: self._add_files([folder]) def _add_files(self, filenames): """Add an image layer to the viewer. If multiple images are selected, they are stacked along the 0th axis. Parameters ------- filenames : list List of filenames to be opened """ if len(filenames) > 0: self.viewer.add_image(path=filenames) self._last_visited_dir = os.path.dirname(filenames[0]) def _on_interactive(self, event): """Link interactive attributes of view and viewer. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ self.view.interactive = self.viewer.interactive def _on_cursor(self, event): """Set the appearance of the mouse cursor. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ cursor = self.viewer.cursor size = self.viewer.cursor_size if cursor == 'square': if size < 10 or size > 300: q_cursor = self._cursors['cross'] else: q_cursor = QCursor( QPixmap(':/icons/cursor/cursor_square.png').scaledToHeight( size)) else: q_cursor = self._cursors[cursor] self.canvas.native.setCursor(q_cursor) def _on_reset_view(self, event): """Reset view of the rendered scene. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ if isinstance(self.view.camera, ArcballCamera): quat = self.view.camera._quaternion.create_from_axis_angle( *event.quaternion) self.view.camera._quaternion = quat self.view.camera.center = event.center self.view.camera.scale_factor = event.scale_factor else: # Assumes default camera has the same properties as PanZoomCamera self.view.camera.rect = event.rect def _update_palette(self, palette): """Update the napari GUI theme. Parameters ---------- palette : dict of str: str Color palette with which to style the viewer. Property of napari.components.viewer_model.ViewerModel """ # template and apply the primary stylesheet themed_stylesheet = template(self.raw_stylesheet, **palette) self.console.style_sheet = themed_stylesheet self.console.syntax_style = palette['syntax_style'] bracket_color = QtGui.QColor(*str_to_rgb(palette['highlight'])) self.console._bracket_matcher.format.setBackground(bracket_color) self.setStyleSheet(themed_stylesheet) self.canvas.bgcolor = palette['canvas'] def toggle_console(self): """Toggle console visible and not visible.""" viz = not self.dockConsole.isVisible() # modulate visibility at the dock widget level as console is docakable self.dockConsole.setVisible(viz) if self.dockConsole.isFloating(): self.dockConsole.setFloating(True) self.viewerButtons.consoleButton.setProperty( 'expanded', self.dockConsole.isVisible()) self.viewerButtons.consoleButton.style().unpolish( self.viewerButtons.consoleButton) self.viewerButtons.consoleButton.style().polish( self.viewerButtons.consoleButton) def show_keybindings_dialog(self, event=None): dialog = QtAboutKeybindings(self.viewer, parent=self) dialog.show() def on_mouse_press(self, event): """Called whenever mouse pressed in canvas. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ if event.pos is None: return event = ReadOnlyWrapper(event) mouse_press_callbacks(self.viewer, event) layer = self.viewer.active_layer if layer is not None: # Line bellow needed until layer mouse callbacks are refactored self.layer_to_visual[layer].on_mouse_press(event) mouse_press_callbacks(layer, event) def on_mouse_move(self, event): """Called whenever mouse moves over canvas. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ if event.pos is None: return mouse_move_callbacks(self.viewer, event) layer = self.viewer.active_layer if layer is not None: # Line bellow needed until layer mouse callbacks are refactored self.layer_to_visual[layer].on_mouse_move(event) mouse_move_callbacks(layer, event) def on_mouse_release(self, event): """Called whenever mouse released in canvas. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ mouse_release_callbacks(self.viewer, event) layer = self.viewer.active_layer if layer is not None: # Line bellow needed until layer mouse callbacks are refactored self.layer_to_visual[layer].on_mouse_release(event) mouse_release_callbacks(layer, event) def on_key_press(self, event): """Called whenever key pressed in canvas. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ if (event.native is not None and event.native.isAutoRepeat() and event.key.name not in ['Up', 'Down', 'Left', 'Right' ]) or event.key is None: # pass is no key is present or if key is held down, unless the # key being held down is one of the navigation keys return comb = components_to_key_combo(event.key.name, event.modifiers) layer = self.viewer.active_layer if layer is not None and comb in layer.keymap: parent = layer elif comb in self.viewer.keymap: parent = self.viewer else: return func = parent.keymap[comb] gen = func(parent) if inspect.isgenerator(gen): try: next(gen) except StopIteration: # only one statement pass else: self._key_release_generators[event.key] = gen def on_key_release(self, event): """Called whenever key released in canvas. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ try: next(self._key_release_generators[event.key]) except (KeyError, StopIteration): pass def on_draw(self, event): """Called whenever drawn in canvas. Called for all layers, not just top Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ for visual in self.layer_to_visual.values(): visual.on_draw(event) def keyPressEvent(self, event): """Called whenever a key is pressed. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ self.canvas._backend._keyEvent(self.canvas.events.key_press, event) event.accept() def keyReleaseEvent(self, event): """Called whenever a key is released. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ self.canvas._backend._keyEvent(self.canvas.events.key_release, event) event.accept() def dragEnterEvent(self, event): """Ignore event if not dragging & dropping a file or URL to open. Using event.ignore() here allows the event to pass through the parent widget to its child widget, otherwise the parent widget would catch the event and not pass it on to the child widget. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ if event.mimeData().hasUrls(): event.accept() else: event.ignore() def dropEvent(self, event): """Add local files and web URLS with drag and drop. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ filenames = [] for url in event.mimeData().urls(): if url.isLocalFile(): filenames.append(url.toLocalFile()) else: filenames.append(url.toString()) self._add_files(filenames) def closeEvent(self, event): """Clear pool of worker threads and close. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ # if the viewer.QtDims object is playing an axis, we need to terminate # the AnimationThread before close, otherwise it will cauyse a segFault # or Abort trap. (calling stop() when no animation is occuring is also # not a problem) self.dims.stop() self.canvas.native.deleteLater() self.console.close() self.dockConsole.deleteLater() if not self.pool.waitForDone(10000): raise TimeoutError("Timed out waiting for QtViewer.pool to finish") event.accept()
class QtViewer(QSplitter): with open(osp.join(resources_dir, 'stylesheet.qss'), 'r') as f: raw_stylesheet = f.read() themed_stylesheet = template(raw_stylesheet, **palette) def __init__(self, viewer): super().__init__() QCoreApplication.setAttribute( Qt.AA_UseStyleSheetPropagationInWidgetStyles, True) self.setStyleSheet(self.themed_stylesheet) self.viewer = viewer self.viewer._qtviewer = self self.canvas = SceneCanvas(keys=None, vsync=True) self.canvas.native.setMinimumSize(QSize(100, 100)) self.canvas.connect(self.on_mouse_move) self.canvas.connect(self.on_mouse_press) self.canvas.connect(self.on_mouse_release) self.canvas.connect(self.on_key_press) self.canvas.connect(self.on_key_release) self.view = self.canvas.central_widget.add_view() # Set 2D camera (the camera will scale to the contents in the scene) self.view.camera = PanZoomCamera(aspect=1) # flip y-axis to have correct aligment self.view.camera.flip = (0, 1, 0) self.view.camera.set_range() self.view.camera.viewbox_key_event = viewbox_key_event center = QWidget() center_layout = QVBoxLayout() center_layout.setContentsMargins(15, 20, 15, 10) center_layout.addWidget(self.canvas.native) self.dims = QtDims(self.viewer.dims) center_layout.addWidget(self.dims) center.setLayout(center_layout) # Add controls, center, and layerlist self.control_panel = QtControls(viewer) self.addWidget(self.control_panel) self.addWidget(center) right = QWidget() right_layout = QVBoxLayout() self.layers = QtLayers(self.viewer.layers) right_layout.addWidget(self.layers) self.buttons = QtLayersButtons(viewer) right_layout.addWidget(self.buttons) right.setLayout(right_layout) right.setMinimumSize(QSize(308, 250)) self.addWidget(right) self._cursors = { 'disabled': QCursor( QPixmap(':/icons/cursor/cursor_disabled.png').scaled(20, 20)), 'cross': Qt.CrossCursor, 'forbidden': Qt.ForbiddenCursor, 'pointing': Qt.PointingHandCursor, 'standard': QCursor() } def set_cursor(self, cursor, size=10): if cursor == 'square': if size < 10 or size > 300: q_cursor = self._cursors['cross'] else: q_cursor = QCursor( QPixmap(':/icons/cursor/cursor_square.png').scaledToHeight( size)) else: q_cursor = self._cursors[cursor] self.canvas.native.setCursor(q_cursor) def on_mouse_move(self, event): """Called whenever mouse moves over canvas. """ layer = self.viewer._top if layer is not None: layer.on_mouse_move(event) def on_mouse_press(self, event): """Called whenever mouse pressed in canvas. """ layer = self.viewer._top if layer is not None: layer.on_mouse_press(event) def on_mouse_release(self, event): """Called whenever mouse released in canvas. """ layer = self.viewer._top if layer is not None: layer.on_mouse_release(event) def on_key_press(self, event): """Called whenever key pressed in canvas. """ if (event.text in self.viewer.key_bindings and not event.native.isAutoRepeat()): self.viewer.key_bindings[event.text](self.viewer) return layer = self.viewer._top if layer is not None: layer.on_key_press(event) def on_key_release(self, event): """Called whenever key released in canvas. """ layer = self.viewer._top if layer is not None: layer.on_key_release(event)
class QtViewer(QSplitter): """Qt view for the napari Viewer model. Parameters ---------- viewer : napari.components.ViewerModel Napari viewer containing the rendered scene, layers, and controls. Attributes ---------- canvas : vispy.scene.SceneCanvas Canvas for rendering the current view. console : QtConsole iPython console terminal integrated into the napari GUI. controls : QtControls Qt view for GUI controls. dims : napari.qt_dims.QtDims Dimension sliders; Qt View for Dims model. dockConsole : QtViewerDockWidget QWidget wrapped in a QDockWidget with forwarded viewer events. aboutKeybindings : QtAboutKeybindings Key bindings for the 'About' Qt dialog. dockLayerControls : QtViewerDockWidget QWidget wrapped in a QDockWidget with forwarded viewer events. dockLayerList : QtViewerDockWidget QWidget wrapped in a QDockWidget with forwarded viewer events. layerButtons : QtLayerButtons Button controls for napari layers. layers : QtLayerList Qt view for LayerList controls. layer_to_visual : dict Dictionary mapping napari layers with their corresponding vispy_layers. view : vispy scene widget View displayed by vispy canvas. Adds a vispy ViewBox as a child widget. viewer : napari.components.ViewerModel Napari viewer containing the rendered scene, layers, and controls. viewerButtons : QtViewerButtons Button controls for the napari viewer. """ raw_stylesheet = get_stylesheet() def __init__(self, viewer): super().__init__() self.setAttribute(Qt.WA_DeleteOnClose) QCoreApplication.setAttribute( Qt.AA_UseStyleSheetPropagationInWidgetStyles, True) self.viewer = viewer self.dims = QtDims(self.viewer.dims) self.controls = QtControls(self.viewer) self.layers = QtLayerList(self.viewer.layers) self.layerButtons = QtLayerButtons(self.viewer) self.viewerButtons = QtViewerButtons(self.viewer) self._console = None layerList = QWidget() layerList.setObjectName('layerList') layerListLayout = QVBoxLayout() layerListLayout.addWidget(self.layerButtons) layerListLayout.addWidget(self.layers) layerListLayout.addWidget(self.viewerButtons) layerListLayout.setContentsMargins(8, 4, 8, 6) layerList.setLayout(layerListLayout) self.dockLayerList = QtViewerDockWidget( self, layerList, name='layer list', area='left', allowed_areas=['left', 'right'], ) self.dockLayerControls = QtViewerDockWidget( self, self.controls, name='layer controls', area='left', allowed_areas=['left', 'right'], ) self.dockConsole = QtViewerDockWidget( self, QWidget(), name='console', area='bottom', allowed_areas=['top', 'bottom'], shortcut='Ctrl+Shift+C', ) self.dockConsole.setVisible(False) # because the console is loaded lazily in the @getter, this line just # gets (or creates) the console when the dock console is made visible. self.dockConsole.visibilityChanged.connect(lambda visible: self.console if visible else None) self.dockLayerControls.visibilityChanged.connect(self._constrain_width) self.dockLayerList.setMaximumWidth(258) self.dockLayerList.setMinimumWidth(258) # This dictionary holds the corresponding vispy visual for each layer self.layer_to_visual = {} self.viewerButtons.consoleButton.clicked.connect( self.toggle_console_visibility) self.canvas = SceneCanvas(keys=None, vsync=True, parent=self) self.canvas.events.ignore_callback_errors = False self.canvas.events.draw.connect(self.dims.enable_play) self.canvas.native.setMinimumSize(QSize(200, 200)) self.canvas.context.set_depth_func('lequal') self.canvas.connect(self.on_mouse_move) self.canvas.connect(self.on_mouse_press) self.canvas.connect(self.on_mouse_release) self.canvas.connect(self.on_key_press) self.canvas.connect(self.on_key_release) self.view = self.canvas.central_widget.add_view() self._update_camera() main_widget = QWidget() main_layout = QVBoxLayout() main_layout.setContentsMargins(10, 22, 10, 2) main_layout.addWidget(self.canvas.native) main_layout.addWidget(self.dims) main_layout.setSpacing(10) main_widget.setLayout(main_layout) self.setOrientation(Qt.Vertical) self.addWidget(main_widget) self._last_visited_dir = str(Path.home()) self._cursors = { 'cross': Qt.CrossCursor, 'forbidden': Qt.ForbiddenCursor, 'pointing': Qt.PointingHandCursor, 'standard': QCursor(), } self._update_palette() self.viewer.events.interactive.connect(self._on_interactive) self.viewer.events.cursor.connect(self._on_cursor) self.viewer.events.reset_view.connect(self._on_reset_view) self.viewer.events.palette.connect(self._update_palette) self.viewer.layers.events.reordered.connect(self._reorder_layers) self.viewer.layers.events.added.connect(self._add_layer) self.viewer.layers.events.removed.connect(self._remove_layer) self.viewer.dims.events.camera.connect( lambda event: self._update_camera()) # stop any animations whenever the layers change self.viewer.events.layers_change.connect(lambda x: self.dims.stop()) self.setAcceptDrops(True) @property def console(self): """QtConsole: iPython console terminal integrated into the napari GUI. """ if self._console is None: from .qt_console import QtConsole self.console = QtConsole({'viewer': self.viewer}) return self._console @console.setter def console(self, console): self._console = console self.dockConsole.widget = console self._update_palette() def _constrain_width(self, event): """Allow the layer controls to be wider, only if floated. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ if self.dockLayerControls.isFloating(): self.controls.setMaximumWidth(700) else: self.controls.setMaximumWidth(220) def _add_layer(self, event): """When a layer is added, set its parent and order. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ layers = event.source layer = event.item vispy_layer = create_vispy_visual(layer) vispy_layer.node.parent = self.view.scene vispy_layer.order = len(layers) self.canvas.connect(vispy_layer.on_draw) self.layer_to_visual[layer] = vispy_layer def _remove_layer(self, event): """When a layer is removed, remove its parent. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ layer = event.item vispy_layer = self.layer_to_visual[layer] self.canvas.events.draw.disconnect(vispy_layer.on_draw) vispy_layer.node.transforms = ChainTransform() vispy_layer.node.parent = None del vispy_layer def _reorder_layers(self, event): """When the list is reordered, propagate changes to draw order. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ for i, layer in enumerate(self.viewer.layers): vispy_layer = self.layer_to_visual[layer] vispy_layer.order = i self.canvas._draw_order.clear() self.canvas.update() def _update_camera(self): """Update the viewer camera.""" if self.viewer.dims.ndisplay == 3: # Set a 3D camera if not isinstance(self.view.camera, ArcballCamera): self.view.camera = ArcballCamera(name="ArcballCamera", fov=0) # flip y-axis to have correct alignment # self.view.camera.flip = (0, 1, 0) self.view.camera.viewbox_key_event = viewbox_key_event self.viewer.reset_view() else: # Set 2D camera if not isinstance(self.view.camera, PanZoomCamera): self.view.camera = PanZoomCamera(aspect=1, name="PanZoomCamera") # flip y-axis to have correct alignment self.view.camera.flip = (0, 1, 0) self.view.camera.viewbox_key_event = viewbox_key_event self.viewer.reset_view() def _save_layers_dialog(self, selected=False): """Save layers (all or selected) to disk, using ``LayerList.save()``. Parameters ---------- selected : bool If True, only layers that are selected in the viewer will be saved. By default, all layers are saved. """ msg = '' if not len(self.viewer.layers): msg = "There are no layers in the viewer to save" elif selected and not len(self.viewer.layers.selected): msg = ('Please select one or more layers to save,' '\nor use "Save all layers..."') if msg: QMessageBox.warning(self, "Nothing to save", msg, QMessageBox.Ok) return filename, _ = QFileDialog.getSaveFileName( parent=self, caption=f'Save {"selected" if selected else "all"} layers', directory=self._last_visited_dir, # home dir by default ) if filename: self.viewer.layers.save(filename, selected=selected) def screenshot(self, path=None): """Take currently displayed screen and convert to an image array. Parmeters --------- path : str Filename for saving screenshot image. Returns ------- image : array Numpy array of type ubyte and shape (h, w, 4). Index [0, 0] is the upper-left corner of the rendered region. """ img = self.canvas.native.grabFramebuffer() if path is not None: imsave(path, QImg2array(img)) # scikit-image imsave method return QImg2array(img) def _screenshot_dialog(self): """Save screenshot of current display, default .png""" filename, _ = QFileDialog.getSaveFileName( parent=self, caption='Save screenshot', directory=self._last_visited_dir, # home dir by default filter= "Image files (*.png *.bmp *.gif *.tif *.tiff)", # first one used by default # jpg and jpeg not included as they don't support an alpha channel ) if (filename != '') and (filename is not None): # double check that an appropriate extension has been added as the # filter option does not always add an extension on linux and windows # see https://bugreports.qt.io/browse/QTBUG-27186 image_extensions = ('.bmp', '.gif', '.png', '.tif', '.tiff') if not filename.endswith(image_extensions): filename = filename + '.png' self.screenshot(path=filename) def _open_files_dialog(self): """Add files from the menubar.""" filenames, _ = QFileDialog.getOpenFileNames( parent=self, caption='Select file(s)...', directory=self._last_visited_dir, # home dir by default ) if (filenames != []) and (filenames is not None): self.viewer.open(filenames) def _open_files_dialog_as_stack_dialog(self): """Add files as a stack, from the menubar.""" filenames, _ = QFileDialog.getOpenFileNames( parent=self, caption='Select files...', directory=self._last_visited_dir, # home dir by default ) if (filenames != []) and (filenames is not None): self.viewer.open(filenames, stack=True) def _open_folder_dialog(self): """Add a folder of files from the menubar.""" folder = QFileDialog.getExistingDirectory( parent=self, caption='Select folder...', directory=self._last_visited_dir, # home dir by default ) if folder not in {'', None}: self.viewer.open([folder]) def _on_interactive(self, event): """Link interactive attributes of view and viewer. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ self.view.interactive = self.viewer.interactive def _on_cursor(self, event): """Set the appearance of the mouse cursor. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ cursor = self.viewer.cursor if cursor == 'square': size = self.viewer.cursor_size # make sure the square fits within the current canvas if size < 8 or size > ( min(*self.viewer.window.qt_viewer.canvas.size) - 4): q_cursor = self._cursors['cross'] else: q_cursor = QCursor(square_pixmap(size)) else: q_cursor = self._cursors[cursor] self.canvas.native.setCursor(q_cursor) def _on_reset_view(self, event): """Reset view of the rendered scene. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ if isinstance(self.view.camera, ArcballCamera): quat = self.view.camera._quaternion.create_from_axis_angle( *event.quaternion) self.view.camera._quaternion = quat self.view.camera.center = event.center self.view.camera.scale_factor = event.scale_factor else: # Assumes default camera has the same properties as PanZoomCamera self.view.camera.rect = event.rect def _update_palette(self, event=None): """Update the napari GUI theme.""" # template and apply the primary stylesheet themed_stylesheet = template(self.raw_stylesheet, **self.viewer.palette) if self._console is not None: self.console._update_palette(self.viewer.palette, themed_stylesheet) self.setStyleSheet(themed_stylesheet) self.canvas.bgcolor = self.viewer.palette['canvas'] def toggle_console_visibility(self, event=None): """Toggle console visible and not visible. Imports the console the first time it is requested. """ # force instantiation of console if not already instantiated _ = self.console viz = not self.dockConsole.isVisible() # modulate visibility at the dock widget level as console is docakable self.dockConsole.setVisible(viz) if self.dockConsole.isFloating(): self.dockConsole.setFloating(True) self.viewerButtons.consoleButton.setProperty( 'expanded', self.dockConsole.isVisible()) self.viewerButtons.consoleButton.style().unpolish( self.viewerButtons.consoleButton) self.viewerButtons.consoleButton.style().polish( self.viewerButtons.consoleButton) def show_key_bindings_dialog(self, event=None): dialog = QtAboutKeyBindings(self.viewer, parent=self) dialog.show() def on_mouse_press(self, event): """Called whenever mouse pressed in canvas. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ if event.pos is None: return event = ReadOnlyWrapper(event) mouse_press_callbacks(self.viewer, event) layer = self.viewer.active_layer if layer is not None: # update cursor position in visual and layer visual = self.layer_to_visual[layer] visual._position = list(event.pos) layer.position = visual._transform_position(visual._position) mouse_press_callbacks(layer, event) def on_mouse_move(self, event): """Called whenever mouse moves over canvas. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ if event.pos is None: return mouse_move_callbacks(self.viewer, event) layer = self.viewer.active_layer if layer is not None: # update cursor position in visual and layer visual = self.layer_to_visual[layer] visual._position = list(event.pos) layer.position = visual._transform_position(visual._position) mouse_move_callbacks(layer, event) def on_mouse_release(self, event): """Called whenever mouse released in canvas. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ if event.pos is None: return mouse_release_callbacks(self.viewer, event) layer = self.viewer.active_layer if layer is not None: # update cursor position in visual and layer visual = self.layer_to_visual[layer] visual._position = list(event.pos) layer.position = visual._transform_position(visual._position) mouse_release_callbacks(layer, event) def on_key_press(self, event): """Called whenever key pressed in canvas. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ if (event.native is not None and event.native.isAutoRepeat() and event.key.name not in ['Up', 'Down', 'Left', 'Right' ]) or event.key is None: # pass if no key is present or if key is held down, unless the # key being held down is one of the navigation keys # this helps for scrolling, etc. return combo = components_to_key_combo(event.key.name, event.modifiers) self.viewer.press_key(combo) def on_key_release(self, event): """Called whenever key released in canvas. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ if event.key is None: return combo = components_to_key_combo(event.key.name, event.modifiers) self.viewer.release_key(combo) def keyPressEvent(self, event): """Called whenever a key is pressed. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ self.canvas._backend._keyEvent(self.canvas.events.key_press, event) event.accept() def keyReleaseEvent(self, event): """Called whenever a key is released. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ self.canvas._backend._keyEvent(self.canvas.events.key_release, event) event.accept() def dragEnterEvent(self, event): """Ignore event if not dragging & dropping a file or URL to open. Using event.ignore() here allows the event to pass through the parent widget to its child widget, otherwise the parent widget would catch the event and not pass it on to the child widget. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ if event.mimeData().hasUrls(): event.accept() else: event.ignore() def dropEvent(self, event): """Add local files and web URLS with drag and drop. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ shift_down = QGuiApplication.keyboardModifiers() & Qt.ShiftModifier filenames = [] for url in event.mimeData().urls(): if url.isLocalFile(): filenames.append(url.toLocalFile()) else: filenames.append(url.toString()) self.viewer.open(filenames, stack=bool(shift_down)) def closeEvent(self, event): """Cleanup and close. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ # if the viewer.QtDims object is playing an axis, we need to terminate # the AnimationThread before close, otherwise it will cauyse a segFault # or Abort trap. (calling stop() when no animation is occuring is also # not a problem) self.dims.stop() self.canvas.native.deleteLater() if self._console is not None: self.console.close() self.dockConsole.deleteLater() event.accept()
class QtViewer(QSplitter): with open(os.path.join(resources_dir, 'stylesheet.qss'), 'r') as f: raw_stylesheet = f.read() def __init__(self, viewer): super().__init__() QCoreApplication.setAttribute( Qt.AA_UseStyleSheetPropagationInWidgetStyles, True) self.viewer = viewer self.dims = QtDims(self.viewer.dims) self.controls = QtControls(self.viewer) self.layers = QtLayerList(self.viewer.layers) self.layerButtons = QtLayerButtons(self.viewer) self.viewerButtons = QtViewerButtons(self.viewer) self.console = QtConsole({'viewer': self.viewer}) # This dictionary holds the corresponding vispy visual for each layer self.layer_to_visual = {} if self.console.shell is not None: self.console.style().unpolish(self.console) self.console.style().polish(self.console) self.console.hide() self.viewerButtons.consoleButton.clicked.connect( lambda: self._toggle_console()) else: self.viewerButtons.consoleButton.setEnabled(False) self.canvas = SceneCanvas(keys=None, vsync=True) self.canvas.native.setMinimumSize(QSize(200, 200)) self.canvas.context.set_depth_func('lequal') self.canvas.connect(self.on_mouse_move) self.canvas.connect(self.on_mouse_press) self.canvas.connect(self.on_mouse_release) self.canvas.connect(self.on_key_press) self.canvas.connect(self.on_key_release) self.canvas.connect(self.on_draw) self.view = self.canvas.central_widget.add_view() self._update_camera() main_widget = QWidget() main_layout = QGridLayout() main_layout.setContentsMargins(15, 20, 15, 10) main_layout.addWidget(self.canvas.native, 0, 1, 3, 1) main_layout.addWidget(self.dims, 3, 1) main_layout.addWidget(self.controls, 0, 0) main_layout.addWidget(self.layerButtons, 1, 0) main_layout.addWidget(self.layers, 2, 0) main_layout.addWidget(self.viewerButtons, 3, 0) main_layout.setColumnStretch(1, 1) main_layout.setSpacing(10) main_widget.setLayout(main_layout) self.setOrientation(Qt.Vertical) self.addWidget(main_widget) if self.console.shell is not None: self.addWidget(self.console) self._last_visited_dir = str(Path.home()) self._cursors = { 'disabled': QCursor( QPixmap(':/icons/cursor/cursor_disabled.png').scaled(20, 20)), 'cross': Qt.CrossCursor, 'forbidden': Qt.ForbiddenCursor, 'pointing': Qt.PointingHandCursor, 'standard': QCursor(), } self._update_palette(viewer.palette) self._key_release_generators = {} self.viewer.events.interactive.connect(self._on_interactive) self.viewer.events.cursor.connect(self._on_cursor) self.viewer.events.reset_view.connect(self._on_reset_view) self.viewer.events.palette.connect( lambda event: self._update_palette(event.palette)) self.viewer.layers.events.reordered.connect(self._reorder_layers) self.viewer.layers.events.added.connect(self._add_layer) self.viewer.layers.events.removed.connect(self._remove_layer) self.viewer.dims.events.camera.connect( lambda event: self._update_camera()) self.setAcceptDrops(True) def _add_layer(self, event): """When a layer is added, set its parent and order.""" layers = event.source layer = event.item vispy_layer = create_vispy_visual(layer) vispy_layer.camera = self.view.camera vispy_layer.node.parent = self.view.scene vispy_layer.order = len(layers) self.layer_to_visual[layer] = vispy_layer def _remove_layer(self, event): """When a layer is removed, remove its parent.""" layer = event.item vispy_layer = self.layer_to_visual[layer] vispy_layer.node.transforms = ChainTransform() vispy_layer.node.parent = None del self.layer_to_visual[layer] def _reorder_layers(self, event): """When the list is reordered, propagate changes to draw order.""" for i, layer in enumerate(self.viewer.layers): vispy_layer = self.layer_to_visual[layer] vispy_layer.order = i self.canvas._draw_order.clear() self.canvas.update() def _update_camera(self): if self.viewer.dims.ndisplay == 3: # Set a 3D camera if not isinstance(self.view.camera, ArcballCamera): self.view.camera = ArcballCamera(name="ArcballCamera") # flip y-axis to have correct alignment # self.view.camera.flip = (0, 1, 0) self.view.camera.viewbox_key_event = viewbox_key_event self.viewer.reset_view() else: # Set 2D camera if not isinstance(self.view.camera, PanZoomCamera): self.view.camera = PanZoomCamera(aspect=1, name="PanZoomCamera") # flip y-axis to have correct alignment self.view.camera.flip = (0, 1, 0) self.view.camera.viewbox_key_event = viewbox_key_event self.viewer.reset_view() def screenshot(self): """Take currently displayed screen and convert to an image array. Returns ------- image : array Numpy array of type ubyte and shape (h, w, 4). Index [0, 0] is the upper-left corner of the rendered region. """ img = self.canvas.native.grabFramebuffer() b = img.constBits() h, w, c = img.height(), img.width(), 4 # As vispy doesn't use qtpy we need to reconcile the differences # between the `QImage` API for `PySide2` and `PyQt5` on how to convert # a QImage to a numpy array. if API_NAME == 'PySide2': arr = np.array(b).reshape(h, w, c) else: b.setsize(h * w * c) arr = np.frombuffer(b, np.uint8).reshape(h, w, c) # Format of QImage is ARGB32_Premultiplied, but color channels are # reversed. arr = arr[:, :, [2, 1, 0, 3]] return arr def _open_images(self): """Add image files from the menubar.""" filenames, _ = QFileDialog.getOpenFileNames( parent=self, caption='Select image(s)...', directory=self._last_visited_dir, # home dir by default ) self._add_files(filenames) def _add_files(self, filenames): """Add an image layer to the viewer. Whether the image is rgb is determined by :func:`napari.util.misc.is_rgb`. If multiple images are selected, they are stacked along the 0th axis. Parameters ------- filenames : list List of filenames to be opened """ if len(filenames) > 0: image = io.magic_read(filenames) self.viewer.add_image(image, rgb=is_rgb(image.shape)) self._last_visited_dir = os.path.dirname(filenames[0]) def _on_interactive(self, event): self.view.interactive = self.viewer.interactive def _on_cursor(self, event): cursor = self.viewer.cursor size = self.viewer.cursor_size if cursor == 'square': if size < 10 or size > 300: q_cursor = self._cursors['cross'] else: q_cursor = QCursor( QPixmap(':/icons/cursor/cursor_square.png').scaledToHeight( size)) else: q_cursor = self._cursors[cursor] self.canvas.native.setCursor(q_cursor) def _on_reset_view(self, event): if isinstance(self.view.camera, ArcballCamera): self.view.camera._quaternion = self.view.camera._quaternion.create_from_axis_angle( *event.quaternion) self.view.camera.center = event.center self.view.camera.scale_factor = event.scale_factor else: # Assumes default camera has the same properties as PanZoomCamera self.view.camera.rect = event.rect def _update_palette(self, palette): # template and apply the primary stylesheet themed_stylesheet = template(self.raw_stylesheet, **palette) self.console.style_sheet = themed_stylesheet self.console.syntax_style = palette['syntax_style'] self.setStyleSheet(themed_stylesheet) self.canvas.bgcolor = palette['canvas'] def _toggle_console(self): """Toggle console visible and not visible.""" self.console.setVisible(not self.console.isVisible()) self.viewerButtons.consoleButton.setProperty('expanded', self.console.isVisible()) self.viewerButtons.consoleButton.style().unpolish( self.viewerButtons.consoleButton) self.viewerButtons.consoleButton.style().polish( self.viewerButtons.consoleButton) def on_mouse_move(self, event): """Called whenever mouse moves over canvas. """ layer = self.viewer.active_layer if layer is not None: self.layer_to_visual[layer].on_mouse_move(event) def on_mouse_press(self, event): """Called whenever mouse pressed in canvas. """ layer = self.viewer.active_layer if layer is not None: self.layer_to_visual[layer].on_mouse_press(event) def on_mouse_release(self, event): """Called whenever mouse released in canvas. """ layer = self.viewer.active_layer if layer is not None: self.layer_to_visual[layer].on_mouse_release(event) def on_key_press(self, event): """Called whenever key pressed in canvas. """ if (event.native.isAutoRepeat() and event.key.name not in ['Up', 'Down', 'Left', 'Right']) or event.key is None: # pass is no key is present or if key is held down, unless the # key being held down is one of the navigation keys return comb = components_to_key_combo(event.key.name, event.modifiers) layer = self.viewer.active_layer if layer is not None and comb in layer.keymap: parent = layer elif comb in self.viewer.keymap: parent = self.viewer else: return func = parent.keymap[comb] gen = func(parent) if inspect.isgeneratorfunction(func): try: next(gen) except StopIteration: # only one statement pass else: self._key_release_generators[event.key] = gen def on_key_release(self, event): """Called whenever key released in canvas. """ try: next(self._key_release_generators[event.key]) except (KeyError, StopIteration): pass def on_draw(self, event): """Called whenever drawn in canvas. Called for all layers, not just top """ for layer in self.viewer.layers: self.layer_to_visual[layer].on_draw(event) def keyPressEvent(self, event): self.canvas._backend._keyEvent(self.canvas.events.key_press, event) event.accept() def keyReleaseEvent(self, event): self.canvas._backend._keyEvent(self.canvas.events.key_release, event) event.accept() def dragEnterEvent(self, event): if event.mimeData().hasUrls(): event.accept() else: event.ignore() def dropEvent(self, event): """Add local files and web URLS with drag and drop.""" filenames = [] for url in event.mimeData().urls(): path = url.toString() if os.path.isfile(path): filenames.append(path) elif os.path.isdir(path) and not path.endswith('.zarr'): filenames = filenames + list(glob(os.path.join(path, '*'))) else: filenames.append(path) self._add_files(filenames)
class QtViewer(QSplitter): with open(os.path.join(resources_dir, 'stylesheet.qss'), 'r') as f: raw_stylesheet = f.read() def __init__(self, viewer): super().__init__() QCoreApplication.setAttribute( Qt.AA_UseStyleSheetPropagationInWidgetStyles, True ) self.viewer = viewer self.canvas = SceneCanvas(keys=None, vsync=True) self.canvas.native.setMinimumSize(QSize(100, 100)) self.canvas.connect(self.on_mouse_move) self.canvas.connect(self.on_mouse_press) self.canvas.connect(self.on_mouse_release) self.canvas.connect(self.on_key_press) self.canvas.connect(self.on_key_release) self.canvas.connect(self.on_draw) self.view = self.canvas.central_widget.add_view() # Set 2D camera (the camera will scale to the contents in the scene) self.view.camera = PanZoomCamera(aspect=1) # flip y-axis to have correct aligment self.view.camera.flip = (0, 1, 0) self.view.camera.set_range() self.view.camera.viewbox_key_event = viewbox_key_event # TO DO: Remove self.viewer._view = self.view center = QWidget() center_layout = QVBoxLayout() center_layout.setContentsMargins(15, 20, 15, 10) center_layout.addWidget(self.canvas.native) self.dims = QtDims(self.viewer.dims) center_layout.addWidget(self.dims) center.setLayout(center_layout) # Add controls, center, and layerlist self.control_panel = QtControls(viewer) self.addWidget(self.control_panel) self.addWidget(center) right = QWidget() right_layout = QVBoxLayout() self.layers = QtLayerList(self.viewer.layers) right_layout.addWidget(self.layers) self.buttons = QtLayersButtons(viewer) right_layout.addWidget(self.buttons) right.setLayout(right_layout) right.setMinimumSize(QSize(308, 250)) self.addWidget(right) self._last_visited_dir = str(Path.home()) self._cursors = { 'disabled': QCursor( QPixmap(':/icons/cursor/cursor_disabled.png').scaled(20, 20) ), 'cross': Qt.CrossCursor, 'forbidden': Qt.ForbiddenCursor, 'pointing': Qt.PointingHandCursor, 'standard': QCursor(), } self._update_palette(viewer.palette) self._key_release_generators = {} self.viewer.events.interactive.connect(self._on_interactive) self.viewer.events.cursor.connect(self._on_cursor) self.viewer.events.reset_view.connect(self._on_reset_view) self.viewer.events.palette.connect( lambda event: self._update_palette(event.palette) ) self.viewer.layers.events.reordered.connect(self._update_canvas) self.setAcceptDrops(True) def screenshot(self, region=None, size=None, bgcolor=None): """Render the scene to an offscreen buffer and return the image array. Parameters ---------- region : tuple | None Specifies the region of the canvas to render. Format is (x, y, w, h). By default, the entire canvas is rendered. size : tuple | None Specifies the size of the image array to return. If no size is given, then the size of the *region* is used, multiplied by the pixel scaling factor of the canvas (see `pixel_scale`). This argument allows the scene to be rendered at resolutions different from the native canvas resolution. bgcolor : instance of Color | None The background color to use. Returns ------- image : array Numpy array of type ubyte and shape (h, w, 4). Index [0, 0] is the upper-left corner of the rendered region. """ return self.canvas.render(region, size, bgcolor) def _open_images(self): """Adds image files from the menubar.""" filenames, _ = QFileDialog.getOpenFileNames( parent=self, caption='Select image(s)...', directory=self._last_visited_dir, # home dir by default ) self._add_files(filenames) def _add_files(self, filenames): """Adds an image layer to the viewer. Whether the image is multichannel is determined by :func:`napari.util.misc.is_multichannel`. If multiple images are selected, they are stacked along the 0th axis. Parameters ------- filenames : list List of filenames to be opened """ if len(filenames) > 0: image = read(filenames) self.viewer.add_image( image, multichannel=is_multichannel(image.shape) ) self._last_visited_dir = os.path.dirname(filenames[0]) def _on_interactive(self, event): self.view.interactive = self.viewer.interactive def _on_cursor(self, event): cursor = self.viewer.cursor size = self.viewer.cursor_size if cursor == 'square': if size < 10 or size > 300: q_cursor = self._cursors['cross'] else: q_cursor = QCursor( QPixmap(':/icons/cursor/cursor_square.png').scaledToHeight( size ) ) else: q_cursor = self._cursors[cursor] self.canvas.native.setCursor(q_cursor) def _on_reset_view(self, event): self.view.camera.rect = event.viewbox def _update_canvas(self, event): """Clears draw order and refreshes canvas. Usefeul for when layers are reoredered. """ self.canvas._draw_order.clear() self.canvas.update() def _update_palette(self, palette): # template and apply the primary stylesheet themed_stylesheet = template(self.raw_stylesheet, **palette) self.setStyleSheet(themed_stylesheet) def on_mouse_move(self, event): """Called whenever mouse moves over canvas. """ layer = self.viewer.active_layer if layer is not None: layer.on_mouse_move(event) def on_mouse_press(self, event): """Called whenever mouse pressed in canvas. """ layer = self.viewer.active_layer if layer is not None: layer.on_mouse_press(event) def on_mouse_release(self, event): """Called whenever mouse released in canvas. """ layer = self.viewer.active_layer if layer is not None: layer.on_mouse_release(event) def on_key_press(self, event): """Called whenever key pressed in canvas. """ if event.native.isAutoRepeat() or event.key is None: return comb = components_to_key_combo(event.key.name, event.modifiers) layer = self.viewer.active_layer # TODO: remove me once keybinding system converted if layer is not None: layer.on_key_press(event) if layer is not None and comb in layer.keymap: parent = layer elif comb in self.viewer.keymap: parent = self.viewer else: return func = parent.keymap[comb] gen = func(parent) if inspect.isgeneratorfunction(func): try: next(gen) except StopIteration: # only one statement pass else: self._key_release_generators[event.key] = gen def on_key_release(self, event): """Called whenever key released in canvas. """ try: next(self._key_release_generators[event.key]) except (KeyError, StopIteration): pass # TODO: remove me once keybinding system converted layer = self.viewer.active_layer if layer is not None: layer.on_key_release(event) def on_draw(self, event): """Called whenever drawn in canvas. Called for all layers, not just top """ for layer in self.viewer.layers: layer.on_draw(event) def dragEnterEvent(self, event): if event.mimeData().hasUrls(): event.accept() else: event.ignore() def dropEvent(self, event): """Add local files and web URLS with drag and drop.""" filenames = [] for url in event.mimeData().urls(): path = url.toString() if os.path.isfile(path): filenames.append(path) elif os.path.isdir(path): filenames = filenames + list(glob(os.path.join(path, '*'))) else: filenames.append(path) self._add_files(filenames)
class QtViewer(QSplitter): with open(osp.join(resources_dir, 'stylesheet.qss'), 'r') as f: default_stylesheet = f.read() def __init__(self, viewer): super().__init__() QCoreApplication.setAttribute( Qt.AA_UseStyleSheetPropagationInWidgetStyles, True) self.setStyleSheet(self.default_stylesheet) self.viewer = viewer self.canvas = SceneCanvas(keys=None, vsync=True) self.canvas.native.setMinimumSize(QSize(100, 100)) self.canvas.connect(self.on_mouse_move) self.canvas.connect(self.on_mouse_press) self.canvas.connect(self.on_mouse_release) self.canvas.connect(self.on_key_press) self.canvas.connect(self.on_key_release) self.view = self.canvas.central_widget.add_view() # Set 2D camera (the camera will scale to the contents in the scene) self.view.camera = PanZoomCamera(aspect=1) # flip y-axis to have correct aligment self.view.camera.flip = (0, 1, 0) self.view.camera.set_range() center = QWidget() layout = QVBoxLayout() layout.addWidget(self.canvas.native) layout.addWidget(self.viewer.dims._qt) center.setLayout(layout) # Add controls, center, and layerlist self.control_panel = QtControls(viewer) self.addWidget(self.control_panel) self.addWidget(center) self.addWidget(self.viewer.layers._qt) viewer.dims._qt.setFixedHeight(0) self._cursors = { 'disabled': QCursor(QPixmap(':/icons/cursor_disabled.png').scaled(20, 20)), 'cross': Qt.CrossCursor, 'forbidden': Qt.ForbiddenCursor, 'pointing': Qt.PointingHandCursor, 'standard': QCursor() } def set_cursor(self, cursor): self.canvas.native.setCursor(self._cursors[cursor]) def on_mouse_move(self, event): """Called whenever mouse moves over canvas. """ layer = self.viewer._top if layer is not None: layer.on_mouse_move(event) def on_mouse_press(self, event): """Called whenever mouse pressed in canvas. """ layer = self.viewer._top if layer is not None: layer.on_mouse_press(event) def on_mouse_release(self, event): """Called whenever mouse released in canvas. """ layer = self.viewer._top if layer is not None: layer.on_mouse_release(event) def on_key_press(self, event): """Called whenever key pressed in canvas. """ layer = self.viewer._top if layer is not None: layer.on_key_press(event) def on_key_release(self, event): """Called whenever key released in canvas. """ layer = self.viewer._top if layer is not None: layer.on_key_release(event)