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 }