Adding secondary functionality to arrow keys in QLineEdit besides moving cursor QT C++

451 Views Asked by At

I am writing a gui for a basic hex editor in QT and have a QGridLayout that contains 64 QLineEdits. I have it working that when I use the up and down arrow keys, the focus will shift to the lineEdit that is directly above or below respectively. The left and right functionality is written to be similar, but the focus only shifts to the previous/next lineEdit if the cursorPosition is at index 0 or 2 respectively (each lineEdit is limited to 2 characters). My issue is, the left and right arrow keys are only able to change the cursorPosition. If I remap the functionality for left and right to the up and down keys, the function works as intended.

How can I give the left and right arrow keys a secondary functionality given that the cursorPosition is at the correct position

Here is the event filter:

bool autoGenWidget::eventFilter(QObject *object, QEvent *event)
{
    if(event->type() == QEvent::KeyPress)
    {
      QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
      if(keyEvent->key() == Qt::Key_Left && (focusedLineEdit->cursorPosition() == 0))
      {
          arrowKeyNavigation(focusRow, focusCol, arrowLeft);
      }
      else if(keyEvent->key() == Qt::Key_Right && (focusedLineEdit->cursorPosition() == 2))
      {
          arrowKeyNavigation(focusRow, focusCol, arrowRight);
      }
      else if(keyEvent->key() == Qt::Key_Up)
      {
          int cursor = focusedLineEdit->cursorPosition();
          arrowKeyNavigation(focusRow, focusCol, arrowUp);

      }
      else if(keyEvent->key() == Qt::Key_Down)
      {
          arrowKeyNavigation(focusRow, focusCol, arrowDown);
      }

      return true;
    }

    return false;
}

And here is the navigation function

void autoGenWidget::arrowKeyNavigation(int row, int col, int state)
{
    if(state == arrowLeft && focusedLineEdit->cursorPosition() == 0)
    {
        if(numRows - 1 > row && col == 0)
        {
            ui->dataGridLayout->itemAtPosition(row, 15)->widget()->setFocus();
            focusedLineEdit->setCursorPosition(2);
        }
        else if(numRows - 1 == row && col == 0)
        {
            ui->dataGridLayout->itemAtPosition(row, numBottomRowCols - 1)->widget()->setFocus();
            focusedLineEdit->setCursorPosition(2);
        }
        else if(col != 0)
        {
            ui->dataGridLayout->itemAtPosition(row, col - 1)->widget()->setFocus();
            focusedLineEdit->setCursorPosition(2);
        }
    }
    else if(state == arrowRight && focusedLineEdit->cursorPosition() == 2)
    {
        if((numRows - 1 > row && col == 15) || (numRows - 1 == row && col == numBottomRowCols - 1))
        {
            ui->dataGridLayout->itemAtPosition(row, 0)->widget()->setFocus();
            focusedLineEdit->setCursorPosition(0);
        }
        else if(col != 15)
        {
            ui->dataGridLayout->itemAtPosition(row, col + 1)->widget()->setFocus();
            focusedLineEdit->setCursorPosition(0);
        }
    }
    else if(state == arrowUp)
    {
        if(row == 0 && col <= numBottomRowCols - 1)
        {
           ui->dataGridLayout->itemAtPosition(numRows - 1, col)->widget()->setFocus();
           focusedLineEdit->setCursorPosition(0);
        }
        else if(row == 0 && col > numBottomRowCols - 1)
        {
            ui->dataGridLayout->itemAtPosition(numRows - 2, col)->widget()->setFocus();
            focusedLineEdit->setCursorPosition(0);
        }
        else if(row > 0)
        {
            ui->dataGridLayout->itemAtPosition(row - 1, col)->widget()->setFocus();
            focusedLineEdit->setCursorPosition(0);
        }
    }
    else if(state == arrowDown)
    {
        if(row == numRows - 1 || (row == numRows - 2 && col > numBottomRowCols - 1))
        {
           ui->dataGridLayout->itemAtPosition(0, col)->widget()->setFocus();
           focusedLineEdit->setCursorPosition(0);
        }
        else
        {
            ui->dataGridLayout->itemAtPosition(row + 1, col)->widget()->setFocus();
            focusedLineEdit->setCursorPosition(0);
        }
    }
}

Again, the functionality works as intended but the left and right arrow keys do not. Any help would be greatly appreciated.

1

There are 1 best solutions below

0
musicamante On

While the question title refers to C++, the tags also include PyQt, so I'm providing a possible solution for that.

I would start by creating a QLineEdit subclass that ignores key events based on the key and the cursor position:

class HexLineEdit(QtWidgets.QLineEdit):
    hexValidator = QtGui.QRegExpValidator(QtCore.QRegExp(
        r'[{}]{{0,2}}'.format(HexLetters)))
    def __init__(self, parent=None):
        super().__init__(parent, maxLength=2, frame=False)
        self.setValidator(self.hexValidator)

    def keyPressEvent(self, event):
        if (
                (event.key() in (QtCore.Qt.Key_Right, QtCore.Qt.Key_Home)
                    and self.cursorPosition() == len(self.text()))
                or
                (event.key() in (QtCore.Qt.Key_Left, QtCore.Qt.Key_End)
                    and self.cursorPosition() == 0)
            ):
                event.ignore()
                return
        super().keyPressEvent(event)

Considering this, the parent will always process key events that are ignored by the children, so it's just a matter of finding the current grid "cell" of the focus widget and then setting the focus based on the key:

class HexGrid(QtWidgets.QWidget):
    NavigationKeys = (
        QtCore.Qt.Key_Left, QtCore.Qt.Key_Right, 
        QtCore.Qt.Key_Up, QtCore.Qt.Key_Down, 
        QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown, 
        QtCore.Qt.Key_Home, QtCore.Qt.Key_End
    )
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        layout = QtWidgets.QGridLayout(self)
        layout.setSpacing(1)
        for r in range(16):
            for c in range(32):
                layout.addWidget(HexLineEdit(), r, c)

    def keyPressEvent(self, event):
        if not event.key() in self.NavigationKeys:
            super().keyPressEvent(event)
            return

        editor = self.focusWidget()
        layout = self.layout()
        if not editor:
            layout.itemAtPosition(0, 0).widget().setFocus()
            return
        
        maxRow = layout.rowCount() - 1
        maxColumn = layout.columnCount() - 1
        row, column, rs, cs = layout.getItemPosition(
            layout.indexOf(editor))

        key = event.key()

        if key == QtCore.Qt.Key_Left:
            column -= 1
            if column < 0 and row:
                column = maxColumn
                row -= 1
        elif key == QtCore.Qt.Key_Right:
            column += 1
            if column >= maxColumn and row < maxRow:
                column = 0
                row += 1
        elif key == QtCore.Qt.Key_Up:
            row -= 1
            if row < 0 and column:
                row = maxRow
                column -= 1
        elif key == QtCore.Qt.Key_Down:
            row += 1
            if row >= maxRow and column < maxColumn:
                row = 0
                column += 1
        elif key == QtCore.Qt.Key_PageUp:
            if event.modifiers() == QtCore.Qt.ControlModifier:
                row = 0
            else:
                row -= 8
        elif key == QtCore.Qt.Key_PageDown:
            if event.modifiers() == QtCore.Qt.ControlModifier:
                row = maxRow
            else:
                row += 8
        elif key == QtCore.Qt.Key_Home:
            column = 0
            if event.modifiers() == QtCore.Qt.ControlModifier:
                row = 0
        elif key == QtCore.Qt.Key_End:
            column = maxColumn
            if event.modifiers() == QtCore.Qt.ControlModifier:
                row = maxRow

        row = max(0, min(row, maxRow))
        column = max(0, min(column, maxColumn))
        layout.itemAtPosition(row, column).widget().setFocus()

This implementation works, but I would suggest another approach, that could add more functionality to the whole editor, and that's by using a QTableWidget (or a QTableView with a custom QAbstractTableModel that is used to map the hex data).

Not only we can still use the QLineEdit subclass above, but the whole keyboard navigation is automatically implemented in the view, and also allows easy switching of the current cell.

class HexDelegate(QtWidgets.QStyledItemDelegate):
    def createEditor(self, parent, opt, index):
        return HexLineEdit(parent)

    def eventFilter(self, editor, event):
        if event.type() == event.KeyPress:
            if event.text() and event.text() in HexLetters:
                editor.event(event)
                if editor.cursorPosition() == 2:
                    self.commitData.emit(editor)
                    self.closeEditor.emit(editor, self.EditNextItem)
                return True
            elif (event.key() == QtCore.Qt.Key_Backspace
                and editor.cursorPosition() == 0):
                    self.commitData.emit(editor)
                    self.closeEditor.emit(editor, self.EditPreviousItem)
        return super().eventFilter(editor, event)


class HexGrid(QtWidgets.QTableWidget):
    def __init__(self):
        super().__init__(16, 32)
        self.setItemDelegate(HexDelegate(self))
        fm = self.fontMetrics()
        minWidth = 0
        for l in HexLetters:
            minWidth = max(minWidth, fm.boundingRect(l).width())
        margin = self.style().pixelMetric(
            QtWidgets.QStyle.PM_HeaderMargin, None, self) * 2
        self.horizontalHeader().setDefaultSectionSize(minWidth + margin)
        self.verticalHeader().setDefaultSectionSize(fm.height() + margin)
        for r in range(16):
            for c in range(32):
                # use the default QTableView implementation, otherwise we would
                # need to create items for each cell; since QTableWidget
                # already does it on its own, let's make things simpler.
                QtWidgets.QTableView.openPersistentEditor(self, 
                    self.model().index(r, c))