"""
Custom Qt widgets that serve as native objects that the public-facing elements
wrap.
"""
import contextlib
import inspect
import os
import sys
import time
import warnings
from typing import (
    TYPE_CHECKING,
    Any,
    ClassVar,
    List,
    MutableMapping,
    Optional,
    Sequence,
    Tuple,
    Union,
    cast,
)
from weakref import WeakValueDictionary
from qtpy.QtCore import (
    QEvent,
    QEventLoop,
    QPoint,
    QProcess,
    QRect,
    QSize,
    Qt,
    Slot,
)
from qtpy.QtGui import QIcon
from qtpy.QtWidgets import (
    QApplication,
    QDialog,
    QDockWidget,
    QHBoxLayout,
    QMainWindow,
    QMenu,
    QShortcut,
    QToolTip,
    QWidget,
)
from superqt.utils import QSignalThrottler
from napari._app_model.constants import MenuId
from napari._app_model.context import get_context
from napari._qt import menus
from napari._qt._qapp_model import build_qmodel_menu
from napari._qt._qapp_model.qactions import init_qactions
from napari._qt.dialogs.confirm_close_dialog import ConfirmCloseDialog
from napari._qt.dialogs.preferences_dialog import PreferencesDialog
from napari._qt.dialogs.qt_activity_dialog import QtActivityDialog
from napari._qt.dialogs.qt_notification import NapariQtNotification
from napari._qt.qt_event_loop import (
    NAPARI_ICON_PATH,
    get_app,
    quit_app as quit_app_,
)
from napari._qt.qt_resources import get_stylesheet
from napari._qt.qt_viewer import QtViewer
from napari._qt.utils import QImg2array, qbytearray_to_str, str_to_qbytearray
from napari._qt.widgets.qt_viewer_dock_widget import (
    _SHORTCUT_DEPRECATION_STRING,
    QtViewerDockWidget,
)
from napari._qt.widgets.qt_viewer_status_bar import ViewerStatusBar
from napari.plugins import (
    menu_item_template as plugin_menu_item_template,
    plugin_manager,
)
from napari.plugins._npe2 import _rebuild_npe1_samples_menu
from napari.settings import get_settings
from napari.utils import perf
from napari.utils._proxies import PublicOnlyProxy
from napari.utils.io import imsave
from napari.utils.misc import (
    in_ipython,
    in_jupyter,
    in_python_repl,
    running_as_constructor_app,
)
from napari.utils.notifications import Notification
from napari.utils.theme import _themes, get_system_theme
from napari.utils.translations import trans
_sentinel = object()
if TYPE_CHECKING:
    from magicgui.widgets import Widget
    from qtpy.QtGui import QImage
    from napari.viewer import Viewer
class _QtMainWindow(QMainWindow):
    # This was added so that someone can patch
    # `napari._qt.qt_main_window._QtMainWindow._window_icon`
    # to their desired window icon
    _window_icon = NAPARI_ICON_PATH
    # To track window instances and facilitate getting the "active" viewer...
    # We use this instead of QApplication.activeWindow for compatibility with
    # IPython usage. When you activate IPython, it will appear that there are
    # *no* active windows, so we want to track the most recently active windows
    _instances: ClassVar[List['_QtMainWindow']] = []
    # `window` is passed through on construction, so it's available to a window
    # provider for dependency injection
    # See https://github.com/napari/napari/pull/4826
    def __init__(
        self, viewer: 'Viewer', window: 'Window', parent=None
    ) -> None:
        super().__init__(parent)
        self._ev = None
        self._window = window
        self._qt_viewer = QtViewer(viewer, show_welcome_screen=True)
        self._quit_app = False
        self.setWindowIcon(QIcon(self._window_icon))
        self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
        center = QWidget(self)
        center.setLayout(QHBoxLayout())
        center.layout().addWidget(self._qt_viewer)
        center.layout().setContentsMargins(4, 0, 4, 0)
        self.setCentralWidget(center)
        self.setWindowTitle(self._qt_viewer.viewer.title)
        self._maximized_flag = False
        self._fullscreen_flag = False
        self._normal_geometry = QRect()
        self._window_size = None
        self._window_pos = None
        self._old_size = None
        self._positions = []
        self._toggle_menubar_visibility = False
        self._is_close_dialog = {False: True, True: True}
        # this ia sa workaround for #5335 issue. The dict is used to not
        # collide shortcuts for close and close all windows
        act_dlg = QtActivityDialog(self._qt_viewer._welcome_widget)
        self._qt_viewer._welcome_widget.resized.connect(
            act_dlg.move_to_bottom_right
        )
        act_dlg.hide()
        self._activity_dialog = act_dlg
        self.setStatusBar(ViewerStatusBar(self))
        # Prevent QLineEdit based widgets to keep focus even when clicks are
        # done outside the widget. See #1571
        self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
        settings = get_settings()
        # TODO:
        # settings.plugins.defaults.call_order = plugin_manager.call_order()
        # set the values in plugins to match the ones saved in settings
        if settings.plugins.call_order is not None:
            plugin_manager.set_call_order(settings.plugins.call_order)
        _QtMainWindow._instances.append(self)
        # since we initialize canvas before the window,
        # we need to manually connect them again.
        handle = self.windowHandle()
        if handle is not None:
            handle.screenChanged.connect(self._qt_viewer.canvas.screen_changed)
        # this is the line that initializes any Qt-based app-model Actions that
        # were defined somewhere in the `_qt` module and imported in init_qactions
        init_qactions()
        self.status_throttler = QSignalThrottler(parent=self)
        self.status_throttler.setTimeout(50)
        self._throttle_cursor_to_status_connection(viewer)
    def _throttle_cursor_to_status_connection(self, viewer: 'Viewer'):
        # In the GUI we expect lots of changes to the cursor position, so
        # replace the direct connection with a throttled one.
        with contextlib.suppress(IndexError):
            viewer.cursor.events.position.disconnect(
                viewer._update_status_bar_from_cursor
            )
        viewer.cursor.events.position.connect(self.status_throttler.throttle)
        self.status_throttler.triggered.connect(
            viewer._update_status_bar_from_cursor
        )
    def statusBar(self) -> 'ViewerStatusBar':
        return super().statusBar()
    @classmethod
    def current(cls) -> Optional['_QtMainWindow']:
        return cls._instances[-1] if cls._instances else None
    @classmethod
    def current_viewer(cls):
        window = cls.current()
        return window._qt_viewer.viewer if window else None
    def event(self, e: QEvent) -> bool:
        if (
            e.type() == QEvent.Type.ToolTip
            and self._qt_viewer.viewer.tooltip.visible
        ):
            # globalPos is for Qt5 e.globalPosition().toPoint() is for QT6
            # https://doc-snapshots.qt.io/qt6-dev/qmouseevent-obsolete.html#globalPos
            pnt = (
                e.globalPosition().toPoint()
                if hasattr(e, "globalPosition")
                else e.globalPos()
            )
            QToolTip.showText(pnt, self._qt_viewer.viewer.tooltip.text, self)
        if e.type() in {QEvent.Type.WindowActivate, QEvent.Type.ZOrderChange}:
            # upon activation or raise_, put window at the end of _instances
            with contextlib.suppress(ValueError):
                inst = _QtMainWindow._instances
                inst.append(inst.pop(inst.index(self)))
        res = super().event(e)
        if e.type() == QEvent.Type.Close and e.isAccepted():
            # when we close the MainWindow, remove it from the instance list
            with contextlib.suppress(ValueError):
                _QtMainWindow._instances.remove(self)
        return res
    def isFullScreen(self):
        # Needed to prevent errors when going to fullscreen mode on Windows
        # Use a flag attribute to determine if the window is in full screen mode
        # See https://bugreports.qt.io/browse/QTBUG-41309
        # Based on https://github.com/spyder-ide/spyder/pull/7720
        return self._fullscreen_flag
    def showNormal(self):
        # Needed to prevent errors when going to fullscreen mode on Windows. Here we:
        #   * Set fullscreen flag
        #   * Remove `Qt.FramelessWindowHint` and `Qt.WindowStaysOnTopHint` window flags if needed
        #   * Set geometry to previously stored normal geometry or default empty QRect
        # Always call super `showNormal` to set Qt window state
        # See https://bugreports.qt.io/browse/QTBUG-41309
        # Based on https://github.com/spyder-ide/spyder/pull/7720
        self._fullscreen_flag = False
        if os.name == 'nt':
            self.setWindowFlags(
                self.windowFlags()
                ^ (
                    Qt.WindowType.FramelessWindowHint
                    | Qt.WindowType.WindowStaysOnTopHint
                )
            )
            self.setGeometry(self._normal_geometry)
        super().showNormal()
    def showFullScreen(self):
        # Needed to prevent errors when going to fullscreen mode on Windows. Here we:
        #   * Set fullscreen flag
        #   * Add `Qt.FramelessWindowHint` and `Qt.WindowStaysOnTopHint` window flags if needed
        #   * Call super `showNormal` to update the normal screen geometry to apply it later if needed
        #   * Save window normal geometry if needed
        #   * Get screen geometry
        #   * Set geometry window to use total screen geometry +1 in every direction if needed
        # If the workaround is not needed just call super `showFullScreen`
        # See https://bugreports.qt.io/browse/QTBUG-41309
        # Based on https://github.com/spyder-ide/spyder/pull/7720
        self._fullscreen_flag = True
        if os.name == 'nt':
            self.setWindowFlags(
                self.windowFlags()
                | Qt.WindowType.FramelessWindowHint
                | Qt.WindowType.WindowStaysOnTopHint
            )
            super().showNormal()
            self._normal_geometry = self.normalGeometry()
            screen_rect = self.windowHandle().screen().geometry()
            self.setGeometry(
                screen_rect.left() - 1,
                screen_rect.top() - 1,
                screen_rect.width() + 2,
                screen_rect.height() + 2,
            )
        else:
            super().showFullScreen()
    def eventFilter(self, source, event):
        # Handle showing hidden menubar on mouse move event.
        # We do not hide menubar when a menu is being shown or
        # we are not in menubar toggled state
        if (
            QApplication.activePopupWidget() is None
            and self._toggle_menubar_visibility
        ):
            if event.type() == QEvent.Type.MouseMove:
                if self.menuBar().isHidden():
                    rect = self.geometry()
                    # set mouse-sensitive zone to trigger showing the menubar
                    rect.setHeight(25)
                    if rect.contains(event.globalPos()):
                        self.menuBar().show()
                else:
                    rect = QRect(
                        self.menuBar().mapToGlobal(QPoint(0, 0)),
                        self.menuBar().size(),
                    )
                    if not rect.contains(event.globalPos()):
                        self.menuBar().hide()
            elif event.type() == QEvent.Type.Leave and source is self:
                self.menuBar().hide()
        return QMainWindow.eventFilter(self, source, event)
    def _load_window_settings(self):
        """
        Load window layout settings from configuration.
        """
        settings = get_settings()
        window_position = settings.application.window_position
        # It's necessary to verify if the window/position value is valid with
        # the current screen.
        if not window_position:
            window_position = (self.x(), self.y())
        else:
            origin_x, origin_y = window_position
            screen = QApplication.screenAt(QPoint(origin_x, origin_y))
            screen_geo = screen.geometry() if screen else None
            if not screen_geo:
                window_position = (self.x(), self.y())
        return (
            settings.application.window_state,
            settings.application.window_size,
            window_position,
            settings.application.window_maximized,
            settings.application.window_fullscreen,
        )
    def _get_window_settings(self):
        """Return current window settings.
        Symmetric to the 'set_window_settings' setter.
        """
        window_fullscreen = self.isFullScreen()
        if window_fullscreen:
            window_maximized = self._maximized_flag
        else:
            window_maximized = self.isMaximized()
        window_state = qbytearray_to_str(self.saveState())
        return (
            window_state,
            self._window_size or (self.width(), self.height()),
            self._window_pos or (self.x(), self.y()),
            window_maximized,
            window_fullscreen,
        )
    def _set_window_settings(
        self,
        window_state,
        window_size,
        window_position,
        window_maximized,
        window_fullscreen,
    ):
        """
        Set window settings.
        Symmetric to the 'get_window_settings' accessor.
        """
        self.setUpdatesEnabled(False)
        self.setWindowState(Qt.WindowState.WindowNoState)
        if window_position:
            window_position = QPoint(*window_position)
            self.move(window_position)
        if window_size:
            window_size = QSize(*window_size)
            self.resize(window_size)
        if window_state:
            self.restoreState(str_to_qbytearray(window_state))
        # Toggling the console visibility is disabled when it is not
        # available, so ensure that it is hidden.
        if in_ipython() or in_jupyter() or in_python_repl():
            self._qt_viewer.dockConsole.setVisible(False)
        if window_fullscreen:
            self._maximized_flag = window_maximized
            self.showFullScreen()
        elif window_maximized:
            self.setWindowState(Qt.WindowState.WindowMaximized)
        self.setUpdatesEnabled(True)
    def _save_current_window_settings(self):
        """Save the current geometry of the main window."""
        (
            window_state,
            window_size,
            window_position,
            window_maximized,
            window_fullscreen,
        ) = self._get_window_settings()
        settings = get_settings()
        if settings.application.save_window_geometry:
            settings.application.window_maximized = window_maximized
            settings.application.window_fullscreen = window_fullscreen
            settings.application.window_position = window_position
            settings.application.window_size = window_size
            settings.application.window_statusbar = (
                not self.statusBar().isHidden()
            )
        if settings.application.save_window_state:
            settings.application.window_state = window_state
    def close(self, quit_app=False, confirm_need=False):
        """Override to handle closing app or just the window."""
        if hasattr(self.status_throttler, "_timer"):
            self.status_throttler._timer.stop()
        if not quit_app and not self._qt_viewer.viewer.layers:
            return super().close()
        confirm_need_local = confirm_need and self._is_close_dialog[quit_app]
        self._is_close_dialog[quit_app] = False
        # here we save information that we could request confirmation on close
        # So fi function `close` is called again, we don't ask again but just close
        if (
            not confirm_need_local
            or not get_settings().application.confirm_close_window
            or ConfirmCloseDialog(self, quit_app).exec_() == QDialog.Accepted
        ):
            self._quit_app = quit_app
            self._is_close_dialog[quit_app] = True
            # here we inform that confirmation dialog is not open
            self._qt_viewer.dims.stop()
            return super().close()
        self._is_close_dialog[quit_app] = True
        return None
        # here we inform that confirmation dialog is not open
    def close_window(self):
        """Close active dialog or active window."""
        parent = QApplication.focusWidget()
        while parent is not None:
            if isinstance(parent, QMainWindow):
                self.close()
                break
            if isinstance(parent, QDialog):
                parent.close()
                break
            try:
                parent = parent.parent()
            except AttributeError:
                parent = getattr(parent, "_parent", None)
    def show(self, block=False):
        super().show()
        self._qt_viewer.setFocus()
        if block:
            self._ev = QEventLoop()
            self._ev.exec()
    def changeEvent(self, event):
        """Handle window state changes."""
        if event.type() == QEvent.Type.WindowStateChange:
            # TODO: handle maximization issue. When double clicking on the
            # title bar on Mac the resizeEvent is called an varying amount
            # of times which makes it hard to track the original size before
            # maximization.
            condition = (
                self.isMaximized() if os.name == "nt" else self.isFullScreen()
            )
            if condition and self._old_size is not None:
                if self._positions and len(self._positions) > 1:
                    self._window_pos = self._positions[-2]
                self._window_size = (
                    self._old_size.width(),
                    self._old_size.height(),
                )
            else:
                self._old_size = None
                self._window_pos = None
                self._window_size = None
                self._positions = []
        super().changeEvent(event)
    def keyPressEvent(self, event):
        """Called whenever a key is pressed.
        Parameters
        ----------
        event : qtpy.QtCore.QEvent
            Event from the Qt context.
        """
        self._qt_viewer.canvas._scene_canvas._backend._keyEvent(
            self._qt_viewer.canvas._scene_canvas.events.key_press, event
        )
        event.accept()
    def keyReleaseEvent(self, event):
        """Called whenever a key is released.
        Parameters
        ----------
        event : qtpy.QtCore.QEvent
            Event from the Qt context.
        """
        self._qt_viewer.canvas._scene_canvas._backend._keyEvent(
            self._qt_viewer.canvas._scene_canvas.events.key_release, event
        )
        event.accept()
    def resizeEvent(self, event):
        """Override to handle original size before maximizing."""
        # the first resize event will have nonsense positions that we don't
        # want to store (and potentially restore)
        if event.oldSize().isValid():
            self._old_size = event.oldSize()
            self._positions.append((self.x(), self.y()))
            if self._positions and len(self._positions) >= 2:
                self._window_pos = self._positions[-2]
                self._positions = self._positions[-2:]
        super().resizeEvent(event)
    def closeEvent(self, event):
        """This method will be called when the main window is closing.
        Regardless of whether cmd Q, cmd W, or the close button is used...
        """
        if (
            event.spontaneous()
            and get_settings().application.confirm_close_window
            and self._qt_viewer.viewer.layers
            and ConfirmCloseDialog(self, False).exec_() != QDialog.Accepted
        ):
            event.ignore()
            return
        if self._ev and self._ev.isRunning():
            self._ev.quit()
        # Close any floating dockwidgets
        for dock in self.findChildren(QtViewerDockWidget):
            if isinstance(dock, QWidget) and dock.isFloating():
                dock.setFloating(False)
        self._save_current_window_settings()
        # 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.isFullScreen():
            self.showNormal()
            for _ in range(5):
                time.sleep(0.1)
                QApplication.processEvents()
        self._qt_viewer.dims.stop()
        if self._quit_app:
            quit_app_()
        event.accept()
    def restart(self):
        """Restart the napari application in a detached process."""
        process = QProcess()
        process.setProgram(sys.executable)
        if not running_as_constructor_app():
            process.setArguments(sys.argv)
        process.startDetached()
        self.close(quit_app=True)
    def toggle_menubar_visibility(self):
        """
        Change menubar to be shown or to be hidden and shown on mouse movement.
        For the mouse movement functionality see the `eventFilter` implementation.
        """
        self._toggle_menubar_visibility = not self._toggle_menubar_visibility
        self.menuBar().setVisible(not self._toggle_menubar_visibility)
        return self._toggle_menubar_visibility
    @staticmethod
    @Slot(Notification)
    def show_notification(notification: Notification):
        """Show notification coming from a thread."""
        NapariQtNotification.show_notification(notification)
[docs]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.
    view_menu : qtpy.QtWidgets.QMenu
        View menu.
    window_menu : qtpy.QtWidgets.QMenu
        Window menu.
    """
    def __init__(self, viewer: 'Viewer', *, show: bool = True) -> None:
        # create QApplication if it doesn't already exist
        qapp = get_app()
        # Dictionary holding dock widgets
        self._dock_widgets: MutableMapping[
            str, QtViewerDockWidget
        ] = WeakValueDictionary()
        self._unnamed_dockwidget_count = 1
        self._pref_dialog = None
        # Connect the Viewer and create the Main Window
        self._qt_window = _QtMainWindow(viewer, self)
        qapp.installEventFilter(self._qt_window)
        # 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.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()
        self._update_theme_font_size()
        get_settings().appearance.events.theme.connect(self._update_theme)
        get_settings().appearance.events.font_size.connect(
            self._update_theme_font_size
        )
        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.help.connect(self._help_changed)
        viewer.events.title.connect(self._title_changed)
        viewer.events.theme.connect(self._update_theme)
        viewer.events.status.connect(self._status_changed)
        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.Orientation.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._update_theme_no_event)
        theme.events.font_size.connect(self._update_theme_no_event)
        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._update_theme_no_event)
        theme.events.font_size.disconnect(self._update_theme_no_event)
        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)
    @property
    def qt_viewer(self):
        warnings.warn(
            trans._(
                'Public access to Window.qt_viewer is deprecated and will be removed in\n'
                'v0.6.0. It is considered an "implementation detail" of the napari\napplication, '
                'not part of the napari viewer model. If your use case\n'
                'requires 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 _update_menu_state(self, menu):
        """Update enabled/visible state of menu item with context."""
        layerlist = self._qt_viewer._layers.model().sourceModel()._root
        menu_model = getattr(self, menu)
        menu_model.update_from_context(get_context(layerlist))
    def _setup_npe1_samples_menu(self):
        """Register npe1 sample data, build menu and connect to events."""
        plugin_manager.discover_sample_data()
        plugin_manager.events.enabled.connect(_rebuild_npe1_samples_menu)
        plugin_manager.events.disabled.connect(_rebuild_npe1_samples_menu)
        plugin_manager.events.registered.connect(_rebuild_npe1_samples_menu)
        plugin_manager.events.unregistered.connect(_rebuild_npe1_samples_menu)
        _rebuild_npe1_samples_menu()
    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
        )
        # file menu
        self.file_menu = build_qmodel_menu(
            MenuId.MENUBAR_FILE, title=trans._('&File'), parent=self._qt_window
        )
        self._setup_npe1_samples_menu()
        self.file_menu.aboutToShow.connect(
            lambda: self._update_menu_state('file_menu')
        )
        self.main_menu.addMenu(self.file_menu)
        # view menu
        self.view_menu = build_qmodel_menu(
            MenuId.MENUBAR_VIEW, title=trans._('&View'), parent=self._qt_window
        )
        self.view_menu.aboutToShow.connect(
            lambda: self._update_menu_state('view_menu')
        )
        self.main_menu.addMenu(self.view_menu)
        # plugin menu
        self.plugins_menu = menus.PluginsMenu(self)
        self.main_menu.addMenu(self.plugins_menu)
        # window menu
        self.window_menu = menus.WindowMenu(self)
        self.main_menu.addMenu(self.window_menu)
        # help menu
        self.help_menu = build_qmodel_menu(
            MenuId.MENUBAR_HELP, title=trans._('&Help'), parent=self._qt_window
        )
        self.help_menu.aboutToShow.connect(
            lambda: self._update_menu_state('help_menu')
        )
        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.
        """
        toggle_menubar_visibility = self._qt_window.toggle_menubar_visibility()
        self._main_menu_shortcut.setEnabled(toggle_menubar_visibility)
    def _toggle_fullscreen(self):
        """Toggle fullscreen mode."""
        if self._qt_window.isFullScreen():
            self._qt_window.showNormal()
        else:
            self._qt_window.showFullScreen()
    def _toggle_play(self):
        """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_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 None
        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_viewer_dock_widget(
        self,
        dock_widget: QtViewerDockWidget,
        tabify: bool = False,
        menu: Optional[QMenu] = 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.
        menu : QMenu, optional
            Menu bar to add toggle action to. If `None` nothing added to menu.
        """
        # 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.Orientation.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)
        # see #3663, to fix #3624 more generally
        dock_widget.setFloating(False)
    def _remove_dock_widget(self, event):
        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)
[docs]    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) 
[docs]    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) 
[docs]    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() 
[docs]    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) as e:
            raise RuntimeError(
                trans._(
                    "This viewer has already been closed and deleted. Please create a new one.",
                    deferred=True,
                )
            ) from e
        if settings.application.first_time:
            settings.application.first_time = False
            try:
                self._qt_window.resize(self._qt_window.layout().sizeHint())
            except (AttributeError, RuntimeError) as e:
                raise RuntimeError(
                    trans._(
                        "This viewer has already been closed and deleted. Please create a new one.",
                        deferred=True,
                    )
                ) from e
        else:
            try:
                if settings.application.save_window_geometry:
                    self._qt_window._set_window_settings(
                        *self._qt_window._load_window_settings()
                    )
            except Exception as err:  # noqa: BLE001
                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() 
[docs]    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_font_size(self, event=None):
        settings = get_settings()
        font_size = event.value if event else settings.appearance.font_size
        extra_variables = {"font_size": f"{font_size}pt"}
        self._update_theme(extra_variables=extra_variables)
    def _update_theme(self, event=None, extra_variables=None):
        """Update widget color theme."""
        if extra_variables is None:
            extra_variables = {}
        settings = get_settings()
        with contextlib.suppress(AttributeError, RuntimeError):
            value = event.value if event else settings.appearance.theme
            self._qt_viewer.viewer.theme = value
            actual_theme_name = value
            if value == "system":
                # system isn't a theme, so get the name
                actual_theme_name = get_system_theme()
            # check `font_size` value is always passed when updating style
            if "font_size" not in extra_variables:
                extra_variables.update(
                    {"font_size": f"{settings.appearance.font_size}pt"}
                )
            # set the style sheet with the theme name and extra_variables
            self._qt_window.setStyleSheet(
                get_stylesheet(
                    actual_theme_name, extra_variables=extra_variables
                )
            )
    def _status_changed(self, event):
        """Update status bar.
        Parameters
        ----------
        event : napari.utils.event.Event
            The napari event that triggered this method.
        """
        if isinstance(event.value, str):
            self._status_bar.setStatusText(event.value)
        else:
            status_info = event.value
            self._status_bar.setStatusText(
                layer_base=status_info['layer_base'],
                source_type=status_info['source_type'],
                plugin=status_info['plugin'],
                coordinates=status_info['coordinates'],
            )
    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 height x width) 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 napari._qt.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(
                        trans._(
                            'screenshot size must be 2 values, got {len_size}',
                            len_size=len(size),
                        )
                    )
                # Scale the requested size to account for HiDPI
                size = tuple(
                    int(dim / self._qt_window.devicePixelRatio())
                    for dim in size
                )
                canvas.size = size
            if scale is not None:
                # multiply canvas dimensions by the scale factor to get new size
                canvas.size = tuple(int(dim * scale) for dim in canvas.size)
            try:
                img = canvas.screenshot()
                if flash:
                    add_flash_animation(self._qt_viewer._welcome_widget)
            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
[docs]    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 includes 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)
        return img 
[docs]    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.removed.disconnect(self._remove_theme)
[docs]    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 
    def _open_preferences_dialog(self) -> PreferencesDialog:
        """Edit preferences from the menubar."""
        if self._pref_dialog is None:
            win = PreferencesDialog(parent=self._qt_window)
            self._pref_dialog = win
            app_pref = get_settings().application
            if app_pref.preferences_size:
                win.resize(*app_pref.preferences_size)
            @win.resized.connect
            def _save_size(sz: QSize):
                app_pref.preferences_size = (sz.width(), sz.height())
            def _clean_pref_dialog():
                self._pref_dialog = None
            win.finished.connect(_clean_pref_dialog)
            win.show()
        else:
            self._pref_dialog.raise_()
        return self._pref_dialog
    def _screenshot_dialog(self):
        """Save screenshot of current display with viewer, default .png"""
        from napari._qt.dialogs.screenshot_dialog import ScreenshotDialog
        from napari.utils.history import get_save_history, update_save_history
        hist = get_save_history()
        dial = ScreenshotDialog(
            self.screenshot, self._qt_viewer, hist[0], hist
        )
        if dial.exec_():
            update_save_history(dial.selectedFiles()[0]) 
def _instantiate_dock_widget(wdg_cls, viewer: 'Viewer'):
    # if the signature is looking a for a napari viewer, pass it.
    from napari.viewer import Viewer
    kwargs = {}
    try:
        sig = inspect.signature(wdg_cls.__init__)
    # Inspection can fail when adding to bundle as it thinks widget is a builtin
    except ValueError:
        pass
    else:
        for param in sig.parameters.values():
            if param.name == 'napari_viewer':
                kwargs['napari_viewer'] = PublicOnlyProxy(viewer)
                break
            if param.annotation in ('napari.viewer.Viewer', Viewer):
                kwargs[param.name] = PublicOnlyProxy(viewer)
                break
            # cannot look for param.kind == param.VAR_KEYWORD because
            # QWidget allows **kwargs but errs on unknown keyword arguments
    # instantiate the widget
    return wdg_cls(**kwargs)