class Window: """Application window that contains the menu bar and viewer. Parameters ---------- qt_viewer : QtViewer Contained viewer widget. Attributes ---------- qt_viewer : QtViewer Contained viewer widget. """ def __init__(self, qt_viewer, *, show=True): self.qt_viewer = qt_viewer self._qt_window = QMainWindow() self._qt_window.setUnifiedTitleAndToolBarOnMac(True) self._qt_center = QWidget() self._qt_window.setCentralWidget(self._qt_center) self._qt_window.setWindowTitle(self.qt_viewer.viewer.title) self._qt_center.setLayout(QHBoxLayout()) self._status_bar = self._qt_window.statusBar() self._qt_window.closeEvent = self.closeEvent self.close = self._qt_window.close self._add_menubar() self._add_file_menu() self._add_view_menu() self._add_window_menu() self._status_bar.showMessage('Ready') self._help = QLabel('') self._status_bar.addPermanentWidget(self._help) self._qt_center.layout().addWidget(self.qt_viewer) self._qt_center.layout().setContentsMargins(4, 0, 4, 0) self._update_palette(qt_viewer.viewer.palette) self.qt_viewer.viewer.events.status.connect(self._status_changed) self.qt_viewer.viewer.events.help.connect(self._help_changed) self.qt_viewer.viewer.events.title.connect(self._title_changed) self.qt_viewer.viewer.events.palette.connect( lambda event: self._update_palette(event.palette)) if show: self.show() def _add_menubar(self): self.main_menu = self._qt_window.menuBar() # Menubar shortcuts are only active when the menubar is visible. # Therefore, we set a global shortcut not associated with the menubar # to toggle visibility, *but*, in order to not shadow the menubar # shortcut, we disable it, and only enable it when the menubar is # hidden. See this stackoverflow link for details: # https://stackoverflow.com/questions/50537642/how-to-keep-the-shortcuts-of-a-hidden-widget-in-pyqt5 self._main_menu_shortcut = QShortcut(QKeySequence('Ctrl+M'), self._qt_window) self._main_menu_shortcut.activated.connect( self._toggle_menubar_visible) self._main_menu_shortcut.setEnabled(False) def _toggle_menubar_visible(self): """Toggle visibility of app menubar. This function also disables or enables a global keyboard shortcut to show the menubar, since menubar shortcuts are only available while the menubar is visible. """ if self.main_menu.isVisible(): self.main_menu.setVisible(False) self._main_menu_shortcut.setEnabled(True) else: self.main_menu.setVisible(True) self._main_menu_shortcut.setEnabled(False) def _add_file_menu(self): open_images = QAction('Open', self._qt_window) open_images.setShortcut('Ctrl+O') open_images.setStatusTip('Open image file(s)') open_images.triggered.connect(self.qt_viewer._open_images) self.file_menu = self.main_menu.addMenu('&File') self.file_menu.addAction(open_images) def _add_view_menu(self): toggle_visible = QAction('Toggle menubar visibility', self._qt_window) toggle_visible.setShortcut('Ctrl+M') toggle_visible.setStatusTip('Hide Menubar') toggle_visible.triggered.connect(self._toggle_menubar_visible) self.view_menu = self.main_menu.addMenu('&View') self.view_menu.addAction(toggle_visible) def _add_window_menu(self): exit_action = QAction("Close window", self._qt_window) exit_action.setShortcut("Ctrl+W") exit_action.setStatusTip('Close napari window') exit_action.triggered.connect(self._qt_window.close) self.window_menu = self.main_menu.addMenu('&Window') self.window_menu.addAction(exit_action) def resize(self, width, height): """Resize the window. Parameters ---------- width : int Width in logical pixels. height : int Height in logical pixels. """ self._qt_window.resize(width, height) def show(self): """Resize, show, and bring forward the window. """ self._qt_window.resize(self._qt_window.layout().sizeHint()) self._qt_window.show() self._qt_window.raise_() def _update_palette(self, palette): # set window styles which don't use the primary stylesheet # FIXME: this is a problem with the stylesheet not using properties self._status_bar.setStyleSheet( template( 'QStatusBar { background: {{ background }}; ' 'color: {{ text }}; }', **palette, )) self._qt_center.setStyleSheet( template('QWidget { background: {{ background }}; }', **palette)) def _status_changed(self, event): """Update status bar. """ self._status_bar.showMessage(event.text) def _title_changed(self, event): """Update window title. """ self._qt_window.setWindowTitle(event.text) def _help_changed(self, event): """Update help message on status bar. """ self._help.setText(event.text) def closeEvent(self, event): # Forward close event to the console to trigger proper shutdown self.qt_viewer.console.shutdown() event.accept()
class Window: """Application window that contains the menu bar and viewer. Parameters ---------- viewer : napari.components.ViewerModel Contained viewer widget. Attributes ---------- file_menu : qtpy.QtWidgets.QMenu File menu. help_menu : qtpy.QtWidgets.QMenu Help menu. main_menu : qtpy.QtWidgets.QMainWindow.menuBar Main menubar. qt_viewer : QtViewer Contained viewer widget. view_menu : qtpy.QtWidgets.QMenu View menu. window_menu : qtpy.QtWidgets.QMenu Window menu. """ raw_stylesheet = get_stylesheet() def __init__(self, viewer, *, show: bool = True): # Check there is a running app # instance() returns the singleton instance if it exists, or None app = QApplication.instance() # if None, raise a RuntimeError with the appropriate message if app is None: message = ( "napari requires a Qt event loop to run. To create one, " "try one of the following: \n" " - use the `napari.gui_qt()` context manager. See " "https://github.com/napari/napari/tree/master/examples for" " usage examples.\n" " - In IPython or a local Jupyter instance, use the " "`%gui qt` magic command.\n" " - Launch IPython with the option `--gui=qt`.\n" " - (recommended) in your IPython configuration file, add" " or uncomment the line `c.TerminalIPythonApp.gui = 'qt'`." " Then, restart IPython." ) raise RuntimeError(message) if perf_config: if perf_config.trace_qt_events: from .tracing.qt_event_tracing import convert_app_for_tracing # For tracing Qt events we need a special QApplication. If # using `gui_qt` we already have the special one, and no # conversion is done here. However when running inside # IPython or Jupyter this is where we switch out the # QApplication. app = convert_app_for_tracing(app) # Will patch based on config file. perf_config.patch_callables() _napari_app_id = getattr( viewer, "_napari_app_id", 'napari.napari.viewer.' + str(__version__), ) if ( platform.system() == "Windows" and not getattr(sys, 'frozen', False) and _napari_app_id ): import ctypes ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( _napari_app_id ) logopath = os.path.join( os.path.dirname(__file__), '..', 'resources', 'logo.png' ) if getattr(viewer, "_napari_global_logo", True): app = QApplication.instance() app.setWindowIcon(QIcon(logopath)) # see docstring of `wait_for_workers_to_quit` for caveats on killing # workers at shutdown. app.aboutToQuit.connect(wait_for_workers_to_quit) # Connect the Viewer and create the Main Window self.qt_viewer = QtViewer(viewer) self._qt_window = QMainWindow() self._qt_window.setWindowIcon(QIcon(logopath)) self._qt_window.setAttribute(Qt.WA_DeleteOnClose) self._qt_window.setUnifiedTitleAndToolBarOnMac(True) # since we initialize canvas before window, we need to manually connect them again. if self._qt_window.windowHandle() is not None: self._qt_window.windowHandle().screenChanged.connect( self.qt_viewer.canvas._backend.screen_changed ) self._qt_center = QWidget(self._qt_window) self._qt_window.setCentralWidget(self._qt_center) self._qt_window.setWindowTitle(self.qt_viewer.viewer.title) self._qt_center.setLayout(QHBoxLayout()) self._status_bar = QStatusBar() self._qt_window.setStatusBar(self._status_bar) self._add_menubar() self._add_file_menu() self._add_view_menu() self._add_window_menu() if not os.getenv("DISABLE_ALL_PLUGINS"): self._add_plugins_menu() self._add_help_menu() self._status_bar.showMessage('Ready') self._help = QLabel('') self._status_bar.addPermanentWidget(self._help) self._qt_center.layout().addWidget(self.qt_viewer) self._qt_center.layout().setContentsMargins(4, 0, 4, 0) self._update_palette() self._add_viewer_dock_widget(self.qt_viewer.dockConsole) self._add_viewer_dock_widget(self.qt_viewer.dockLayerControls) self._add_viewer_dock_widget(self.qt_viewer.dockLayerList) self.qt_viewer.viewer.events.status.connect(self._status_changed) self.qt_viewer.viewer.events.help.connect(self._help_changed) self.qt_viewer.viewer.events.title.connect(self._title_changed) self.qt_viewer.viewer.events.palette.connect(self._update_palette) if perf.USE_PERFMON: # Add DebugMenu and dockPerformance if using perfmon. self._debug_menu = DebugMenu(self) self._add_viewer_dock_widget(self.qt_viewer.dockPerformance) else: self._debug_menu = None if show: self.show() def _add_menubar(self): """Add menubar to napari app.""" self.main_menu = self._qt_window.menuBar() # Menubar shortcuts are only active when the menubar is visible. # Therefore, we set a global shortcut not associated with the menubar # to toggle visibility, *but*, in order to not shadow the menubar # shortcut, we disable it, and only enable it when the menubar is # hidden. See this stackoverflow link for details: # https://stackoverflow.com/questions/50537642/how-to-keep-the-shortcuts-of-a-hidden-widget-in-pyqt5 self._main_menu_shortcut = QShortcut( QKeySequence('Ctrl+M'), self._qt_window ) self._main_menu_shortcut.activated.connect( self._toggle_menubar_visible ) self._main_menu_shortcut.setEnabled(False) def _toggle_menubar_visible(self): """Toggle visibility of app menubar. This function also disables or enables a global keyboard shortcut to show the menubar, since menubar shortcuts are only available while the menubar is visible. """ if self.main_menu.isVisible(): self.main_menu.setVisible(False) self._main_menu_shortcut.setEnabled(True) else: self.main_menu.setVisible(True) self._main_menu_shortcut.setEnabled(False) def _add_file_menu(self): """Add 'File' menu to app menubar.""" open_images = QAction('Open File(s)...', self._qt_window) open_images.setShortcut('Ctrl+O') open_images.setStatusTip('Open file(s)') open_images.triggered.connect(self.qt_viewer._open_files_dialog) open_stack = QAction('Open Files as Stack...', self._qt_window) open_stack.setShortcut('Ctrl+Alt+O') open_stack.setStatusTip('Open files') open_stack.triggered.connect( self.qt_viewer._open_files_dialog_as_stack_dialog ) open_folder = QAction('Open Folder...', self._qt_window) open_folder.setShortcut('Ctrl+Shift+O') open_folder.setStatusTip('Open a folder') open_folder.triggered.connect(self.qt_viewer._open_folder_dialog) save_selected_layers = QAction( 'Save Selected Layer(s)...', self._qt_window ) save_selected_layers.setShortcut('Ctrl+S') save_selected_layers.setStatusTip('Save selected layers') save_selected_layers.triggered.connect( lambda: self.qt_viewer._save_layers_dialog(selected=True) ) save_all_layers = QAction('Save All Layers...', self._qt_window) save_all_layers.setShortcut('Ctrl+Shift+S') save_all_layers.setStatusTip('Save all layers') save_all_layers.triggered.connect( lambda: self.qt_viewer._save_layers_dialog(selected=False) ) screenshot = QAction('Save Screenshot...', self._qt_window) screenshot.setShortcut('Alt+S') screenshot.setStatusTip( 'Save screenshot of current display, default .png' ) screenshot.triggered.connect(self.qt_viewer._screenshot_dialog) screenshot_wv = QAction( 'Save Screenshot with Viewer...', self._qt_window ) screenshot_wv.setShortcut('Alt+Shift+S') screenshot_wv.setStatusTip( 'Save screenshot of current display with the viewer, default .png' ) screenshot_wv.triggered.connect(self._screenshot_dialog) # OS X will rename this to Quit and put it in the app menu. exitAction = QAction('Exit', self._qt_window) exitAction.setShortcut('Ctrl+Q') exitAction.setMenuRole(QAction.QuitRole) def handle_exit(): # if the event loop was started in gui_qt() then the app will be # named 'napari'. Since the Qapp was started by us, just close it. if QApplication.applicationName() == 'napari': QApplication.closeAllWindows() QApplication.quit() # otherwise, something else created the QApp before us (such as # %gui qt IPython magic). If we quit the app in this case, then # *later* attempts to instantiate a napari viewer won't work until # the event loop is restarted with app.exec_(). So rather than # quit just close all the windows (and clear our app icon). else: QApplication.setWindowIcon(QIcon()) self.close() if perf.USE_PERFMON: # Write trace file before exit, if we were writing one. # Is there a better place to make sure this is done on exit? perf.timers.stop_trace_file() _stop_monitor() exitAction.triggered.connect(handle_exit) self.file_menu = self.main_menu.addMenu('&File') self.file_menu.addAction(open_images) self.file_menu.addAction(open_stack) self.file_menu.addAction(open_folder) self.file_menu.addSeparator() self.file_menu.addAction(save_selected_layers) self.file_menu.addAction(save_all_layers) self.file_menu.addAction(screenshot) self.file_menu.addAction(screenshot_wv) self.file_menu.addSeparator() self.file_menu.addAction(exitAction) def _add_view_menu(self): """Add 'View' menu to app menubar.""" toggle_visible = QAction('Toggle Menubar Visibility', self._qt_window) toggle_visible.setShortcut('Ctrl+M') toggle_visible.setStatusTip('Hide Menubar') toggle_visible.triggered.connect(self._toggle_menubar_visible) toggle_theme = QAction('Toggle Theme', self._qt_window) toggle_theme.setShortcut('Ctrl+Shift+T') toggle_theme.setStatusTip('Toggle theme') toggle_theme.triggered.connect(self.qt_viewer.viewer._toggle_theme) toggle_fullscreen = QAction('Toggle Full Screen', self._qt_window) toggle_fullscreen.setShortcut('Ctrl+F') toggle_fullscreen.setStatusTip('Toggle full screen') toggle_fullscreen.triggered.connect(self._toggle_fullscreen) toggle_play = QAction('Toggle Play', self._qt_window) toggle_play.triggered.connect(self._toggle_play) toggle_play.setShortcut('Ctrl+Alt+P') toggle_play.setStatusTip('Toggle Play') self.view_menu = self.main_menu.addMenu('&View') self.view_menu.addAction(toggle_fullscreen) self.view_menu.addAction(toggle_visible) self.view_menu.addAction(toggle_theme) self.view_menu.addAction(toggle_play) self.view_menu.addSeparator() # Add octree actions. if config.async_octree: toggle_outline = QAction('Toggle Chunk Outlines', self._qt_window) toggle_outline.triggered.connect( self.qt_viewer._toggle_chunk_outlines ) toggle_outline.setShortcut('Ctrl+Alt+O') toggle_outline.setStatusTip('Toggle Chunk Outlines') self.view_menu.addAction(toggle_outline) # Add axes menu axes_menu = QMenu('Axes', parent=self._qt_window) axes_visible_action = QAction( 'Visible', parent=self._qt_window, checkable=True, checked=self.qt_viewer.viewer.axes.visible, ) axes_visible_action.triggered.connect(self._toggle_axes_visible) axes_colored_action = QAction( 'Colored', parent=self._qt_window, checkable=True, checked=self.qt_viewer.viewer.axes.colored, ) axes_colored_action.triggered.connect(self._toggle_axes_colored) axes_labels_action = QAction( 'Labels', parent=self._qt_window, checkable=True, checked=self.qt_viewer.viewer.axes.labels, ) axes_labels_action.triggered.connect(self._toggle_axes_labels) axes_dashed_action = QAction( 'Dashed', parent=self._qt_window, checkable=True, checked=self.qt_viewer.viewer.axes.dashed, ) axes_dashed_action.triggered.connect(self._toggle_axes_dashed) axes_arrows_action = QAction( 'Arrows', parent=self._qt_window, checkable=True, checked=self.qt_viewer.viewer.axes.arrows, ) axes_arrows_action.triggered.connect(self._toggle_axes_arrows) axes_menu.addAction(axes_visible_action) axes_menu.addAction(axes_colored_action) axes_menu.addAction(axes_labels_action) axes_menu.addAction(axes_dashed_action) axes_menu.addAction(axes_arrows_action) self.view_menu.addMenu(axes_menu) # Add scale bar menu scale_bar_menu = QMenu('Scale Bar', parent=self._qt_window) scale_bar_visible_action = QAction( 'Visible', parent=self._qt_window, checkable=True, checked=self.qt_viewer.viewer.scale_bar.visible, ) scale_bar_visible_action.triggered.connect( self._toggle_scale_bar_visible ) scale_bar_colored_action = QAction( 'Colored', parent=self._qt_window, checkable=True, checked=self.qt_viewer.viewer.scale_bar.colored, ) scale_bar_colored_action.triggered.connect( self._toggle_scale_bar_colored ) scale_bar_ticks_action = QAction( 'Ticks', parent=self._qt_window, checkable=True, checked=self.qt_viewer.viewer.scale_bar.ticks, ) scale_bar_ticks_action.triggered.connect(self._toggle_scale_bar_ticks) scale_bar_menu.addAction(scale_bar_visible_action) scale_bar_menu.addAction(scale_bar_colored_action) scale_bar_menu.addAction(scale_bar_ticks_action) self.view_menu.addMenu(scale_bar_menu) self.view_menu.addSeparator() def _add_window_menu(self): """Add 'Window' menu to app menubar.""" exit_action = QAction("Close Window", self._qt_window) exit_action.setShortcut("Ctrl+W") exit_action.setStatusTip('Close napari window') exit_action.triggered.connect(self._qt_window.close) self.window_menu = self.main_menu.addMenu('&Window') self.window_menu.addAction(exit_action) def _add_plugins_menu(self): """Add 'Plugins' menu to app menubar.""" self.plugins_menu = self.main_menu.addMenu('&Plugins') pip_install_action = QAction( "Install/Uninstall Package(s)...", self._qt_window ) pip_install_action.triggered.connect(self._show_plugin_install_dialog) self.plugins_menu.addAction(pip_install_action) order_plugin_action = QAction("Plugin Call Order...", self._qt_window) order_plugin_action.setStatusTip('Change call order for plugins') order_plugin_action.triggered.connect(self._show_plugin_sorter) self.plugins_menu.addAction(order_plugin_action) report_plugin_action = QAction("Plugin Errors...", self._qt_window) report_plugin_action.setStatusTip( 'Review stack traces for plugin exceptions and notify developers' ) report_plugin_action.triggered.connect(self._show_plugin_err_reporter) self.plugins_menu.addAction(report_plugin_action) def _show_plugin_sorter(self): """Show dialog that allows users to sort the call order of plugins.""" plugin_sorter = QtPluginSorter(parent=self._qt_window) if hasattr(self, 'plugin_sorter_widget'): self.plugin_sorter_widget.show() else: self.plugin_sorter_widget = self.add_dock_widget( plugin_sorter, name='Plugin Sorter', area="right" ) def _show_plugin_install_dialog(self): """Show dialog that allows users to sort the call order of plugins.""" self.plugin_dialog = QtPluginDialog(self._qt_window) self.plugin_dialog.exec_() def _show_plugin_err_reporter(self): """Show dialog that allows users to review and report plugin errors.""" QtPluginErrReporter(parent=self._qt_window).exec_() def _add_help_menu(self): """Add 'Help' menu to app menubar.""" self.help_menu = self.main_menu.addMenu('&Help') about_action = QAction("napari Info", self._qt_window) about_action.setShortcut("Ctrl+/") about_action.setStatusTip('About napari') about_action.triggered.connect( lambda e: QtAbout.showAbout(self.qt_viewer) ) self.help_menu.addAction(about_action) about_key_bindings = QAction("Show Key Bindings", self._qt_window) about_key_bindings.setShortcut("Ctrl+Alt+/") about_key_bindings.setShortcutContext(Qt.ApplicationShortcut) about_key_bindings.setStatusTip('key_bindings') about_key_bindings.triggered.connect( self.qt_viewer.show_key_bindings_dialog ) self.help_menu.addAction(about_key_bindings) def _toggle_scale_bar_visible(self, state): self.qt_viewer.viewer.scale_bar.visible = state def _toggle_scale_bar_colored(self, state): self.qt_viewer.viewer.scale_bar.colored = state def _toggle_scale_bar_ticks(self, state): self.qt_viewer.viewer.scale_bar.ticks = state def _toggle_axes_visible(self, state): self.qt_viewer.viewer.axes.visible = state def _toggle_axes_colored(self, state): self.qt_viewer.viewer.axes.colored = state def _toggle_axes_labels(self, state): self.qt_viewer.viewer.axes.labels = state def _toggle_axes_dashed(self, state): self.qt_viewer.viewer.axes.dashed = state def _toggle_axes_arrows(self, state): self.qt_viewer.viewer.axes.arrows = state def _toggle_fullscreen(self, event): """Toggle fullscreen mode.""" if self._qt_window.isFullScreen(): self._qt_window.showNormal() else: self._qt_window.showFullScreen() def _toggle_play(self, state): """Toggle play.""" if self.qt_viewer.dims.is_playing: self.qt_viewer.dims.stop() else: axis = self.qt_viewer.viewer.dims.last_used or 0 self.qt_viewer.dims.play(axis) def add_dock_widget( self, widget: QWidget, *, name: str = '', area: str = 'bottom', allowed_areas=None, shortcut=None, ): """Convenience method to add a QDockWidget to the main window Parameters ---------- widget : QWidget `widget` will be added as QDockWidget's main widget. name : str, optional Name of dock widget to appear in window menu. area : str Side of the main window to which the new dock widget will be added. Must be in {'left', 'right', 'top', 'bottom'} allowed_areas : list[str], optional Areas, relative to main window, that the widget is allowed dock. Each item in list must be in {'left', 'right', 'top', 'bottom'} By default, all areas are allowed. shortcut : str, optional Keyboard shortcut to appear in dropdown menu. Returns ------- dock_widget : QtViewerDockWidget `dock_widget` that can pass viewer events. """ dock_widget = QtViewerDockWidget( self.qt_viewer, widget, name=name, area=area, allowed_areas=allowed_areas, shortcut=shortcut, ) self._add_viewer_dock_widget(dock_widget) return dock_widget def _add_viewer_dock_widget(self, dock_widget: QtViewerDockWidget): """Add a QtViewerDockWidget to the main window Parameters ---------- dock_widget : QtViewerDockWidget `dock_widget` will be added to the main window. """ dock_widget.setParent(self._qt_window) self._qt_window.addDockWidget(dock_widget.qt_area, dock_widget) action = dock_widget.toggleViewAction() action.setStatusTip(dock_widget.name) action.setText(dock_widget.name) if dock_widget.shortcut is not None: action.setShortcut(dock_widget.shortcut) self.window_menu.addAction(action) def remove_dock_widget(self, widget): """Removes specified dock widget. Parameters ---------- widget : QWidget | str If widget == 'all', all docked widgets will be removed. """ if widget == 'all': for dw in self._qt_window.findChildren(QDockWidget): self._qt_window.removeDockWidget(dw) else: self._qt_window.removeDockWidget(widget) def resize(self, width, height): """Resize the window. Parameters ---------- width : int Width in logical pixels. height : int Height in logical pixels. """ self._qt_window.resize(width, height) def show(self): """Resize, show, and bring forward the window.""" self._qt_window.resize(self._qt_window.layout().sizeHint()) self._qt_window.show() # Resize axis labels now that window is shown self.qt_viewer.dims._resize_axis_labels() # We want to bring the viewer to the front when # A) it is our own (gui_qt) event loop OR we are running in jupyter # B) it is not the first time a QMainWindow is being created # `app_name` will be "napari" iff the application was instantiated in # gui_qt(). isActiveWindow() will be True if it is the second time a # _qt_window has been created. # See #721, #732, #735, #795, #1594 app_name = QApplication.instance().applicationName() if ( app_name == 'napari' or in_jupyter() ) and self._qt_window.isActiveWindow(): self.activate() def activate(self): """Make the viewer the currently active window.""" self._qt_window.raise_() # for macOS self._qt_window.activateWindow() # for Windows def _update_palette(self, event=None): """Update widget color palette.""" # set window styles which don't use the primary stylesheet # FIXME: this is a problem with the stylesheet not using properties palette = self.qt_viewer.viewer.palette self._status_bar.setStyleSheet( template( 'QStatusBar { background: {{ background }}; ' 'color: {{ text }}; }', **palette, ) ) self._qt_center.setStyleSheet( template('QWidget { background: {{ background }}; }', **palette) ) self._qt_window.setStyleSheet(template(self.raw_stylesheet, **palette)) def _status_changed(self, event): """Update status bar. Parameters ---------- event : napari.utils.event.Event The napari event that triggered this method. """ self._status_bar.showMessage(event.text) def _title_changed(self, event): """Update window title. Parameters ---------- event : napari.utils.event.Event The napari event that triggered this method. """ self._qt_window.setWindowTitle(event.text) def _help_changed(self, event): """Update help message on status bar. Parameters ---------- event : napari.utils.event.Event The napari event that triggered this method. """ self._help.setText(event.text) def _screenshot_dialog(self): """Save screenshot of current display with viewer, default .png""" dial = ScreenshotDialog( self.screenshot, self.qt_viewer, self.qt_viewer._last_visited_dir ) if dial.exec_(): self._last_visited_dir = os.path.dirname(dial.selectedFiles()[0]) def screenshot(self, path=None): """Take currently displayed viewer and convert to an image array. Parameters ---------- 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._qt_window.grab().toImage() if path is not None: imsave(path, QImg2array(img)) # scikit-image imsave method return QImg2array(img) def close(self): """Close the viewer window and cleanup sub-widgets.""" # Someone is closing us twice? Only try to delete self._qt_window # if we still have one. if hasattr(self, '_qt_window'): self._delete_qt_window() def _delete_qt_window(self): """Delete our self._qt_window.""" # On some versions of Darwin, exiting while fullscreen seems to tickle # some bug deep in NSWindow. This forces the fullscreen keybinding # test to complete its draw cycle, then pop back out of fullscreen. if self._qt_window.isFullScreen(): self._qt_window.showNormal() for i in range(8): time.sleep(0.1) QApplication.processEvents() self.qt_viewer.close() self._qt_window.close() del self._qt_window
class Window: """Application window that contains the menu bar and viewer. Parameters ---------- qt_viewer : QtViewer Contained viewer widget. Attributes ---------- file_menu : qtpy.QtWidgets.QMenu File menu. help_menu : qtpy.QtWidgets.QMenu Help menu. main_menu : qtpy.QtWidgets.QMainWindow.menuBar Main menubar. qt_viewer : QtViewer Contained viewer widget. view_menu : qtpy.QtWidgets.QMenu View menu. window_menu : qtpy.QtWidgets.QMenu Window menu. """ raw_stylesheet = combine_stylesheets() def __init__(self, qt_viewer, *, show=True): self.qt_viewer = qt_viewer self._qt_window = QMainWindow() self._qt_window.setAttribute(Qt.WA_DeleteOnClose) self._qt_window.setUnifiedTitleAndToolBarOnMac(True) self._qt_center = QWidget(self._qt_window) self._qt_window.setCentralWidget(self._qt_center) self._qt_window.setWindowTitle(self.qt_viewer.viewer.title) self._qt_center.setLayout(QHBoxLayout()) self._status_bar = QStatusBar() self._qt_window.setStatusBar(self._status_bar) self._add_menubar() self._add_file_menu() self._add_view_menu() self._add_window_menu() self._add_help_menu() self._status_bar.showMessage('Ready') self._help = QLabel('') self._status_bar.addPermanentWidget(self._help) self._qt_center.layout().addWidget(self.qt_viewer) self._qt_center.layout().setContentsMargins(4, 0, 4, 0) self._update_palette(qt_viewer.viewer.palette) if self.qt_viewer.console.shell is not None: self._add_viewer_dock_widget(self.qt_viewer.dockConsole) self._add_viewer_dock_widget(self.qt_viewer.dockLayerControls) self._add_viewer_dock_widget(self.qt_viewer.dockLayerList) self.qt_viewer.viewer.events.status.connect(self._status_changed) self.qt_viewer.viewer.events.help.connect(self._help_changed) self.qt_viewer.viewer.events.title.connect(self._title_changed) self.qt_viewer.viewer.events.palette.connect( lambda event: self._update_palette(event.palette)) if show: self.show() def _add_menubar(self): """Add menubar to napari app.""" self.main_menu = self._qt_window.menuBar() # Menubar shortcuts are only active when the menubar is visible. # Therefore, we set a global shortcut not associated with the menubar # to toggle visibility, *but*, in order to not shadow the menubar # shortcut, we disable it, and only enable it when the menubar is # hidden. See this stackoverflow link for details: # https://stackoverflow.com/questions/50537642/how-to-keep-the-shortcuts-of-a-hidden-widget-in-pyqt5 self._main_menu_shortcut = QShortcut(QKeySequence('Ctrl+M'), self._qt_window) self._main_menu_shortcut.activated.connect( self._toggle_menubar_visible) self._main_menu_shortcut.setEnabled(False) def _toggle_menubar_visible(self): """Toggle visibility of app menubar. This function also disables or enables a global keyboard shortcut to show the menubar, since menubar shortcuts are only available while the menubar is visible. """ if self.main_menu.isVisible(): self.main_menu.setVisible(False) self._main_menu_shortcut.setEnabled(True) else: self.main_menu.setVisible(True) self._main_menu_shortcut.setEnabled(False) def _add_file_menu(self): """Add 'File' menu to app menubar.""" open_images = QAction('Open image(s)...', self._qt_window) open_images.setShortcut('Ctrl+O') open_images.setStatusTip('Open image file(s)') open_images.triggered.connect(self.qt_viewer._open_images) open_folder = QAction('Open Folder...', self._qt_window) open_folder.setShortcut('Ctrl+Shift+O') open_folder.setStatusTip( 'Open a folder of image file(s) or a zarr file') open_folder.triggered.connect(self.qt_viewer._open_folder) screenshot = QAction('Screenshot', self._qt_window) screenshot.setShortcut('Ctrl+Alt+S') screenshot.setStatusTip( 'Save screenshot of current display, default .png') screenshot.triggered.connect(self.qt_viewer._save_screenshot) self.file_menu = self.main_menu.addMenu('&File') self.file_menu.addAction(open_images) self.file_menu.addAction(open_folder) self.file_menu.addAction(screenshot) def _add_view_menu(self): """Add 'View' menu to app menubar.""" toggle_visible = QAction('Toggle menubar visibility', self._qt_window) toggle_visible.setShortcut('Ctrl+M') toggle_visible.setStatusTip('Hide Menubar') toggle_visible.triggered.connect(self._toggle_menubar_visible) toggle_theme = QAction('Toggle theme', self._qt_window) toggle_theme.setShortcut('Ctrl+Shift+T') toggle_theme.setStatusTip('Toggle theme') toggle_theme.triggered.connect(self.qt_viewer.viewer._toggle_theme) self.view_menu = self.main_menu.addMenu('&View') self.view_menu.addAction(toggle_visible) self.view_menu.addAction(toggle_theme) def _add_window_menu(self): """Add 'Window' menu to app menubar.""" exit_action = QAction("Close window", self._qt_window) exit_action.setShortcut("Ctrl+W") exit_action.setStatusTip('Close napari window') exit_action.triggered.connect(self._qt_window.close) self.window_menu = self.main_menu.addMenu('&Window') self.window_menu.addAction(exit_action) def _add_help_menu(self): """Add 'Help' menu to app menubar.""" self.help_menu = self.main_menu.addMenu('&Help') about_action = QAction("napari info", self._qt_window) about_action.setShortcut("Ctrl+/") about_action.setStatusTip('About napari') about_action.triggered.connect( lambda e: QtAbout.showAbout(self.qt_viewer)) self.help_menu.addAction(about_action) about_keybindings = QAction("keybindings", self._qt_window) about_keybindings.setShortcut("Ctrl+Alt+/") about_keybindings.setShortcutContext(Qt.ApplicationShortcut) about_keybindings.setStatusTip('keybindings') about_keybindings.triggered.connect( self.qt_viewer.show_keybindings_dialog) self.help_menu.addAction(about_keybindings) def add_dock_widget( self, widget: QWidget, *, name: str = '', area: str = 'bottom', allowed_areas=None, shortcut=None, ): """Convenience method to add a QDockWidget to the main window Parameters ---------- widget : QWidget `widget` will be added as QDockWidget's main widget. name : str, optional Name of dock widget to appear in window menu. area : str Side of the main window to which the new dock widget will be added. Must be in {'left', 'right', 'top', 'bottom'} allowed_areas : list[str], optional Areas, relative to main window, that the widget is allowed dock. Each item in list must be in {'left', 'right', 'top', 'bottom'} By default, all areas are allowed. shortcut : str, optional Keyboard shortcut to appear in dropdown menu. Returns ------- dock_widget : QtViewerDockWidget `dock_widget` that can pass viewer events. """ dock_widget = QtViewerDockWidget( self.qt_viewer, widget, name=name, area=area, allowed_areas=allowed_areas, shortcut=shortcut, ) self._add_viewer_dock_widget(dock_widget) return dock_widget def _add_viewer_dock_widget(self, dock_widget: QtViewerDockWidget): """Add a QtViewerDockWidget to the main window Parameters ---------- dock_widget : QtViewerDockWidget `dock_widget` will be added to the main window. """ dock_widget.setParent(self._qt_window) self._qt_window.addDockWidget(dock_widget.qt_area, dock_widget) action = dock_widget.toggleViewAction() action.setStatusTip(dock_widget.name) action.setText(dock_widget.name) if dock_widget.shortcut is not None: action.setShortcut(dock_widget.shortcut) self.window_menu.addAction(action) def remove_dock_widget(self, widget): """Removes specified dock widget. Parameters ---------- widget : QWidget | str If widget == 'all', all docked widgets will be removed. """ if widget == 'all': for dw in self._qt_window.findChildren(QDockWidget): self._qt_window.removeDockWidget(dw) else: self._qt_window.removeDockWidget(widget) def resize(self, width, height): """Resize the window. Parameters ---------- width : int Width in logical pixels. height : int Height in logical pixels. """ self._qt_window.resize(width, height) def show(self): """Resize, show, and bring forward the window.""" self._qt_window.resize(self._qt_window.layout().sizeHint()) self._qt_window.show() # We want to call Window._qt_window.raise_() in every case *except* # when instantiating a viewer within a gui_qt() context for the # _first_ time within the Qt app's lifecycle. # # `app_name` will be "napari" iff the application was instantiated in # gui_qt(). isActiveWindow() will be True if it is the second time a # _qt_window has been created. See #732 app_name = QApplication.instance().applicationName() if app_name != 'napari' or self._qt_window.isActiveWindow(): self._qt_window.raise_() # for macOS self._qt_window.activateWindow() # for Windows def _update_palette(self, palette): """Update widget color palette. Parameters ---------- palette : qtpy.QtGui.QPalette Color palette for each widget state (Active, Disabled, Inactive). """ # set window styles which don't use the primary stylesheet # FIXME: this is a problem with the stylesheet not using properties self._status_bar.setStyleSheet( template( 'QStatusBar { background: {{ background }}; ' 'color: {{ text }}; }', **palette, )) self._qt_center.setStyleSheet( template('QWidget { background: {{ background }}; }', **palette)) self._qt_window.setStyleSheet(template(self.raw_stylesheet, **palette)) def _status_changed(self, event): """Update status bar. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ self._status_bar.showMessage(event.text) def _title_changed(self, event): """Update window title. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ self._qt_window.setWindowTitle(event.text) def _help_changed(self, event): """Update help message on status bar. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ self._help.setText(event.text) def screenshot(self, path=None): """Take currently displayed viewer and convert to an image array. Parameters ---------- 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._qt_window.grab().toImage() if path is not None: imsave(path, QImg2array(img)) # scikit-image imsave method return QImg2array(img) def close(self): """Close the viewer window and cleanup sub-widgets.""" # on some versions of Darwin, exiting while fullscreen seems to tickle # some bug deep in NSWindow. This forces the fullscreen keybinding # test to complete its draw cycle, then pop back out of fullscreen. if self._qt_window.isFullScreen(): self._qt_window.showNormal() for i in range(8): time.sleep(0.1) QApplication.processEvents() self.qt_viewer.close() self._qt_window.close() del self._qt_window
class Window: """Application window that contains the menu bar and viewer. Parameters ---------- qt_viewer : QtViewer Contained viewer widget. Attributes ---------- qt_viewer : QtViewer Contained viewer widget. """ with open(os.path.join(resources_dir, 'stylesheet.qss'), 'r') as f: raw_stylesheet = f.read() def __init__(self, qt_viewer, *, show=True): self.qt_viewer = qt_viewer self._qt_window = QMainWindow() self._qt_window.setUnifiedTitleAndToolBarOnMac(True) self._qt_center = QWidget(self._qt_window) self._qt_window.setCentralWidget(self._qt_center) self._qt_window.setWindowTitle(self.qt_viewer.viewer.title) self._qt_center.setLayout(QHBoxLayout()) self._status_bar = QStatusBar() self._qt_window.setStatusBar(self._status_bar) self._qt_window.closeEvent = self.closeEvent self.close = self._qt_window.close self._add_menubar() self._add_file_menu() self._add_view_menu() self._add_window_menu() self._add_help_menu() self._status_bar.showMessage('Ready') self._help = QLabel('') self._status_bar.addPermanentWidget(self._help) self._qt_center.layout().addWidget(self.qt_viewer) self._qt_center.layout().setContentsMargins(4, 0, 4, 0) self._update_palette(qt_viewer.viewer.palette) if self.qt_viewer.console.shell is not None: self._add_viewer_dock_widget(self.qt_viewer.dockConsole) self._add_viewer_dock_widget(self.qt_viewer.dockLayerControls) self._add_viewer_dock_widget(self.qt_viewer.dockLayerList) self.qt_viewer.viewer.events.status.connect(self._status_changed) self.qt_viewer.viewer.events.help.connect(self._help_changed) self.qt_viewer.viewer.events.title.connect(self._title_changed) self.qt_viewer.viewer.events.palette.connect( lambda event: self._update_palette(event.palette)) if show: self.show() def _add_menubar(self): self.main_menu = self._qt_window.menuBar() # Menubar shortcuts are only active when the menubar is visible. # Therefore, we set a global shortcut not associated with the menubar # to toggle visibility, *but*, in order to not shadow the menubar # shortcut, we disable it, and only enable it when the menubar is # hidden. See this stackoverflow link for details: # https://stackoverflow.com/questions/50537642/how-to-keep-the-shortcuts-of-a-hidden-widget-in-pyqt5 self._main_menu_shortcut = QShortcut(QKeySequence('Ctrl+M'), self._qt_window) self._main_menu_shortcut.activated.connect( self._toggle_menubar_visible) self._main_menu_shortcut.setEnabled(False) def _toggle_menubar_visible(self): """Toggle visibility of app menubar. This function also disables or enables a global keyboard shortcut to show the menubar, since menubar shortcuts are only available while the menubar is visible. """ if self.main_menu.isVisible(): self.main_menu.setVisible(False) self._main_menu_shortcut.setEnabled(True) else: self.main_menu.setVisible(True) self._main_menu_shortcut.setEnabled(False) def _add_file_menu(self): open_images = QAction('Open image(s)...', self._qt_window) open_images.setShortcut('Ctrl+O') open_images.setStatusTip('Open image file(s)') open_images.triggered.connect(self.qt_viewer._open_images) open_folder = QAction('Open Folder...', self._qt_window) open_folder.setShortcut('Ctrl-Shift-O') open_folder.setStatusTip( 'Open a folder of image file(s) or a zarr file') open_folder.triggered.connect(self.qt_viewer._open_folder) self.file_menu = self.main_menu.addMenu('&File') self.file_menu.addAction(open_images) self.file_menu.addAction(open_folder) def _add_view_menu(self): toggle_visible = QAction('Toggle menubar visibility', self._qt_window) toggle_visible.setShortcut('Ctrl+M') toggle_visible.setStatusTip('Hide Menubar') toggle_visible.triggered.connect(self._toggle_menubar_visible) self.view_menu = self.main_menu.addMenu('&View') self.view_menu.addAction(toggle_visible) def _add_window_menu(self): exit_action = QAction("Close window", self._qt_window) exit_action.setShortcut("Ctrl+W") exit_action.setStatusTip('Close napari window') exit_action.triggered.connect(self._qt_window.close) self.window_menu = self.main_menu.addMenu('&Window') self.window_menu.addAction(exit_action) def _add_help_menu(self): self.help_menu = self.main_menu.addMenu('&Help') about_action = QAction("napari info", self._qt_window) about_action.setShortcut("Ctrl+/") about_action.setStatusTip('About napari') about_action.triggered.connect( lambda e: QtAbout.showAbout(self.qt_viewer)) self.help_menu.addAction(about_action) about_keybindings = QAction("keybindings", self._qt_window) about_keybindings.setShortcut("Ctrl+Alt+/") about_keybindings.setShortcutContext(Qt.ApplicationShortcut) about_keybindings.setStatusTip('keybindings') about_keybindings.triggered.connect( self.qt_viewer.aboutKeybindings.toggle_visible) self.help_menu.addAction(about_keybindings) def add_dock_widget( self, widget: QWidget, *, name: str = '', area: str = 'bottom', allowed_areas=None, shortcut=None, ): """Convenience method to add a QDockWidget to the main window Parameters ---------- widget : QWidget `widget` will be added as QDockWidget's main widget. name : str, optional Name of dock widget to appear in window menu. area : str Side of the main window to which the new dock widget will be added. Must be in {'left', 'right', 'top', 'bottom'} allowed_areas : list[str], optional Areas, relative to main window, that the widget is allowed dock. Each item in list must be in {'left', 'right', 'top', 'bottom'} By default, all areas are allowed. shortcut : str, optional Keyboard shortcut to appear in dropdown menu. Returns ------- dock_widget : QtViewerDockWidget `dock_widget` that can pass viewer events. """ dock_widget = QtViewerDockWidget( self.qt_viewer, widget, name=name, area=area, allowed_areas=allowed_areas, shortcut=shortcut, ) self._add_viewer_dock_widget(dock_widget) return dock_widget def _add_viewer_dock_widget(self, dock_widget: QtViewerDockWidget): """Add a QtViewerDockWidget to the main window Parameters ---------- dock_widget : QtViewerDockWidget `dock_widget` will be added to the main window. """ dock_widget.setParent(self._qt_window) self._qt_window.addDockWidget(dock_widget.qt_area, dock_widget) action = dock_widget.toggleViewAction() action.setStatusTip(dock_widget.name) action.setText(dock_widget.name) if dock_widget.shortcut is not None: action.setShortcut(dock_widget.shortcut) self.window_menu.addAction(action) def remove_dock_widget(self, widget): """Removes specified dock widget. Parameters ---------- widget : QWidget | str If widget == 'all', all docked widgets will be removed. """ if widget == 'all': for dw in self._qt_window.findChildren(QDockWidget): self._qt_window.removeDockWidget(dw) else: self._qt_window.removeDockWidget(widget) def resize(self, width, height): """Resize the window. Parameters ---------- width : int Width in logical pixels. height : int Height in logical pixels. """ self._qt_window.resize(width, height) def show(self): """Resize, show, and bring forward the window.""" self._qt_window.resize(self._qt_window.layout().sizeHint()) self._qt_window.show() # We want to call Window._qt_window.raise_() in every case *except* # when instantiating a viewer within a gui_qt() context for the # _first_ time within the Qt app's lifecycle. # # `app_name` will be "napari" iff the application was instantiated in # gui_qt(). isActiveWindow() will be True if it is the second time a # _qt_window has been created. See #732 app_name = QApplication.instance().applicationName() if app_name != 'napari' or self._qt_window.isActiveWindow(): self._qt_window.raise_() # for macOS self._qt_window.activateWindow() # for Windows def _update_palette(self, palette): # set window styles which don't use the primary stylesheet # FIXME: this is a problem with the stylesheet not using properties self._status_bar.setStyleSheet( template( 'QStatusBar { background: {{ background }}; ' 'color: {{ text }}; }', **palette, )) self._qt_center.setStyleSheet( template('QWidget { background: {{ background }}; }', **palette)) self._qt_window.setStyleSheet(template(self.raw_stylesheet, **palette)) def _status_changed(self, event): """Update status bar. """ self._status_bar.showMessage(event.text) def _title_changed(self, event): """Update window title. """ self._qt_window.setWindowTitle(event.text) def _help_changed(self, event): """Update help message on status bar. """ self._help.setText(event.text) def screenshot(self): """Take currently displayed viewer 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._qt_window.grab().toImage() return QImg2array(img) def closeEvent(self, event): # Forward close event to the console to trigger proper shutdown self.qt_viewer.console.shutdown() # 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.qt_viewer.dims.stop() event.accept()
class Window: """Application window that contains the menu bar and viewer. Parameters ---------- viewer : napari.components.ViewerModel Contained viewer widget. Attributes ---------- file_menu : qtpy.QtWidgets.QMenu File menu. help_menu : qtpy.QtWidgets.QMenu Help menu. main_menu : qtpy.QtWidgets.QMainWindow.menuBar Main menubar. qt_viewer : QtViewer Contained viewer widget. view_menu : qtpy.QtWidgets.QMenu View menu. window_menu : qtpy.QtWidgets.QMenu Window menu. """ def __init__(self, viewer, *, show: bool = True): # create QApplication if it doesn't already exist get_app() self._unnamed_dockwidget_count = 1 # Connect the Viewer and create the Main Window self.qt_viewer = QtViewer(viewer, show_welcome_screen=True) self._qt_window = _QtMainWindow(self.qt_viewer) self._status_bar = self._qt_window.statusBar() # Dictionary holding dock widgets self._dock_widgets: Dict[str, QtViewerDockWidget] = {} # since we initialize canvas before window, we need to manually connect them again. if self._qt_window.windowHandle() is not None: self._qt_window.windowHandle().screenChanged.connect( self.qt_viewer.canvas._backend.screen_changed) self._add_menubar() self._add_file_menu() self._add_view_menu() self._add_window_menu() self._add_plugins_menu() self._add_help_menu() self._status_bar.showMessage(trans._('Ready')) self._help = QLabel('') self._status_bar.addPermanentWidget(self._help) self.qt_viewer.viewer.theme = SETTINGS.appearance.theme self._update_theme() self._add_viewer_dock_widget(self.qt_viewer.dockConsole, tabify=False) self._add_viewer_dock_widget(self.qt_viewer.dockLayerControls, tabify=False) self._add_viewer_dock_widget(self.qt_viewer.dockLayerList, tabify=False) self._add_viewer_dock_widget(self.qt_viewer.activityDock, tabify=False) self.window_menu.addSeparator() SETTINGS.appearance.events.theme.connect(self._update_theme) viewer.events.status.connect(self._status_changed) viewer.events.help.connect(self._help_changed) viewer.events.title.connect(self._title_changed) viewer.events.theme.connect(self._update_theme) if perf.USE_PERFMON: # Add DebugMenu and dockPerformance if using perfmon. self._debug_menu = DebugMenu(self) self._add_viewer_dock_widget(self.qt_viewer.dockPerformance) else: self._debug_menu = None if show: self.show() def _add_menubar(self): """Add menubar to napari app.""" self.main_menu = self._qt_window.menuBar() # Menubar shortcuts are only active when the menubar is visible. # Therefore, we set a global shortcut not associated with the menubar # to toggle visibility, *but*, in order to not shadow the menubar # shortcut, we disable it, and only enable it when the menubar is # hidden. See this stackoverflow link for details: # https://stackoverflow.com/questions/50537642/how-to-keep-the-shortcuts-of-a-hidden-widget-in-pyqt5 self._main_menu_shortcut = QShortcut(QKeySequence('Ctrl+M'), self._qt_window) self._main_menu_shortcut.activated.connect( self._toggle_menubar_visible) self._main_menu_shortcut.setEnabled(False) def _toggle_menubar_visible(self): """Toggle visibility of app menubar. This function also disables or enables a global keyboard shortcut to show the menubar, since menubar shortcuts are only available while the menubar is visible. """ if self.main_menu.isVisible(): self.main_menu.setVisible(False) self._main_menu_shortcut.setEnabled(True) else: self.main_menu.setVisible(True) self._main_menu_shortcut.setEnabled(False) def _add_file_menu(self): """Add 'File' menu to app menubar.""" open_images = QAction(trans._('Open File(s)...'), self._qt_window) open_images.setShortcut('Ctrl+O') open_images.setStatusTip(trans._('Open file(s)')) open_images.triggered.connect(self.qt_viewer._open_files_dialog) open_stack = QAction(trans._('Open Files as Stack...'), self._qt_window) open_stack.setShortcut('Ctrl+Alt+O') open_stack.setStatusTip(trans._('Open files')) open_stack.triggered.connect( self.qt_viewer._open_files_dialog_as_stack_dialog) open_folder = QAction(trans._('Open Folder...'), self._qt_window) open_folder.setShortcut('Ctrl+Shift+O') open_folder.setStatusTip(trans._('Open a folder')) open_folder.triggered.connect(self.qt_viewer._open_folder_dialog) # OS X will rename this to Quit and put it in the app menu. preferences = QAction(trans._('Preferences'), self._qt_window) preferences.setShortcut('Ctrl+Shift+P') preferences.setStatusTip(trans._('Open preferences dialog')) preferences.setMenuRole(QAction.PreferencesRole) preferences.triggered.connect(self._open_preferences) save_selected_layers = QAction(trans._('Save Selected Layer(s)...'), self._qt_window) save_selected_layers.setShortcut('Ctrl+S') save_selected_layers.setStatusTip(trans._('Save selected layers')) save_selected_layers.triggered.connect( lambda: self.qt_viewer._save_layers_dialog(selected=True)) save_all_layers = QAction(trans._('Save All Layers...'), self._qt_window) save_all_layers.setShortcut('Ctrl+Shift+S') save_all_layers.setStatusTip(trans._('Save all layers')) save_all_layers.triggered.connect( lambda: self.qt_viewer._save_layers_dialog(selected=False)) screenshot = QAction(trans._('Save Screenshot...'), self._qt_window) screenshot.setShortcut('Alt+S') screenshot.setStatusTip( trans._('Save screenshot of current display, default .png')) screenshot.triggered.connect(self.qt_viewer._screenshot_dialog) screenshot_wv = QAction(trans._('Save Screenshot with Viewer...'), self._qt_window) screenshot_wv.setShortcut('Alt+Shift+S') screenshot_wv.setStatusTip( trans. _('Save screenshot of current display with the viewer, default .png' )) screenshot_wv.triggered.connect(self._screenshot_dialog) # OS X will rename this to Quit and put it in the app menu. # This quits the entire QApplication and all windows that may be open. quitAction = QAction(trans._('Exit'), self._qt_window) quitAction.setShortcut('Ctrl+Q') quitAction.setMenuRole(QAction.QuitRole) quitAction.triggered.connect( lambda: self._qt_window.close(quit_app=True)) if running_as_bundled_app(): restartAction = QAction(trans._('Restart'), self._qt_window) restartAction.triggered.connect(self._qt_window.restart) closeAction = QAction(trans._('Close Window'), self._qt_window) closeAction.setShortcut('Ctrl+W') closeAction.triggered.connect(self._qt_window.close_window) plugin_manager.discover_sample_data() open_sample_menu = QMenu(trans._('Open Sample'), self._qt_window) for plugin_name, samples in plugin_manager._sample_data.items(): multiprovider = len(samples) > 1 if multiprovider: menu = QMenu(plugin_name, self._qt_window) open_sample_menu.addMenu(menu) else: menu = open_sample_menu for samp_name, samp_dict in samples.items(): display_name = samp_dict['display_name'] if multiprovider: action = QAction(display_name, parent=self._qt_window) else: full_name = plugin_menu_item_template.format( plugin_name, display_name) action = QAction(full_name, parent=self._qt_window) def _add_sample(*args, plg=plugin_name, smp=samp_name): self.qt_viewer.viewer.open_sample(plg, smp) menu.addAction(action) action.triggered.connect(_add_sample) self.file_menu = self.main_menu.addMenu(trans._('&File')) self.file_menu.addAction(open_images) self.file_menu.addAction(open_stack) self.file_menu.addAction(open_folder) self.file_menu.addMenu(open_sample_menu) self.file_menu.addSeparator() self.file_menu.addAction(preferences) self.file_menu.addSeparator() self.file_menu.addAction(save_selected_layers) self.file_menu.addAction(save_all_layers) self.file_menu.addAction(screenshot) self.file_menu.addAction(screenshot_wv) self.file_menu.addSeparator() self.file_menu.addAction(closeAction) if running_as_bundled_app(): self.file_menu.addAction(restartAction) self.file_menu.addAction(quitAction) def _open_preferences(self): """Edit preferences from the menubar.""" if self._qt_window._preferences_dialog is None: win = PreferencesDialog(parent=self._qt_window) win.resized.connect( self._qt_window._update_preferences_dialog_size) if self._qt_window._preferences_dialog_size: win.resize(self._qt_window._preferences_dialog_size) self._qt_window._preferences_dialog = win win.closed.connect(self._on_preferences_closed) win.show() else: self._qt_window._preferences_dialog.raise_() def _on_preferences_closed(self): """Reset preferences dialog variable.""" self._qt_window._preferences_dialog = None def _add_view_menu(self): """Add 'View' menu to app menubar.""" toggle_visible = QAction(trans._('Toggle Menubar Visibility'), self._qt_window) toggle_visible.setShortcut('Ctrl+M') toggle_visible.setStatusTip(trans._('Hide Menubar')) toggle_visible.triggered.connect(self._toggle_menubar_visible) toggle_fullscreen = QAction(trans._('Toggle Full Screen'), self._qt_window) toggle_fullscreen.setShortcut('Ctrl+F') toggle_fullscreen.setStatusTip(trans._('Toggle full screen')) toggle_fullscreen.triggered.connect(self._toggle_fullscreen) toggle_play = QAction(trans._('Toggle Play'), self._qt_window) toggle_play.triggered.connect(self._toggle_play) toggle_play.setShortcut('Ctrl+Alt+P') toggle_play.setStatusTip(trans._('Toggle Play')) self.view_menu = self.main_menu.addMenu(trans._('&View')) self.view_menu.addAction(toggle_fullscreen) self.view_menu.addAction(toggle_visible) self.view_menu.addAction(toggle_play) self.view_menu.addSeparator() # Add octree actions. if config.async_octree: toggle_outline = QAction(trans._('Toggle Chunk Outlines'), self._qt_window) toggle_outline.triggered.connect( self.qt_viewer._toggle_chunk_outlines) toggle_outline.setShortcut('Ctrl+Alt+O') toggle_outline.setStatusTip(trans._('Toggle Chunk Outlines')) self.view_menu.addAction(toggle_outline) # Add axes menu axes = self.qt_viewer.viewer.axes axes_menu = QMenu(trans._('Axes'), parent=self._qt_window) axes_visible_action = QAction( trans._('Visible'), parent=self._qt_window, checkable=True, checked=self.qt_viewer.viewer.axes.visible, ) axes_visible_action.triggered.connect(self._toggle_axes_visible) self._event_to_action(axes_visible_action, axes.events.visible) axes_colored_action = QAction( trans._('Colored'), parent=self._qt_window, checkable=True, checked=self.qt_viewer.viewer.axes.colored, ) axes_colored_action.triggered.connect(self._toggle_axes_colored) self._event_to_action(axes_colored_action, axes.events.colored) axes_labels_action = QAction( trans._('Labels'), parent=self._qt_window, checkable=True, checked=self.qt_viewer.viewer.axes.labels, ) axes_labels_action.triggered.connect(self._toggle_axes_labels) self._event_to_action(axes_labels_action, axes.events.labels) axes_dashed_action = QAction( trans._('Dashed'), parent=self._qt_window, checkable=True, checked=self.qt_viewer.viewer.axes.dashed, ) axes_dashed_action.triggered.connect(self._toggle_axes_dashed) self._event_to_action(axes_dashed_action, axes.events.dashed) axes_arrows_action = QAction( trans._('Arrows'), parent=self._qt_window, checkable=True, checked=self.qt_viewer.viewer.axes.arrows, ) axes_arrows_action.triggered.connect(self._toggle_axes_arrows) self._event_to_action(axes_arrows_action, axes.events.arrows) axes_menu.addAction(axes_visible_action) axes_menu.addAction(axes_colored_action) axes_menu.addAction(axes_labels_action) axes_menu.addAction(axes_dashed_action) axes_menu.addAction(axes_arrows_action) self.view_menu.addMenu(axes_menu) # Add scale bar menu scale_bar = self.qt_viewer.viewer.scale_bar scale_bar_menu = QMenu(trans._('Scale Bar'), parent=self._qt_window) scale_bar_visible_action = QAction( trans._('Visible'), parent=self._qt_window, checkable=True, checked=self.qt_viewer.viewer.scale_bar.visible, ) scale_bar_visible_action.triggered.connect( self._toggle_scale_bar_visible) self._event_to_action(scale_bar_visible_action, scale_bar.events.visible) scale_bar_colored_action = QAction( trans._('Colored'), parent=self._qt_window, checkable=True, checked=self.qt_viewer.viewer.scale_bar.colored, ) scale_bar_colored_action.triggered.connect( self._toggle_scale_bar_colored) self._event_to_action(scale_bar_colored_action, scale_bar.events.colored) scale_bar_ticks_action = QAction( trans._('Ticks'), parent=self._qt_window, checkable=True, checked=self.qt_viewer.viewer.scale_bar.ticks, ) scale_bar_ticks_action.triggered.connect(self._toggle_scale_bar_ticks) self._event_to_action(scale_bar_ticks_action, scale_bar.events.ticks) scale_bar_menu.addAction(scale_bar_visible_action) scale_bar_menu.addAction(scale_bar_colored_action) scale_bar_menu.addAction(scale_bar_ticks_action) self.view_menu.addMenu(scale_bar_menu) self.view_menu.addSeparator() def _event_to_action(self, action, event): """Connect triggered event in model to respective action in menu.""" # TODO: use action manager to keep in sync event.connect(lambda e: action.setChecked(e.value)) def _add_window_menu(self): """Add 'Window' menu to app menubar.""" clear_action = QAction(trans._("Remove Dock Widgets"), self._qt_window) clear_action.setStatusTip(trans._('Remove all dock widgets')) clear_action.triggered.connect( lambda e: self.remove_dock_widget('all')) self.window_menu = self.main_menu.addMenu(trans._('&Window')) self.window_menu.addAction(clear_action) self.window_menu.addSeparator() def _add_plugins_menu(self): """Add 'Plugins' menu to app menubar.""" self.plugins_menu = self.main_menu.addMenu(trans._('&Plugins')) pip_install_action = QAction( trans._("Install/Uninstall Package(s)..."), self._qt_window) pip_install_action.triggered.connect(self._show_plugin_install_dialog) self.plugins_menu.addAction(pip_install_action) report_plugin_action = QAction(trans._("Plugin Errors..."), self._qt_window) report_plugin_action.setStatusTip( trans. _('Review stack traces for plugin exceptions and notify developers' )) report_plugin_action.triggered.connect(self._show_plugin_err_reporter) self.plugins_menu.addAction(report_plugin_action) self._plugin_dock_widget_menu = QMenu(trans._('Add Dock Widget'), self._qt_window) plugin_manager.discover_widgets() # Add a menu item (QAction) for each available plugin widget for hook_type, (plugin_name, widgets) in plugin_manager.iter_widgets(): multiprovider = len(widgets) > 1 if multiprovider: menu = QMenu(plugin_name, self._qt_window) self._plugin_dock_widget_menu.addMenu(menu) else: menu = self._plugin_dock_widget_menu for wdg_name in widgets: key = (plugin_name, wdg_name) if multiprovider: action = QAction(wdg_name, parent=self._qt_window) else: full_name = plugin_menu_item_template.format(*key) action = QAction(full_name, parent=self._qt_window) def _add_widget(*args, key=key, hook_type=hook_type): if hook_type == 'dock': self.add_plugin_dock_widget(*key) else: self._add_plugin_function_widget(*key) menu.addAction(action) action.triggered.connect(_add_widget) self.plugins_menu.addMenu(self._plugin_dock_widget_menu) def _show_plugin_install_dialog(self): """Show dialog that allows users to sort the call order of plugins.""" self.plugin_dialog = QtPluginDialog(self._qt_window) self.plugin_dialog.exec_() def _show_plugin_err_reporter(self): """Show dialog that allows users to review and report plugin errors.""" QtPluginErrReporter(parent=self._qt_window).exec_() def _add_help_menu(self): """Add 'Help' menu to app menubar.""" self.help_menu = self.main_menu.addMenu(trans._('&Help')) about_action = QAction(trans._("napari Info"), self._qt_window) about_action.setShortcut("Ctrl+/") about_action.setStatusTip(trans._('About napari')) about_action.triggered.connect( lambda e: QtAbout.showAbout(self.qt_viewer, self._qt_window)) self.help_menu.addAction(about_action) about_key_bindings = QAction(trans._("Show Key Bindings"), self._qt_window) about_key_bindings.setShortcut("Ctrl+Alt+/") about_key_bindings.setShortcutContext(Qt.ApplicationShortcut) about_key_bindings.setStatusTip(trans._('key_bindings')) about_key_bindings.triggered.connect( self.qt_viewer.show_key_bindings_dialog) self.help_menu.addAction(about_key_bindings) def _toggle_scale_bar_visible(self, state): self.qt_viewer.viewer.scale_bar.visible = state def _toggle_scale_bar_colored(self, state): self.qt_viewer.viewer.scale_bar.colored = state def _toggle_scale_bar_ticks(self, state): self.qt_viewer.viewer.scale_bar.ticks = state def _toggle_axes_visible(self, state): self.qt_viewer.viewer.axes.visible = state def _toggle_axes_colored(self, state): self.qt_viewer.viewer.axes.colored = state def _toggle_axes_labels(self, state): self.qt_viewer.viewer.axes.labels = state def _toggle_axes_dashed(self, state): self.qt_viewer.viewer.axes.dashed = state def _toggle_axes_arrows(self, state): self.qt_viewer.viewer.axes.arrows = state def _toggle_fullscreen(self, event): """Toggle fullscreen mode.""" if self._qt_window.isFullScreen(): self._qt_window.showNormal() else: self._qt_window.showFullScreen() def _toggle_play(self, state): """Toggle play.""" if self.qt_viewer.dims.is_playing: self.qt_viewer.dims.stop() else: axis = self.qt_viewer.viewer.dims.last_used or 0 self.qt_viewer.dims.play(axis) def add_plugin_dock_widget( self, plugin_name: str, widget_name: str = None) -> Tuple[QtViewerDockWidget, Any]: """Add plugin dock widget if not already added. Parameters ---------- plugin_name : str Name of a plugin providing a widget widget_name : str, optional Name of a widget provided by `plugin_name`. If `None`, and the specified plugin provides only a single widget, that widget will be returned, otherwise a ValueError will be raised, by default None Returns ------- tuple A 2-tuple containing (the DockWidget instance, the plugin widget instance). """ from ..viewer import Viewer Widget, dock_kwargs = plugin_manager.get_widget( plugin_name, widget_name) if not widget_name: # if widget_name wasn't provided, `get_widget` will have # ensured that there is a single widget available. widget_name = list(plugin_manager._dock_widgets[plugin_name])[0] full_name = plugin_menu_item_template.format(plugin_name, widget_name) if full_name in self._dock_widgets: dock_widget = self._dock_widgets[full_name] dock_widget.show() wdg = dock_widget.widget() if hasattr(wdg, '_magic_widget'): wdg = wdg._magic_widget return dock_widget, wdg # if the signature is looking a for a napari viewer, pass it. kwargs = {} for param in inspect.signature(Widget.__init__).parameters.values(): if param.name == 'napari_viewer': kwargs['napari_viewer'] = self.qt_viewer.viewer break if param.annotation in ('napari.viewer.Viewer', Viewer): kwargs[param.name] = self.qt_viewer.viewer break # cannot look for param.kind == param.VAR_KEYWORD because # QWidget allows **kwargs but errs on unknown keyword arguments # instantiate the widget wdg = Widget(**kwargs) # Add dock widget dock_kwargs.pop('name', None) dock_widget = self.add_dock_widget(wdg, name=full_name, **dock_kwargs) return dock_widget, wdg def _add_plugin_function_widget(self, plugin_name: str, widget_name: str): """Add plugin function widget if not already added. Parameters ---------- plugin_name : str Name of a plugin providing a widget widget_name : str, optional Name of a widget provided by `plugin_name`. If `None`, and the specified plugin provides only a single widget, that widget will be returned, otherwise a ValueError will be raised, by default None """ full_name = plugin_menu_item_template.format(plugin_name, widget_name) if full_name in self._dock_widgets: self._dock_widgets[full_name].show() return func = plugin_manager._function_widgets[plugin_name][widget_name] # Add function widget self.add_function_widget(func, name=full_name, area=None, allowed_areas=None) def add_dock_widget( self, widget: QWidget, *, name: str = '', area: str = 'right', allowed_areas: Optional[Sequence[str]] = None, shortcut=_sentinel, add_vertical_stretch=True, ): """Convenience method to add a QDockWidget to the main window. If name is not provided a generic name will be addded to avoid `saveState` warnings on close. Parameters ---------- widget : QWidget `widget` will be added as QDockWidget's main widget. name : str, optional Name of dock widget to appear in window menu. area : str Side of the main window to which the new dock widget will be added. Must be in {'left', 'right', 'top', 'bottom'} allowed_areas : list[str], optional Areas, relative to main window, that the widget is allowed dock. Each item in list must be in {'left', 'right', 'top', 'bottom'} By default, all areas are allowed. shortcut : str, optional Keyboard shortcut to appear in dropdown menu. add_vertical_stretch : bool, optional Whether to add stretch to the bottom of vertical widgets (pushing widgets up towards the top of the allotted area, instead of letting them distribute across the vertical space). By default, True. .. deprecated:: 0.4.8 The shortcut parameter is deprecated since version 0.4.8, please use the action and shortcut manager APIs. The new action manager and shortcut API allow user configuration and localisation. Returns ------- dock_widget : QtViewerDockWidget `dock_widget` that can pass viewer events. """ if not name: try: name = widget.objectName() except AttributeError: name = trans._( "Dock widget {number}", number=self._unnamed_dockwidget_count, ) self._unnamed_dockwidget_count += 1 if shortcut is not _sentinel: warnings.warn( _SHORTCUT_DEPRECATION_STRING.format(shortcut=shortcut), FutureWarning, stacklevel=2, ) dock_widget = QtViewerDockWidget( self.qt_viewer, widget, name=name, area=area, allowed_areas=allowed_areas, shortcut=shortcut, add_vertical_stretch=add_vertical_stretch, ) else: dock_widget = QtViewerDockWidget( self.qt_viewer, widget, name=name, area=area, allowed_areas=allowed_areas, add_vertical_stretch=add_vertical_stretch, ) self._add_viewer_dock_widget(dock_widget) if hasattr(widget, 'reset_choices'): # Keep the dropdown menus in the widget in sync with the layer model # if widget has a `reset_choices`, which is true for all magicgui # `CategoricalWidget`s layers_events = self.qt_viewer.viewer.layers.events layers_events.inserted.connect(widget.reset_choices) layers_events.removed.connect(widget.reset_choices) layers_events.reordered.connect(widget.reset_choices) # Add dock widget to dictionary self._dock_widgets[dock_widget.name] = dock_widget return dock_widget def _add_viewer_dock_widget(self, dock_widget: QtViewerDockWidget, tabify=False): """Add a QtViewerDockWidget to the main window If other widgets already present in area then will tabify. Parameters ---------- dock_widget : QtViewerDockWidget `dock_widget` will be added to the main window. tabify : bool Flag to tabify dockwidget or not. """ # Find if any othe dock widgets are currently in area current_dws_in_area = [ dw for dw in self._qt_window.findChildren(QDockWidget) if self._qt_window.dockWidgetArea(dw) == dock_widget.qt_area ] self._qt_window.addDockWidget(dock_widget.qt_area, dock_widget) # If another dock widget present in area then tabify if current_dws_in_area: if tabify: self._qt_window.tabifyDockWidget(current_dws_in_area[-1], dock_widget) dock_widget.show() dock_widget.raise_() elif dock_widget.area in ('right', 'left'): _wdg = current_dws_in_area + [dock_widget] # add sizes to push lower widgets up sizes = list(range(1, len(_wdg) * 4, 4)) self._qt_window.resizeDocks(_wdg, sizes, Qt.Vertical) action = dock_widget.toggleViewAction() action.setStatusTip(dock_widget.name) action.setText(dock_widget.name) import warnings with warnings.catch_warnings(): warnings.simplefilter("ignore", FutureWarning) # deprecating with 0.4.8, but let's try to keep compatibility. shortcut = dock_widget.shortcut if shortcut is not None: action.setShortcut(shortcut) self.window_menu.addAction(action) def remove_dock_widget(self, widget: QWidget): """Removes specified dock widget. If a QDockWidget is not provided, the existing QDockWidgets will be searched for one whose inner widget (``.widget()``) is the provided ``widget``. Parameters ---------- widget : QWidget | str If widget == 'all', all docked widgets will be removed. """ if widget == 'all': for dw in list(self._dock_widgets.values()): self.remove_dock_widget(dw) return if not isinstance(widget, QDockWidget): for dw in self._qt_window.findChildren(QDockWidget): if dw.widget() is widget: _dw: QDockWidget = dw break else: raise LookupError( trans._( "Could not find a dock widget containing: {widget}", deferred=True, widget=widget, )) else: _dw = widget if _dw.widget(): _dw.widget().setParent(None) self._qt_window.removeDockWidget(_dw) self.window_menu.removeAction(_dw.toggleViewAction()) # Remove dock widget from dictionary del self._dock_widgets[_dw.name] # Deleting the dock widget means any references to it will no longer # work but it's not really useful anyway, since the inner widget has # been removed. and anyway: people should be using add_dock_widget # rather than directly using _add_viewer_dock_widget _dw.deleteLater() def add_function_widget( self, function, *, magic_kwargs=None, name: str = '', area=None, allowed_areas=None, shortcut=_sentinel, ): """Turn a function into a dock widget via magicgui. Parameters ---------- function : callable Function that you want to add. magic_kwargs : dict, optional Keyword arguments to :func:`magicgui.magicgui` that can be used to specify widget. name : str, optional Name of dock widget to appear in window menu. area : str, optional Side of the main window to which the new dock widget will be added. Must be in {'left', 'right', 'top', 'bottom'}. If not provided the default will be determined by the widget.layout, with 'vertical' layouts appearing on the right, otherwise on the bottom. allowed_areas : list[str], optional Areas, relative to main window, that the widget is allowed dock. Each item in list must be in {'left', 'right', 'top', 'bottom'} By default, only provided areas is allowed. shortcut : str, optional Keyboard shortcut to appear in dropdown menu. Returns ------- dock_widget : QtViewerDockWidget `dock_widget` that can pass viewer events. """ from magicgui import magicgui if magic_kwargs is None: magic_kwargs = { 'auto_call': False, 'call_button': "run", 'layout': 'vertical', } widget = magicgui(function, **magic_kwargs or {}) if area is None: if str(widget.layout) == 'vertical': area = 'right' else: area = 'bottom' if allowed_areas is None: allowed_areas = [area] if shortcut is not _sentinel: return self.add_dock_widget( widget, name=name or function.__name__.replace('_', ' '), area=area, allowed_areas=allowed_areas, shortcut=shortcut, ) else: return self.add_dock_widget( widget, name=name or function.__name__.replace('_', ' '), area=area, allowed_areas=allowed_areas, ) def resize(self, width, height): """Resize the window. Parameters ---------- width : int Width in logical pixels. height : int Height in logical pixels. """ self._qt_window.resize(width, height) def show(self): """Resize, show, and bring forward the window. Raises ------ RuntimeError If the viewer.window has already been closed and deleted. """ try: self._qt_window.show() except (AttributeError, RuntimeError): raise RuntimeError( trans._( "This viewer has already been closed and deleted. Please create a new one.", deferred=True, )) if SETTINGS.application.first_time: SETTINGS.application.first_time = False try: self._qt_window.resize(self._qt_window.layout().sizeHint()) except (AttributeError, RuntimeError): raise RuntimeError( trans._( "This viewer has already been closed and deleted. Please create a new one.", deferred=True, )) else: try: if SETTINGS.application.save_window_geometry: self._qt_window._set_window_settings( *self._qt_window._load_window_settings()) except Exception as err: import warnings warnings.warn( trans._( "The window geometry settings could not be loaded due to the following error: {err}", deferred=True, err=err, ), category=RuntimeWarning, stacklevel=2, ) # Resize axis labels now that window is shown self.qt_viewer.dims._resize_axis_labels() # We want to bring the viewer to the front when # A) it is our own event loop OR we are running in jupyter # B) it is not the first time a QMainWindow is being created # `app_name` will be "napari" iff the application was instantiated in # get_app(). isActiveWindow() will be True if it is the second time a # _qt_window has been created. # See #721, #732, #735, #795, #1594 app_name = QApplication.instance().applicationName() if (app_name == 'napari' or in_jupyter()) and self._qt_window.isActiveWindow(): self.activate() def activate(self): """Make the viewer the currently active window.""" self._qt_window.raise_() # for macOS self._qt_window.activateWindow() # for Windows def _update_theme(self, event=None): """Update widget color theme.""" if event: value = event.value SETTINGS.appearance.theme = value self.qt_viewer.viewer.theme = value else: value = self.qt_viewer.viewer.theme try: self._qt_window.setStyleSheet(get_stylesheet(value)) except AttributeError: pass except RuntimeError: # wrapped C/C++ object may have been deleted pass def _status_changed(self, event): """Update status bar. Parameters ---------- event : napari.utils.event.Event The napari event that triggered this method. """ self._status_bar.showMessage(event.value) def _title_changed(self, event): """Update window title. Parameters ---------- event : napari.utils.event.Event The napari event that triggered this method. """ self._qt_window.setWindowTitle(event.value) def _help_changed(self, event): """Update help message on status bar. Parameters ---------- event : napari.utils.event.Event The napari event that triggered this method. """ self._help.setText(event.value) def _screenshot_dialog(self): """Save screenshot of current display with viewer, default .png""" hist = get_save_history() dial = ScreenshotDialog(self.screenshot, self.qt_viewer, hist[0], hist) if dial.exec_(): update_save_history(dial.selectedFiles()[0]) def _restart(self): """Restart the napari application.""" self._qt_window.restart() def screenshot(self, path=None): """Take currently displayed viewer and convert to an image array. Parameters ---------- 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._qt_window.grab().toImage() if path is not None: imsave(path, QImg2array(img)) # scikit-image imsave method return QImg2array(img) def close(self): """Close the viewer window and cleanup sub-widgets.""" # Someone is closing us twice? Only try to delete self._qt_window # if we still have one. if hasattr(self, '_qt_window'): self.qt_viewer.close() self._qt_window.close() del self._qt_window
class Window: """Application window that contains the menu bar and viewer. Parameters ---------- qt_viewer : QtViewer Contained viewer widget. Attributes ---------- file_menu : qtpy.QtWidgets.QMenu File menu. help_menu : qtpy.QtWidgets.QMenu Help menu. main_menu : qtpy.QtWidgets.QMainWindow.menuBar Main menubar. qt_viewer : QtViewer Contained viewer widget. view_menu : qtpy.QtWidgets.QMenu View menu. window_menu : qtpy.QtWidgets.QMenu Window menu. """ raw_stylesheet = get_stylesheet() def __init__(self, qt_viewer: QtViewer, *, show: bool = True): self.qt_viewer = qt_viewer self._qt_window = QMainWindow() self._qt_window.setAttribute(Qt.WA_DeleteOnClose) self._qt_window.setUnifiedTitleAndToolBarOnMac(True) self._qt_center = QWidget(self._qt_window) self._qt_window.setCentralWidget(self._qt_center) self._qt_window.setWindowTitle(self.qt_viewer.viewer.title) self._qt_center.setLayout(QHBoxLayout()) self._status_bar = QStatusBar() self._qt_window.setStatusBar(self._status_bar) self._add_menubar() self._add_file_menu() self._add_view_menu() self._add_window_menu() self._add_plugins_menu() self._add_help_menu() self._status_bar.showMessage('Ready') self._help = QLabel('') self._status_bar.addPermanentWidget(self._help) self._qt_center.layout().addWidget(self.qt_viewer) self._qt_center.layout().setContentsMargins(4, 0, 4, 0) self._update_palette() self._add_viewer_dock_widget(self.qt_viewer.dockConsole) self._add_viewer_dock_widget(self.qt_viewer.dockLayerControls) self._add_viewer_dock_widget(self.qt_viewer.dockLayerList) self.qt_viewer.viewer.events.status.connect(self._status_changed) self.qt_viewer.viewer.events.help.connect(self._help_changed) self.qt_viewer.viewer.events.title.connect(self._title_changed) self.qt_viewer.viewer.events.palette.connect(self._update_palette) if perf.USE_PERFMON: # Add DebugMenu if using perfmon. The DebugMenu is intended to # contain non-perfmon stuff as well. When it does we will want # a separate env variable for it. self._debug_menu = DebugMenu(self) # The QtPerformance widget only exists if we are using perfmon. self._add_viewer_dock_widget(self.qt_viewer.dockPerformance) else: self._debug_menu = None if show: self.show() def _add_menubar(self): """Add menubar to napari app.""" self.main_menu = self._qt_window.menuBar() # Menubar shortcuts are only active when the menubar is visible. # Therefore, we set a global shortcut not associated with the menubar # to toggle visibility, *but*, in order to not shadow the menubar # shortcut, we disable it, and only enable it when the menubar is # hidden. See this stackoverflow link for details: # https://stackoverflow.com/questions/50537642/how-to-keep-the-shortcuts-of-a-hidden-widget-in-pyqt5 self._main_menu_shortcut = QShortcut(QKeySequence('Ctrl+M'), self._qt_window) self._main_menu_shortcut.activated.connect( self._toggle_menubar_visible) self._main_menu_shortcut.setEnabled(False) def _toggle_menubar_visible(self): """Toggle visibility of app menubar. This function also disables or enables a global keyboard shortcut to show the menubar, since menubar shortcuts are only available while the menubar is visible. """ if self.main_menu.isVisible(): self.main_menu.setVisible(False) self._main_menu_shortcut.setEnabled(True) else: self.main_menu.setVisible(True) self._main_menu_shortcut.setEnabled(False) def _add_file_menu(self): """Add 'File' menu to app menubar.""" open_images = QAction('Open File(s)...', self._qt_window) open_images.setShortcut('Ctrl+O') open_images.setStatusTip('Open file(s)') open_images.triggered.connect(self.qt_viewer._open_files_dialog) open_stack = QAction('Open Files as Stack...', self._qt_window) open_stack.setShortcut('Ctrl+Alt+O') open_stack.setStatusTip('Open files') open_stack.triggered.connect( self.qt_viewer._open_files_dialog_as_stack_dialog) open_folder = QAction('Open Folder...', self._qt_window) open_folder.setShortcut('Ctrl+Shift+O') open_folder.setStatusTip('Open a folder') open_folder.triggered.connect(self.qt_viewer._open_folder_dialog) save_selected_layers = QAction('Save Selected Layer(s)...', self._qt_window) save_selected_layers.setShortcut('Ctrl+S') save_selected_layers.setStatusTip('Save selected layers') save_selected_layers.triggered.connect( lambda: self.qt_viewer._save_layers_dialog(selected=True)) save_all_layers = QAction('Save All Layers...', self._qt_window) save_all_layers.setShortcut('Ctrl+Shift+S') save_all_layers.setStatusTip('Save all layers') save_all_layers.triggered.connect( lambda: self.qt_viewer._save_layers_dialog(selected=False)) screenshot = QAction('Save Screenshot...', self._qt_window) screenshot.setShortcut('Alt+S') screenshot.setStatusTip( 'Save screenshot of current display, default .png') screenshot.triggered.connect(self.qt_viewer._screenshot_dialog) screenshot_wv = QAction('Save Screenshot with Viewer...', self._qt_window) screenshot_wv.setShortcut('Alt+Shift+S') screenshot_wv.setStatusTip( 'Save screenshot of current display with the viewer, default .png') screenshot_wv.triggered.connect(self._screenshot_dialog) # OS X will rename this to Quit and put it in the app menu. exitAction = QAction('Exit', self._qt_window) exitAction.setShortcut('Ctrl+Q') exitAction.setMenuRole(QAction.QuitRole) def handle_exit(): # if the event loop was started in gui_qt() then the app will be # named 'napari'. Since the Qapp was started by us, just close it. if QApplication.applicationName() == 'napari': QApplication.closeAllWindows() QApplication.quit() # otherwise, something else created the QApp before us (such as # %gui qt IPython magic). If we quit the app in this case, then # *later* attempts to instantiate a napari viewer won't work until # the event loop is restarted with app.exec_(). So rather than # quit just close all the windows (and clear our app icon). else: QApplication.setWindowIcon(QIcon()) self.close() if perf.USE_PERFMON: # Write trace file before exit, if we were writing one. # Is there a better place to make sure this is done on exit? perf.timers.stop_trace_file() exitAction.triggered.connect(handle_exit) self.file_menu = self.main_menu.addMenu('&File') self.file_menu.addAction(open_images) self.file_menu.addAction(open_stack) self.file_menu.addAction(open_folder) self.file_menu.addSeparator() self.file_menu.addAction(save_selected_layers) self.file_menu.addAction(save_all_layers) self.file_menu.addAction(screenshot) self.file_menu.addAction(screenshot_wv) self.file_menu.addSeparator() self.file_menu.addAction(exitAction) def _add_view_menu(self): """Add 'View' menu to app menubar.""" toggle_visible = QAction('Toggle Menubar Visibility', self._qt_window) toggle_visible.setShortcut('Ctrl+M') toggle_visible.setStatusTip('Hide Menubar') toggle_visible.triggered.connect(self._toggle_menubar_visible) toggle_theme = QAction('Toggle Theme', self._qt_window) toggle_theme.setShortcut('Ctrl+Shift+T') toggle_theme.setStatusTip('Toggle theme') toggle_theme.triggered.connect(self.qt_viewer.viewer._toggle_theme) self.view_menu = self.main_menu.addMenu('&View') self.view_menu.addAction(toggle_visible) self.view_menu.addAction(toggle_theme) def _add_window_menu(self): """Add 'Window' menu to app menubar.""" exit_action = QAction("Close Window", self._qt_window) exit_action.setShortcut("Ctrl+W") exit_action.setStatusTip('Close napari window') exit_action.triggered.connect(self._qt_window.close) self.window_menu = self.main_menu.addMenu('&Window') self.window_menu.addAction(exit_action) def _add_plugins_menu(self): """Add 'Plugins' menu to app menubar.""" self.plugins_menu = self.main_menu.addMenu('&Plugins') list_plugins_action = QAction("List Installed Plugins...", self._qt_window) list_plugins_action.setStatusTip('List installed plugins') list_plugins_action.triggered.connect(self._show_plugin_list) self.plugins_menu.addAction(list_plugins_action) order_plugin_action = QAction("Plugin Call Order...", self._qt_window) order_plugin_action.setStatusTip('Change call order for plugins') order_plugin_action.triggered.connect(self._show_plugin_sorter) self.plugins_menu.addAction(order_plugin_action) report_plugin_action = QAction("Plugin Errors...", self._qt_window) report_plugin_action.setStatusTip( 'Review stack traces for plugin exceptions and notify developers') report_plugin_action.triggered.connect(self._show_plugin_err_reporter) self.plugins_menu.addAction(report_plugin_action) def _show_plugin_list(self, plugin_manager=None): """Show dialog with a table of installed plugins and metadata.""" QtPluginTable(self._qt_window).exec_() def _show_plugin_sorter(self): """Show dialog that allows users to sort the call order of plugins.""" plugin_sorter = QtPluginSorter(parent=self._qt_window) dock_widget = self.add_dock_widget(plugin_sorter, name='Plugin Sorter', area="right") plugin_sorter.finished.connect(dock_widget.close) plugin_sorter.finished.connect(plugin_sorter.deleteLater) plugin_sorter.finished.connect(dock_widget.deleteLater) def _show_plugin_err_reporter(self): """Show dialog that allows users to review and report plugin errors.""" plugin_sorter = QtPluginErrReporter(parent=self._qt_window) plugin_sorter.exec_() def _add_help_menu(self): """Add 'Help' menu to app menubar.""" self.help_menu = self.main_menu.addMenu('&Help') about_action = QAction("napari Info", self._qt_window) about_action.setShortcut("Ctrl+/") about_action.setStatusTip('About napari') about_action.triggered.connect( lambda e: QtAbout.showAbout(self.qt_viewer)) self.help_menu.addAction(about_action) about_key_bindings = QAction("Show Key Bindings", self._qt_window) about_key_bindings.setShortcut("Ctrl+Alt+/") about_key_bindings.setShortcutContext(Qt.ApplicationShortcut) about_key_bindings.setStatusTip('key_bindings') about_key_bindings.triggered.connect( self.qt_viewer.show_key_bindings_dialog) self.help_menu.addAction(about_key_bindings) def add_dock_widget( self, widget: QWidget, *, name: str = '', area: str = 'bottom', allowed_areas=None, shortcut=None, ): """Convenience method to add a QDockWidget to the main window Parameters ---------- widget : QWidget `widget` will be added as QDockWidget's main widget. name : str, optional Name of dock widget to appear in window menu. area : str Side of the main window to which the new dock widget will be added. Must be in {'left', 'right', 'top', 'bottom'} allowed_areas : list[str], optional Areas, relative to main window, that the widget is allowed dock. Each item in list must be in {'left', 'right', 'top', 'bottom'} By default, all areas are allowed. shortcut : str, optional Keyboard shortcut to appear in dropdown menu. Returns ------- dock_widget : QtViewerDockWidget `dock_widget` that can pass viewer events. """ dock_widget = QtViewerDockWidget( self.qt_viewer, widget, name=name, area=area, allowed_areas=allowed_areas, shortcut=shortcut, ) self._add_viewer_dock_widget(dock_widget) return dock_widget def _add_viewer_dock_widget(self, dock_widget: QtViewerDockWidget): """Add a QtViewerDockWidget to the main window Parameters ---------- dock_widget : QtViewerDockWidget `dock_widget` will be added to the main window. """ dock_widget.setParent(self._qt_window) self._qt_window.addDockWidget(dock_widget.qt_area, dock_widget) action = dock_widget.toggleViewAction() action.setStatusTip(dock_widget.name) action.setText(dock_widget.name) if dock_widget.shortcut is not None: action.setShortcut(dock_widget.shortcut) self.window_menu.addAction(action) def remove_dock_widget(self, widget): """Removes specified dock widget. Parameters ---------- widget : QWidget | str If widget == 'all', all docked widgets will be removed. """ if widget == 'all': for dw in self._qt_window.findChildren(QDockWidget): self._qt_window.removeDockWidget(dw) else: self._qt_window.removeDockWidget(widget) def resize(self, width, height): """Resize the window. Parameters ---------- width : int Width in logical pixels. height : int Height in logical pixels. """ self._qt_window.resize(width, height) def show(self): """Resize, show, and bring forward the window.""" self._qt_window.resize(self._qt_window.layout().sizeHint()) self._qt_window.show() # Resize axis labels now that window is shown self.qt_viewer.dims._resize_axis_labels() # We want to call Window._qt_window.raise_() in every case *except* # when instantiating a viewer within a gui_qt() context for the # _first_ time within the Qt app's lifecycle. # # `app_name` will be "napari" iff the application was instantiated in # gui_qt(). isActiveWindow() will be True if it is the second time a # _qt_window has been created. See #732 app_name = QApplication.instance().applicationName() if app_name != 'napari' or self._qt_window.isActiveWindow(): self._qt_window.raise_() # for macOS self._qt_window.activateWindow() # for Windows def _update_palette(self, event=None): """Update widget color palette.""" # set window styles which don't use the primary stylesheet # FIXME: this is a problem with the stylesheet not using properties palette = self.qt_viewer.viewer.palette self._status_bar.setStyleSheet( template( 'QStatusBar { background: {{ background }}; ' 'color: {{ text }}; }', **palette, )) self._qt_center.setStyleSheet( template('QWidget { background: {{ background }}; }', **palette)) self._qt_window.setStyleSheet(template(self.raw_stylesheet, **palette)) def _status_changed(self, event): """Update status bar. Parameters ---------- event : napari.utils.event.Event The napari event that triggered this method. """ self._status_bar.showMessage(event.text) def _title_changed(self, event): """Update window title. Parameters ---------- event : napari.utils.event.Event The napari event that triggered this method. """ self._qt_window.setWindowTitle(event.text) def _help_changed(self, event): """Update help message on status bar. Parameters ---------- event : napari.utils.event.Event The napari event that triggered this method. """ self._help.setText(event.text) def _screenshot_dialog(self): """Save screenshot of current display with viewer, default .png""" filename, _ = QFileDialog.getSaveFileName( parent=self.qt_viewer, caption='Save screenshot with viewer', directory=self.qt_viewer._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 screenshot(self, path=None): """Take currently displayed viewer and convert to an image array. Parameters ---------- 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._qt_window.grab().toImage() if path is not None: imsave(path, QImg2array(img)) # scikit-image imsave method return QImg2array(img) def close(self): """Close the viewer window and cleanup sub-widgets.""" # on some versions of Darwin, exiting while fullscreen seems to tickle # some bug deep in NSWindow. This forces the fullscreen keybinding # test to complete its draw cycle, then pop back out of fullscreen. if self._qt_window.isFullScreen(): self._qt_window.showNormal() for i in range(8): time.sleep(0.1) QApplication.processEvents() self.qt_viewer.close() self._qt_window.close() del self._qt_window
class Window: """Application window that contains the menu bar and viewer. Parameters ---------- viewer : napari.components.ViewerModel Contained viewer widget. Attributes ---------- file_menu : qtpy.QtWidgets.QMenu File menu. help_menu : qtpy.QtWidgets.QMenu Help menu. main_menu : qtpy.QtWidgets.QMainWindow.menuBar Main menubar. qt_viewer : QtViewer Contained viewer widget. view_menu : qtpy.QtWidgets.QMenu View menu. window_menu : qtpy.QtWidgets.QMenu Window menu. """ def __init__(self, viewer: 'Viewer', *, show: bool = True): # create QApplication if it doesn't already exist get_app() # Dictionary holding dock widgets self._dock_widgets: Dict[ str, QtViewerDockWidget ] = WeakValueDictionary() self._unnamed_dockwidget_count = 1 # Connect the Viewer and create the Main Window self._qt_window = _QtMainWindow(viewer) # connect theme events before collecting plugin-provided themes # to ensure icons from the plugins are generated correctly. _themes.events.added.connect(self._add_theme) _themes.events.added.connect(register_napari_themes) _themes.events.removed.connect(self._remove_theme) # discover any themes provided by plugins plugin_manager.discover_themes() self._setup_existing_themes() self._add_menus() self._update_theme() get_settings().appearance.events.theme.connect(self._update_theme) self._add_viewer_dock_widget( self._qt_viewer.dockConsole, tabify=False, menu=self.window_menu ) self._add_viewer_dock_widget( self._qt_viewer.dockLayerControls, tabify=False, menu=self.window_menu, ) self._add_viewer_dock_widget( self._qt_viewer.dockLayerList, tabify=False, menu=self.window_menu ) if perf.USE_PERFMON: self._add_viewer_dock_widget( self._qt_viewer.dockPerformance, menu=self.window_menu ) viewer.events.status.connect(self._status_changed) viewer.events.help.connect(self._help_changed) viewer.events.title.connect(self._title_changed) viewer.events.theme.connect(self._update_theme) viewer.layers.events.connect(self.file_menu.update) if show: self.show() # Ensure the controls dock uses the minimum height self._qt_window.resizeDocks( [ self._qt_viewer.dockLayerControls, self._qt_viewer.dockLayerList, ], [self._qt_viewer.dockLayerControls.minimumHeight(), 10000], Qt.Vertical, ) def _setup_existing_themes(self, connect: bool = True): """This function is only executed once at the startup of napari to connect events to themes that have not been connected yet. Parameters ---------- connect : bool Determines whether the `connect` or `disconnect` method should be used. """ for theme in _themes.values(): if connect: self._connect_theme(theme) else: self._disconnect_theme(theme) def _connect_theme(self, theme): # connect events to update theme. Here, we don't want to pass the event # since it won't have the right `value` attribute. theme.events.background.connect(self._update_theme_no_event) theme.events.foreground.connect(self._update_theme_no_event) theme.events.primary.connect(self._update_theme_no_event) theme.events.secondary.connect(self._update_theme_no_event) theme.events.highlight.connect(self._update_theme_no_event) theme.events.text.connect(self._update_theme_no_event) theme.events.warning.connect(self._update_theme_no_event) theme.events.current.connect(self._update_theme_no_event) theme.events.icon.connect(self._theme_icon_changed) theme.events.canvas.connect( lambda _: self._qt_viewer.canvas._set_theme_change( get_settings().appearance.theme ) ) # connect console-specific attributes only if QtConsole # is present. The `console` is called which might slow # things down a little. if self._qt_viewer._console: theme.events.console.connect(self._qt_viewer.console._update_theme) theme.events.syntax_style.connect( self._qt_viewer.console._update_theme ) def _disconnect_theme(self, theme): theme.events.background.disconnect(self._update_theme_no_event) theme.events.foreground.disconnect(self._update_theme_no_event) theme.events.primary.disconnect(self._update_theme_no_event) theme.events.secondary.disconnect(self._update_theme_no_event) theme.events.highlight.disconnect(self._update_theme_no_event) theme.events.text.disconnect(self._update_theme_no_event) theme.events.warning.disconnect(self._update_theme_no_event) theme.events.current.disconnect(self._update_theme_no_event) theme.events.icon.disconnect(self._theme_icon_changed) theme.events.canvas.disconnect( lambda _: self._qt_viewer.canvas._set_theme_change( get_settings().appearance.theme ) ) # disconnect console-specific attributes only if QtConsole # is present and they were previously connected if self._qt_viewer._console: theme.events.console.disconnect( self._qt_viewer.console._update_theme ) theme.events.syntax_style.disconnect( self._qt_viewer.console._update_theme ) def _add_theme(self, event): """Add new theme and connect events.""" theme = event.value self._connect_theme(theme) def _remove_theme(self, event): """Remove theme and disconnect events.""" theme = event.value self._disconnect_theme(theme) def _theme_icon_changed(self): """Trigger rebuild of theme and all resources. This is really only required whenever there are changes to the `icon` attribute on the `Theme` model. Most other attributes simply update the stylesheet. """ from .._qt.qt_resources import ( _register_napari_resources, _unregister_napari_resources, ) _unregister_napari_resources() _register_napari_resources(True, force_rebuild=True) self._update_theme() @property def qt_viewer(self): warnings.warn( trans._( 'Public access to Window.qt_viewer is deprecated and will be removed in\nv0.5.0. It is considered an "implementation detail" of the napari\napplication, not part of the napari viewer model. If your use case\nrequires access to qt_viewer, please open an issue to discuss.', deferred=True, ), category=FutureWarning, stacklevel=2, ) return self._qt_window._qt_viewer @property def _qt_viewer(self): # this is starting to be "vestigial"... this property could be removed return self._qt_window._qt_viewer @property def _status_bar(self): # TODO: remove from window return self._qt_window.statusBar() def _add_menus(self): """Add menubar to napari app.""" # TODO: move this to _QMainWindow... but then all of the Menu() # items will not have easy access to the methods on this Window obj. self.main_menu = self._qt_window.menuBar() # Menubar shortcuts are only active when the menubar is visible. # Therefore, we set a global shortcut not associated with the menubar # to toggle visibility, *but*, in order to not shadow the menubar # shortcut, we disable it, and only enable it when the menubar is # hidden. See this stackoverflow link for details: # https://stackoverflow.com/questions/50537642/how-to-keep-the-shortcuts-of-a-hidden-widget-in-pyqt5 self._main_menu_shortcut = QShortcut('Ctrl+M', self._qt_window) self._main_menu_shortcut.setEnabled(False) self._main_menu_shortcut.activated.connect( self._toggle_menubar_visible ) self.file_menu = menus.FileMenu(self) self.main_menu.addMenu(self.file_menu) self.view_menu = menus.ViewMenu(self) self.main_menu.addMenu(self.view_menu) self.window_menu = menus.WindowMenu(self) self.main_menu.addMenu(self.window_menu) self.plugins_menu = menus.PluginsMenu(self) self.main_menu.addMenu(self.plugins_menu) self.help_menu = menus.HelpMenu(self) self.main_menu.addMenu(self.help_menu) if perf.USE_PERFMON: self._debug_menu = menus.DebugMenu(self) self.main_menu.addMenu(self._debug_menu) def _toggle_menubar_visible(self): """Toggle visibility of app menubar. This function also disables or enables a global keyboard shortcut to show the menubar, since menubar shortcuts are only available while the menubar is visible. """ self.main_menu.setVisible(not self.main_menu.isVisible()) self._main_menu_shortcut.setEnabled(not self.main_menu.isVisible()) def _tooltip_visibility_toggle(self, value): get_settings().appearance.layer_tooltip_visibility = value def _tooltip_visibility_toggled(self, event): self.tooltip_menu.setChecked( get_settings().appearance.layer_tooltip_visibility ) def _toggle_fullscreen(self, event=None): """Toggle fullscreen mode.""" if self._qt_window.isFullScreen(): self._qt_window.showNormal() else: self._qt_window.showFullScreen() def _toggle_play(self, state=None): """Toggle play.""" if self._qt_viewer.dims.is_playing: self._qt_viewer.dims.stop() else: axis = self._qt_viewer.viewer.dims.last_used or 0 self._qt_viewer.dims.play(axis) def add_plugin_dock_widget( self, plugin_name: str, widget_name: str = None ) -> Tuple[QtViewerDockWidget, Any]: """Add plugin dock widget if not already added. Parameters ---------- plugin_name : str Name of a plugin providing a widget widget_name : str, optional Name of a widget provided by `plugin_name`. If `None`, and the specified plugin provides only a single widget, that widget will be returned, otherwise a ValueError will be raised, by default None Returns ------- tuple A 2-tuple containing (the DockWidget instance, the plugin widget instance). """ from ..plugins import _npe2 Widget = None dock_kwargs = {} result = _npe2.get_widget_contribution(plugin_name, widget_name) if result: Widget, widget_name = result if Widget is None: Widget, dock_kwargs = plugin_manager.get_widget( plugin_name, widget_name ) if not widget_name: # if widget_name wasn't provided, `get_widget` will have # ensured that there is a single widget available. widget_name = list(plugin_manager._dock_widgets[plugin_name])[0] full_name = plugin_menu_item_template.format(plugin_name, widget_name) if full_name in self._dock_widgets: dock_widget = self._dock_widgets[full_name] wdg = dock_widget.widget() if hasattr(wdg, '_magic_widget'): wdg = wdg._magic_widget return dock_widget, wdg wdg = _instantiate_dock_widget(Widget, self._qt_viewer.viewer) # Add dock widget dock_kwargs.pop('name', None) dock_widget = self.add_dock_widget(wdg, name=full_name, **dock_kwargs) return dock_widget, wdg def _add_plugin_function_widget(self, plugin_name: str, widget_name: str): """Add plugin function widget if not already added. Parameters ---------- plugin_name : str Name of a plugin providing a widget widget_name : str, optional Name of a widget provided by `plugin_name`. If `None`, and the specified plugin provides only a single widget, that widget will be returned, otherwise a ValueError will be raised, by default None """ full_name = plugin_menu_item_template.format(plugin_name, widget_name) if full_name in self._dock_widgets: return func = plugin_manager._function_widgets[plugin_name][widget_name] # Add function widget return self.add_function_widget( func, name=full_name, area=None, allowed_areas=None ) def add_dock_widget( self, widget: QWidget, *, name: str = '', area: str = 'right', allowed_areas: Optional[Sequence[str]] = None, shortcut=_sentinel, add_vertical_stretch=True, menu=None, ): """Convenience method to add a QDockWidget to the main window. If name is not provided a generic name will be addded to avoid `saveState` warnings on close. Parameters ---------- widget : QWidget `widget` will be added as QDockWidget's main widget. name : str, optional Name of dock widget to appear in window menu. area : str Side of the main window to which the new dock widget will be added. Must be in {'left', 'right', 'top', 'bottom'} allowed_areas : list[str], optional Areas, relative to main window, that the widget is allowed dock. Each item in list must be in {'left', 'right', 'top', 'bottom'} By default, all areas are allowed. shortcut : str, optional Keyboard shortcut to appear in dropdown menu. add_vertical_stretch : bool, optional Whether to add stretch to the bottom of vertical widgets (pushing widgets up towards the top of the allotted area, instead of letting them distribute across the vertical space). By default, True. .. deprecated:: 0.4.8 The shortcut parameter is deprecated since version 0.4.8, please use the action and shortcut manager APIs. The new action manager and shortcut API allow user configuration and localisation. Returns ------- dock_widget : QtViewerDockWidget `dock_widget` that can pass viewer events. """ if not name: try: name = widget.objectName() except AttributeError: pass name = name or trans._( "Dock widget {number}", number=self._unnamed_dockwidget_count, ) self._unnamed_dockwidget_count += 1 if shortcut is not _sentinel: warnings.warn( _SHORTCUT_DEPRECATION_STRING.format(shortcut=shortcut), FutureWarning, stacklevel=2, ) dock_widget = QtViewerDockWidget( self._qt_viewer, widget, name=name, area=area, allowed_areas=allowed_areas, shortcut=shortcut, add_vertical_stretch=add_vertical_stretch, ) else: dock_widget = QtViewerDockWidget( self._qt_viewer, widget, name=name, area=area, allowed_areas=allowed_areas, add_vertical_stretch=add_vertical_stretch, ) self._add_viewer_dock_widget(dock_widget, menu=menu) if hasattr(widget, 'reset_choices'): # Keep the dropdown menus in the widget in sync with the layer model # if widget has a `reset_choices`, which is true for all magicgui # `CategoricalWidget`s layers_events = self._qt_viewer.viewer.layers.events layers_events.inserted.connect(widget.reset_choices) layers_events.removed.connect(widget.reset_choices) layers_events.reordered.connect(widget.reset_choices) # Add dock widget to dictionary self._dock_widgets[dock_widget.name] = dock_widget return dock_widget def _add_viewer_dock_widget( self, dock_widget: QtViewerDockWidget, tabify=False, menu=None ): """Add a QtViewerDockWidget to the main window If other widgets already present in area then will tabify. Parameters ---------- dock_widget : QtViewerDockWidget `dock_widget` will be added to the main window. tabify : bool Flag to tabify dockwidget or not. """ # Find if any othe dock widgets are currently in area current_dws_in_area = [ dw for dw in self._qt_window.findChildren(QDockWidget) if self._qt_window.dockWidgetArea(dw) == dock_widget.qt_area ] self._qt_window.addDockWidget(dock_widget.qt_area, dock_widget) # If another dock widget present in area then tabify if current_dws_in_area: if tabify: self._qt_window.tabifyDockWidget( current_dws_in_area[-1], dock_widget ) dock_widget.show() dock_widget.raise_() elif dock_widget.area in ('right', 'left'): _wdg = current_dws_in_area + [dock_widget] # add sizes to push lower widgets up sizes = list(range(1, len(_wdg) * 4, 4)) self._qt_window.resizeDocks(_wdg, sizes, Qt.Vertical) if menu: action = dock_widget.toggleViewAction() action.setStatusTip(dock_widget.name) action.setText(dock_widget.name) import warnings with warnings.catch_warnings(): warnings.simplefilter("ignore", FutureWarning) # deprecating with 0.4.8, but let's try to keep compatibility. shortcut = dock_widget.shortcut if shortcut is not None: action.setShortcut(shortcut) menu.addAction(action) # self.window_menu.addAction(action) # see #3663, to fix #3624 more generally dock_widget.setFloating(False) def _remove_dock_widget(self, event=None): names = list(self._dock_widgets.keys()) for widget_name in names: if event.value in widget_name: # remove this widget widget = self._dock_widgets[widget_name] self.remove_dock_widget(widget) def remove_dock_widget(self, widget: QWidget, menu=None): """Removes specified dock widget. If a QDockWidget is not provided, the existing QDockWidgets will be searched for one whose inner widget (``.widget()``) is the provided ``widget``. Parameters ---------- widget : QWidget | str If widget == 'all', all docked widgets will be removed. """ if widget == 'all': for dw in list(self._dock_widgets.values()): self.remove_dock_widget(dw) return if not isinstance(widget, QDockWidget): for dw in self._qt_window.findChildren(QDockWidget): if dw.widget() is widget: _dw: QDockWidget = dw break else: raise LookupError( trans._( "Could not find a dock widget containing: {widget}", deferred=True, widget=widget, ) ) else: _dw = widget if _dw.widget(): _dw.widget().setParent(None) self._qt_window.removeDockWidget(_dw) if menu is not None: menu.removeAction(_dw.toggleViewAction()) # Remove dock widget from dictionary self._dock_widgets.pop(_dw.name, None) # Deleting the dock widget means any references to it will no longer # work but it's not really useful anyway, since the inner widget has # been removed. and anyway: people should be using add_dock_widget # rather than directly using _add_viewer_dock_widget _dw.deleteLater() def add_function_widget( self, function, *, magic_kwargs=None, name: str = '', area=None, allowed_areas=None, shortcut=_sentinel, ): """Turn a function into a dock widget via magicgui. Parameters ---------- function : callable Function that you want to add. magic_kwargs : dict, optional Keyword arguments to :func:`magicgui.magicgui` that can be used to specify widget. name : str, optional Name of dock widget to appear in window menu. area : str, optional Side of the main window to which the new dock widget will be added. Must be in {'left', 'right', 'top', 'bottom'}. If not provided the default will be determined by the widget.layout, with 'vertical' layouts appearing on the right, otherwise on the bottom. allowed_areas : list[str], optional Areas, relative to main window, that the widget is allowed dock. Each item in list must be in {'left', 'right', 'top', 'bottom'} By default, only provided areas is allowed. shortcut : str, optional Keyboard shortcut to appear in dropdown menu. Returns ------- dock_widget : QtViewerDockWidget `dock_widget` that can pass viewer events. """ from magicgui import magicgui if magic_kwargs is None: magic_kwargs = { 'auto_call': False, 'call_button': "run", 'layout': 'vertical', } widget = magicgui(function, **magic_kwargs or {}) if area is None: area = 'right' if str(widget.layout) == 'vertical' else 'bottom' if allowed_areas is None: allowed_areas = [area] if shortcut is not _sentinel: return self.add_dock_widget( widget, name=name or function.__name__.replace('_', ' '), area=area, allowed_areas=allowed_areas, shortcut=shortcut, ) else: return self.add_dock_widget( widget, name=name or function.__name__.replace('_', ' '), area=area, allowed_areas=allowed_areas, ) def resize(self, width, height): """Resize the window. Parameters ---------- width : int Width in logical pixels. height : int Height in logical pixels. """ self._qt_window.resize(width, height) def set_geometry(self, left, top, width, height): """Set the geometry of the widget Parameters ---------- left : int X coordinate of the upper left border. top : int Y coordinate of the upper left border. width : int Width of the rectangle shape of the window. height : int Height of the rectangle shape of the window. """ self._qt_window.setGeometry(left, top, width, height) def geometry(self) -> Tuple[int, int, int, int]: """Get the geometry of the widget Returns ------- left : int X coordinate of the upper left border. top : int Y coordinate of the upper left border. width : int Width of the rectangle shape of the window. height : int Height of the rectangle shape of the window. """ rect = self._qt_window.geometry() return rect.left(), rect.top(), rect.width(), rect.height() def show(self, *, block=False): """Resize, show, and bring forward the window. Raises ------ RuntimeError If the viewer.window has already been closed and deleted. """ settings = get_settings() try: self._qt_window.show(block=block) except (AttributeError, RuntimeError): raise RuntimeError( trans._( "This viewer has already been closed and deleted. Please create a new one.", deferred=True, ) ) if settings.application.first_time: settings.application.first_time = False try: self._qt_window.resize(self._qt_window.layout().sizeHint()) except (AttributeError, RuntimeError): raise RuntimeError( trans._( "This viewer has already been closed and deleted. Please create a new one.", deferred=True, ) ) else: try: if settings.application.save_window_geometry: self._qt_window._set_window_settings( *self._qt_window._load_window_settings() ) except Exception as err: import warnings warnings.warn( trans._( "The window geometry settings could not be loaded due to the following error: {err}", deferred=True, err=err, ), category=RuntimeWarning, stacklevel=2, ) # Resize axis labels now that window is shown self._qt_viewer.dims._resize_axis_labels() # We want to bring the viewer to the front when # A) it is our own event loop OR we are running in jupyter # B) it is not the first time a QMainWindow is being created # `app_name` will be "napari" iff the application was instantiated in # get_app(). isActiveWindow() will be True if it is the second time a # _qt_window has been created. # See #721, #732, #735, #795, #1594 app_name = QApplication.instance().applicationName() if ( app_name == 'napari' or in_jupyter() ) and self._qt_window.isActiveWindow(): self.activate() def activate(self): """Make the viewer the currently active window.""" self._qt_window.raise_() # for macOS self._qt_window.activateWindow() # for Windows def _update_theme_no_event(self): self._update_theme() def _update_theme(self, event=None): """Update widget color theme.""" settings = get_settings() try: if event: value = event.value self._qt_viewer.viewer.theme = value settings.appearance.theme = value else: if settings.appearance.theme == "system": value = get_system_theme() else: value = self._qt_viewer.viewer.theme self._qt_window.setStyleSheet(get_stylesheet(value)) except (AttributeError, RuntimeError): # wrapped C/C++ object may have been deleted? pass def _status_changed(self, event): """Update status bar. Parameters ---------- event : napari.utils.event.Event The napari event that triggered this method. """ self._status_bar.showMessage(event.value) def _title_changed(self, event): """Update window title. Parameters ---------- event : napari.utils.event.Event The napari event that triggered this method. """ self._qt_window.setWindowTitle(event.value) def _help_changed(self, event): """Update help message on status bar. Parameters ---------- event : napari.utils.event.Event The napari event that triggered this method. """ self._status_bar.setHelpText(event.value) def _restart(self): """Restart the napari application.""" self._qt_window.restart() def _screenshot( self, size=None, scale=None, flash=True, canvas_only=False ) -> 'QImage': """Capture screenshot of the currently displayed viewer. Parameters ---------- flash : bool Flag to indicate whether flash animation should be shown after the screenshot was captured. size : tuple (int, int) Size (resolution) of the screenshot. By default, the currently displayed size. Only used if `canvas_only` is True. scale : float Scale factor used to increase resolution of canvas for the screenshot. By default, the currently displayed resolution. Only used if `canvas_only` is True. canvas_only : bool If True, screenshot shows only the image display canvas, and if False include the napari viewer frame in the screenshot, By default, True. Returns ------- img : QImage """ from .utils import add_flash_animation if canvas_only: canvas = self._qt_viewer.canvas prev_size = canvas.size if size is not None: if len(size) != 2: raise ValueError( f'screenshot size must be 2 values, got {len(size)}' ) # Scale the requested size to account for HiDPI size = tuple( dim / self._qt_window.devicePixelRatio() for dim in size ) canvas.size = size[::-1] # invert x ad y for vispy if scale is not None: # multiply canvas dimensions by the scale factor to get new size canvas.size = tuple(dim * scale for dim in canvas.size) try: img = self._qt_viewer.canvas.native.grabFramebuffer() if flash: add_flash_animation(self._qt_viewer._canvas_overlay) finally: # make sure we always go back to the right canvas size if size is not None or scale is not None: canvas.size = prev_size else: img = self._qt_window.grab().toImage() if flash: add_flash_animation(self._qt_window) return img def screenshot( self, path=None, size=None, scale=None, flash=True, canvas_only=False ): """Take currently displayed viewer and convert to an image array. Parameters ---------- path : str Filename for saving screenshot image. size : tuple (int, int) Size (resolution) of the screenshot. By default, the currently displayed size. Only used if `canvas_only` is True. scale : float Scale factor used to increase resolution of canvas for the screenshot. By default, the currently displayed resolution. Only used if `canvas_only` is True. flash : bool Flag to indicate whether flash animation should be shown after the screenshot was captured. canvas_only : bool If True, screenshot shows only the image display canvas, and if False include the napari viewer frame in the screenshot, By default, True. 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 = QImg2array(self._screenshot(size, scale, flash, canvas_only)) if path is not None: imsave(path, img) # scikit-image imsave method return img def clipboard(self, flash=True, canvas_only=False): """Copy screenshot of current viewer to the clipboard. Parameters ---------- flash : bool Flag to indicate whether flash animation should be shown after the screenshot was captured. canvas_only : bool If True, screenshot shows only the image display canvas, and if False include the napari viewer frame in the screenshot, By default, True. """ img = self._screenshot(flash=flash, canvas_only=canvas_only) QApplication.clipboard().setImage(img) def _teardown(self): """Carry out various teardown tasks such as event disconnection.""" self._setup_existing_themes(False) _themes.events.added.disconnect(self._add_theme) _themes.events.added.disconnect(register_napari_themes) _themes.events.removed.disconnect(self._remove_theme) self._qt_viewer.viewer.layers.events.disconnect(self.file_menu.update) for menu in self.file_menu._INSTANCES: try: menu._destroy() except RuntimeError: pass def close(self): """Close the viewer window and cleanup sub-widgets.""" # Someone is closing us twice? Only try to delete self._qt_window # if we still have one. if hasattr(self, '_qt_window'): self._teardown() self._qt_viewer.close() self._qt_window.close() del self._qt_window
class Window: """Application window that contains the menu bar and viewer. Parameters ---------- viewer : napari.components.ViewerModel Contained viewer widget. Attributes ---------- file_menu : qtpy.QtWidgets.QMenu File menu. help_menu : qtpy.QtWidgets.QMenu Help menu. main_menu : qtpy.QtWidgets.QMainWindow.menuBar Main menubar. qt_viewer : QtViewer Contained viewer widget. view_menu : qtpy.QtWidgets.QMenu View menu. window_menu : qtpy.QtWidgets.QMenu Window menu. """ raw_stylesheet = get_stylesheet() def __init__(self, viewer, *, show: bool = True): # create QApplication if it doesn't already exist # note: the return value must be retained to prevent garbage collection _ = get_app() # Connect the Viewer and create the Main Window self.qt_viewer = QtViewer(viewer) self._qt_window = _QtMainWindow() self._qt_window.setWindowTitle(self.qt_viewer.viewer.title) self._qt_center = self._qt_window.centralWidget() self._status_bar = self._qt_window.statusBar() # Dictionary holding dock widgets self._dock_widgets: Dict[str, QtViewerDockWidget] = {} self._plugin_menus: Dict[str, QMenu] = {} # since we initialize canvas before window, we need to manually connect them again. if self._qt_window.windowHandle() is not None: self._qt_window.windowHandle().screenChanged.connect( self.qt_viewer.canvas._backend.screen_changed) self._add_menubar() self._add_file_menu() self._add_view_menu() self._add_window_menu() self._add_plugins_menu() self._add_help_menu() self._status_bar.showMessage('Ready') self._help = QLabel('') self._status_bar.addPermanentWidget(self._help) self._qt_center.layout().addWidget(self.qt_viewer) self._qt_center.layout().setContentsMargins(4, 0, 4, 0) self._update_theme() self._add_viewer_dock_widget(self.qt_viewer.dockConsole, tabify=False) self._add_viewer_dock_widget(self.qt_viewer.dockLayerControls, tabify=False) self._add_viewer_dock_widget(self.qt_viewer.dockLayerList, tabify=False) self.window_menu.addSeparator() self.qt_viewer.viewer.events.status.connect(self._status_changed) self.qt_viewer.viewer.events.help.connect(self._help_changed) self.qt_viewer.viewer.events.title.connect(self._title_changed) self.qt_viewer.viewer.events.theme.connect(self._update_theme) if perf.USE_PERFMON: # Add DebugMenu and dockPerformance if using perfmon. self._debug_menu = DebugMenu(self) self._add_viewer_dock_widget(self.qt_viewer.dockPerformance) else: self._debug_menu = None if show: self.show() def _add_menubar(self): """Add menubar to napari app.""" self.main_menu = self._qt_window.menuBar() # Menubar shortcuts are only active when the menubar is visible. # Therefore, we set a global shortcut not associated with the menubar # to toggle visibility, *but*, in order to not shadow the menubar # shortcut, we disable it, and only enable it when the menubar is # hidden. See this stackoverflow link for details: # https://stackoverflow.com/questions/50537642/how-to-keep-the-shortcuts-of-a-hidden-widget-in-pyqt5 self._main_menu_shortcut = QShortcut(QKeySequence('Ctrl+M'), self._qt_window) self._main_menu_shortcut.activated.connect( self._toggle_menubar_visible) self._main_menu_shortcut.setEnabled(False) def _toggle_menubar_visible(self): """Toggle visibility of app menubar. This function also disables or enables a global keyboard shortcut to show the menubar, since menubar shortcuts are only available while the menubar is visible. """ if self.main_menu.isVisible(): self.main_menu.setVisible(False) self._main_menu_shortcut.setEnabled(True) else: self.main_menu.setVisible(True) self._main_menu_shortcut.setEnabled(False) def _add_file_menu(self): """Add 'File' menu to app menubar.""" open_images = QAction('Open File(s)...', self._qt_window) open_images.setShortcut('Ctrl+O') open_images.setStatusTip('Open file(s)') open_images.triggered.connect(self.qt_viewer._open_files_dialog) open_stack = QAction('Open Files as Stack...', self._qt_window) open_stack.setShortcut('Ctrl+Alt+O') open_stack.setStatusTip('Open files') open_stack.triggered.connect( self.qt_viewer._open_files_dialog_as_stack_dialog) open_folder = QAction('Open Folder...', self._qt_window) open_folder.setShortcut('Ctrl+Shift+O') open_folder.setStatusTip('Open a folder') open_folder.triggered.connect(self.qt_viewer._open_folder_dialog) save_selected_layers = QAction('Save Selected Layer(s)...', self._qt_window) save_selected_layers.setShortcut('Ctrl+S') save_selected_layers.setStatusTip('Save selected layers') save_selected_layers.triggered.connect( lambda: self.qt_viewer._save_layers_dialog(selected=True)) save_all_layers = QAction('Save All Layers...', self._qt_window) save_all_layers.setShortcut('Ctrl+Shift+S') save_all_layers.setStatusTip('Save all layers') save_all_layers.triggered.connect( lambda: self.qt_viewer._save_layers_dialog(selected=False)) screenshot = QAction('Save Screenshot...', self._qt_window) screenshot.setShortcut('Alt+S') screenshot.setStatusTip( 'Save screenshot of current display, default .png') screenshot.triggered.connect(self.qt_viewer._screenshot_dialog) screenshot_wv = QAction('Save Screenshot with Viewer...', self._qt_window) screenshot_wv.setShortcut('Alt+Shift+S') screenshot_wv.setStatusTip( 'Save screenshot of current display with the viewer, default .png') screenshot_wv.triggered.connect(self._screenshot_dialog) # OS X will rename this to Quit and put it in the app menu. exitAction = QAction('Exit', self._qt_window) exitAction.setShortcut('Ctrl+Q') exitAction.setMenuRole(QAction.QuitRole) def handle_exit(): # if the event loop was started in gui_qt() then the app will be # named 'napari'. Since the Qapp was started by us, just close it. if QApplication.applicationName() == 'napari': QApplication.closeAllWindows() QApplication.quit() # otherwise, something else created the QApp before us (such as # %gui qt IPython magic). If we quit the app in this case, then # *later* attempts to instantiate a napari viewer won't work until # the event loop is restarted with app.exec_(). So rather than # quit just close all the windows (and clear our app icon). else: QApplication.setWindowIcon(QIcon()) self.close() if perf.USE_PERFMON: # Write trace file before exit, if we were writing one. # Is there a better place to make sure this is done on exit? perf.timers.stop_trace_file() _stop_monitor() _shutdown_chunkloader() exitAction.triggered.connect(handle_exit) self.file_menu = self.main_menu.addMenu('&File') self.file_menu.addAction(open_images) self.file_menu.addAction(open_stack) self.file_menu.addAction(open_folder) self.file_menu.addSeparator() self.file_menu.addAction(save_selected_layers) self.file_menu.addAction(save_all_layers) self.file_menu.addAction(screenshot) self.file_menu.addAction(screenshot_wv) self.file_menu.addSeparator() self.file_menu.addAction(exitAction) def _add_view_menu(self): """Add 'View' menu to app menubar.""" toggle_visible = QAction('Toggle Menubar Visibility', self._qt_window) toggle_visible.setShortcut('Ctrl+M') toggle_visible.setStatusTip('Hide Menubar') toggle_visible.triggered.connect(self._toggle_menubar_visible) toggle_theme = QAction('Toggle Theme', self._qt_window) toggle_theme.setShortcut('Ctrl+Shift+T') toggle_theme.setStatusTip('Toggle theme') toggle_theme.triggered.connect(self.qt_viewer.viewer._toggle_theme) toggle_fullscreen = QAction('Toggle Full Screen', self._qt_window) toggle_fullscreen.setShortcut('Ctrl+F') toggle_fullscreen.setStatusTip('Toggle full screen') toggle_fullscreen.triggered.connect(self._toggle_fullscreen) toggle_play = QAction('Toggle Play', self._qt_window) toggle_play.triggered.connect(self._toggle_play) toggle_play.setShortcut('Ctrl+Alt+P') toggle_play.setStatusTip('Toggle Play') self.view_menu = self.main_menu.addMenu('&View') self.view_menu.addAction(toggle_fullscreen) self.view_menu.addAction(toggle_visible) self.view_menu.addAction(toggle_theme) self.view_menu.addAction(toggle_play) self.view_menu.addSeparator() # Add octree actions. if config.async_octree: toggle_outline = QAction('Toggle Chunk Outlines', self._qt_window) toggle_outline.triggered.connect( self.qt_viewer._toggle_chunk_outlines) toggle_outline.setShortcut('Ctrl+Alt+O') toggle_outline.setStatusTip('Toggle Chunk Outlines') self.view_menu.addAction(toggle_outline) # Add axes menu axes_menu = QMenu('Axes', parent=self._qt_window) axes_visible_action = QAction( 'Visible', parent=self._qt_window, checkable=True, checked=self.qt_viewer.viewer.axes.visible, ) axes_visible_action.triggered.connect(self._toggle_axes_visible) axes_colored_action = QAction( 'Colored', parent=self._qt_window, checkable=True, checked=self.qt_viewer.viewer.axes.colored, ) axes_colored_action.triggered.connect(self._toggle_axes_colored) axes_labels_action = QAction( 'Labels', parent=self._qt_window, checkable=True, checked=self.qt_viewer.viewer.axes.labels, ) axes_labels_action.triggered.connect(self._toggle_axes_labels) axes_dashed_action = QAction( 'Dashed', parent=self._qt_window, checkable=True, checked=self.qt_viewer.viewer.axes.dashed, ) axes_dashed_action.triggered.connect(self._toggle_axes_dashed) axes_arrows_action = QAction( 'Arrows', parent=self._qt_window, checkable=True, checked=self.qt_viewer.viewer.axes.arrows, ) axes_arrows_action.triggered.connect(self._toggle_axes_arrows) axes_menu.addAction(axes_visible_action) axes_menu.addAction(axes_colored_action) axes_menu.addAction(axes_labels_action) axes_menu.addAction(axes_dashed_action) axes_menu.addAction(axes_arrows_action) self.view_menu.addMenu(axes_menu) # Add scale bar menu scale_bar_menu = QMenu('Scale Bar', parent=self._qt_window) scale_bar_visible_action = QAction( 'Visible', parent=self._qt_window, checkable=True, checked=self.qt_viewer.viewer.scale_bar.visible, ) scale_bar_visible_action.triggered.connect( self._toggle_scale_bar_visible) scale_bar_colored_action = QAction( 'Colored', parent=self._qt_window, checkable=True, checked=self.qt_viewer.viewer.scale_bar.colored, ) scale_bar_colored_action.triggered.connect( self._toggle_scale_bar_colored) scale_bar_ticks_action = QAction( 'Ticks', parent=self._qt_window, checkable=True, checked=self.qt_viewer.viewer.scale_bar.ticks, ) scale_bar_ticks_action.triggered.connect(self._toggle_scale_bar_ticks) scale_bar_menu.addAction(scale_bar_visible_action) scale_bar_menu.addAction(scale_bar_colored_action) scale_bar_menu.addAction(scale_bar_ticks_action) self.view_menu.addMenu(scale_bar_menu) self.view_menu.addSeparator() def _add_window_menu(self): """Add 'Window' menu to app menubar.""" exit_action = QAction("Close Window", self._qt_window) exit_action.setShortcut("Ctrl+W") exit_action.setStatusTip('Close napari window') exit_action.triggered.connect(self._qt_window.close) clear_action = QAction("Remove Dock Widgets", self._qt_window) clear_action.setStatusTip('Remove all dock widgets') clear_action.triggered.connect( lambda e: self.remove_dock_widget('all')) self.window_menu = self.main_menu.addMenu('&Window') self.window_menu.addAction(exit_action) self.window_menu.addAction(clear_action) self.window_menu.addSeparator() def _add_plugins_menu(self): """Add 'Plugins' menu to app menubar.""" self.plugins_menu = self.main_menu.addMenu('&Plugins') pip_install_action = QAction("Install/Uninstall Package(s)...", self._qt_window) pip_install_action.triggered.connect(self._show_plugin_install_dialog) self.plugins_menu.addAction(pip_install_action) order_plugin_action = QAction("Plugin Call Order...", self._qt_window) order_plugin_action.setStatusTip('Change call order for plugins') order_plugin_action.triggered.connect(self._show_plugin_sorter) self.plugins_menu.addAction(order_plugin_action) report_plugin_action = QAction("Plugin Errors...", self._qt_window) report_plugin_action.setStatusTip( 'Review stack traces for plugin exceptions and notify developers') report_plugin_action.triggered.connect(self._show_plugin_err_reporter) self.plugins_menu.addAction(report_plugin_action) self._plugin_dock_widget_menu = QMenu('Add Dock Widget', self._qt_window) # Get names of all plugins providing dock widgets or functions plugin_widgets = chain(plugins.dock_widgets, plugins.function_widgets) plugin_counts = Counter(plug_name for plug_name, _ in plugin_widgets) # Add submenu for each plugin with more than 1 item for plugin_name, count in plugin_counts.items(): if count > 1: menu = QMenu(plugin_name, self._qt_window) self._plugin_menus[plugin_name] = menu self._plugin_dock_widget_menu.addMenu(menu) # Add a menu item (QAction) for each available plugin widget docks = zip(repeat("dock"), plugins.dock_widgets) funcs = zip(repeat("func"), plugins.function_widgets) for hook_type, key in chain(docks, funcs): plugin_name, wdg_name = key if plugin_name in self._plugin_menus: # this plugin has a submenu. action = QAction(wdg_name, parent=self._qt_window) self._plugin_menus[plugin_name].addAction(action) else: # this plugin only has one widget, add a namespaced menu item full_name = plugins.menu_item_template.format(*key) action = QAction(full_name, parent=self._qt_window) self._plugin_dock_widget_menu.addAction(action) def _add_widget(*args, key=key, hook_type=hook_type): if hook_type == 'dock': self._add_plugin_dock_widget(key) else: self._add_plugin_function_widget(key) action.triggered.connect(_add_widget) self.plugins_menu.addMenu(self._plugin_dock_widget_menu) def _show_plugin_sorter(self): """Show dialog that allows users to sort the call order of plugins.""" plugin_sorter = QtPluginSorter(parent=self._qt_window) if hasattr(self, 'plugin_sorter_widget'): self.plugin_sorter_widget.show() else: self.plugin_sorter_widget = self.add_dock_widget( plugin_sorter, name='Plugin Sorter', area="right") def _show_plugin_install_dialog(self): """Show dialog that allows users to sort the call order of plugins.""" self.plugin_dialog = QtPluginDialog(self._qt_window) self.plugin_dialog.exec_() def _show_plugin_err_reporter(self): """Show dialog that allows users to review and report plugin errors.""" QtPluginErrReporter(parent=self._qt_window).exec_() def _add_help_menu(self): """Add 'Help' menu to app menubar.""" self.help_menu = self.main_menu.addMenu('&Help') about_action = QAction("napari Info", self._qt_window) about_action.setShortcut("Ctrl+/") about_action.setStatusTip('About napari') about_action.triggered.connect( lambda e: QtAbout.showAbout(self.qt_viewer)) self.help_menu.addAction(about_action) about_key_bindings = QAction("Show Key Bindings", self._qt_window) about_key_bindings.setShortcut("Ctrl+Alt+/") about_key_bindings.setShortcutContext(Qt.ApplicationShortcut) about_key_bindings.setStatusTip('key_bindings') about_key_bindings.triggered.connect( self.qt_viewer.show_key_bindings_dialog) self.help_menu.addAction(about_key_bindings) def _toggle_scale_bar_visible(self, state): self.qt_viewer.viewer.scale_bar.visible = state def _toggle_scale_bar_colored(self, state): self.qt_viewer.viewer.scale_bar.colored = state def _toggle_scale_bar_ticks(self, state): self.qt_viewer.viewer.scale_bar.ticks = state def _toggle_axes_visible(self, state): self.qt_viewer.viewer.axes.visible = state def _toggle_axes_colored(self, state): self.qt_viewer.viewer.axes.colored = state def _toggle_axes_labels(self, state): self.qt_viewer.viewer.axes.labels = state def _toggle_axes_dashed(self, state): self.qt_viewer.viewer.axes.dashed = state def _toggle_axes_arrows(self, state): self.qt_viewer.viewer.axes.arrows = state def _toggle_fullscreen(self, event): """Toggle fullscreen mode.""" if self._qt_window.isFullScreen(): self._qt_window.showNormal() else: self._qt_window.showFullScreen() def _toggle_play(self, state): """Toggle play.""" if self.qt_viewer.dims.is_playing: self.qt_viewer.dims.stop() else: axis = self.qt_viewer.viewer.dims.last_used or 0 self.qt_viewer.dims.play(axis) def _add_plugin_dock_widget(self, key): """Add plugin dock widget if not already added. Parameters ---------- key : 2-tuple of str Plugin name and widget name. """ from ..viewer import Viewer full_name = plugins.menu_item_template.format(*key) if full_name in self._dock_widgets: warnings.warn(f'Dock widget {key!r} already added') return Widget, dock_kwargs = plugins.dock_widgets[key] # if the signature is looking a for a napari viewer, pass it. kwargs = {} for param in inspect.signature(Widget.__init__).parameters.values(): if param.name == 'napari_viewer': kwargs['napari_viewer'] = self.qt_viewer.viewer break if param.annotation in ('napari.viewer.Viewer', Viewer): kwargs[param.name] = self.qt_viewer.viewer break # cannot look for param.kind == param.VAR_KEYWORD because # QWidget allows **kwargs but errs on unknown keyword arguments # instantiate the widget wdg = Widget(**kwargs) # Add dock widget self.add_dock_widget( wdg, name=plugins.menu_item_template.format(*key), area=dock_kwargs.get('area', 'right'), allowed_areas=dock_kwargs.get('allowed_areas', None), ) def _add_plugin_function_widget(self, key): """Add plugin function widget if not already added. Parameters ---------- key : 2-tuple of str Plugin name and function name. """ full_name = plugins.menu_item_template.format(*key) if full_name in self._dock_widgets: warnings.warn(f'Dock widget {key!r} already added') return func, magic_kwargs, dock_kwargs = plugins.function_widgets[key] # Add function widget self.add_function_widget( func, magic_kwargs=magic_kwargs, name=plugins.menu_item_template.format(*key), area=dock_kwargs.get('area', None), allowed_areas=dock_kwargs.get('allowed_areas', None), ) def add_dock_widget( self, widget: QWidget, *, name: str = '', area: str = 'bottom', allowed_areas=None, shortcut=None, ): """Convenience method to add a QDockWidget to the main window Parameters ---------- widget : QWidget `widget` will be added as QDockWidget's main widget. name : str, optional Name of dock widget to appear in window menu. area : str Side of the main window to which the new dock widget will be added. Must be in {'left', 'right', 'top', 'bottom'} allowed_areas : list[str], optional Areas, relative to main window, that the widget is allowed dock. Each item in list must be in {'left', 'right', 'top', 'bottom'} By default, all areas are allowed. shortcut : str, optional Keyboard shortcut to appear in dropdown menu. Returns ------- dock_widget : QtViewerDockWidget `dock_widget` that can pass viewer events. """ dock_widget = QtViewerDockWidget( self.qt_viewer, widget, name=name, area=area, allowed_areas=allowed_areas, shortcut=shortcut, ) self._add_viewer_dock_widget(dock_widget) if hasattr(widget, 'reset_choices'): # Keep the dropdown menus in the widget in sync with the layer model # if widget has a `reset_choices`, which is true for all magicgui # `CategoricalWidget`s layers_events = self.qt_viewer.viewer.layers.events layers_events.inserted.connect(widget.reset_choices) layers_events.removed.connect(widget.reset_choices) layers_events.reordered.connect(widget.reset_choices) # Add dock widget to dictionary self._dock_widgets[dock_widget.name] = dock_widget return dock_widget def _add_viewer_dock_widget(self, dock_widget: QtViewerDockWidget, tabify=False): """Add a QtViewerDockWidget to the main window If other widgets already present in area then will tabify. Parameters ---------- dock_widget : QtViewerDockWidget `dock_widget` will be added to the main window. tabify : bool Flag to tabify dockwidget or not. """ # Find if any othe dock widgets are currently in area current_dws_in_area = [] for dw in self._qt_window.findChildren(QDockWidget): if self._qt_window.dockWidgetArea(dw) == dock_widget.qt_area: current_dws_in_area.append(dw) self._qt_window.addDockWidget(dock_widget.qt_area, dock_widget) # If another dock widget present in area then tabify if len(current_dws_in_area) > 0 and tabify: self._qt_window.tabifyDockWidget(current_dws_in_area[-1], dock_widget) dock_widget.show() dock_widget.raise_() action = dock_widget.toggleViewAction() action.setStatusTip(dock_widget.name) action.setText(dock_widget.name) if dock_widget.shortcut is not None: action.setShortcut(dock_widget.shortcut) self.window_menu.addAction(action) def remove_dock_widget(self, widget: QWidget): """Removes specified dock widget. If a QDockWidget is not provided, the existing QDockWidgets will be searched for one whose inner widget (``.widget()``) is the provided ``widget``. Parameters ---------- widget : QWidget | str If widget == 'all', all docked widgets will be removed. """ if widget == 'all': for dw in list(self._dock_widgets.values()): self.remove_dock_widget(dw) return if not isinstance(widget, QDockWidget): for dw in self._qt_window.findChildren(QDockWidget): if dw.widget() is widget: _dw: QDockWidget = dw break else: raise LookupError( f"Could not find a dock widget containing: {widget}") else: _dw = widget if _dw.widget(): _dw.widget().setParent(None) self._qt_window.removeDockWidget(_dw) self.window_menu.removeAction(_dw.toggleViewAction()) # Remove dock widget from dictionary del self._dock_widgets[_dw.name] # Deleting the dock widget means any references to it will no longer # work but it's not really useful anyway, since the inner widget has # been removed. and anyway: people should be using add_dock_widget # rather than directly using _add_viewer_dock_widget _dw.deleteLater() def add_function_widget( self, function, *, magic_kwargs=None, name: str = '', area=None, allowed_areas=None, shortcut=None, ): """Turn a function into a dock widget via magicgui. Parameters ---------- function : callable Function that you want to add. magic_kwargs : dict, optional Keyword arguments to :func:`magicgui.magicgui` that can be used to specify widget. name : str, optional Name of dock widget to appear in window menu. area : str, optional Side of the main window to which the new dock widget will be added. Must be in {'left', 'right', 'top', 'bottom'}. If not provided the default will be determined by the widget.layout, with 'vertical' layouts appearing on the right, otherwise on the bottom. allowed_areas : list[str], optional Areas, relative to main window, that the widget is allowed dock. Each item in list must be in {'left', 'right', 'top', 'bottom'} By default, only provided areas is allowed. shortcut : str, optional Keyboard shortcut to appear in dropdown menu. Returns ------- dock_widget : QtViewerDockWidget `dock_widget` that can pass viewer events. """ from magicgui import magicgui widget = magicgui(function, **magic_kwargs or {}) if area is None: if str(widget.layout) == 'vertical': area = 'right' else: area = 'bottom' if allowed_areas is None: allowed_areas = [area] return self.add_dock_widget( widget, name=name or function.__name__.replace('_', ' '), area=area, allowed_areas=allowed_areas, shortcut=shortcut, ) def resize(self, width, height): """Resize the window. Parameters ---------- width : int Width in logical pixels. height : int Height in logical pixels. """ self._qt_window.resize(width, height) def show(self): """Resize, show, and bring forward the window.""" self._qt_window.resize(self._qt_window.layout().sizeHint()) self._qt_window.show() # Resize axis labels now that window is shown self.qt_viewer.dims._resize_axis_labels() # We want to bring the viewer to the front when # A) it is our own (gui_qt) event loop OR we are running in jupyter # B) it is not the first time a QMainWindow is being created # `app_name` will be "napari" iff the application was instantiated in # gui_qt(). isActiveWindow() will be True if it is the second time a # _qt_window has been created. # See #721, #732, #735, #795, #1594 app_name = QApplication.instance().applicationName() if (app_name == 'napari' or in_jupyter()) and self._qt_window.isActiveWindow(): self.activate() def activate(self): """Make the viewer the currently active window.""" self._qt_window.raise_() # for macOS self._qt_window.activateWindow() # for Windows def _update_theme(self, event=None): """Update widget color theme.""" # set window styles which don't use the primary stylesheet # FIXME: this is a problem with the stylesheet not using properties theme = get_theme(self.qt_viewer.viewer.theme) self._status_bar.setStyleSheet( template( 'QStatusBar { background: {{ background }}; ' 'color: {{ text }}; }', **theme, )) self._qt_center.setStyleSheet( template('QWidget { background: {{ background }}; }', **theme)) self._qt_window.setStyleSheet(template(self.raw_stylesheet, **theme)) def _status_changed(self, event): """Update status bar. Parameters ---------- event : napari.utils.event.Event The napari event that triggered this method. """ self._status_bar.showMessage(event.value) def _title_changed(self, event): """Update window title. Parameters ---------- event : napari.utils.event.Event The napari event that triggered this method. """ self._qt_window.setWindowTitle(event.value) def _help_changed(self, event): """Update help message on status bar. Parameters ---------- event : napari.utils.event.Event The napari event that triggered this method. """ self._help.setText(event.value) def _screenshot_dialog(self): """Save screenshot of current display with viewer, default .png""" dial = ScreenshotDialog(self.screenshot, self.qt_viewer, self.qt_viewer._last_visited_dir) if dial.exec_(): self._last_visited_dir = os.path.dirname(dial.selectedFiles()[0]) def screenshot(self, path=None): """Take currently displayed viewer and convert to an image array. Parameters ---------- 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._qt_window.grab().toImage() if path is not None: imsave(path, QImg2array(img)) # scikit-image imsave method return QImg2array(img) def close(self): """Close the viewer window and cleanup sub-widgets.""" # Someone is closing us twice? Only try to delete self._qt_window # if we still have one. if hasattr(self, '_qt_window'): self._delete_qt_window() def _delete_qt_window(self): """Delete our self._qt_window.""" # On some versions of Darwin, exiting while fullscreen seems to tickle # some bug deep in NSWindow. This forces the fullscreen keybinding # test to complete its draw cycle, then pop back out of fullscreen. if self._qt_window.isFullScreen(): self._qt_window.showNormal() for i in range(8): time.sleep(0.1) QApplication.processEvents() self.qt_viewer.close() self._qt_window.close() del self._qt_window
class Window: """Application window that contains the menu bar and viewer. Parameters ---------- qt_viewer : QtViewer Contained viewer widget. Attributes ---------- qt_viewer : QtViewer Contained viewer widget. """ with open(os.path.join(resources_dir, 'stylesheet.qss'), 'r') as f: raw_stylesheet = f.read() def __init__(self, qt_viewer, *, show=True): self.qt_viewer = qt_viewer self._qt_window = QMainWindow() self._qt_window.setUnifiedTitleAndToolBarOnMac(True) self._qt_center = QWidget(self._qt_window) self._qt_window.setCentralWidget(self._qt_center) self._qt_window.setWindowTitle(self.qt_viewer.viewer.title) self._qt_center.setLayout(QHBoxLayout()) self._status_bar = QStatusBar() self._qt_window.setStatusBar(self._status_bar) self._qt_window.closeEvent = self.closeEvent self.close = self._qt_window.close self._add_menubar() self._add_file_menu() self._add_view_menu() self._add_window_menu() self._add_help_menu() self._status_bar.showMessage('Ready') self._help = QLabel('') self._status_bar.addPermanentWidget(self._help) self._qt_center.layout().addWidget(self.qt_viewer) self._qt_center.layout().setContentsMargins(4, 0, 4, 0) self._update_palette(qt_viewer.viewer.palette) self.qt_viewer.viewer.events.status.connect(self._status_changed) self.qt_viewer.viewer.events.help.connect(self._help_changed) self.qt_viewer.viewer.events.title.connect(self._title_changed) self.qt_viewer.viewer.events.palette.connect( lambda event: self._update_palette(event.palette)) if show: self.show() def _add_menubar(self): self.main_menu = self._qt_window.menuBar() # Menubar shortcuts are only active when the menubar is visible. # Therefore, we set a global shortcut not associated with the menubar # to toggle visibility, *but*, in order to not shadow the menubar # shortcut, we disable it, and only enable it when the menubar is # hidden. See this stackoverflow link for details: # https://stackoverflow.com/questions/50537642/how-to-keep-the-shortcuts-of-a-hidden-widget-in-pyqt5 self._main_menu_shortcut = QShortcut(QKeySequence('Ctrl+M'), self._qt_window) self._main_menu_shortcut.activated.connect( self._toggle_menubar_visible) self._main_menu_shortcut.setEnabled(False) def _toggle_menubar_visible(self): """Toggle visibility of app menubar. This function also disables or enables a global keyboard shortcut to show the menubar, since menubar shortcuts are only available while the menubar is visible. """ if self.main_menu.isVisible(): self.main_menu.setVisible(False) self._main_menu_shortcut.setEnabled(True) else: self.main_menu.setVisible(True) self._main_menu_shortcut.setEnabled(False) def _add_file_menu(self): open_images = QAction('Open image(s)...', self._qt_window) open_images.setShortcut('Ctrl+O') open_images.setStatusTip('Open image file(s)') open_images.triggered.connect(self.qt_viewer._open_images) open_folder = QAction('Open Folder...', self._qt_window) open_folder.setShortcut('Ctrl-Shift-O') open_folder.setStatusTip( 'Open a folder of image file(s) or a zarr file') open_folder.triggered.connect(self.qt_viewer._open_folder) self.file_menu = self.main_menu.addMenu('&File') self.file_menu.addAction(open_images) self.file_menu.addAction(open_folder) def _add_view_menu(self): toggle_visible = QAction('Toggle menubar visibility', self._qt_window) toggle_visible.setShortcut('Ctrl+M') toggle_visible.setStatusTip('Hide Menubar') toggle_visible.triggered.connect(self._toggle_menubar_visible) self.view_menu = self.main_menu.addMenu('&View') self.view_menu.addAction(toggle_visible) def _add_window_menu(self): exit_action = QAction("Close window", self._qt_window) exit_action.setShortcut("Ctrl+W") exit_action.setStatusTip('Close napari window') exit_action.triggered.connect(self._qt_window.close) self.window_menu = self.main_menu.addMenu('&Window') self.window_menu.addAction(exit_action) def _add_help_menu(self): self.help_menu = self.main_menu.addMenu('&Help') about_action = QAction("napari info", self._qt_window) about_action.setStatusTip('About napari') about_action.triggered.connect( lambda e: QtAbout.showAbout(self.qt_viewer)) self.help_menu.addAction(about_action) keybidings_action = QAction("keybindings", self._qt_window) keybidings_action.setShortcut("Ctrl+/") keybidings_action.setStatusTip('About keybindings') keybidings_action.triggered.connect( lambda e: QtAboutKeybindings.showAbout(self.qt_viewer)) self.help_menu.addAction(keybidings_action) def add_dock_widget(self, area: str = 'bottom', widget: QWidget = None, allowed_areas=None): """Convenience method to add a QDockWidget to the main window Parameters ---------- area : str Side of the main window to which the new dock widget will be added. Must be in {'left', 'right', 'top', 'bottom'} widget : QWidget, optional If provided, `widget` will be added as QDockWidget's main widget allowed_areas : Qt.DockWidgetArea, optional Areas, relative to main window, that the new dock is allowed to go. """ areas = { 'left': Qt.LeftDockWidgetArea, 'right': Qt.RightDockWidgetArea, 'top': Qt.TopDockWidgetArea, 'bottom': Qt.BottomDockWidgetArea, } if area not in areas: raise ValueError(f'side argument must be in {list(areas.keys())}') dock_widget = QDockWidget(self._qt_window) dock_widget.setAllowedAreas(allowed_areas or (Qt.LeftDockWidgetArea | Qt.BottomDockWidgetArea | Qt.RightDockWidgetArea | Qt.TopDockWidgetArea)) dock_widget.setMinimumHeight(50) dock_widget.setMinimumWidth(50) if isinstance(widget, QWidget): dock_widget.setWidget(widget) widget.setParent(dock_widget) self._qt_window.addDockWidget(areas[area], dock_widget) return dock_widget def remove_dock_widget(self, widget): """Removes specified dock widget. Parameters ---------- widget : QWidget | str If widget == 'all', all docked widgets will be removed. """ if widget == 'all': for dw in self._qt_window.findChildren(QDockWidget): self._qt_window.removeDockWidget(dw) else: self._qt_window.removeDockWidget(widget) def resize(self, width, height): """Resize the window. Parameters ---------- width : int Width in logical pixels. height : int Height in logical pixels. """ self._qt_window.resize(width, height) def show(self): """Resize, show, and bring forward the window. """ self._qt_window.resize(self._qt_window.layout().sizeHint()) self._qt_window.show() def _update_palette(self, palette): # set window styles which don't use the primary stylesheet # FIXME: this is a problem with the stylesheet not using properties self._status_bar.setStyleSheet( template( 'QStatusBar { background: {{ background }}; ' 'color: {{ text }}; }', **palette, )) self._qt_center.setStyleSheet( template('QWidget { background: {{ background }}; }', **palette)) self._qt_window.setStyleSheet(template(self.raw_stylesheet, **palette)) def _status_changed(self, event): """Update status bar. """ self._status_bar.showMessage(event.text) def _title_changed(self, event): """Update window title. """ self._qt_window.setWindowTitle(event.text) def _help_changed(self, event): """Update help message on status bar. """ self._help.setText(event.text) def closeEvent(self, event): # Forward close event to the console to trigger proper shutdown self.qt_viewer.console.shutdown() # 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.qt_viewer.dims.stop() event.accept()
class Window: """Application window that contains the menu bar and viewer. Parameters ---------- qt_viewer : QtViewer Contained viewer widget. Attributes ---------- file_menu : qtpy.QtWidgets.QMenu File menu. help_menu : qtpy.QtWidgets.QMenu Help menu. main_menu : qtpy.QtWidgets.QMainWindow.menuBar Main menubar. qt_viewer : QtViewer Contained viewer widget. view_menu : qtpy.QtWidgets.QMenu View menu. window_menu : qtpy.QtWidgets.QMenu Window menu. """ raw_stylesheet = get_stylesheet() def __init__(self, qt_viewer, *, show=True): self.qt_viewer = qt_viewer self._qt_window = QMainWindow() self._qt_window.setAttribute(Qt.WA_DeleteOnClose) self._qt_window.setUnifiedTitleAndToolBarOnMac(True) self._qt_center = QWidget(self._qt_window) self._qt_window.setCentralWidget(self._qt_center) self._qt_window.setWindowTitle(self.qt_viewer.viewer.title) self._qt_center.setLayout(QHBoxLayout()) self._status_bar = QStatusBar() self._qt_window.setStatusBar(self._status_bar) self._add_menubar() self._add_file_menu() self._add_view_menu() self._add_window_menu() self._add_plugins_menu() self._add_help_menu() self._status_bar.showMessage('Ready') self._help = QLabel('') self._status_bar.addPermanentWidget(self._help) self._qt_center.layout().addWidget(self.qt_viewer) self._qt_center.layout().setContentsMargins(4, 0, 4, 0) self._update_palette() self._add_viewer_dock_widget(self.qt_viewer.dockConsole) self._add_viewer_dock_widget(self.qt_viewer.dockLayerControls) self._add_viewer_dock_widget(self.qt_viewer.dockLayerList) self.qt_viewer.viewer.events.status.connect(self._status_changed) self.qt_viewer.viewer.events.help.connect(self._help_changed) self.qt_viewer.viewer.events.title.connect(self._title_changed) self.qt_viewer.viewer.events.palette.connect(self._update_palette) if show: self.show() def _add_menubar(self): """Add menubar to napari app.""" self.main_menu = self._qt_window.menuBar() # Menubar shortcuts are only active when the menubar is visible. # Therefore, we set a global shortcut not associated with the menubar # to toggle visibility, *but*, in order to not shadow the menubar # shortcut, we disable it, and only enable it when the menubar is # hidden. See this stackoverflow link for details: # https://stackoverflow.com/questions/50537642/how-to-keep-the-shortcuts-of-a-hidden-widget-in-pyqt5 self._main_menu_shortcut = QShortcut(QKeySequence('Ctrl+M'), self._qt_window) self._main_menu_shortcut.activated.connect( self._toggle_menubar_visible) self._main_menu_shortcut.setEnabled(False) def _toggle_menubar_visible(self): """Toggle visibility of app menubar. This function also disables or enables a global keyboard shortcut to show the menubar, since menubar shortcuts are only available while the menubar is visible. """ if self.main_menu.isVisible(): self.main_menu.setVisible(False) self._main_menu_shortcut.setEnabled(True) else: self.main_menu.setVisible(True) self._main_menu_shortcut.setEnabled(False) def _add_file_menu(self): """Add 'File' menu to app menubar.""" open_images = QAction('Open image(s)...', self._qt_window) open_images.setShortcut('Ctrl+O') open_images.setStatusTip('Open image file(s)') open_images.triggered.connect(self.qt_viewer._open_images) open_stack = QAction('Open image series as stack...', self._qt_window) open_stack.setShortcut('Ctrl+Alt+O') open_stack.setStatusTip('Open image files') open_stack.triggered.connect(self.qt_viewer._open_images_as_stack) open_folder = QAction('Open Folder...', self._qt_window) open_folder.setShortcut('Ctrl+Shift+O') open_folder.setStatusTip( 'Open a folder of image file(s) or a zarr file') open_folder.triggered.connect(self.qt_viewer._open_folder) screenshot = QAction('Screenshot', self._qt_window) screenshot.setShortcut('Ctrl+Alt+S') screenshot.setStatusTip( 'Save screenshot of current display, default .png') screenshot.triggered.connect(self.qt_viewer._save_screenshot) # OS X will rename this to Quit and put it in the app menu. exitAction = QAction('Exit', self._qt_window) exitAction.setShortcut('Ctrl+Q') exitAction.setMenuRole(QAction.QuitRole) def handle_exit(): # if the event loop was started in gui_qt() then the app will be # named 'napari'. Since the Qapp was started by us, just close it. if QApplication.applicationName() == 'napari': QApplication.closeAllWindows() QApplication.quit() # otherwise, something else created the QApp before us (such as # %gui qt IPython magic). If we quit the app in this case, then # *later* attemps to instantiate a napari viewer won't work until # the event loop is restarted with app.exec_(). So rather than # quit just close all the windows (and clear our app icon). else: QApplication.setWindowIcon(QIcon()) self.close() exitAction.triggered.connect(handle_exit) self.file_menu = self.main_menu.addMenu('&File') self.file_menu.addAction(open_images) self.file_menu.addAction(open_stack) self.file_menu.addAction(open_folder) self.file_menu.addAction(screenshot) self.file_menu.addAction(exitAction) def _add_view_menu(self): """Add 'View' menu to app menubar.""" toggle_visible = QAction('Toggle menubar visibility', self._qt_window) toggle_visible.setShortcut('Ctrl+M') toggle_visible.setStatusTip('Hide Menubar') toggle_visible.triggered.connect(self._toggle_menubar_visible) toggle_theme = QAction('Toggle theme', self._qt_window) toggle_theme.setShortcut('Ctrl+Shift+T') toggle_theme.setStatusTip('Toggle theme') toggle_theme.triggered.connect(self.qt_viewer.viewer._toggle_theme) self.view_menu = self.main_menu.addMenu('&View') self.view_menu.addAction(toggle_visible) self.view_menu.addAction(toggle_theme) def _add_window_menu(self): """Add 'Window' menu to app menubar.""" exit_action = QAction("Close window", self._qt_window) exit_action.setShortcut("Ctrl+W") exit_action.setStatusTip('Close napari window') exit_action.triggered.connect(self._qt_window.close) self.window_menu = self.main_menu.addMenu('&Window') self.window_menu.addAction(exit_action) def _add_plugins_menu(self): """Add 'Plugins' menu to app menubar.""" self.plugins_menu = self.main_menu.addMenu('&Plugins') list_plugins_action = QAction("List installed plugins...", self._qt_window) list_plugins_action.setStatusTip('List installed plugins') list_plugins_action.triggered.connect(self._show_plugin_list) self.plugins_menu.addAction(list_plugins_action) order_plugin_action = QAction("Plugin call order...", self._qt_window) order_plugin_action.setStatusTip('Change call order for plugins') order_plugin_action.triggered.connect(self._show_plugin_sorter) self.plugins_menu.addAction(order_plugin_action) report_plugin_action = QAction("Plugin errors...", self._qt_window) report_plugin_action.setStatusTip( 'Review stack traces for plugin exceptions and notify developers') report_plugin_action.triggered.connect(self._show_plugin_err_reporter) self.plugins_menu.addAction(report_plugin_action) def _show_plugin_list(self): """Show dialog with a table of installed plugins and metadata.""" from ..plugins import plugin_manager dialog = QDialog(self._qt_window) dialog.setMaximumHeight(800) dialog.setMaximumWidth(1280) layout = QVBoxLayout() # maybe someday add a search bar here? title = QLabel("Installed Plugins") title.setObjectName("h2") layout.addWidget(title) # get metadata for successfully registered plugins data = [ v for k, v in plugin_manager._plugin_meta.items() if k in plugin_manager._name2plugin ] # create a table for it dialog.table = QtDictTable( self._qt_window, data, headers=[ 'plugin', 'package', 'version', 'url', 'author', 'license', ], min_section_width=60, ) dialog.table.setObjectName("pluginTable") dialog.table.horizontalHeader().setObjectName("pluginTableHeader") dialog.table.verticalHeader().setObjectName("pluginTableHeader") dialog.table.setGridStyle(Qt.NoPen) # prevent editing of table dialog.table.setEditTriggers(QAbstractItemView.NoEditTriggers) layout.addWidget(dialog.table) dialog.setLayout(layout) dialog.setAttribute(Qt.WA_DeleteOnClose) self._plugin_list = dialog dialog.exec_() def _show_plugin_sorter(self): """Show dialog that allows users to sort the call order of plugins.""" plugin_sorter = QtPluginSorter(parent=self._qt_window) dock_widget = self.add_dock_widget(plugin_sorter, name='Plugin Sorter', area="right") plugin_sorter.finished.connect(dock_widget.close) plugin_sorter.finished.connect(plugin_sorter.deleteLater) plugin_sorter.finished.connect(dock_widget.deleteLater) def _show_plugin_err_reporter(self): """Show dialog that allows users to review and report plugin errors.""" plugin_sorter = QtPluginErrReporter(parent=self._qt_window) plugin_sorter.exec_() def _add_help_menu(self): """Add 'Help' menu to app menubar.""" self.help_menu = self.main_menu.addMenu('&Help') about_action = QAction("napari info", self._qt_window) about_action.setShortcut("Ctrl+/") about_action.setStatusTip('About napari') about_action.triggered.connect( lambda e: QtAbout.showAbout(self.qt_viewer)) self.help_menu.addAction(about_action) about_key_bindings = QAction("Show key bindings", self._qt_window) about_key_bindings.setShortcut("Ctrl+Alt+/") about_key_bindings.setShortcutContext(Qt.ApplicationShortcut) about_key_bindings.setStatusTip('key_bindings') about_key_bindings.triggered.connect( self.qt_viewer.show_key_bindings_dialog) self.help_menu.addAction(about_key_bindings) def add_dock_widget( self, widget: QWidget, *, name: str = '', area: str = 'bottom', allowed_areas=None, shortcut=None, ): """Convenience method to add a QDockWidget to the main window Parameters ---------- widget : QWidget `widget` will be added as QDockWidget's main widget. name : str, optional Name of dock widget to appear in window menu. area : str Side of the main window to which the new dock widget will be added. Must be in {'left', 'right', 'top', 'bottom'} allowed_areas : list[str], optional Areas, relative to main window, that the widget is allowed dock. Each item in list must be in {'left', 'right', 'top', 'bottom'} By default, all areas are allowed. shortcut : str, optional Keyboard shortcut to appear in dropdown menu. Returns ------- dock_widget : QtViewerDockWidget `dock_widget` that can pass viewer events. """ dock_widget = QtViewerDockWidget( self.qt_viewer, widget, name=name, area=area, allowed_areas=allowed_areas, shortcut=shortcut, ) self._add_viewer_dock_widget(dock_widget) return dock_widget def _add_viewer_dock_widget(self, dock_widget: QtViewerDockWidget): """Add a QtViewerDockWidget to the main window Parameters ---------- dock_widget : QtViewerDockWidget `dock_widget` will be added to the main window. """ dock_widget.setParent(self._qt_window) self._qt_window.addDockWidget(dock_widget.qt_area, dock_widget) action = dock_widget.toggleViewAction() action.setStatusTip(dock_widget.name) action.setText(dock_widget.name) if dock_widget.shortcut is not None: action.setShortcut(dock_widget.shortcut) self.window_menu.addAction(action) def remove_dock_widget(self, widget): """Removes specified dock widget. Parameters ---------- widget : QWidget | str If widget == 'all', all docked widgets will be removed. """ if widget == 'all': for dw in self._qt_window.findChildren(QDockWidget): self._qt_window.removeDockWidget(dw) else: self._qt_window.removeDockWidget(widget) def resize(self, width, height): """Resize the window. Parameters ---------- width : int Width in logical pixels. height : int Height in logical pixels. """ self._qt_window.resize(width, height) def show(self): """Resize, show, and bring forward the window.""" self._qt_window.resize(self._qt_window.layout().sizeHint()) self._qt_window.show() # We want to call Window._qt_window.raise_() in every case *except* # when instantiating a viewer within a gui_qt() context for the # _first_ time within the Qt app's lifecycle. # # `app_name` will be "napari" iff the application was instantiated in # gui_qt(). isActiveWindow() will be True if it is the second time a # _qt_window has been created. See #732 app_name = QApplication.instance().applicationName() if app_name != 'napari' or self._qt_window.isActiveWindow(): self._qt_window.raise_() # for macOS self._qt_window.activateWindow() # for Windows def _update_palette(self, event=None): """Update widget color palette.""" # set window styles which don't use the primary stylesheet # FIXME: this is a problem with the stylesheet not using properties palette = self.qt_viewer.viewer.palette self._status_bar.setStyleSheet( template( 'QStatusBar { background: {{ background }}; ' 'color: {{ text }}; }', **palette, )) self._qt_center.setStyleSheet( template('QWidget { background: {{ background }}; }', **palette)) self._qt_window.setStyleSheet(template(self.raw_stylesheet, **palette)) def _status_changed(self, event): """Update status bar. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ self._status_bar.showMessage(event.text) def _title_changed(self, event): """Update window title. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ self._qt_window.setWindowTitle(event.text) def _help_changed(self, event): """Update help message on status bar. Parameters ---------- event : qtpy.QtCore.QEvent Event from the Qt context. """ self._help.setText(event.text) def screenshot(self, path=None): """Take currently displayed viewer and convert to an image array. Parameters ---------- 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._qt_window.grab().toImage() if path is not None: imsave(path, QImg2array(img)) # scikit-image imsave method return QImg2array(img) def close(self): """Close the viewer window and cleanup sub-widgets.""" # on some versions of Darwin, exiting while fullscreen seems to tickle # some bug deep in NSWindow. This forces the fullscreen keybinding # test to complete its draw cycle, then pop back out of fullscreen. if self._qt_window.isFullScreen(): self._qt_window.showNormal() for i in range(8): time.sleep(0.1) QApplication.processEvents() self.qt_viewer.close() self._qt_window.close() del self._qt_window