コード例 #1
0
class QtAboutKeyBindings(QDialog):
    """Qt dialog window for displaying keybinding information.

    Parameters
    ----------
    viewer : napari.components.ViewerModel
        Napari viewer containing the rendered scene, layers, and controls.

    Attributes
    ----------
    key_bindings_strs : collections.OrderedDict
        Ordered dictionary of hotkey shortcuts and associated key bindings.
        Dictionary keys include:
        - 'All active key bindings'
        - 'Image layer'
        - 'Labels layer'
        - 'Points layer'
        - 'Shapes layer'
        - 'Surface layer'
        - 'Vectors layer'
    layout : qtpy.QtWidgets.QVBoxLayout
        Layout of the widget.
    layerTypeComboBox : qtpy.QtWidgets.QComboBox
        Dropdown menu to select layer type.
    textEditBox : qtpy.QtWidgets.QTextEdit
        Text box widget containing table of key bindings information.
    viewer : napari.components.ViewerModel
        Napari viewer containing the rendered scene, layers, and controls.
    """

    ALL_ACTIVE_KEYBINDINGS = 'All active key bindings'

    def __init__(self, viewer, parent=None):
        super().__init__(parent=parent)

        self.viewer = viewer
        self.layout = QVBoxLayout()

        self.setWindowTitle('Keybindings')
        self.setWindowModality(Qt.NonModal)
        self.setLayout(self.layout)

        # stacked key bindings widgets
        self.textEditBox = QTextEdit()
        self.textEditBox.setTextInteractionFlags(Qt.TextSelectableByMouse)
        self.textEditBox.setMinimumWidth(360)
        # Can switch to a normal dict when our minimum Python is 3.7
        self.key_bindings_strs = OrderedDict()
        self.key_bindings_strs[self.ALL_ACTIVE_KEYBINDINGS] = ''
        theme = get_theme(self.qt_viewer.viewer.theme)
        col = theme['secondary']
        layers = [
            napari.layers.Image,
            napari.layers.Labels,
            napari.layers.Points,
            napari.layers.Shapes,
            napari.layers.Surface,
            napari.layers.Vectors,
        ]
        for layer in layers:
            if len(layer.class_keymap) == 0:
                text = 'No key bindings'
            else:
                text = get_key_bindings_summary(layer.class_keymap, col=col)
            self.key_bindings_strs[f"{layer.__name__} layer"] = text

        # layer type selection
        self.layerTypeComboBox = QComboBox()
        self.layerTypeComboBox.addItems(list(self.key_bindings_strs))
        self.layerTypeComboBox.activated[str].connect(self.change_layer_type)
        self.layerTypeComboBox.setCurrentText(self.ALL_ACTIVE_KEYBINDINGS)
        # self.change_layer_type(current_layer)
        layer_type_layout = QHBoxLayout()
        layer_type_layout.setContentsMargins(10, 5, 0, 0)
        layer_type_layout.addWidget(self.layerTypeComboBox)
        layer_type_layout.addStretch(1)
        layer_type_layout.setSpacing(0)
        self.layout.addLayout(layer_type_layout)
        self.layout.addWidget(self.textEditBox, 1)

        self.viewer.events.active_layer.connect(self.update_active_layer)
        self.viewer.events.theme.connect(self.update_active_layer)
        self.update_active_layer()

    def change_layer_type(self, text):
        """Change layer type selected in dropdown menu.

        Parameters
        ----------
        text : str
            Dictionary key to access key bindings associated with the layer.
            Available keys include:
            - 'All active key bindings'
            - 'Image layer'
            - 'Labels layer'
            - 'Points layer'
            - 'Shapes layer'
            - 'Surface layer'
            - 'Vectors layer'
        """
        self.textEditBox.setHtml(self.key_bindings_strs[text])

    def update_active_layer(self, event=None):
        """Update the active layer and display key bindings for that layer type.

        Parameters
        ----------
        event : napari.utils.event.Event, optional
            The napari event that triggered this method, by default None.
        """
        theme = get_theme(self.qt_viewer.viewer.theme)
        col = theme['secondary']
        # Add class and instance viewer key bindings
        text = get_key_bindings_summary(self.viewer.active_keymap, col=col)

        # Update layer speficic key bindings if all active are displayed
        self.key_bindings_strs[self.ALL_ACTIVE_KEYBINDINGS] = text
        if self.layerTypeComboBox.currentText() == self.ALL_ACTIVE_KEYBINDINGS:
            self.textEditBox.setHtml(text)
コード例 #2
0
ファイル: qt_plugin_report.py プロジェクト: wwymak/napari
class QtPluginErrReporter(QDialog):
    """Dialog that allows users to review and report PluginError tracebacks.

    Parameters
    ----------
    parent : QWidget, optional
        Optional parent widget for this widget.
    initial_plugin : str, optional
        If provided, errors from ``initial_plugin`` will be shown when the
        dialog is created, by default None

    Attributes
    ----------
    text_area : qtpy.QtWidgets.QTextEdit
        The text area where traceback information will be shown.
    plugin_combo : qtpy.QtWidgets.QComboBox
        The dropdown menu used to select the current plugin
    github_button : qtpy.QtWidgets.QPushButton
        A button that, when pressed, will open an issue at the current plugin's
        github issue tracker, prepopulated with a formatted traceback.  Button
        is only visible if a github URL is detected in the package metadata for
        the current plugin.
    clipboard_button : qtpy.QtWidgets.QPushButton
        A button that, when pressed, copies the current traceback information
        to the clipboard.  (HTML tags are removed in the copied text.)
    plugin_meta : qtpy.QtWidgets.QLabel
        A label that will show available plugin metadata (such as home page).
    """

    NULL_OPTION = 'select plugin... '

    def __init__(
        self,
        plugin_manager: Optional[PluginManager] = None,
        *,
        parent: Optional[QWidget] = None,
        initial_plugin: Optional[str] = None,
    ) -> None:
        super().__init__(parent)
        if not plugin_manager:
            from ..plugins import plugin_manager as _pm

            self.plugin_manager = _pm
        else:
            self.plugin_manager = plugin_manager

        self.setWindowTitle('Recorded Plugin Exceptions')
        self.setWindowModality(Qt.NonModal)
        self.layout = QVBoxLayout()
        self.layout.setSpacing(0)
        self.layout.setContentsMargins(10, 10, 10, 10)
        self.setLayout(self.layout)

        self.text_area = QTextEdit()
        self.text_area.setTextInteractionFlags(Qt.TextSelectableByMouse)
        self.text_area.setMinimumWidth(360)

        # Create plugin dropdown menu
        self.plugin_combo = QComboBox()
        self.plugin_combo.addItem(self.NULL_OPTION)
        bad_plugins = [e.plugin_name for e in self.plugin_manager.get_errors()]
        self.plugin_combo.addItems(list(sorted(set(bad_plugins))))
        self.plugin_combo.currentTextChanged.connect(self.set_plugin)
        self.plugin_combo.setCurrentText(self.NULL_OPTION)

        # create github button (gets connected in self.set_plugin)
        self.github_button = QPushButton('Open issue on GitHub', self)
        self.github_button.setToolTip(
            "Open a web browser to submit this error log\n"
            "to the developer's GitHub issue tracker")
        self.github_button.hide()

        # create copy to clipboard button
        self.clipboard_button = QPushButton()
        self.clipboard_button.hide()
        self.clipboard_button.setObjectName("QtCopyToClipboardButton")
        self.clipboard_button.setToolTip("Copy error log to clipboard")
        self.clipboard_button.clicked.connect(self.copyToClipboard)

        # plugin_meta contains a URL to the home page, (and/or other details)
        self.plugin_meta = QLabel('', parent=self)
        self.plugin_meta.setObjectName("pluginInfo")
        self.plugin_meta.setTextFormat(Qt.RichText)
        self.plugin_meta.setTextInteractionFlags(Qt.TextBrowserInteraction)
        self.plugin_meta.setOpenExternalLinks(True)
        self.plugin_meta.setAlignment(Qt.AlignRight)

        # make layout
        row_1_layout = QHBoxLayout()
        row_1_layout.setContentsMargins(11, 5, 10, 0)
        row_1_layout.addStretch(1)
        row_1_layout.addWidget(self.plugin_meta)
        row_2_layout = QHBoxLayout()
        row_2_layout.setContentsMargins(11, 5, 10, 0)
        row_2_layout.addWidget(self.plugin_combo)
        row_2_layout.addStretch(1)
        row_2_layout.addWidget(self.github_button)
        row_2_layout.addWidget(self.clipboard_button)
        row_2_layout.setSpacing(5)
        self.layout.addLayout(row_1_layout)
        self.layout.addLayout(row_2_layout)
        self.layout.addWidget(self.text_area, 1)
        self.setMinimumWidth(750)
        self.setMinimumHeight(600)

        if initial_plugin:
            self.set_plugin(initial_plugin)

    def set_plugin(self, plugin: str) -> None:
        """Set the current plugin shown in the dropdown and text area.

        Parameters
        ----------
        plugin : str
            name of a plugin that has created an error this session.
        """
        self.github_button.hide()
        self.clipboard_button.hide()
        try:
            self.github_button.clicked.disconnect()
        # when disconnecting a non-existent signal
        # PySide2 raises runtimeError, PyQt5 raises TypeError
        except (RuntimeError, TypeError):
            pass

        if not plugin or (plugin == self.NULL_OPTION):
            self.plugin_meta.setText('')
            self.text_area.setHtml('')
            return

        if not self.plugin_manager.get_errors(plugin):
            raise ValueError(f"No errors reported for plugin '{plugin}'")
        self.plugin_combo.setCurrentText(plugin)

        err_string = format_exceptions(plugin, as_html=True)
        self.text_area.setHtml(err_string)
        self.clipboard_button.show()

        # set metadata and outbound links/buttons
        err0 = self.plugin_manager.get_errors(plugin)[0]
        meta = standard_metadata(err0.plugin) if err0.plugin else {}
        meta_text = ''
        if not meta:
            self.plugin_meta.setText(meta_text)
            return

        url = meta.get('url')
        if url:
            meta_text += (
                '<span style="color:#999;">plugin home page:&nbsp;&nbsp;'
                f'</span><a href="{url}" style="color:#999">{url}</a>')
            if 'github.com' in url:

                def onclick():
                    import webbrowser

                    err = format_exceptions(plugin, as_html=False)
                    err = (
                        "<!--Provide detail on the error here-->\n\n\n\n"
                        "<details>\n<summary>Traceback from napari</summary>"
                        f"\n\n```\n{err}\n```\n</details>")
                    url = f'{meta.get("url")}/issues/new?&body={err}'
                    webbrowser.open(url, new=2)

                self.github_button.clicked.connect(onclick)
                self.github_button.show()
        self.plugin_meta.setText(meta_text)

    def copyToClipboard(self) -> None:
        """Copy current plugin traceback info to clipboard as plain text."""
        plugin = self.plugin_combo.currentText()
        err_string = format_exceptions(plugin, as_html=False)
        cb = QGuiApplication.clipboard()
        cb.setText(err_string)
コード例 #3
0
class QtAboutKeybindings(QDialog):

    ALL_ACTIVE_KEYBINDINGS = 'All active keybindings'

    def __init__(self, viewer):
        super().__init__()

        self.viewer = viewer
        self.layout = QVBoxLayout()

        self.setWindowTitle('Keybindings')
        self.setWindowModality(Qt.NonModal)
        self.setLayout(self.layout)

        # stacked keybindings widgets
        self.textEditBox = QTextEdit()
        self.textEditBox.setTextInteractionFlags(Qt.TextSelectableByMouse)
        self.textEditBox.setMinimumWidth(360)
        # Can switch to a normal dict when our minimum Python is 3.7
        self.keybindings_strs = OrderedDict()
        self.keybindings_strs[self.ALL_ACTIVE_KEYBINDINGS] = ''
        col = self.viewer.palette['secondary']
        layers = [
            napari.layers.Image,
            napari.layers.Labels,
            napari.layers.Points,
            napari.layers.Shapes,
            napari.layers.Surface,
            napari.layers.Vectors,
        ]
        for layer in layers:
            if len(layer.class_keymap) == 0:
                text = 'No keybindings'
            else:
                text = get_keybindings_summary(layer.class_keymap, col=col)
            self.keybindings_strs[f"{layer.__name__} layer"] = text

        # layer type selection
        self.layerTypeComboBox = QComboBox()
        self.layerTypeComboBox.addItems(list(self.keybindings_strs))
        self.layerTypeComboBox.activated[str].connect(self.change_layer_type)
        self.layerTypeComboBox.setCurrentText(self.ALL_ACTIVE_KEYBINDINGS)
        # self.change_layer_type(current_layer)
        layer_type_layout = QHBoxLayout()
        layer_type_layout.setContentsMargins(10, 5, 0, 0)
        layer_type_layout.addWidget(self.layerTypeComboBox)
        layer_type_layout.addStretch(1)
        layer_type_layout.setSpacing(0)
        self.layout.addLayout(layer_type_layout)
        self.layout.addWidget(self.textEditBox, 1)

        self.viewer.events.active_layer.connect(self.update_active_layer)
        self.viewer.events.palette.connect(self.update_active_layer)
        self.update_active_layer()

    def change_layer_type(self, text):
        self.textEditBox.setHtml(self.keybindings_strs[text])

    def update_active_layer(self, event=None):
        col = self.viewer.palette['secondary']
        text = ''
        # Add class and instance viewer keybindings
        text += get_keybindings_summary(self.viewer.class_keymap, col=col)
        text += get_keybindings_summary(self.viewer.keymap, col=col)

        layer = self.viewer.active_layer
        if layer is not None:
            # Add class and instance layer keybindings for the active layer
            text += get_keybindings_summary(layer.class_keymap, col=col)
            text += get_keybindings_summary(layer.keymap, col=col)

        # Update layer speficic keybindings if all active are displayed
        self.keybindings_strs[self.ALL_ACTIVE_KEYBINDINGS] = text
        if self.layerTypeComboBox.currentText() == self.ALL_ACTIVE_KEYBINDINGS:
            self.textEditBox.setHtml(text)

    def toggle_visible(self, event):
        if self.isVisible():
            self.hide()
        else:
            self.show()
            self.raise_()