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 }