Exemple #1
0
def _copy_table_data(table: QTableWidget) -> None:
    """Copy item text to clipboard."""
    text = table.currentItem().text()
    if text:
        QApplication.clipboard().setText(text)
class ShortcutEditor(QWidget):
    """Widget to edit keybindings for napari."""

    valueChanged = Signal(dict)
    VIEWER_KEYBINDINGS = trans._('Viewer key bindings')

    def __init__(
        self,
        parent: QWidget = None,
        description: str = "",
        value: dict = None,
    ):

        super().__init__(parent=parent)

        # Flag to not run _set_keybinding method after setting special symbols.
        # When changing line edit to special symbols, the _set_keybinding
        # method will be called again (and breaks) and is not needed.
        self._skip = False

        layers = [
            Image,
            Labels,
            Points,
            Shapes,
            Surface,
            Vectors,
        ]

        self.key_bindings_strs = OrderedDict()

        # widgets
        self.layer_combo_box = QComboBox(self)
        self._label = QLabel(self)
        self._table = QTableWidget(self)
        self._table.setSelectionBehavior(QAbstractItemView.SelectItems)
        self._table.setSelectionMode(QAbstractItemView.SingleSelection)
        self._table.setShowGrid(False)
        self._restore_button = QPushButton(trans._("Reset All Keybindings"))

        # Set up dictionary for layers and associated actions.
        all_actions = action_manager._actions.copy()
        self.key_bindings_strs[self.VIEWER_KEYBINDINGS] = []

        for layer in layers:
            if len(layer.class_keymap) == 0:
                actions = []
            else:
                actions = action_manager._get_layer_actions(layer)
                for name, action in actions.items():
                    all_actions.pop(name)
            self.key_bindings_strs[f"{layer.__name__} layer"] = actions

        # Left over actions can go here.
        self.key_bindings_strs[self.VIEWER_KEYBINDINGS] = all_actions

        # Widget set up
        self.layer_combo_box.addItems(list(self.key_bindings_strs))
        self.layer_combo_box.activated[str].connect(self._set_table)
        self.layer_combo_box.setCurrentText(self.VIEWER_KEYBINDINGS)
        self._set_table()
        self._label.setText("Group")
        self._restore_button.clicked.connect(self.restore_defaults)

        # layout
        hlayout1 = QHBoxLayout()
        hlayout1.addWidget(self._label)
        hlayout1.addWidget(self.layer_combo_box)
        hlayout1.setContentsMargins(0, 0, 0, 0)
        hlayout1.setSpacing(20)
        hlayout1.addStretch(0)

        hlayout2 = QHBoxLayout()
        hlayout2.addLayout(hlayout1)
        hlayout2.addWidget(self._restore_button)

        layout = QVBoxLayout()
        layout.addLayout(hlayout2)
        layout.addWidget(self._table)

        self.setLayout(layout)

    def restore_defaults(self):
        """Launches dialog to confirm restore choice."""

        self._reset_dialog = ConfirmDialog(
            parent=self,
            text=trans._(
                "Are you sure you want to restore default shortcuts?"
            ),
        )
        self._reset_dialog.valueChanged.connect(self._reset_shortcuts)
        self._reset_dialog.exec_()

    def _reset_shortcuts(self, event=None):
        """Reset shortcuts to default settings.

        Parameters
        ----------
        event: Bool
            Event will indicate whether user confirmed resetting shortcuts.
        """

        # event is True if the user confirmed reset shortcuts
        if event is True:
            get_settings().reset(sections=['shortcuts'])
            for (
                action,
                shortcuts,
            ) in get_settings().shortcuts.shortcuts.items():
                action_manager.unbind_shortcut(action)
                for shortcut in shortcuts:
                    action_manager.bind_shortcut(action, shortcut)

            self._set_table(layer_str=self.layer_combo_box.currentText())

    def _set_table(self, layer_str=''):
        """Builds and populates keybindings table.

        Parameters
        ----------
        layer_str = str
            If layer_str is not empty, then show the specified layers'
            keybinding shortcut table.
        """

        # Keep track of what is in each column.
        self._action_name_col = 0
        self._icon_col = 1
        self._shortcut_col = 2
        self._action_col = 3

        # Set header strings for table.
        header_strs = ['', '', '', '']
        header_strs[self._action_name_col] = trans._('Action')
        header_strs[self._shortcut_col] = trans._('Keybinding')

        # If no layer_str, then set the page to the viewer keybindings page.
        if layer_str == '':
            layer_str = self.VIEWER_KEYBINDINGS

        # If rebuilding the table, then need to disconnect the connection made
        # previously as well as clear the table contents.
        try:
            self._table.cellChanged.disconnect(self._set_keybinding)
        except TypeError:
            # if building the first time, the cells are not yet connected so this would fail.
            pass
        except RuntimeError:
            # Needed to pass some tests.
            pass

        self._table.clearContents()

        # Table styling set up.
        self._table.horizontalHeader().setStretchLastSection(True)
        self._table.horizontalHeader().setStyleSheet(
            'border-bottom: 2px solid white;'
        )

        # Get all actions for the layer.
        actions = self.key_bindings_strs[layer_str]

        if len(actions) > 0:

            # Set up table based on number of actions and needed columns.
            self._table.setRowCount(len(actions))
            self._table.setColumnCount(4)
            # Set up delegate in order to capture keybindings.
            self._table.setItemDelegateForColumn(
                self._shortcut_col, ShortcutDelegate(self._table)
            )
            self._table.setHorizontalHeaderLabels(header_strs)
            self._table.verticalHeader().setVisible(False)

            # Hide the column with action names.  These are kept here for reference when needed.
            self._table.setColumnHidden(self._action_col, True)

            # Column set up.
            self._table.setColumnWidth(self._action_name_col, 250)
            self._table.setColumnWidth(self._shortcut_col, 200)
            self._table.setColumnWidth(self._icon_col, 50)

            # Go through all the actions in the layer and add them to the table.
            for row, (action_name, action) in enumerate(actions.items()):

                shortcuts = action_manager._shortcuts.get(action_name, [])
                # Set action description.  Make sure its not selectable/editable.
                item = QTableWidgetItem(action.description)
                item.setFlags(Qt.NoItemFlags)
                self._table.setItem(row, self._action_name_col, item)

                # Create empty item in order to make sure this column is not
                # selectable/editable.
                item = QTableWidgetItem("")
                item.setFlags(Qt.NoItemFlags)
                self._table.setItem(row, self._icon_col, item)

                # Set the shortcuts in table.
                item_shortcut = QTableWidgetItem(
                    Shortcut(list(shortcuts)[0]).platform if shortcuts else ""
                )
                self._table.setItem(row, self._shortcut_col, item_shortcut)

                # action_name is stored in the table to use later, but is not shown on dialog.
                item_action = QTableWidgetItem(action_name)
                self._table.setItem(row, self._action_col, item_action)

            # If a cell is changed, run .set_keybinding.
            self._table.cellChanged.connect(self._set_keybinding)
        else:
            # Display that there are no actions for this layer.
            self._table.setRowCount(1)
            self._table.setColumnCount(4)
            self._table.setHorizontalHeaderLabels(header_strs)
            self._table.verticalHeader().setVisible(False)

            self._table.setColumnHidden(self._action_col, True)
            item = QTableWidgetItem('No key bindings')
            item.setFlags(Qt.NoItemFlags)
            self._table.setItem(0, 0, item)

    def _set_keybinding(self, row, col):
        """Checks the new keybinding to determine if it can be set.

        Parameters
        ----------
        row: int
            Row in keybindings table that is being edited.
        col: int
            Column being edited (shortcut column).
        """

        if self._skip is True:
            # Do nothing if the text is setting to a symbol.
            # Its already been handled.
            self._skip = False
            return

        if col == self._shortcut_col:
            # Get all layer actions and viewer actions in order to determine
            # the new shortcut is not already set to an action.

            current_layer_text = self.layer_combo_box.currentText()
            layer_actions = self.key_bindings_strs[current_layer_text]
            actions_all = layer_actions.copy()
            if current_layer_text is not self.VIEWER_KEYBINDINGS:
                viewer_actions = self.key_bindings_strs[
                    self.VIEWER_KEYBINDINGS
                ]

                actions_all.update(viewer_actions)

            # get the current item from shortcuts column
            current_item = self._table.currentItem()
            new_shortcut = current_item.text()
            new_shortcut = new_shortcut[0].upper() + new_shortcut[1:]

            # get the current action name
            current_action = self._table.item(row, self._action_col).text()

            # get the original shortcutS
            current_shortcuts = list(
                action_manager._shortcuts.get(current_action, {})
            )

            # Flag to indicate whether to set the new shortcut.
            replace = True
            # Go through all layer actions to determine if the new shortcut is already here.
            for row1, (action_name, action) in enumerate(actions_all.items()):
                shortcuts = action_manager._shortcuts.get(action_name, [])

                if new_shortcut in shortcuts:
                    # Shortcut is here (either same action or not), don't replace in settings.
                    replace = False
                    if action_name != current_action:
                        # the shortcut is saved to a different action

                        # show warning symbols
                        self._show_warning_icons([row, row1])

                        # show warning message
                        message = trans._(
                            "The keybinding <b>{new_shortcut}</b>  "
                            + "is already assigned to <b>{action_description}</b>; change or clear "
                            + "that shortcut before assigning <b>{new_shortcut}</b> to this one.",
                            new_shortcut=new_shortcut,
                            action_description=action.description,
                        )
                        self._show_warning(new_shortcut, action, row, message)

                        if len(current_shortcuts) > 0:
                            # If there was a shortcut set originally, then format it and reset the text.
                            format_shortcut = Shortcut(
                                current_shortcuts[0]
                            ).platform

                            if format_shortcut != current_shortcuts[0]:
                                # only skip the next round if there are special symbols
                                self._skip = True

                            current_item.setText(format_shortcut)

                        else:
                            # There wasn't a shortcut here.
                            current_item.setText("")

                        self._cleanup_warning_icons([row, row1])

                        break

                    else:
                        # This shortcut was here.  Reformat and reset text.
                        format_shortcut = Shortcut(new_shortcut).platform
                        if format_shortcut != new_shortcut:
                            # Only skip the next round if there are special symbols in shortcut.
                            self._skip = True

                        current_item.setText(format_shortcut)

            if replace is True:
                # This shortcut is not taken.

                #  Unbind current action from shortcuts in action manager.
                action_manager.unbind_shortcut(current_action)

                if new_shortcut != "":
                    # Bind the new shortcut.
                    try:
                        action_manager.bind_shortcut(
                            current_action, new_shortcut
                        )
                    except TypeError:
                        # Shortcut is not valid.
                        action_manager._shortcuts[current_action] = set()
                        # need to rebind the old shortcut
                        action_manager.unbind_shortcut(current_action)
                        action_manager.bind_shortcut(
                            current_action, current_shortcuts[0]
                        )

                        # Show warning message to let user know this shortcut is invalid.
                        self._show_warning_icons([row])

                        message = trans._(
                            "<b>{new_shortcut}</b> is not a valid keybinding.",
                            new_shortcut=new_shortcut,
                        )
                        self._show_warning(new_shortcut, action, row, message)

                        self._cleanup_warning_icons([row])

                        format_shortcut = Shortcut(
                            current_shortcuts[0]
                        ).platform
                        if format_shortcut != current_shortcuts[0]:
                            # Skip the next round if there are special symbols.
                            self._skip = True

                        # Update text to formated shortcut.
                        current_item.setText(format_shortcut)
                        return

                    # The new shortcut is valid and can be displayed in widget.

                    # Keep track of what changed.
                    new_value_dict = {current_action: [new_shortcut]}

                    # Format new shortcut.
                    format_shortcut = Shortcut(new_shortcut).platform
                    if format_shortcut != new_shortcut:
                        # Skip the next round because there are special symbols.
                        self._skip = True

                    # Update text to formated shortcut.
                    current_item.setText(format_shortcut)

                else:
                    # There is not a new shortcut to bind.  Keep track of it.
                    if action_manager._shortcuts[current_action] != "":
                        new_value_dict = {current_action: [""]}

                if new_value_dict:
                    # Emit signal when new value set for shortcut.
                    self.valueChanged.emit(new_value_dict)

    def _show_warning_icons(self, rows):
        """Creates and displays the warning icons.

        Parameters
        ----------
        rows: list[int]
            List of row numbers that should have the icon.
        """

        for row in rows:
            self.warning_indicator = QLabel(self)
            self.warning_indicator.setObjectName("error_label")

            self._table.setCellWidget(
                row, self._icon_col, self.warning_indicator
            )

    def _cleanup_warning_icons(self, rows):
        """Remove the warning icons from the shortcut table.

        Paramters
        ---------
        rows: list[int]
            List of row numbers to remove warning icon from.

        """
        for row in rows:
            self._table.setCellWidget(row, self._icon_col, QLabel(""))

    def _show_warning(self, new_shortcut='', action=None, row=0, message=''):
        """Creates and displays warning message when shortcut is already assigned.

        Parameters
        ----------
        new_shortcut: str
            The new shortcut attempting to be set.
        action: Action
            Action that is already assigned with the shortcut.
        row: int
            Row in table where the shortcut is attempting to be set.
        message: str
            Message to be displayed in warning pop up.
        """

        # Determine placement of warning message.
        delta_y = 105
        delta_x = 10
        global_point = self.mapToGlobal(
            QPoint(
                self._table.columnViewportPosition(self._shortcut_col)
                + delta_x,
                self._table.rowViewportPosition(row) + delta_y,
            )
        )

        # Create warning pop up and move it to desired position.
        self._warn_dialog = KeyBindWarnPopup(
            text=message,
        )
        self._warn_dialog.move(global_point)

        # Styling adjustments.
        self._warn_dialog.resize(250, self._warn_dialog.sizeHint().height())

        self._warn_dialog._message.resize(
            200, self._warn_dialog._message.sizeHint().height()
        )

        self._warn_dialog.exec_()

    def value(self):
        """Return the actions and shortcuts currently assigned in action manager.

        Returns
        -------
        value: dict

            Dictionary of action names and shortcuts assigned to them.
        """

        value = {}

        for row, (action_name, action) in enumerate(
            action_manager._actions.items()
        ):
            shortcuts = action_manager._shortcuts.get(action_name, [])
            value[action_name] = list(shortcuts)

        return value