diff --git a/qt/pinlineedit.cpp b/qt/pinlineedit.cpp index 49751c6..286195d 100644 --- a/qt/pinlineedit.cpp +++ b/qt/pinlineedit.cpp @@ -1,221 +1,232 @@ /* pinlineedit.cpp - Modified QLineEdit widget. * Copyright (C) 2018 Damien Goutte-Gattat * Copyright (C) 2021 g10 Code GmbH * * Software engineering by Ingo Klöcker * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . * SPDX-License-Identifier: GPL-2.0+ */ #include "pinlineedit.h" #include #include #include static const int FormattedPassphraseGroupSize = 5; static const QChar FormattedPassphraseSeparator = QChar::Nbsp; namespace { struct Selection { bool empty() const { return start < 0 || start >= end; } int length() const { return empty() ? 0 : end - start; } int start; int end; }; } class PinLineEdit::Private { PinLineEdit *const q; public: Private(PinLineEdit *q) : q{q} {} QString formatted(QString text) const { const int dashCount = text.size() / FormattedPassphraseGroupSize; text.reserve(text.size() + dashCount); for (int i = FormattedPassphraseGroupSize; i < text.size(); i += FormattedPassphraseGroupSize + 1) { text.insert(i, FormattedPassphraseSeparator); } return text; } Selection formattedSelection(Selection selection) const { if (selection.empty()) { return selection; } return { selection.start + selection.start / FormattedPassphraseGroupSize, selection.end + (selection.end - 1) / FormattedPassphraseGroupSize }; } QString unformatted(QString text) const { for (int i = FormattedPassphraseGroupSize; i < text.size(); i += FormattedPassphraseGroupSize) { text.remove(i, 1); } return text; } Selection unformattedSelection(Selection selection) const { if (selection.empty()) { return selection; } return { selection.start - selection.start / (FormattedPassphraseGroupSize + 1), selection.end - selection.end / (FormattedPassphraseGroupSize + 1) }; } void copyToClipboard() { if (q->echoMode() != QLineEdit::Normal) { return; } QString text = q->selectedText(); if (mFormattedPassphrase) { text.remove(FormattedPassphraseSeparator); } if (!text.isEmpty()) { QGuiApplication::clipboard()->setText(text); } } int selectionEnd() { #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) return q->selectionEnd(); #else return q->selectionStart() + q->selectedText().size(); #endif } public: bool mFormattedPassphrase = false; }; PinLineEdit::PinLineEdit(QWidget *parent) : QLineEdit(parent) , d{new Private{this}} { connect(this, SIGNAL(textEdited(QString)), this, SLOT(textEdited())); } PinLineEdit::~PinLineEdit() = default; void PinLineEdit::setFormattedPassphrase(bool on) { if (on == d->mFormattedPassphrase) { return; } d->mFormattedPassphrase = on; Selection selection{selectionStart(), d->selectionEnd()}; if (d->mFormattedPassphrase) { setText(d->formatted(text())); selection = d->formattedSelection(selection); } else { setText(d->unformatted(text())); selection = d->unformattedSelection(selection); } if (!selection.empty()) { setSelection(selection.start, selection.length()); } } void PinLineEdit::copy() const { d->copyToClipboard(); } void PinLineEdit::cut() { if (hasSelectedText()) { copy(); del(); } } void PinLineEdit::setPin(const QString &pin) { setText(d->mFormattedPassphrase ? d->formatted(pin) : pin); } QString PinLineEdit::pin() const { if (d->mFormattedPassphrase) { return d->unformatted(text()); } else { return text(); } } void PinLineEdit::keyPressEvent(QKeyEvent *e) { if (e == QKeySequence::Copy) { copy(); return; } else if (e == QKeySequence::Cut) { if (!isReadOnly() && hasSelectedText()) { copy(); del(); } return; } else if (e == QKeySequence::DeleteEndOfLine) { if (!isReadOnly()) { setSelection(cursorPosition(), text().size()); copy(); del(); } return; } else if (e == QKeySequence::DeleteCompleteLine) { if (!isReadOnly()) { setSelection(0, text().size()); copy(); del(); } return; } QLineEdit::keyPressEvent(e); if (e->key() == Qt::Key::Key_Backspace) { emit backspacePressed(); } } void PinLineEdit::textEdited() { if (!d->mFormattedPassphrase) { return; } - setText(d->formatted(text().remove(FormattedPassphraseSeparator))); + auto currentText = text(); + // first calculate the cursor position in the reformatted text; the cursor + // is put left of the separators, so that backspace works as expected + auto cursorPos = cursorPosition(); + cursorPos -= QStringView{currentText}.left(cursorPos).count(FormattedPassphraseSeparator); + cursorPos += std::max(cursorPos - 1, 0) / FormattedPassphraseGroupSize; + // then reformat the text + currentText.remove(FormattedPassphraseSeparator); + currentText = d->formatted(currentText); + // finally, set reformatted text and updated cursor position + setText(currentText); + setCursorPosition(cursorPos); } #include "pinlineedit.moc"