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)
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: ' 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)
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_()