File indexing completed on 2024-04-28 05:35:28

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