Line data Source code
1 : // SPDX-FileCopyrightText: 2026 Anne Jan Brouwer
2 : // SPDX-License-Identifier: GPL-3.0-or-later
3 : /**
4 : * @file
5 : * @brief Implements SecretLineEdit: the four copy paths (Ctrl+C, Cut, context
6 : * menu, X11 primary selection) routed through buildClipboardMimeData().
7 : */
8 : #include "secretlineedit.h"
9 : #include "clipboardmanager.h"
10 : #include <QAction>
11 : #include <QApplication>
12 : #include <QContextMenuEvent>
13 : #include <QKeyEvent>
14 : #include <QMenu>
15 : #include <QMouseEvent>
16 :
17 181 : SecretLineEdit::SecretLineEdit(QWidget *parent) : QLineEdit(parent) {}
18 :
19 2 : auto SecretLineEdit::createSecretContextMenu() -> QMenu * {
20 2 : QMenu *menu = createStandardContextMenu();
21 16 : for (QAction *action : menu->actions()) {
22 : // The standard actions call QLineEdit::copy() and cut(), which are not
23 : // virtual.
24 24 : if (action->objectName() == QLatin1String("edit-copy")) {
25 2 : disconnect(action, &QAction::triggered, this, nullptr);
26 2 : connect(action, &QAction::triggered, this,
27 5 : [this] { copySelection(QClipboard::Clipboard); });
28 20 : } else if (action->objectName() == QLatin1String("edit-cut")) {
29 1 : disconnect(action, &QAction::triggered, this, nullptr);
30 2 : connect(action, &QAction::triggered, this, [this] { cutSelection(); });
31 : }
32 : }
33 2 : return menu;
34 : }
35 :
36 133 : void SecretLineEdit::keyPressEvent(QKeyEvent *event) {
37 133 : if (event->matches(QKeySequence::Copy)) {
38 6 : copySelection(QClipboard::Clipboard);
39 : event->accept();
40 6 : return;
41 : }
42 127 : if (event->matches(QKeySequence::Cut)) {
43 3 : cutSelection();
44 : event->accept();
45 3 : return;
46 : }
47 : // QLineEdit copies what these delete (Ctrl+K and Ctrl+U with the X11 key
48 : // bindings), as a cut.
49 124 : const bool toEnd = event->matches(QKeySequence::DeleteEndOfLine);
50 124 : if (toEnd || event->matches(QKeySequence::DeleteCompleteLine)) {
51 0 : if (!isReadOnly()) {
52 0 : const int from = toEnd ? cursorPosition() : 0;
53 0 : setSelection(from, static_cast<int>(text().size()) - from);
54 0 : cutSelection();
55 : }
56 : event->accept();
57 0 : return;
58 : }
59 372 : withoutPlainSelectionCopy([this, event] { QLineEdit::keyPressEvent(event); });
60 : // Where QLineEdit would have copied: after a key it handled, not after a
61 : // lone modifier or another key it ignores.
62 124 : if (event->isAccepted()) {
63 107 : publishSelection();
64 : }
65 : }
66 :
67 0 : void SecretLineEdit::mouseReleaseEvent(QMouseEvent *event) {
68 0 : withoutPlainSelectionCopy(
69 0 : [this, event] { QLineEdit::mouseReleaseEvent(event); });
70 0 : if (event->button() == Qt::LeftButton) {
71 0 : publishSelection();
72 : }
73 0 : }
74 :
75 0 : void SecretLineEdit::contextMenuEvent(QContextMenuEvent *event) {
76 0 : QMenu *menu = createSecretContextMenu();
77 0 : menu->setAttribute(Qt::WA_DeleteOnClose);
78 0 : menu->popup(event->globalPos());
79 0 : }
80 :
81 10 : void SecretLineEdit::copySelection(QClipboard::Mode mode) {
82 : // As QLineEdit itself: nothing leaves a field that shows dots.
83 10 : if (echoMode() != QLineEdit::Normal || !hasSelectedText()) {
84 2 : return;
85 : }
86 8 : const QString text = selectedText();
87 8 : QApplication::clipboard()->setMimeData(buildClipboardMimeData(text), mode);
88 8 : emit copied(text);
89 : }
90 :
91 4 : void SecretLineEdit::cutSelection() {
92 : // As QLineEdit::cut(): a field that shows dots deletes without copying.
93 4 : if (isReadOnly() || !hasSelectedText()) {
94 1 : return;
95 : }
96 3 : copySelection(QClipboard::Clipboard);
97 3 : del();
98 : }
99 :
100 124 : void SecretLineEdit::withoutPlainSelectionCopy(
101 : const std::function<void()> &handler) {
102 : // QLineEdit fills the primary selection with plain text itself, at the end
103 : // of every key it handles and on a left-button release, but only in Normal
104 : // echo mode (QWidgetLineControl::copy()). Replacing that afterwards still
105 : // leaves a moment in which a clipboard manager could take the unmarked
106 : // text, so the base handler runs in Password mode instead: same length,
107 : // so cursor and selection stay where they are, and no copy.
108 : // Editable, the field keeps Normal mode (see the class description) and
109 : // publishSelection() replaces the plain copy.
110 124 : if (echoMode() != QLineEdit::Normal || !isReadOnly()) {
111 122 : handler();
112 122 : return;
113 : }
114 2 : setEchoMode(QLineEdit::Password);
115 2 : handler();
116 2 : setEchoMode(QLineEdit::Normal);
117 : }
118 :
119 107 : void SecretLineEdit::publishSelection() {
120 107 : if (QApplication::clipboard()->supportsSelection()) {
121 0 : copySelection(QClipboard::Selection);
122 : }
123 107 : }
|