File indexing completed on 2024-04-28 16:54:25

0001 /*
0002 
0003     SPDX-FileCopyrightText: Andrew Stanley-Jones <asj@cban.com>
0004     SPDX-FileCopyrightText: 2000 Carsten Pfeiffer <pfeiffer@kde.org>
0005     SPDX-FileCopyrightText: 2004 Esben Mose Hansen <kde@mosehansen.dk>
0006     SPDX-FileCopyrightText: 2008 Dmitry Suzdalev <dimsuz@gmail.com>
0007 
0008     SPDX-License-Identifier: GPL-2.0-or-later
0009 */
0010 
0011 #include "klipper.h"
0012 
0013 #include <unordered_set>
0014 #include <zlib.h>
0015 
0016 #include "klipper_debug.h"
0017 #include <QApplication>
0018 #include <QBoxLayout>
0019 #include <QDBusConnection>
0020 #include <QDialog>
0021 #include <QDir>
0022 #include <QLabel>
0023 #include <QMenu>
0024 #include <QMessageBox>
0025 #include <QPushButton>
0026 #include <QSaveFile>
0027 #include <QtConcurrent>
0028 
0029 #include <KAboutData>
0030 #include <KActionCollection>
0031 #include <KGlobalAccel>
0032 #include <KHelpMenu>
0033 #include <KLocalizedString>
0034 #include <KMessageBox>
0035 #include <KNotification>
0036 #include <KSystemClipboard>
0037 #include <KToggleAction>
0038 #include <KWayland/Client/connection_thread.h>
0039 #include <KWayland/Client/plasmashell.h>
0040 #include <KWayland/Client/registry.h>
0041 #include <KWayland/Client/surface.h>
0042 #include <KWindowSystem>
0043 
0044 #include "../c_ptr.h"
0045 #include "configdialog.h"
0046 #include "history.h"
0047 #include "historyitem.h"
0048 #include "historymodel.h"
0049 #include "historystringitem.h"
0050 #include "klipperpopup.h"
0051 #include "klippersettings.h"
0052 
0053 #include <Prison/Prison>
0054 
0055 #include <config-X11.h>
0056 #if HAVE_X11
0057 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0058 #include <private/qtx11extras_p.h>
0059 #else
0060 #include <QX11Info>
0061 #endif
0062 #include <chrono>
0063 #include <xcb/xcb.h>
0064 
0065 using namespace std::chrono_literals;
0066 #endif
0067 
0068 namespace
0069 {
0070 /**
0071  * Use this when manipulating the clipboard
0072  * from within clipboard-related signals.
0073  *
0074  * This avoids issues such as mouse-selections that immediately
0075  * disappear.
0076  * pattern: Resource Acquisition is Initialisation (RAII)
0077  *
0078  * (This is not threadsafe, so don't try to use such in threaded
0079  * applications).
0080  */
0081 struct Ignore {
0082     Ignore(int &locklevel)
0083         : locklevelref(locklevel)
0084     {
0085         locklevelref++;
0086     }
0087     ~Ignore()
0088     {
0089         locklevelref--;
0090     }
0091 
0092 private:
0093     int &locklevelref;
0094 };
0095 }
0096 
0097 ClipboardContentTextEdit::ClipboardContentTextEdit(QWidget *parent)
0098     : KTextEdit(parent)
0099 {
0100 }
0101 
0102 void ClipboardContentTextEdit::keyPressEvent(QKeyEvent *event)
0103 {
0104     // Handle Ctrl+Enter to accept
0105     const int key = event->key();
0106     if (key == Qt::Key_Return || key == Qt::Key_Enter) {
0107         if ((key == Qt::Key_Enter && (event->modifiers() == Qt::KeypadModifier)) || !event->modifiers()) {
0108             Q_EMIT done();
0109             event->accept();
0110             return;
0111         }
0112     }
0113     KTextEdit::keyPressEvent(event);
0114 }
0115 
0116 // config == KGlobal::config for process, otherwise applet
0117 Klipper::Klipper(QObject *parent, const KSharedConfigPtr &config, KlipperMode mode)
0118     : QObject(parent)
0119     , m_overflowCounter(0)
0120     , m_quitAction(nullptr)
0121     , m_selectionLocklevel(0)
0122     , m_clipboardLocklevel(0)
0123     , m_config(config)
0124     , m_pendingContentsCheck(false)
0125     , m_mode(mode)
0126     , m_saveFileTimer(nullptr)
0127     , m_plasmashell(nullptr)
0128 {
0129     if (m_mode == KlipperMode::Standalone) {
0130         setenv("KSNI_NO_DBUSMENU", "1", 1);
0131     }
0132     QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.klipper"));
0133     QDBusConnection::sessionBus().registerObject(QStringLiteral("/klipper"), this, QDBusConnection::ExportScriptableSlots);
0134 
0135     updateTimestamp(); // read initial X user time
0136     m_clip = KSystemClipboard::instance();
0137 
0138     connect(m_clip, &KSystemClipboard::changed, this, &Klipper::newClipData);
0139 
0140     connect(&m_overflowClearTimer, &QTimer::timeout, this, &Klipper::slotClearOverflow);
0141 
0142     m_pendingCheckTimer.setSingleShot(true);
0143     connect(&m_pendingCheckTimer, &QTimer::timeout, this, &Klipper::slotCheckPending);
0144 
0145     m_history = new History(this);
0146     m_popup = new KlipperPopup(m_history);
0147     m_popup->setWindowFlags(m_popup->windowFlags() | Qt::FramelessWindowHint);
0148     connect(m_history, &History::changed, this, &Klipper::slotHistoryChanged);
0149     connect(m_history, &History::changed, m_popup, &KlipperPopup::slotHistoryChanged);
0150     connect(m_history, &History::topIsUserSelectedSet, m_popup, &KlipperPopup::slotTopIsUserSelectedSet);
0151 
0152     // we need that collection, otherwise KToggleAction is not happy :}
0153     m_collection = new KActionCollection(this);
0154 
0155     m_toggleURLGrabAction = new KToggleAction(this);
0156     m_collection->addAction(QStringLiteral("clipboard_action"), m_toggleURLGrabAction);
0157     m_toggleURLGrabAction->setText(i18nc("@action:inmenu Toggle automatic action", "Automatic Action Popup Menu"));
0158     KGlobalAccel::setGlobalShortcut(m_toggleURLGrabAction, QKeySequence(Qt::META | Qt::CTRL | Qt::Key_X));
0159     connect(m_toggleURLGrabAction, &QAction::toggled, this, &Klipper::setURLGrabberEnabled);
0160 
0161     /*
0162      * Create URL grabber
0163      */
0164     m_myURLGrabber = new URLGrabber(m_history);
0165     connect(m_myURLGrabber, &URLGrabber::sigPopup, this, &Klipper::showPopupMenu);
0166     connect(m_myURLGrabber, &URLGrabber::sigDisablePopup, this, &Klipper::disableURLGrabber);
0167 
0168     /*
0169      * Load configuration settings
0170      */
0171     loadSettings();
0172 
0173     // load previous history if configured
0174     if (m_bKeepContents) {
0175         loadHistory();
0176     }
0177 
0178     m_saveFileTimer = new QTimer(this);
0179     m_saveFileTimer->setSingleShot(true);
0180     m_saveFileTimer->setInterval(5s);
0181     connect(m_saveFileTimer, &QTimer::timeout, this, [this] {
0182 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0183         QtConcurrent::run(this, &Klipper::saveHistory, false);
0184 #else
0185         QtConcurrent::run(&Klipper::saveHistory, this, false);
0186 #endif
0187     });
0188     connect(m_history, &History::changed, this, [this] {
0189         if (m_bKeepContents) {
0190             m_saveFileTimer->start();
0191         }
0192     }); // only connect this signal after loading the history, to avoid the action of loading triggering a save
0193 
0194     m_clearHistoryAction = m_collection->addAction(QStringLiteral("clear-history"));
0195     m_clearHistoryAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-history")));
0196     m_clearHistoryAction->setText(i18nc("@action:inmenu", "C&lear Clipboard History"));
0197     KGlobalAccel::setGlobalShortcut(m_clearHistoryAction, QKeySequence());
0198     connect(m_clearHistoryAction, &QAction::triggered, this, &Klipper::slotAskClearHistory);
0199 
0200     QString CONFIGURE = QStringLiteral("configure");
0201     m_configureAction = m_collection->addAction(CONFIGURE);
0202     m_configureAction->setIcon(QIcon::fromTheme(CONFIGURE));
0203     m_configureAction->setText(i18nc("@action:inmenu", "&Configure Klipper…"));
0204     connect(m_configureAction, &QAction::triggered, this, &Klipper::slotConfigure);
0205 
0206     if (KlipperMode::Standalone == m_mode) {
0207         m_quitAction = m_collection->addAction(QStringLiteral("quit"));
0208         m_quitAction->setIcon(QIcon::fromTheme(QStringLiteral("application-exit")));
0209         m_quitAction->setText(i18nc("@action:inmenu Quit Klipper", "&Quit"));
0210         connect(m_quitAction, &QAction::triggered, this, &Klipper::slotQuit);
0211     }
0212 
0213     m_repeatAction = m_collection->addAction(QStringLiteral("repeat_action"));
0214     m_repeatAction->setText(i18nc("@action:inmenu", "Manually Invoke Action on Current Clipboard"));
0215     m_repeatAction->setIcon(QIcon::fromTheme(QStringLiteral("open-menu-symbolic")));
0216     KGlobalAccel::setGlobalShortcut(m_repeatAction, QKeySequence(Qt::META | Qt::CTRL | Qt::Key_R));
0217     connect(m_repeatAction, &QAction::triggered, this, &Klipper::slotRepeatAction);
0218 
0219     // add an edit-possibility
0220     m_editAction = m_collection->addAction(QStringLiteral("edit_clipboard"));
0221     m_editAction->setIcon(QIcon::fromTheme(QStringLiteral("document-properties")));
0222     m_editAction->setText(i18nc("@action:inmenu", "&Edit Contents…"));
0223     KGlobalAccel::setGlobalShortcut(m_editAction, QKeySequence());
0224     connect(m_editAction, &QAction::triggered, this, [this]() {
0225         editData(m_history->first());
0226     });
0227 
0228     // add barcode for mobile phones
0229     m_showBarcodeAction = m_collection->addAction(QStringLiteral("show-barcode"));
0230     m_showBarcodeAction->setText(i18nc("@action:inmenu", "&Show Barcode…"));
0231     m_showBarcodeAction->setIcon(QIcon::fromTheme(QStringLiteral("view-barcode-qr")));
0232     KGlobalAccel::setGlobalShortcut(m_showBarcodeAction, QKeySequence());
0233     connect(m_showBarcodeAction, &QAction::triggered, this, [this]() {
0234         showBarcode(m_history->first());
0235     });
0236 
0237     // Cycle through history
0238     m_cycleNextAction = m_collection->addAction(QStringLiteral("cycleNextAction"));
0239     m_cycleNextAction->setText(i18nc("@action:inmenu", "Next History Item"));
0240     m_cycleNextAction->setIcon(QIcon::fromTheme(QStringLiteral("go-next")));
0241     KGlobalAccel::setGlobalShortcut(m_cycleNextAction, QKeySequence());
0242     connect(m_cycleNextAction, &QAction::triggered, this, &Klipper::slotCycleNext);
0243     m_cyclePrevAction = m_collection->addAction(QStringLiteral("cyclePrevAction"));
0244     m_cyclePrevAction->setText(i18nc("@action:inmenu", "Previous History Item"));
0245     m_cyclePrevAction->setIcon(QIcon::fromTheme(QStringLiteral("go-previous")));
0246     KGlobalAccel::setGlobalShortcut(m_cyclePrevAction, QKeySequence());
0247     connect(m_cyclePrevAction, &QAction::triggered, this, &Klipper::slotCyclePrev);
0248 
0249     // Action to show items popup on mouse position
0250     m_showOnMousePos = m_collection->addAction(QStringLiteral("show-on-mouse-pos"));
0251     m_showOnMousePos->setText(i18nc("@action:inmenu", "Show Items at Mouse Position"));
0252     m_showOnMousePos->setIcon(QIcon::fromTheme(QStringLiteral("view-list-text")));
0253     KGlobalAccel::setGlobalShortcut(m_showOnMousePos, QKeySequence(Qt::META | Qt::Key_V));
0254     connect(m_showOnMousePos, &QAction::triggered, this, &Klipper::slotPopupMenu);
0255 
0256     connect(history(), &History::topChanged, this, &Klipper::slotHistoryTopChanged);
0257 
0258     if (m_mode == KlipperMode::Standalone) {
0259         // system tray popup menu
0260         m_actionsPopup = new QMenu;
0261         m_actionsPopup->addSection(QIcon::fromTheme(QStringLiteral("klipper")),
0262                                    i18nc("%1 is application display name", "%1 - Clipboard Tool", QGuiApplication::applicationDisplayName()));
0263         m_actionsPopup->addAction(m_toggleURLGrabAction);
0264         m_actionsPopup->addAction(m_clearHistoryAction);
0265         m_actionsPopup->addAction(m_configureAction);
0266         m_actionsPopup->addAction(m_repeatAction);
0267         m_actionsPopup->addAction(m_editAction);
0268         m_actionsPopup->addAction(m_showBarcodeAction);
0269 
0270         m_actionsPopup->addSeparator();
0271 
0272         QMenu *helpMenu = (new KHelpMenu(m_actionsPopup, KAboutData::applicationData(), false))->menu();
0273         helpMenu->setIcon(QIcon::fromTheme(QStringLiteral("help-contents")));
0274         m_actionsPopup->addMenu(helpMenu);
0275 
0276         Q_ASSERT(m_quitAction);
0277         m_actionsPopup->addAction(m_quitAction);
0278         connect(m_actionsPopup, &QMenu::aboutToShow, this, &Klipper::slotStartShowTimer);
0279     }
0280 
0281     // session manager interaction
0282     if (m_mode == KlipperMode::Standalone) {
0283         connect(qApp, &QGuiApplication::commitDataRequest, this, &Klipper::saveSession);
0284     }
0285 
0286     connect(this, &Klipper::passivePopup, this, [this](const QString &caption, const QString &text) {
0287         if (m_notification) {
0288             m_notification->setTitle(caption);
0289             m_notification->setText(text);
0290         } else {
0291             m_notification = KNotification::event(KNotification::Notification, caption, text, QStringLiteral("klipper"));
0292             // When Klipper is run as part of plasma, we still need to pretend to be it for notification settings to work
0293             m_notification->setHint(QStringLiteral("desktop-entry"), QStringLiteral("org.kde.klipper"));
0294         }
0295     });
0296 
0297     if (KWindowSystem::isPlatformWayland()) {
0298         auto registry = new KWayland::Client::Registry(this);
0299         auto connection = KWayland::Client::ConnectionThread::fromApplication(qGuiApp);
0300         connect(registry, &KWayland::Client::Registry::plasmaShellAnnounced, this, [registry, this](quint32 name, quint32 version) {
0301             if (!m_plasmashell) {
0302                 m_plasmashell = registry->createPlasmaShell(name, version);
0303             }
0304         });
0305         registry->create(connection);
0306         registry->setup();
0307     }
0308 }
0309 
0310 Klipper::~Klipper()
0311 {
0312     delete m_myURLGrabber;
0313 }
0314 
0315 // DBUS
0316 QString Klipper::getClipboardContents()
0317 {
0318     return getClipboardHistoryItem(0);
0319 }
0320 
0321 void Klipper::showKlipperPopupMenu()
0322 {
0323     slotPopupMenu();
0324 }
0325 
0326 void Klipper::showKlipperManuallyInvokeActionMenu()
0327 {
0328     slotRepeatAction();
0329 }
0330 
0331 // DBUS - don't call from Klipper itself
0332 void Klipper::setClipboardContents(const QString &s)
0333 {
0334     if (s.isEmpty())
0335         return;
0336     Ignore selectionLock(m_selectionLocklevel);
0337     Ignore clipboardLock(m_clipboardLocklevel);
0338     updateTimestamp();
0339     HistoryItemPtr item(HistoryItemPtr(new HistoryStringItem(s)));
0340     setClipboard(*item, Clipboard | Selection);
0341     history()->insert(item);
0342 }
0343 
0344 // DBUS - don't call from Klipper itself
0345 void Klipper::clearClipboardContents()
0346 {
0347     updateTimestamp();
0348     slotClearClipboard();
0349 }
0350 
0351 // DBUS - don't call from Klipper itself
0352 void Klipper::clearClipboardHistory()
0353 {
0354     updateTimestamp();
0355     history()->slotClear();
0356     saveSession();
0357 }
0358 
0359 // DBUS - don't call from Klipper itself
0360 void Klipper::saveClipboardHistory()
0361 {
0362     if (m_bKeepContents) { // save the clipboard eventually
0363         saveHistory();
0364     }
0365 }
0366 
0367 void Klipper::slotStartShowTimer()
0368 {
0369     m_showTimer.start();
0370 }
0371 
0372 void Klipper::loadSettings()
0373 {
0374     // Security bug 142882: If user has save clipboard turned off, old data should be deleted from disk
0375     static bool firstrun = true;
0376     if (!firstrun && m_bKeepContents && !KlipperSettings::keepClipboardContents()) {
0377         saveHistory(true);
0378     }
0379     firstrun = false;
0380 
0381     m_bKeepContents = KlipperSettings::keepClipboardContents();
0382     m_bReplayActionInHistory = KlipperSettings::replayActionInHistory();
0383     m_bNoNullClipboard = KlipperSettings::preventEmptyClipboard();
0384     // 0 is the id of "Ignore selection" radiobutton
0385     m_bIgnoreSelection = KlipperSettings::ignoreSelection();
0386     m_bIgnoreImages = KlipperSettings::ignoreImages();
0387     m_bSynchronize = KlipperSettings::syncClipboards();
0388     // NOTE: not used atm - kregexpeditor is not ported to kde4
0389     m_bUseGUIRegExpEditor = KlipperSettings::useGUIRegExpEditor();
0390     m_bSelectionTextOnly = KlipperSettings::selectionTextOnly();
0391 
0392     m_bURLGrabber = KlipperSettings::uRLGrabberEnabled();
0393     // this will cause it to loadSettings too
0394     setURLGrabberEnabled(m_bURLGrabber);
0395     history()->setMaxSize(KlipperSettings::maxClipItems());
0396     history()->model()->setDisplayImages(!m_bIgnoreImages);
0397 
0398     // Convert 4.3 settings
0399     if (KlipperSettings::synchronize() != 3) {
0400         // 2 was the id of "Ignore selection" radiobutton
0401         m_bIgnoreSelection = KlipperSettings::synchronize() == 2;
0402         // 0 was the id of "Synchronize contents" radiobutton
0403         m_bSynchronize = KlipperSettings::synchronize() == 0;
0404         KConfigSkeletonItem *item = KlipperSettings::self()->findItem(QStringLiteral("SyncClipboards"));
0405         item->setProperty(m_bSynchronize);
0406         item = KlipperSettings::self()->findItem(QStringLiteral("IgnoreSelection"));
0407         item->setProperty(m_bIgnoreSelection);
0408         item = KlipperSettings::self()->findItem(QStringLiteral("Synchronize")); // Mark property as converted.
0409         item->setProperty(3);
0410         KlipperSettings::self()->save();
0411         KlipperSettings::self()->load();
0412     }
0413 }
0414 
0415 void Klipper::saveSettings() const
0416 {
0417     m_myURLGrabber->saveSettings();
0418     KlipperSettings::self()->setVersion(QStringLiteral(KLIPPER_VERSION_STRING));
0419     KlipperSettings::self()->save();
0420 
0421     // other settings should be saved automatically by KConfigDialog
0422 }
0423 
0424 void Klipper::showPopupMenu(QMenu *menu)
0425 {
0426     Q_ASSERT(menu != nullptr);
0427     if (m_plasmashell) {
0428         menu->hide();
0429     }
0430     menu->popup(QCursor::pos());
0431     if (m_plasmashell) {
0432         menu->windowHandle()->installEventFilter(this);
0433     }
0434 }
0435 
0436 bool Klipper::eventFilter(QObject *filtered, QEvent *event)
0437 {
0438     const bool ret = QObject::eventFilter(filtered, event);
0439     auto menuWindow = qobject_cast<QWindow *>(filtered);
0440     if (menuWindow && event->type() == QEvent::Expose && menuWindow->isVisible()) {
0441         auto surface = KWayland::Client::Surface::fromWindow(menuWindow);
0442         auto plasmaSurface = m_plasmashell->createSurface(surface, menuWindow);
0443         plasmaSurface->openUnderCursor();
0444         plasmaSurface->setSkipTaskbar(true);
0445         plasmaSurface->setSkipSwitcher(true);
0446         menuWindow->removeEventFilter(this);
0447     }
0448     return ret;
0449 }
0450 
0451 bool Klipper::loadHistory()
0452 {
0453     static const char failed_load_warning[] = "Failed to load history resource. Clipboard history cannot be read.";
0454     // don't use "appdata", klipper is also a kicker applet
0455     QFile history_file(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("klipper/history2.lst")));
0456     if (!history_file.exists()) {
0457         qCWarning(KLIPPER_LOG) << failed_load_warning << ": "
0458                                << "History file does not exist";
0459         return false;
0460     }
0461     if (!history_file.open(QIODevice::ReadOnly)) {
0462         qCWarning(KLIPPER_LOG) << failed_load_warning << ": " << history_file.errorString();
0463         return false;
0464     }
0465     QDataStream file_stream(&history_file);
0466     if (file_stream.atEnd()) {
0467         qCWarning(KLIPPER_LOG) << failed_load_warning << ": "
0468                                << "Error in reading data";
0469         return false;
0470     }
0471     QByteArray data;
0472     quint32 crc;
0473     file_stream >> crc >> data;
0474     if (crc32(0, reinterpret_cast<unsigned char *>(data.data()), data.size()) != crc) {
0475         qCWarning(KLIPPER_LOG) << failed_load_warning << ": "
0476                                << "CRC checksum does not match";
0477         return false;
0478     }
0479     QDataStream history_stream(&data, QIODevice::ReadOnly);
0480 
0481     char *version;
0482     history_stream >> version;
0483     delete[] version;
0484 
0485     QVector<HistoryItemPtr> items;
0486     std::unordered_set<QByteArray> uuidSet;
0487     for (HistoryItemPtr item = HistoryItem::create(history_stream); !item.isNull(); item = HistoryItem::create(history_stream)) {
0488         // BUG 466236 Due to a catastrophic bug existing in 5.27 beta and 5.27.0, there could be
0489         // duplicate items in klipper's history file, so filter them out here.
0490         if (uuidSet.count(item->uuid()) > 0) {
0491             qCWarning(KLIPPER_LOG) << "One item in history has the same UUID with another item, ignored.";
0492             continue;
0493         }
0494         items.append(item);
0495         uuidSet.insert(item->uuid());
0496     }
0497 
0498     history()->clearAndBatchInsert(items);
0499 
0500     if (!history()->empty()) {
0501         setClipboard(*history()->first(), Clipboard | Selection);
0502     }
0503 
0504     return true;
0505 }
0506 
0507 void Klipper::saveHistory(bool empty)
0508 {
0509     QMutexLocker lock(m_history->model()->mutex());
0510     static const char failed_save_warning[] = "Failed to save history. Clipboard history cannot be saved.";
0511     // don't use "appdata", klipper is also a kicker applet
0512     QString history_file_name(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("klipper/history2.lst")));
0513     if (history_file_name.isNull() || history_file_name.isEmpty()) {
0514         // try creating the file
0515         QDir dir(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation));
0516         if (!dir.mkpath(QStringLiteral("klipper"))) {
0517             qCWarning(KLIPPER_LOG) << failed_save_warning;
0518             return;
0519         }
0520         history_file_name = dir.absoluteFilePath(QStringLiteral("klipper/history2.lst"));
0521     }
0522     if (history_file_name.isNull() || history_file_name.isEmpty()) {
0523         qCWarning(KLIPPER_LOG) << failed_save_warning;
0524         return;
0525     }
0526     QSaveFile history_file(history_file_name);
0527     if (!history_file.open(QIODevice::WriteOnly)) {
0528         qCWarning(KLIPPER_LOG) << failed_save_warning;
0529         return;
0530     }
0531     QByteArray data;
0532     QDataStream history_stream(&data, QIODevice::WriteOnly);
0533     history_stream << KLIPPER_VERSION_STRING; // const char*
0534 
0535     if (!empty) {
0536         HistoryItemConstPtr item = history()->first();
0537         if (item) {
0538             do {
0539                 history_stream << item.data();
0540                 item = HistoryItemConstPtr(history()->find(item->next_uuid()));
0541             } while (item != history()->first());
0542         }
0543     }
0544 
0545     quint32 crc = crc32(0, reinterpret_cast<unsigned char *>(data.data()), data.size());
0546     QDataStream ds(&history_file);
0547     ds << crc << data;
0548     if (!history_file.commit()) {
0549         qCWarning(KLIPPER_LOG) << failed_save_warning;
0550     }
0551 }
0552 
0553 // save session on shutdown. Don't simply use the c'tor, as that may not be called.
0554 void Klipper::saveSession()
0555 {
0556     if (m_bKeepContents) { // save the clipboard eventually
0557         saveHistory();
0558     }
0559     saveSettings();
0560 }
0561 
0562 void Klipper::disableURLGrabber()
0563 {
0564     QMessageBox *message = new QMessageBox(QMessageBox::Information,
0565                                            QString(),
0566                                            xi18nc("@info",
0567                                                   "You can enable URL actions later in the "
0568                                                   "<interface>Actions</interface> page of the "
0569                                                   "Clipboard applet's configuration window"));
0570     message->setAttribute(Qt::WA_DeleteOnClose);
0571     message->setModal(false);
0572     message->show();
0573 
0574     setURLGrabberEnabled(false);
0575 }
0576 
0577 void Klipper::slotConfigure()
0578 {
0579     if (KConfigDialog::showDialog(QStringLiteral("preferences"))) {
0580         // This will never happen, because of the WA_DeleteOnClose below.
0581         return;
0582     }
0583 
0584     ConfigDialog *dlg = new ConfigDialog(nullptr, KlipperSettings::self(), this, m_collection);
0585     QMetaObject::invokeMethod(dlg, "setHelp", Qt::DirectConnection, Q_ARG(QString, QString::fromLatin1("preferences")));
0586     // This is necessary to ensure that the dialog is recreated
0587     // and therefore the controls are initialised from the current
0588     // Klipper settings every time that it is shown.
0589     dlg->setAttribute(Qt::WA_DeleteOnClose);
0590 
0591     connect(dlg, &KConfigDialog::settingsChanged, this, &Klipper::loadSettings);
0592     dlg->show();
0593 }
0594 
0595 void Klipper::slotQuit()
0596 {
0597     // If the menu was just opened, likely the user
0598     // selected quit by accident while attempting to
0599     // click the Klipper icon.
0600     if (m_showTimer.elapsed() < 300) {
0601         return;
0602     }
0603 
0604     saveSession();
0605     int autoStart = KMessageBox::questionTwoActionsCancel(nullptr,
0606                                                           i18n("Should Klipper start automatically when you login?"),
0607                                                           i18n("Automatically Start Klipper?"),
0608                                                           KGuiItem(i18n("Start")),
0609                                                           KGuiItem(i18n("Do Not Start")),
0610                                                           KStandardGuiItem::cancel(),
0611                                                           QStringLiteral("StartAutomatically"));
0612 
0613     KConfigGroup config(KSharedConfig::openConfig(), "General");
0614     if (autoStart == KMessageBox::PrimaryAction) {
0615         config.writeEntry("AutoStart", true);
0616     } else if (autoStart == KMessageBox::SecondaryAction) {
0617         config.writeEntry("AutoStart", false);
0618     } else // cancel chosen don't quit
0619         return;
0620     config.sync();
0621 
0622     qApp->quit();
0623 }
0624 
0625 void Klipper::slotPopupMenu()
0626 {
0627     m_popup->ensureClean();
0628     m_popup->slotSetTopActive();
0629     showPopupMenu(m_popup);
0630 }
0631 
0632 void Klipper::slotRepeatAction()
0633 {
0634     auto top = qSharedPointerCast<const HistoryStringItem>(history()->first());
0635     if (top) {
0636         m_myURLGrabber->invokeAction(top);
0637     }
0638 }
0639 
0640 void Klipper::setURLGrabberEnabled(bool enable)
0641 {
0642     if (enable != m_bURLGrabber) {
0643         m_bURLGrabber = enable;
0644         m_lastURLGrabberTextSelection.clear();
0645         m_lastURLGrabberTextClipboard.clear();
0646         KlipperSettings::setURLGrabberEnabled(enable);
0647     }
0648 
0649     m_toggleURLGrabAction->setChecked(enable);
0650 
0651     // make it update its settings
0652     m_myURLGrabber->loadSettings();
0653 }
0654 
0655 void Klipper::slotHistoryTopChanged()
0656 {
0657     if (m_selectionLocklevel || m_clipboardLocklevel) {
0658         return;
0659     }
0660 
0661     auto topitem = history()->first();
0662     if (topitem) {
0663         setClipboard(*topitem, Clipboard | Selection);
0664     }
0665     if (m_bReplayActionInHistory && m_bURLGrabber) {
0666         slotRepeatAction();
0667     }
0668 }
0669 
0670 void Klipper::slotClearClipboard()
0671 {
0672     Ignore selectionLock(m_selectionLocklevel);
0673     Ignore clipboardLock(m_clipboardLocklevel);
0674 
0675     m_clip->clear(QClipboard::Selection);
0676     m_clip->clear(QClipboard::Clipboard);
0677 }
0678 
0679 HistoryItemPtr Klipper::applyClipChanges(const QMimeData *clipData, bool selectionMode)
0680 {
0681     if (selectionMode && m_selectionLocklevel || !selectionMode && m_clipboardLocklevel) {
0682         return HistoryItemPtr();
0683     }
0684     Ignore lock(selectionMode ? m_selectionLocklevel : m_clipboardLocklevel);
0685 
0686     if (!(history()->empty())) {
0687         if (m_bIgnoreImages && history()->first()->type() == HistoryItemType::Image) {
0688             history()->remove(history()->first());
0689         }
0690     }
0691 
0692     HistoryItemPtr item = HistoryItem::create(clipData);
0693 
0694     bool saveToHistory = true;
0695     if (clipData->data(QStringLiteral("x-kde-passwordManagerHint")) == QByteArrayLiteral("secret")) {
0696         saveToHistory = false;
0697     }
0698     if (saveToHistory) {
0699         history()->insert(item);
0700     }
0701 
0702     return item;
0703 }
0704 
0705 void Klipper::newClipData(QClipboard::Mode mode)
0706 {
0707     if ((mode == QClipboard::Clipboard && m_clipboardLocklevel) || (mode == QClipboard::Selection && m_selectionLocklevel)) {
0708         return;
0709     }
0710 
0711     if (mode == QClipboard::Selection && blockFetchingNewData())
0712         return;
0713 
0714     checkClipData(mode == QClipboard::Selection ? true : false);
0715 }
0716 
0717 void Klipper::slotHistoryChanged()
0718 {
0719     if (history()->empty()) {
0720         slotClearClipboard();
0721     }
0722 }
0723 
0724 // Protection against too many clipboard data changes. Lyx responds to clipboard data
0725 // requests with setting new clipboard data, so if Lyx takes over clipboard,
0726 // Klipper notices, requests this data, this triggers "new" clipboard contents
0727 // from Lyx, so Klipper notices again, requests this data, ... you get the idea.
0728 const int MAX_CLIPBOARD_CHANGES = 10; // max changes per second
0729 
0730 bool Klipper::blockFetchingNewData()
0731 {
0732 #if HAVE_X11
0733     // Hacks for #85198 and #80302.
0734     // #85198 - block fetching new clipboard contents if Shift is pressed and mouse is not,
0735     //   this may mean the user is doing selection using the keyboard, in which case
0736     //   it's possible the app sets new clipboard contents after every change - Klipper's
0737     //   history would list them all.
0738     // #80302 - OOo (v1.1.3 at least) has a bug that if Klipper requests its clipboard contents
0739     //   while the user is doing a selection using the mouse, OOo stops updating the clipboard
0740     //   contents, so in practice it's like the user has selected only the part which was
0741     //   selected when Klipper asked first.
0742     // Use XQueryPointer rather than QApplication::mouseButtons()/keyboardModifiers(), because
0743     //   Klipper needs the very current state.
0744     if (!KWindowSystem::isPlatformX11()) {
0745         return false;
0746     }
0747     xcb_connection_t *c = QX11Info::connection();
0748     const xcb_query_pointer_cookie_t cookie = xcb_query_pointer_unchecked(c, QX11Info::appRootWindow());
0749     UniqueCPointer<xcb_query_pointer_reply_t> queryPointer(xcb_query_pointer_reply(c, cookie, nullptr));
0750     if (!queryPointer) {
0751         return false;
0752     }
0753     if (((queryPointer->mask & (XCB_KEY_BUT_MASK_SHIFT | XCB_KEY_BUT_MASK_BUTTON_1)) == XCB_KEY_BUT_MASK_SHIFT) // BUG: 85198
0754         || ((queryPointer->mask & XCB_KEY_BUT_MASK_BUTTON_1) == XCB_KEY_BUT_MASK_BUTTON_1)) { // BUG: 80302
0755         m_pendingContentsCheck = true;
0756         m_pendingCheckTimer.start(100ms);
0757         return true;
0758     }
0759     m_pendingContentsCheck = false;
0760     if (m_overflowCounter == 0)
0761         m_overflowClearTimer.start(1s);
0762     if (++m_overflowCounter > MAX_CLIPBOARD_CHANGES)
0763         return true;
0764 #endif
0765     return false;
0766 }
0767 
0768 void Klipper::slotCheckPending()
0769 {
0770     if (!m_pendingContentsCheck)
0771         return;
0772     m_pendingContentsCheck = false; // blockFetchingNewData() will be called again
0773     updateTimestamp();
0774     newClipData(QClipboard::Selection); // always selection
0775 }
0776 
0777 void Klipper::checkClipData(bool selectionMode)
0778 {
0779     if (ignoreClipboardChanges()) // internal to klipper, ignoring QSpinBox selections
0780     {
0781         // keep our old clipboard, thanks
0782         // This won't quite work, but it's close enough for now.
0783         // The trouble is that the top selection =! top clipboard
0784         // but we don't track that yet. We will....
0785         auto top = history()->first();
0786         if (top) {
0787             setClipboard(*top, selectionMode ? Selection : Clipboard);
0788         }
0789         return;
0790     }
0791 
0792     qCDebug(KLIPPER_LOG) << "Checking clip data";
0793 
0794     const QMimeData *data = m_clip->mimeData(selectionMode ? QClipboard::Selection : QClipboard::Clipboard);
0795 
0796     bool clipEmpty = false;
0797     bool changed = true; // ### FIXME (only relevant under polling, might be better to simply remove polling and rely on XFixes)
0798     if (!data) {
0799         clipEmpty = true;
0800     } else {
0801         clipEmpty = data->formats().isEmpty();
0802         if (clipEmpty) {
0803             // Might be a timeout. Try again
0804             clipEmpty = data->formats().isEmpty();
0805             qCDebug(KLIPPER_LOG) << "was empty. Retried, now " << (clipEmpty ? " still empty" : " no longer empty");
0806         }
0807     }
0808 
0809     if (changed && clipEmpty && m_bNoNullClipboard) {
0810         auto top = history()->first();
0811         if (top) {
0812             // keep old clipboard after someone set it to null
0813             qCDebug(KLIPPER_LOG) << "Resetting clipboard (Prevent empty clipboard)";
0814             setClipboard(*top, selectionMode ? Selection : Clipboard, ClipboardUpdateReason::PreventEmptyClipboard);
0815         }
0816         return;
0817     } else if (clipEmpty) {
0818         return;
0819     }
0820 
0821     // this must be below the "bNoNullClipboard" handling code!
0822     // XXX: I want a better handling of selection/clipboard in general.
0823     // XXX: Order sensitive code. Must die.
0824     if (selectionMode && m_bIgnoreSelection)
0825         return;
0826 
0827     if (selectionMode && m_bSelectionTextOnly && !data->hasText())
0828         return;
0829 
0830     if (data->hasUrls())
0831         ; // ok
0832     else if (data->hasText())
0833         ; // ok
0834     else if (data->hasImage()) {
0835         if (m_bIgnoreImages && !data->hasFormat(QStringLiteral("x-kde-force-image-copy")))
0836             return;
0837     } else // unknown, ignore
0838         return;
0839 
0840     HistoryItemPtr item = applyClipChanges(data, selectionMode);
0841     if (changed) {
0842         qCDebug(KLIPPER_LOG) << "Synchronize?" << m_bSynchronize;
0843         if (m_bSynchronize && item) {
0844             setClipboard(*item, selectionMode ? Clipboard : Selection);
0845         }
0846     }
0847     QString &lastURLGrabberText = selectionMode ? m_lastURLGrabberTextSelection : m_lastURLGrabberTextClipboard;
0848     if (m_bURLGrabber && item && data->hasText()) {
0849         m_myURLGrabber->checkNewData(qSharedPointerConstCast<const HistoryItem>(item));
0850 
0851         // Make sure URLGrabber doesn't repeat all the time if klipper reads the same
0852         // text all the time (e.g. because XFixes is not available and the application
0853         // has broken TIMESTAMP target). Using most recent history item may not always
0854         // work.
0855         if (item->text() != lastURLGrabberText) {
0856             lastURLGrabberText = item->text();
0857         }
0858     } else {
0859         lastURLGrabberText.clear();
0860     }
0861 }
0862 
0863 void Klipper::setClipboard(const HistoryItem &item, int mode, ClipboardUpdateReason updateReason)
0864 {
0865     Ignore lock(mode == Selection ? m_selectionLocklevel : m_clipboardLocklevel);
0866 
0867     Q_ASSERT((mode & 1) == 0); // Warn if trying to pass a boolean as a mode.
0868 
0869     if (mode & Selection) {
0870         qCDebug(KLIPPER_LOG) << "Setting selection to <" << item.text() << ">";
0871         QMimeData *mimeData = item.mimeData();
0872         if (updateReason == ClipboardUpdateReason::PreventEmptyClipboard) {
0873             mimeData->setData(QStringLiteral("application/x-kde-onlyReplaceEmpty"), "1");
0874         }
0875         m_clip->setMimeData(mimeData, QClipboard::Selection);
0876     }
0877     if (mode & Clipboard) {
0878         qCDebug(KLIPPER_LOG) << "Setting clipboard to <" << item.text() << ">";
0879         QMimeData *mimeData = item.mimeData();
0880         if (updateReason == ClipboardUpdateReason::PreventEmptyClipboard) {
0881             mimeData->setData(QStringLiteral("application/x-kde-onlyReplaceEmpty"), "1");
0882         }
0883         m_clip->setMimeData(mimeData, QClipboard::Clipboard);
0884     }
0885 }
0886 
0887 void Klipper::slotClearOverflow()
0888 {
0889     m_overflowClearTimer.stop();
0890 
0891     if (m_overflowCounter > MAX_CLIPBOARD_CHANGES) {
0892         qCDebug(KLIPPER_LOG) << "App owning the clipboard/selection is lame";
0893         // update to the latest data - this unfortunately may trigger the problem again
0894         newClipData(QClipboard::Selection); // Always the selection.
0895     }
0896     m_overflowCounter = 0;
0897 }
0898 
0899 QStringList Klipper::getClipboardHistoryMenu()
0900 {
0901     QStringList menu;
0902     auto item = history()->first();
0903     if (item) {
0904         do {
0905             menu << item->text();
0906             item = history()->find(item->next_uuid());
0907         } while (item != history()->first());
0908     }
0909 
0910     return menu;
0911 }
0912 
0913 QString Klipper::getClipboardHistoryItem(int i)
0914 {
0915     auto item = history()->first();
0916     if (item) {
0917         do {
0918             if (i-- == 0) {
0919                 return item->text();
0920             }
0921             item = history()->find(item->next_uuid());
0922         } while (item != history()->first());
0923     }
0924     return QString();
0925 }
0926 
0927 //
0928 // changing a spinbox in klipper's config-dialog causes the lineedit-contents
0929 // of the spinbox to be selected and hence the clipboard changes. But we don't
0930 // want all those items in klipper's history. See #41917
0931 //
0932 bool Klipper::ignoreClipboardChanges() const
0933 {
0934     QWidget *focusWidget = qApp->focusWidget();
0935     if (focusWidget) {
0936         if (focusWidget->inherits("QSpinBox")
0937             || (focusWidget->parentWidget() && focusWidget->inherits("QLineEdit") && focusWidget->parentWidget()->inherits("QSpinWidget"))) {
0938             return true;
0939         }
0940     }
0941 
0942     return false;
0943 }
0944 
0945 void Klipper::updateTimestamp()
0946 {
0947 #if HAVE_X11
0948     if (KWindowSystem::isPlatformX11()) {
0949         QX11Info::setAppTime(QX11Info::getTimestamp());
0950     }
0951 #endif
0952 }
0953 
0954 void Klipper::editData(const QSharedPointer<const HistoryItem> &item)
0955 {
0956     QPointer<QDialog> dlg(new QDialog());
0957     dlg->setWindowTitle(i18n("Edit Contents"));
0958     QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, dlg);
0959     connect(buttons, &QDialogButtonBox::accepted, dlg.data(), &QDialog::accept);
0960     connect(buttons, &QDialogButtonBox::rejected, dlg.data(), &QDialog::reject);
0961     connect(dlg.data(), &QDialog::finished, dlg.data(), [this, dlg, item](int result) {
0962         Q_EMIT editFinished(item, result);
0963         dlg->deleteLater();
0964     });
0965 
0966     ClipboardContentTextEdit *edit = new ClipboardContentTextEdit(dlg);
0967     edit->setAcceptRichText(false);
0968     if (item) {
0969         edit->setPlainText(item->text());
0970     }
0971     edit->setFocus();
0972     edit->setMinimumSize(300, 40);
0973     QVBoxLayout *layout = new QVBoxLayout(dlg);
0974     layout->addWidget(edit);
0975     layout->addWidget(buttons);
0976     dlg->adjustSize();
0977 
0978     connect(edit, &ClipboardContentTextEdit::done, dlg.data(), &QDialog::accept);
0979     connect(dlg.data(), &QDialog::accepted, this, [this, edit, item]() {
0980         QString text = edit->toPlainText();
0981         if (item) {
0982             m_history->remove(item);
0983         }
0984         m_history->insert(HistoryItemPtr(new HistoryStringItem(text)));
0985         if (m_myURLGrabber) {
0986             m_myURLGrabber->checkNewData(HistoryItemConstPtr(m_history->first()));
0987         }
0988     });
0989 
0990     if (m_mode == KlipperMode::Standalone) {
0991         dlg->setModal(true);
0992         dlg->exec();
0993     } else if (m_mode == KlipperMode::DataEngine) {
0994         dlg->open();
0995     }
0996 }
0997 
0998 class BarcodeLabel : public QLabel
0999 {
1000 public:
1001     BarcodeLabel(Prison::AbstractBarcode *barcode, QWidget *parent = nullptr)
1002         : QLabel(parent)
1003         , m_barcode(barcode)
1004     {
1005         setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
1006         setPixmap(QPixmap::fromImage(m_barcode->toImage(size())));
1007     }
1008 
1009 protected:
1010     void resizeEvent(QResizeEvent *event) override
1011     {
1012         QLabel::resizeEvent(event);
1013         setPixmap(QPixmap::fromImage(m_barcode->toImage(event->size())));
1014     }
1015 
1016 private:
1017     std::unique_ptr<Prison::AbstractBarcode> m_barcode;
1018 };
1019 
1020 void Klipper::showBarcode(const QSharedPointer<const HistoryItem> &item)
1021 {
1022     using namespace Prison;
1023     QPointer<QDialog> dlg(new QDialog());
1024     dlg->setWindowTitle(i18n("Mobile Barcode"));
1025     QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Ok, dlg);
1026     buttons->button(QDialogButtonBox::Ok)->setShortcut(Qt::CTRL | Qt::Key_Return);
1027     connect(buttons, &QDialogButtonBox::accepted, dlg.data(), &QDialog::accept);
1028     connect(dlg.data(), &QDialog::finished, dlg.data(), &QDialog::deleteLater);
1029 
1030     QWidget *mw = new QWidget(dlg);
1031     QHBoxLayout *layout = new QHBoxLayout(mw);
1032 
1033     {
1034         AbstractBarcode *qrCode = createBarcode(QRCode);
1035         if (qrCode) {
1036             if (item) {
1037                 qrCode->setData(item->text());
1038             }
1039             BarcodeLabel *qrCodeLabel = new BarcodeLabel(qrCode, mw);
1040             layout->addWidget(qrCodeLabel);
1041         }
1042     }
1043     {
1044         AbstractBarcode *dataMatrix = createBarcode(DataMatrix);
1045         if (dataMatrix) {
1046             if (item) {
1047                 dataMatrix->setData(item->text());
1048             }
1049             BarcodeLabel *dataMatrixLabel = new BarcodeLabel(dataMatrix, mw);
1050             layout->addWidget(dataMatrixLabel);
1051         }
1052     }
1053 
1054     mw->setFocus();
1055     QVBoxLayout *vBox = new QVBoxLayout(dlg);
1056     vBox->addWidget(mw);
1057     vBox->addWidget(buttons);
1058     dlg->adjustSize();
1059 
1060     if (m_mode == KlipperMode::Standalone) {
1061         dlg->setModal(true);
1062         dlg->exec();
1063     } else if (m_mode == KlipperMode::DataEngine) {
1064         dlg->open();
1065     }
1066 }
1067 
1068 void Klipper::slotAskClearHistory()
1069 {
1070     int clearHist = KMessageBox::warningContinueCancel(nullptr,
1071                                                        i18n("Do you really want to clear and delete the entire clipboard history?"),
1072                                                        i18n("Clear Clipboard History"),
1073                                                        KStandardGuiItem::del(),
1074                                                        KStandardGuiItem::cancel(),
1075                                                        QStringLiteral("klipperClearHistoryAskAgain"),
1076                                                        KMessageBox::Dangerous);
1077     if (clearHist == KMessageBox::Continue) {
1078         history()->slotClear();
1079         saveHistory();
1080     }
1081 }
1082 
1083 void Klipper::slotCycleNext()
1084 {
1085     // do cycle and show popup only if we have something in clipboard
1086     if (m_history->first()) {
1087         m_history->cycleNext();
1088         Q_EMIT passivePopup(i18n("Clipboard history"), cycleText());
1089     }
1090 }
1091 
1092 void Klipper::slotCyclePrev()
1093 {
1094     // do cycle and show popup only if we have something in clipboard
1095     if (m_history->first()) {
1096         m_history->cyclePrev();
1097         Q_EMIT passivePopup(i18n("Clipboard history"), cycleText());
1098     }
1099 }
1100 
1101 QString Klipper::cycleText() const
1102 {
1103     const int WIDTH_IN_PIXEL = 400;
1104 
1105     auto itemprev = m_history->prevInCycle();
1106     auto item = m_history->first();
1107     auto itemnext = m_history->nextInCycle();
1108 
1109     QFontMetrics font_metrics(m_popup->fontMetrics());
1110     QString result(QStringLiteral("<table>"));
1111 
1112     if (itemprev) {
1113         result += QLatin1String("<tr><td>");
1114         result += i18n("up");
1115         result += QLatin1String("</td><td>");
1116         result += font_metrics.elidedText(itemprev->text().simplified().toHtmlEscaped(), Qt::ElideMiddle, WIDTH_IN_PIXEL);
1117         result += QLatin1String("</td></tr>");
1118     }
1119 
1120     result += QLatin1String("<tr><td>");
1121     result += i18n("current");
1122     result += QLatin1String("</td><td><b>");
1123     result += font_metrics.elidedText(item->text().simplified().toHtmlEscaped(), Qt::ElideMiddle, WIDTH_IN_PIXEL);
1124     result += QLatin1String("</b></td></tr>");
1125 
1126     if (itemnext) {
1127         result += QLatin1String("<tr><td>");
1128         result += i18n("down");
1129         result += QLatin1String("</td><td>");
1130         result += font_metrics.elidedText(itemnext->text().simplified().toHtmlEscaped(), Qt::ElideMiddle, WIDTH_IN_PIXEL);
1131         result += QLatin1String("</td></tr>");
1132     }
1133 
1134     result += QLatin1String("</table>");
1135     return result;
1136 }