diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 822cfddc7..5166a0083 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -669,29 +669,6 @@ void DatabaseWidget::copyUsername() void DatabaseWidget::copyPassword() { - // Some platforms do not properly trap Ctrl+C copy shortcut - // if a text edit or label has focus pass the copy operation to it - - bool clearClipboard = config()->get(Config::Security_ClearClipboard).toBool(); - - auto plainTextEdit = qobject_cast(focusWidget()); - if (plainTextEdit && plainTextEdit->textCursor().hasSelection()) { - clipboard()->setText(plainTextEdit->textCursor().selectedText(), clearClipboard); - return; - } - - auto label = qobject_cast(focusWidget()); - if (label && label->hasSelectedText()) { - clipboard()->setText(label->selectedText(), clearClipboard); - return; - } - - auto textEdit = qobject_cast(focusWidget()); - if (textEdit && textEdit->textCursor().hasSelection()) { - clipboard()->setText(textEdit->textCursor().selection().toPlainText(), clearClipboard); - return; - } - auto currentEntry = currentSelectedEntry(); if (currentEntry) { setClipboardTextAndMinimize(currentEntry->resolveMultiplePlaceholders(currentEntry->password())); @@ -732,6 +709,34 @@ void DatabaseWidget::copyAttribute(QAction* action) } } +bool DatabaseWidget::copyFocusedTextSelection() +{ + // If a focused child widget has text selected, copy that text to the clipboard + // and return true. Otherwise, return false. + + const bool clearClipboard = config()->get(Config::Security_ClearClipboard).toBool(); + + const auto plainTextEdit = qobject_cast(focusWidget()); + if (plainTextEdit && plainTextEdit->textCursor().hasSelection()) { + clipboard()->setText(plainTextEdit->textCursor().selectedText(), clearClipboard); + return true; + } + + const auto label = qobject_cast(focusWidget()); + if (label && label->hasSelectedText()) { + clipboard()->setText(label->selectedText(), clearClipboard); + return true; + } + + const auto textEdit = qobject_cast(focusWidget()); + if (textEdit && textEdit->textCursor().hasSelection()) { + clipboard()->setText(textEdit->textCursor().selection().toPlainText(), clearClipboard); + return true; + } + + return false; +} + void DatabaseWidget::filterByTag() { QStringList searchTerms; diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index 1a562cb4c..08b4ec81c 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -176,6 +176,7 @@ public slots: void copyURL(); void copyNotes(); void copyAttribute(QAction* action); + bool copyFocusedTextSelection(); void filterByTag(); void setTag(QAction* action); void showTotp(); diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 4d4f2c23a..57c58cb16 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -77,6 +77,28 @@ #include "mainwindowadaptor.h" #endif +// This filter gets installed on all the QAction objects within the MainWindow. +bool ActionEventFilter::eventFilter(QObject* watched, QEvent* event) +{ + auto databaseWidget = getMainWindow()->m_ui->tabWidget->currentDatabaseWidget(); + if (databaseWidget && event->type() == QEvent::Shortcut) { + // We check if we got a Shortcut event that uses the same key sequence as the + // OS default copy-to-clipboard shortcut. + static const auto stdCopyShortcuts = QKeySequence::keyBindings(QKeySequence::Copy); + if (stdCopyShortcuts.contains(static_cast(event)->key())) { + // If so, we ask the database widget to check if any of its sub-widgets has text + // selected, and to copy it to the clipboard if that is the case. We do this + // because that is what the user likely expects to happen, yet Qt does not + // behave like that on all platforms. + if (databaseWidget->copyFocusedTextSelection()) { + // In that case, we return true to stop further processing of this event. + return true; + } + } + } + return QObject::eventFilter(watched, event); +} + const QString MainWindow::BaseWindowTitle = "KeePassXC"; MainWindow* g_MainWindow = nullptr; @@ -450,6 +472,9 @@ MainWindow::MainWindow() m_ui->actionEntryRemovePasskey->setIcon(icons()->icon("document-close")); #endif + // Handle copy to clipboard shortcut interference + m_ui->actionEntryCopyPassword->installEventFilter(&m_actionEventFilter); + m_actionMultiplexer.connect( SIGNAL(currentModeChanged(DatabaseWidget::Mode)), this, SLOT(setMenuActionState(DatabaseWidget::Mode))); m_actionMultiplexer.connect(SIGNAL(groupChanged()), this, SLOT(setMenuActionState())); diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index 1f96a6ec0..eacbb303c 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -39,6 +39,14 @@ class InactivityTimer; class SearchWidget; class MainWindowEventFilter; +class ActionEventFilter : public QObject +{ + Q_OBJECT + +protected: + bool eventFilter(QObject* obj, QEvent* event) override; +}; + class MainWindow : public QMainWindow { Q_OBJECT @@ -169,6 +177,7 @@ private: const QScopedPointer m_ui; SignalMultiplexer m_actionMultiplexer; + ActionEventFilter m_actionEventFilter; QPointer m_clearHistoryAction; QPointer m_searchWidgetAction; QPointer m_entryContextMenu; @@ -200,6 +209,7 @@ private: QTimer m_trayIconTriggerTimer; QSystemTrayIcon::ActivationReason m_trayIconTriggerReason; + friend class ActionEventFilter; friend class MainWindowEventFilter; }; diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index eebc66300..8e46587b7 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -1045,6 +1045,19 @@ void TestGui::testSearch() searchTextEdit->selectAll(); QTest::keyClick(searchTextEdit, Qt::Key_C, Qt::ControlModifier); QTRY_COMPARE(clipboard->text(), QString("someTHING")); + // Ensure password copies when clicking on copy password button despite selected text + auto copyPasswordAction = m_mainWindow->findChild("actionEntryCopyPassword"); + QVERIFY(copyPasswordAction); + auto copyPasswordWidget = toolBar->widgetForAction(copyPasswordAction); + QVERIFY(copyPasswordWidget); + QTest::mouseClick(copyPasswordWidget, Qt::LeftButton); + QCOMPARE(clipboard->text(), searchedEntry->password()); + // Deselect text and deselect entry, Ctrl+C should now do nothing + clipboard->clear(); + QTest::mouseClick(searchTextEdit, Qt::LeftButton); + entryView->clearSelection(); + QTest::keyClick(searchTextEdit, Qt::Key_C, Qt::ControlModifier); + QCOMPARE(clipboard->text(), QString()); // Test case sensitive search searchWidget->setCaseSensitive(true);