/* SPDX-FileCopyrightText: Andrew Stanley-Jones SPDX-FileCopyrightText: 2004 Esben Mose Hansen SPDX-FileCopyrightText: 2008 Dmitry Suzdalev SPDX-FileCopyrightText: 2024 Fushan Wen SPDX-License-Identifier: GPL-2.0-or-later */ #include "systemclipboard.h" #include #include #include #include #include #include "../c_ptr.h" #include "config-X11.h" #include "historyitem.h" #include "klipper_debug.h" #include #if HAVE_X11 #include #include #include #endif // HAVE_X11 using namespace std::chrono_literals; namespace { // Protection against too many clipboard data changes. Lyx responds to clipboard data // requests with setting new clipboard data, so if Lyx takes over clipboard, // Klipper notices, requests this data, this triggers "new" clipboard contents // from Lyx, so Klipper notices again, requests this data, ... you get the idea. constexpr auto MAX_CLIPBOARD_CHANGES = 10; // max changes per second bool ignoreClipboardChanges() { // Changing a spinbox in klipper's config-dialog causes the lineedit-contents // of the spinbox to be selected and hence the clipboard changes. But we don't // want all those items in klipper's history. See #41917 const auto app = qobject_cast(QCoreApplication::instance()); if (!app) { return false; } QWidget *focusWidget = app->focusWidget(); if (focusWidget) { if (focusWidget->inherits("QSpinBox") || (focusWidget->parentWidget() && focusWidget->inherits("QLineEdit") && focusWidget->parentWidget()->inherits("QSpinWidget"))) { return true; } } return false; } void x11RoundTrip() { #if HAVE_X11 if (auto interface = qGuiApp->nativeInterface()) { const auto cookie = xcb_get_input_focus(interface->connection()); xcb_generic_error_t *error = nullptr; UniqueCPointer sync(xcb_get_input_focus_reply(interface->connection(), cookie, &error)); if (error) { free(error); } } #endif } } std::shared_ptr SystemClipboard::self() { static std::weak_ptr s_clip; if (s_clip.expired()) { std::shared_ptr ptr{new SystemClipboard}; s_clip = ptr; return ptr; } return s_clip.lock(); } SystemClipboard::SystemClipboard() : QObject(nullptr) , m_clip(KSystemClipboard::instance()) { x11RoundTrip(); // read initial X user time connect(m_clip, &KSystemClipboard::changed, this, &SystemClipboard::checkClipData); m_pendingCheckTimer.setSingleShot(true); connect(&m_pendingCheckTimer, &QTimer::timeout, this, &SystemClipboard::slotCheckPending); connect(&m_overflowClearTimer, &QTimer::timeout, this, &SystemClipboard::slotClearOverflow); } SystemClipboard::~SystemClipboard() { } void SystemClipboard::clear(SelectionMode mode) { Q_ASSERT((mode & 1) == 0); // Warn if trying to pass a boolean as a mode. if (mode & Selection) { Ignore lock(m_selectionLocklevel); m_clip->clear(QClipboard::Selection); } if (mode & Clipboard) { Ignore lock(m_clipboardLocklevel); m_clip->clear(QClipboard::Clipboard); } } void SystemClipboard::setMimeData(const HistoryItemConstPtr &data, SelectionMode mode, ClipboardUpdateReason updateReason) { Q_ASSERT((mode & 1) == 0); // Warn if trying to pass a boolean as a mode. if (mode & Selection) { Ignore lock(m_selectionLocklevel); QMimeData *mimeData = data->mimeData(); if (updateReason == ClipboardUpdateReason::PreventEmptyClipboard) { mimeData->setData(QStringLiteral("application/x-kde-onlyReplaceEmpty"), "1"); } qCDebug(KLIPPER_LOG) << "Setting selection to <" << mimeData->text() << ">"; m_clip->setMimeData(mimeData, QClipboard::Selection); } if (mode & Clipboard) { QMimeData *mimeData = data->mimeData(); // NOTE: this has to be a new mimeData because setMimeData will take ownership if (updateReason == ClipboardUpdateReason::PreventEmptyClipboard) { mimeData->setData(QStringLiteral("application/x-kde-onlyReplaceEmpty"), "1"); } else if (updateReason == ClipboardUpdateReason::SyncSelection) { // When plasmashell is not focused, klipper will not receive new clip data immediately. This type is used to filter out selections. mimeData->setData(QStringLiteral("application/x-kde-syncselection"), "1"); } qCDebug(KLIPPER_LOG) << "Setting clipboard to <" << mimeData->text() << ">"; QMetaObject::invokeMethod( this, [this, mimeData]() { Ignore lock(m_clipboardLocklevel); m_clip->setMimeData(mimeData, QClipboard::Clipboard); }, updateReason == ClipboardUpdateReason::SyncSelection ? Qt::QueuedConnection : Qt::DirectConnection); } } bool SystemClipboard::isLocked(QClipboard::Mode mode) { return mode == QClipboard::Selection ? m_selectionLocklevel : m_clipboardLocklevel; } void SystemClipboard::checkClipData(QClipboard::Mode mode) { if ((mode == QClipboard::Clipboard && m_clipboardLocklevel) || (mode == QClipboard::Selection && m_selectionLocklevel)) { return; } if (mode == QClipboard::Selection && blockFetchingNewData()) { return; } Ignore lock(mode == QClipboard::Selection ? m_selectionLocklevel : m_clipboardLocklevel); // internal to klipper, ignoring QSpinBox selections if (ignoreClipboardChanges()) { // keep our old clipboard, thanks // This won't quite work, but it's close enough for now. // The trouble is that the top selection =! top clipboard // but we don't track that yet. We will.... Q_EMIT ignored(mode); return; } qCDebug(KLIPPER_LOG) << "Checking clip data"; const QMimeData *data = m_clip->mimeData(mode); if (!data) { Q_EMIT receivedEmptyClipboard(mode); return; } else if (data->formats().isEmpty()) { // Might be a timeout. Try again x11RoundTrip(); data = m_clip->mimeData(mode); if (!data || data->formats().isEmpty()) { qCDebug(KLIPPER_LOG) << "was empty. Retried, now still empty"; Q_EMIT receivedEmptyClipboard(mode); return; } } if (!data->hasUrls() && !data->hasText() && !data->hasImage()) { return; // unknown, ignore } if (data->hasFormat(QStringLiteral("application/x-kde-syncselection"))) { return; } Q_ASSERT_X((mode == QClipboard::Clipboard && m_clipboardLocklevel == 1) || (mode == QClipboard::Selection && m_selectionLocklevel == 1), Q_FUNC_INFO, qPrintable(QStringLiteral("%1 %2").arg(QString::number(m_clipboardLocklevel), QString::number(m_selectionLocklevel)))); Q_EMIT newClipData(mode, data); } void SystemClipboard::slotClearOverflow() { m_overflowClearTimer.stop(); if (m_overflowCounter > MAX_CLIPBOARD_CHANGES) { qCDebug(KLIPPER_LOG) << "App owning the clipboard/selection is lame"; // update to the latest data - this unfortunately may trigger the problem again if (!m_selectionLocklevel && blockFetchingNewData()) { checkClipData(QClipboard::Selection); // Always selection } } m_overflowCounter = 0; } void SystemClipboard::slotCheckPending() { if (!m_pendingContentsCheck) { return; } m_pendingContentsCheck = false; // blockFetchingNewData() will be called again x11RoundTrip(); checkClipData(QClipboard::Selection); // Always selection } bool SystemClipboard::blockFetchingNewData() { #if HAVE_X11 // Hacks for #85198 and #80302. // #85198 - block fetching new clipboard contents if Shift is pressed and mouse is not, // this may mean the user is doing selection using the keyboard, in which case // it's possible the app sets new clipboard contents after every change - Klipper's // history would list them all. // #80302 - OOo (v1.1.3 at least) has a bug that if Klipper requests its clipboard contents // while the user is doing a selection using the mouse, OOo stops updating the clipboard // contents, so in practice it's like the user has selected only the part which was // selected when Klipper asked first. // Use XQueryPointer rather than QApplication::mouseButtons()/keyboardModifiers(), because // Klipper needs the very current state. auto interface = qGuiApp->nativeInterface(); if (!interface) { return false; } xcb_connection_t *c = interface->connection(); const xcb_query_pointer_cookie_t cookie = xcb_query_pointer_unchecked(c, DefaultRootWindow(interface->display())); UniqueCPointer queryPointer(xcb_query_pointer_reply(c, cookie, nullptr)); if (!queryPointer) { return false; } if (((queryPointer->mask & (XCB_KEY_BUT_MASK_SHIFT | XCB_KEY_BUT_MASK_BUTTON_1)) == XCB_KEY_BUT_MASK_SHIFT) // BUG: 85198 || ((queryPointer->mask & XCB_KEY_BUT_MASK_BUTTON_1) == XCB_KEY_BUT_MASK_BUTTON_1)) { // BUG: 80302 m_pendingContentsCheck = true; m_pendingCheckTimer.start(100ms); return true; } m_pendingContentsCheck = false; if (m_overflowCounter == 0) { m_overflowClearTimer.start(1s); } if (++m_overflowCounter > MAX_CLIPBOARD_CHANGES) { return true; } #endif return false; } #include "moc_systemclipboard.cpp"