/* SPDX-FileCopyrightText: Andrew Stanley-Jones SPDX-FileCopyrightText: 2000 Carsten Pfeiffer SPDX-FileCopyrightText: 2004 Esben Mose Hansen SPDX-FileCopyrightText: 2008 Dmitry Suzdalev SPDX-License-Identifier: GPL-2.0-or-later */ #include "klipper.h" #include #include "klipper_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "configdialog.h" #include "history.h" #include "historyitem.h" #include "historymodel.h" #include "historystringitem.h" #include "klipperpopup.h" #include "klippersettings.h" #include #include #if HAVE_X11 #include #include #endif namespace { /** * Use this when manipulating the clipboard * from within clipboard-related signals. * * This avoids issues such as mouse-selections that immediately * disappear. * pattern: Resource Acquisition is Initialisation (RAII) * * (This is not threadsafe, so don't try to use such in threaded * applications). */ struct Ignore { Ignore(int &locklevel) : locklevelref(locklevel) { locklevelref++; } ~Ignore() { locklevelref--; } private: int &locklevelref; }; } ClipboardContentTextEdit::ClipboardContentTextEdit(QWidget *parent) : KTextEdit(parent) { } void ClipboardContentTextEdit::keyPressEvent(QKeyEvent *event) { // Handle Ctrl+Enter to accept const int key = event->key(); if (key == Qt::Key_Return || key == Qt::Key_Enter) { if ((key == Qt::Key_Enter && (event->modifiers() == Qt::KeypadModifier)) || !event->modifiers()) { Q_EMIT done(); event->accept(); return; } } KTextEdit::keyPressEvent(event); } // config == KGlobal::config for process, otherwise applet Klipper::Klipper(QObject *parent, const KSharedConfigPtr &config, KlipperMode mode) : QObject(parent) , m_overflowCounter(0) , m_quitAction(nullptr) , m_locklevel(0) , m_config(config) , m_pendingContentsCheck(false) , m_mode(mode) { if (m_mode == KlipperMode::Standalone) { setenv("KSNI_NO_DBUSMENU", "1", 1); } QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.klipper")); QDBusConnection::sessionBus().registerObject(QStringLiteral("/klipper"), this, QDBusConnection::ExportScriptableSlots); updateTimestamp(); // read initial X user time m_clip = KSystemClipboard::instance(); connect(m_clip, &KSystemClipboard::changed, this, &Klipper::newClipData); connect(&m_overflowClearTimer, &QTimer::timeout, this, &Klipper::slotClearOverflow); m_pendingCheckTimer.setSingleShot(true); connect(&m_pendingCheckTimer, &QTimer::timeout, this, &Klipper::slotCheckPending); m_history = new History(this); m_popup = new KlipperPopup(m_history); m_popup->setShowHelp(m_mode == KlipperMode::Standalone); connect(m_history, &History::changed, this, &Klipper::slotHistoryChanged); connect(m_history, &History::changed, m_popup, &KlipperPopup::slotHistoryChanged); connect(m_history, &History::topIsUserSelectedSet, m_popup, &KlipperPopup::slotTopIsUserSelectedSet); // we need that collection, otherwise KToggleAction is not happy :} m_collection = new KActionCollection(this); m_toggleURLGrabAction = new KToggleAction(this); m_collection->addAction(QStringLiteral("clipboard_action"), m_toggleURLGrabAction); m_toggleURLGrabAction->setText(i18n("Enable Clipboard Actions")); KGlobalAccel::setGlobalShortcut(m_toggleURLGrabAction, QKeySequence(Qt::ALT + Qt::CTRL + Qt::Key_X)); connect(m_toggleURLGrabAction, &QAction::toggled, this, &Klipper::setURLGrabberEnabled); /* * Create URL grabber */ m_myURLGrabber = new URLGrabber(m_history); connect(m_myURLGrabber, &URLGrabber::sigPopup, this, &Klipper::showPopupMenu); connect(m_myURLGrabber, &URLGrabber::sigDisablePopup, this, &Klipper::disableURLGrabber); /* * Load configuration settings */ loadSettings(); // load previous history if configured if (m_bKeepContents) { loadHistory(); } m_clearHistoryAction = m_collection->addAction(QStringLiteral("clear-history")); m_clearHistoryAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-history"))); m_clearHistoryAction->setText(i18n("C&lear Clipboard History")); KGlobalAccel::setGlobalShortcut(m_clearHistoryAction, QKeySequence()); connect(m_clearHistoryAction, &QAction::triggered, this, &Klipper::slotAskClearHistory); QString CONFIGURE = QStringLiteral("configure"); m_configureAction = m_collection->addAction(CONFIGURE); m_configureAction->setIcon(QIcon::fromTheme(CONFIGURE)); m_configureAction->setText(i18n("&Configure Klipper…")); connect(m_configureAction, &QAction::triggered, this, &Klipper::slotConfigure); if (KlipperMode::Standalone == m_mode) { m_quitAction = m_collection->addAction(QStringLiteral("quit")); m_quitAction->setIcon(QIcon::fromTheme(QStringLiteral("application-exit"))); m_quitAction->setText(i18nc("@item:inmenu Quit Klipper", "&Quit")); connect(m_quitAction, &QAction::triggered, this, &Klipper::slotQuit); } m_repeatAction = m_collection->addAction(QStringLiteral("repeat_action")); m_repeatAction->setText(i18n("Manually Invoke Action on Current Clipboard")); KGlobalAccel::setGlobalShortcut(m_repeatAction, QKeySequence(Qt::ALT + Qt::CTRL + Qt::Key_R)); connect(m_repeatAction, &QAction::triggered, this, &Klipper::slotRepeatAction); // add an edit-possibility m_editAction = m_collection->addAction(QStringLiteral("edit_clipboard")); m_editAction->setIcon(QIcon::fromTheme(QStringLiteral("document-properties"))); m_editAction->setText(i18n("&Edit Contents…")); KGlobalAccel::setGlobalShortcut(m_editAction, QKeySequence()); connect(m_editAction, &QAction::triggered, this, [this]() { editData(m_history->first()); }); // add barcode for mobile phones m_showBarcodeAction = m_collection->addAction(QStringLiteral("show-barcode")); m_showBarcodeAction->setText(i18n("&Show Barcode…")); KGlobalAccel::setGlobalShortcut(m_showBarcodeAction, QKeySequence()); connect(m_showBarcodeAction, &QAction::triggered, this, [this]() { showBarcode(m_history->first()); }); // Cycle through history m_cycleNextAction = m_collection->addAction(QStringLiteral("cycleNextAction")); m_cycleNextAction->setText(i18n("Next History Item")); KGlobalAccel::setGlobalShortcut(m_cycleNextAction, QKeySequence()); connect(m_cycleNextAction, &QAction::triggered, this, &Klipper::slotCycleNext); m_cyclePrevAction = m_collection->addAction(QStringLiteral("cyclePrevAction")); m_cyclePrevAction->setText(i18n("Previous History Item")); KGlobalAccel::setGlobalShortcut(m_cyclePrevAction, QKeySequence()); connect(m_cyclePrevAction, &QAction::triggered, this, &Klipper::slotCyclePrev); // Action to show Klipper popup on mouse position m_showOnMousePos = m_collection->addAction(QStringLiteral("show-on-mouse-pos")); m_showOnMousePos->setText(i18n("Open Klipper at Mouse Position")); KGlobalAccel::setGlobalShortcut(m_showOnMousePos, QKeySequence(Qt::META + Qt::Key_V)); connect(m_showOnMousePos, &QAction::triggered, this, &Klipper::slotPopupMenu); connect(history(), &History::topChanged, this, &Klipper::slotHistoryTopChanged); connect(m_popup, &QMenu::aboutToShow, this, &Klipper::slotStartShowTimer); if (m_mode == KlipperMode::Standalone) { m_popup->plugAction(m_toggleURLGrabAction); m_popup->plugAction(m_clearHistoryAction); m_popup->plugAction(m_configureAction); m_popup->plugAction(m_repeatAction); m_popup->plugAction(m_editAction); m_popup->plugAction(m_showBarcodeAction); Q_ASSERT(m_quitAction); m_popup->plugAction(m_quitAction); } // session manager interaction if (m_mode == KlipperMode::Standalone) { connect(qApp, &QGuiApplication::commitDataRequest, this, &Klipper::saveSession); } connect(this, &Klipper::passivePopup, this, [this](const QString &caption, const QString &text) { if (m_notification) { m_notification->setTitle(caption); m_notification->setText(text); } else { m_notification = KNotification::event(KNotification::Notification, caption, text, QStringLiteral("klipper")); // When Klipper is run as part of plasma, we still need to pretend to be it for notification settings to work m_notification->setHint(QStringLiteral("desktop-entry"), QStringLiteral("org.kde.klipper")); } }); } Klipper::~Klipper() { delete m_myURLGrabber; } // DBUS QString Klipper::getClipboardContents() { return getClipboardHistoryItem(0); } void Klipper::showKlipperPopupMenu() { slotPopupMenu(); } void Klipper::showKlipperManuallyInvokeActionMenu() { slotRepeatAction(); } // DBUS - don't call from Klipper itself void Klipper::setClipboardContents(const QString &s) { if (s.isEmpty()) return; Ignore lock(m_locklevel); updateTimestamp(); HistoryItemPtr item(HistoryItemPtr(new HistoryStringItem(s))); setClipboard(*item, Clipboard | Selection); history()->insert(item); } // DBUS - don't call from Klipper itself void Klipper::clearClipboardContents() { updateTimestamp(); slotClearClipboard(); } // DBUS - don't call from Klipper itself void Klipper::clearClipboardHistory() { updateTimestamp(); history()->slotClear(); saveSession(); } // DBUS - don't call from Klipper itself void Klipper::saveClipboardHistory() { if (m_bKeepContents) { // save the clipboard eventually saveHistory(); } } void Klipper::slotStartShowTimer() { m_showTimer.start(); } void Klipper::loadSettings() { // Security bug 142882: If user has save clipboard turned off, old data should be deleted from disk static bool firstrun = true; if (!firstrun && m_bKeepContents && !KlipperSettings::keepClipboardContents()) { saveHistory(true); } firstrun = false; m_bKeepContents = KlipperSettings::keepClipboardContents(); m_bReplayActionInHistory = KlipperSettings::replayActionInHistory(); m_bNoNullClipboard = KlipperSettings::preventEmptyClipboard(); // 0 is the id of "Ignore selection" radiobutton m_bIgnoreSelection = KlipperSettings::ignoreSelection(); m_bIgnoreImages = KlipperSettings::ignoreImages(); m_bSynchronize = KlipperSettings::syncClipboards(); // NOTE: not used atm - kregexpeditor is not ported to kde4 m_bUseGUIRegExpEditor = KlipperSettings::useGUIRegExpEditor(); m_bSelectionTextOnly = KlipperSettings::selectionTextOnly(); m_bURLGrabber = KlipperSettings::uRLGrabberEnabled(); // this will cause it to loadSettings too setURLGrabberEnabled(m_bURLGrabber); history()->setMaxSize(KlipperSettings::maxClipItems()); history()->model()->setDisplayImages(!m_bIgnoreImages); // Convert 4.3 settings if (KlipperSettings::synchronize() != 3) { // 2 was the id of "Ignore selection" radiobutton m_bIgnoreSelection = KlipperSettings::synchronize() == 2; // 0 was the id of "Synchronize contents" radiobutton m_bSynchronize = KlipperSettings::synchronize() == 0; KConfigSkeletonItem *item = KlipperSettings::self()->findItem(QStringLiteral("SyncClipboards")); item->setProperty(m_bSynchronize); item = KlipperSettings::self()->findItem(QStringLiteral("IgnoreSelection")); item->setProperty(m_bIgnoreSelection); item = KlipperSettings::self()->findItem(QStringLiteral("Synchronize")); // Mark property as converted. item->setProperty(3); KlipperSettings::self()->save(); KlipperSettings::self()->load(); } if (m_bKeepContents && !m_saveFileTimer) { m_saveFileTimer = new QTimer(this); m_saveFileTimer->setSingleShot(true); m_saveFileTimer->setInterval(5000); connect(m_saveFileTimer, &QTimer::timeout, this, [this] { QtConcurrent::run(this, &Klipper::saveHistory, false); }); connect(m_history, &History::changed, m_saveFileTimer, static_cast(&QTimer::start)); } else { delete m_saveFileTimer; m_saveFileTimer = nullptr; } } void Klipper::saveSettings() const { m_myURLGrabber->saveSettings(); KlipperSettings::self()->setVersion(QStringLiteral(KLIPPER_VERSION_STRING)); KlipperSettings::self()->save(); // other settings should be saved automatically by KConfigDialog } void Klipper::showPopupMenu(QMenu *menu) { Q_ASSERT(menu != nullptr); menu->popup(QCursor::pos()); } bool Klipper::loadHistory() { static const char failed_load_warning[] = "Failed to load history resource. Clipboard history cannot be read."; // don't use "appdata", klipper is also a kicker applet QFile history_file(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("klipper/history2.lst"))); if (!history_file.exists()) { qCWarning(KLIPPER_LOG) << failed_load_warning << ": " << "History file does not exist"; return false; } if (!history_file.open(QIODevice::ReadOnly)) { qCWarning(KLIPPER_LOG) << failed_load_warning << ": " << history_file.errorString(); return false; } QDataStream file_stream(&history_file); if (file_stream.atEnd()) { qCWarning(KLIPPER_LOG) << failed_load_warning << ": " << "Error in reading data"; return false; } QByteArray data; quint32 crc; file_stream >> crc >> data; if (crc32(0, reinterpret_cast(data.data()), data.size()) != crc) { qCWarning(KLIPPER_LOG) << failed_load_warning << ": " << "CRC checksum does not match"; return false; } QDataStream history_stream(&data, QIODevice::ReadOnly); char *version; history_stream >> version; delete[] version; // The list needs to be reversed, as it is saved // youngest-first to keep the most important clipboard // items at the top, but the history is created oldest // first. QVector reverseList; for (HistoryItemPtr item = HistoryItem::create(history_stream); !item.isNull(); item = HistoryItem::create(history_stream)) { reverseList.prepend(item); } history()->slotClear(); for (auto it = reverseList.constBegin(); it != reverseList.constEnd(); ++it) { history()->forceInsert(*it); } if (!history()->empty()) { setClipboard(*history()->first(), Clipboard | Selection); } return true; } void Klipper::saveHistory(bool empty) { QMutexLocker lock(m_history->model()->mutex()); static const char failed_save_warning[] = "Failed to save history. Clipboard history cannot be saved."; // don't use "appdata", klipper is also a kicker applet QString history_file_name(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("klipper/history2.lst"))); if (history_file_name.isNull() || history_file_name.isEmpty()) { // try creating the file QDir dir(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation)); if (!dir.mkpath(QStringLiteral("klipper"))) { qCWarning(KLIPPER_LOG) << failed_save_warning; return; } history_file_name = dir.absoluteFilePath(QStringLiteral("klipper/history2.lst")); } if (history_file_name.isNull() || history_file_name.isEmpty()) { qCWarning(KLIPPER_LOG) << failed_save_warning; return; } QSaveFile history_file(history_file_name); if (!history_file.open(QIODevice::WriteOnly)) { qCWarning(KLIPPER_LOG) << failed_save_warning; return; } QByteArray data; QDataStream history_stream(&data, QIODevice::WriteOnly); history_stream << KLIPPER_VERSION_STRING; // const char* if (!empty) { HistoryItemConstPtr item = history()->first(); if (item) { do { history_stream << item.data(); item = HistoryItemConstPtr(history()->find(item->next_uuid())); } while (item != history()->first()); } } quint32 crc = crc32(0, reinterpret_cast(data.data()), data.size()); QDataStream ds(&history_file); ds << crc << data; if (!history_file.commit()) { qCWarning(KLIPPER_LOG) << failed_save_warning; } } // save session on shutdown. Don't simply use the c'tor, as that may not be called. void Klipper::saveSession() { if (m_bKeepContents) { // save the clipboard eventually saveHistory(); } saveSettings(); } void Klipper::disableURLGrabber() { QMessageBox *message = new QMessageBox(QMessageBox::Information, QString(), xi18nc("@info", "You can enable URL actions later in the " "Actions page of the " "Clipboard applet's configuration window")); message->setAttribute(Qt::WA_DeleteOnClose); message->setModal(false); message->show(); setURLGrabberEnabled(false); } void Klipper::slotConfigure() { if (KConfigDialog::showDialog(QStringLiteral("preferences"))) { return; } ConfigDialog *dlg = new ConfigDialog(nullptr, KlipperSettings::self(), this, m_collection); QMetaObject::invokeMethod(dlg, "setHelp", Qt::DirectConnection, Q_ARG(QString, QString::fromLatin1("")), Q_ARG(QString, QString::fromLatin1("klipper"))); connect(dlg, &KConfigDialog::settingsChanged, this, &Klipper::loadSettings); dlg->show(); } void Klipper::slotQuit() { // If the menu was just opened, likely the user // selected quit by accident while attempting to // click the Klipper icon. if (m_showTimer.elapsed() < 300) { return; } saveSession(); int autoStart = KMessageBox::questionYesNoCancel(nullptr, i18n("Should Klipper start automatically when you login?"), i18n("Automatically Start Klipper?"), KGuiItem(i18n("Start")), KGuiItem(i18n("Do Not Start")), KStandardGuiItem::cancel(), QStringLiteral("StartAutomatically")); KConfigGroup config(KSharedConfig::openConfig(), "General"); if (autoStart == KMessageBox::Yes) { config.writeEntry("AutoStart", true); } else if (autoStart == KMessageBox::No) { config.writeEntry("AutoStart", false); } else // cancel chosen don't quit return; config.sync(); qApp->quit(); } void Klipper::slotPopupMenu() { m_popup->ensureClean(); m_popup->slotSetTopActive(); showPopupMenu(m_popup); } void Klipper::slotRepeatAction() { auto top = qSharedPointerCast(history()->first()); if (top) { m_myURLGrabber->invokeAction(top); } } void Klipper::setURLGrabberEnabled(bool enable) { if (enable != m_bURLGrabber) { m_bURLGrabber = enable; m_lastURLGrabberTextSelection.clear(); m_lastURLGrabberTextClipboard.clear(); KlipperSettings::setURLGrabberEnabled(enable); } m_toggleURLGrabAction->setChecked(enable); // make it update its settings m_myURLGrabber->loadSettings(); } void Klipper::slotHistoryTopChanged() { if (m_locklevel) { return; } auto topitem = history()->first(); if (topitem) { setClipboard(*topitem, Clipboard | Selection); } if (m_bReplayActionInHistory && m_bURLGrabber) { slotRepeatAction(); } } void Klipper::slotClearClipboard() { Ignore lock(m_locklevel); m_clip->clear(QClipboard::Selection); m_clip->clear(QClipboard::Clipboard); } HistoryItemPtr Klipper::applyClipChanges(const QMimeData *clipData) { if (m_locklevel) { return HistoryItemPtr(); } Ignore lock(m_locklevel); if (!(history()->empty())) { if (m_bIgnoreImages && history()->first()->type() == HistoryItemType::Image) { history()->remove(history()->first()); } } HistoryItemPtr item = HistoryItem::create(clipData); bool saveToHistory = true; if (clipData->data(QStringLiteral("x-kde-passwordManagerHint")) == QByteArrayLiteral("secret")) { saveToHistory = false; } if (saveToHistory) { history()->insert(item); } return item; } void Klipper::newClipData(QClipboard::Mode mode) { if (m_locklevel) { return; } if (mode == QClipboard::Selection && blockFetchingNewData()) return; checkClipData(mode == QClipboard::Selection ? true : false); } void Klipper::slotHistoryChanged() { if (history()->empty()) { slotClearClipboard(); } } // 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. const int MAX_CLIPBOARD_CHANGES = 10; // max changes per second bool Klipper::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. if (!KWindowSystem::isPlatformX11()) { return false; } xcb_connection_t *c = QX11Info::connection(); const xcb_query_pointer_cookie_t cookie = xcb_query_pointer_unchecked(c, QX11Info::appRootWindow()); QScopedPointer queryPointer(xcb_query_pointer_reply(c, cookie, nullptr)); if (queryPointer.isNull()) { 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(100); return true; } m_pendingContentsCheck = false; if (m_overflowCounter == 0) m_overflowClearTimer.start(1000); if (++m_overflowCounter > MAX_CLIPBOARD_CHANGES) return true; #endif return false; } void Klipper::slotCheckPending() { if (!m_pendingContentsCheck) return; m_pendingContentsCheck = false; // blockFetchingNewData() will be called again updateTimestamp(); newClipData(QClipboard::Selection); // always selection } void Klipper::checkClipData(bool selectionMode) { if (ignoreClipboardChanges()) // internal to klipper, ignoring QSpinBox selections { // 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.... auto top = history()->first(); if (top) { setClipboard(*top, selectionMode ? Selection : Clipboard); } return; } qCDebug(KLIPPER_LOG) << "Checking clip data"; const QMimeData *data = m_clip->mimeData(selectionMode ? QClipboard::Selection : QClipboard::Clipboard); bool clipEmpty = false; bool changed = true; // ### FIXME (only relevant under polling, might be better to simply remove polling and rely on XFixes) if (!data) { clipEmpty = true; } else { clipEmpty = data->formats().isEmpty(); if (clipEmpty) { // Might be a timeout. Try again clipEmpty = data->formats().isEmpty(); qCDebug(KLIPPER_LOG) << "was empty. Retried, now " << (clipEmpty ? " still empty" : " no longer empty"); } } if (changed && clipEmpty && m_bNoNullClipboard) { auto top = history()->first(); if (top) { // keep old clipboard after someone set it to null qCDebug(KLIPPER_LOG) << "Resetting clipboard (Prevent empty clipboard)"; setClipboard(*top, selectionMode ? Selection : Clipboard, ClipboardUpdateReason::PreventEmptyClipboard); } return; } else if (clipEmpty) { return; } // this must be below the "bNoNullClipboard" handling code! // XXX: I want a better handling of selection/clipboard in general. // XXX: Order sensitive code. Must die. if (selectionMode && m_bIgnoreSelection) return; if (selectionMode && m_bSelectionTextOnly && !data->hasText()) return; if (data->hasUrls()) ; // ok else if (data->hasText()) ; // ok else if (data->hasImage()) { if (m_bIgnoreImages && !data->hasFormat(QStringLiteral("x-kde-force-image-copy"))) return; } else // unknown, ignore return; HistoryItemPtr item = applyClipChanges(data); if (changed) { qCDebug(KLIPPER_LOG) << "Synchronize?" << m_bSynchronize; if (m_bSynchronize && item) { setClipboard(*item, selectionMode ? Clipboard : Selection); } } QString &lastURLGrabberText = selectionMode ? m_lastURLGrabberTextSelection : m_lastURLGrabberTextClipboard; if (m_bURLGrabber && item && data->hasText()) { m_myURLGrabber->checkNewData(qSharedPointerConstCast(item)); // Make sure URLGrabber doesn't repeat all the time if klipper reads the same // text all the time (e.g. because XFixes is not available and the application // has broken TIMESTAMP target). Using most recent history item may not always // work. if (item->text() != lastURLGrabberText) { lastURLGrabberText = item->text(); } } else { lastURLGrabberText.clear(); } } void Klipper::setClipboard(const HistoryItem &item, int mode, ClipboardUpdateReason updateReason) { Ignore lock(m_locklevel); Q_ASSERT((mode & 1) == 0); // Warn if trying to pass a boolean as a mode. if (mode & Selection) { qCDebug(KLIPPER_LOG) << "Setting selection to <" << item.text() << ">"; QMimeData *mimeData = item.mimeData(); if (updateReason == ClipboardUpdateReason::PreventEmptyClipboard) { mimeData->setData(QStringLiteral("application/x-kde-onlyReplaceEmpty"), "1"); } m_clip->setMimeData(mimeData, QClipboard::Selection); } if (mode & Clipboard) { qCDebug(KLIPPER_LOG) << "Setting clipboard to <" << item.text() << ">"; QMimeData *mimeData = item.mimeData(); if (updateReason == ClipboardUpdateReason::PreventEmptyClipboard) { mimeData->setData(QStringLiteral("application/x-kde-onlyReplaceEmpty"), "1"); } m_clip->setMimeData(mimeData, QClipboard::Clipboard); } } void Klipper::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 newClipData(QClipboard::Selection); // Always the selection. } m_overflowCounter = 0; } QStringList Klipper::getClipboardHistoryMenu() { QStringList menu; auto item = history()->first(); if (item) { do { menu << item->text(); item = history()->find(item->next_uuid()); } while (item != history()->first()); } return menu; } QString Klipper::getClipboardHistoryItem(int i) { auto item = history()->first(); if (item) { do { if (i-- == 0) { return item->text(); } item = history()->find(item->next_uuid()); } while (item != history()->first()); } return QString(); } // // 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 // bool Klipper::ignoreClipboardChanges() const { QWidget *focusWidget = qApp->focusWidget(); if (focusWidget) { if (focusWidget->inherits("QSpinBox") || (focusWidget->parentWidget() && focusWidget->inherits("QLineEdit") && focusWidget->parentWidget()->inherits("QSpinWidget"))) { return true; } } return false; } void Klipper::updateTimestamp() { #if HAVE_X11 if (KWindowSystem::isPlatformX11()) { QX11Info::setAppTime(QX11Info::getTimestamp()); } #endif } void Klipper::editData(const QSharedPointer &item) { QPointer dlg(new QDialog()); dlg->setWindowTitle(i18n("Edit Contents")); QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, dlg); connect(buttons, &QDialogButtonBox::accepted, dlg.data(), &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, dlg.data(), &QDialog::reject); connect(dlg.data(), &QDialog::finished, dlg.data(), [this, dlg, item](int result) { Q_EMIT editFinished(item, result); dlg->deleteLater(); }); ClipboardContentTextEdit *edit = new ClipboardContentTextEdit(dlg); edit->setAcceptRichText(false); if (item) { edit->setPlainText(item->text()); } edit->setFocus(); edit->setMinimumSize(300, 40); QVBoxLayout *layout = new QVBoxLayout(dlg); layout->addWidget(edit); layout->addWidget(buttons); dlg->adjustSize(); connect(edit, &ClipboardContentTextEdit::done, dlg.data(), &QDialog::accept); connect(dlg.data(), &QDialog::accepted, this, [this, edit, item]() { QString text = edit->toPlainText(); if (item) { m_history->remove(item); } m_history->insert(HistoryItemPtr(new HistoryStringItem(text))); if (m_myURLGrabber) { m_myURLGrabber->checkNewData(HistoryItemConstPtr(m_history->first())); } }); if (m_mode == KlipperMode::Standalone) { dlg->setModal(true); dlg->exec(); } else if (m_mode == KlipperMode::DataEngine) { dlg->open(); } } class BarcodeLabel : public QLabel { public: BarcodeLabel(Prison::AbstractBarcode *barcode, QWidget *parent = nullptr) : QLabel(parent) , m_barcode(barcode) { setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); setPixmap(QPixmap::fromImage(m_barcode->toImage(size()))); } protected: void resizeEvent(QResizeEvent *event) override { QLabel::resizeEvent(event); setPixmap(QPixmap::fromImage(m_barcode->toImage(event->size()))); } private: QScopedPointer m_barcode; }; void Klipper::showBarcode(const QSharedPointer &item) { using namespace Prison; QPointer dlg(new QDialog()); dlg->setWindowTitle(i18n("Mobile Barcode")); QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Ok, dlg); buttons->button(QDialogButtonBox::Ok)->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttons, &QDialogButtonBox::accepted, dlg.data(), &QDialog::accept); connect(dlg.data(), &QDialog::finished, dlg.data(), &QDialog::deleteLater); QWidget *mw = new QWidget(dlg); QHBoxLayout *layout = new QHBoxLayout(mw); { AbstractBarcode *qrCode = createBarcode(QRCode); if (qrCode) { if (item) { qrCode->setData(item->text()); } BarcodeLabel *qrCodeLabel = new BarcodeLabel(qrCode, mw); layout->addWidget(qrCodeLabel); } } { AbstractBarcode *dataMatrix = createBarcode(DataMatrix); if (dataMatrix) { if (item) { dataMatrix->setData(item->text()); } BarcodeLabel *dataMatrixLabel = new BarcodeLabel(dataMatrix, mw); layout->addWidget(dataMatrixLabel); } } mw->setFocus(); QVBoxLayout *vBox = new QVBoxLayout(dlg); vBox->addWidget(mw); vBox->addWidget(buttons); dlg->adjustSize(); if (m_mode == KlipperMode::Standalone) { dlg->setModal(true); dlg->exec(); } else if (m_mode == KlipperMode::DataEngine) { dlg->open(); } } void Klipper::slotAskClearHistory() { int clearHist = KMessageBox::warningContinueCancel(nullptr, i18n("Really delete entire clipboard history?"), i18n("Delete clipboard history?"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QStringLiteral("klipperClearHistoryAskAgain"), KMessageBox::Dangerous); if (clearHist == KMessageBox::Continue) { history()->slotClear(); saveHistory(); } } void Klipper::slotCycleNext() { // do cycle and show popup only if we have something in clipboard if (m_history->first()) { m_history->cycleNext(); Q_EMIT passivePopup(i18n("Clipboard history"), cycleText()); } } void Klipper::slotCyclePrev() { // do cycle and show popup only if we have something in clipboard if (m_history->first()) { m_history->cyclePrev(); Q_EMIT passivePopup(i18n("Clipboard history"), cycleText()); } } QString Klipper::cycleText() const { const int WIDTH_IN_PIXEL = 400; auto itemprev = m_history->prevInCycle(); auto item = m_history->first(); auto itemnext = m_history->nextInCycle(); QFontMetrics font_metrics(m_popup->fontMetrics()); QString result(QStringLiteral("")); if (itemprev) { result += QLatin1String(""); } result += QLatin1String(""); if (itemnext) { result += QLatin1String(""); } result += QLatin1String("
"); result += i18n("up"); result += QLatin1String(""); result += font_metrics.elidedText(itemprev->text().simplified().toHtmlEscaped(), Qt::ElideMiddle, WIDTH_IN_PIXEL); result += QLatin1String("
"); result += i18n("current"); result += QLatin1String(""); result += font_metrics.elidedText(item->text().simplified().toHtmlEscaped(), Qt::ElideMiddle, WIDTH_IN_PIXEL); result += QLatin1String("
"); result += i18n("down"); result += QLatin1String(""); result += font_metrics.elidedText(itemnext->text().simplified().toHtmlEscaped(), Qt::ElideMiddle, WIDTH_IN_PIXEL); result += QLatin1String("
"); return result; }