diff --git a/klipper/history.cpp b/klipper/history.cpp index 9640c23b0..36b00e90e 100644 --- a/klipper/history.cpp +++ b/klipper/history.cpp @@ -1,229 +1,240 @@ /* This file is part of the KDE project Copyright (C) 2004 Esben Mose Hansen Copyright (C) by Andrew Stanley-Jones Copyright (C) 2000 by Carsten Pfeiffer Copyright (C) 2014 by Martin Gräßlin This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "history.h" #include #include "historyitem.h" #include "historystringitem.h" #include "historymodel.h" class CycleBlocker { public: CycleBlocker(); ~CycleBlocker(); static bool isBlocked(); private: static int s_blocker; }; int CycleBlocker::s_blocker = 0; CycleBlocker::CycleBlocker() { s_blocker++; } CycleBlocker::~CycleBlocker() { s_blocker--; } bool CycleBlocker::isBlocked() { return s_blocker; } History::History( QObject* parent ) : QObject( parent ), m_topIsUserSelected( false ), m_model(new HistoryModel(this)) { connect(m_model, &HistoryModel::rowsInserted, this, [this](const QModelIndex &parent, int start) { Q_UNUSED(parent) if (start == 0) { emit topChanged(); } emit changed(); } ); connect(m_model, &HistoryModel::rowsMoved, this, [this](const QModelIndex &sourceParent, int sourceStart, int sourceEnd, const QModelIndex &destinationParent, int destinationRow) { Q_UNUSED(sourceParent) Q_UNUSED(sourceEnd) Q_UNUSED(destinationParent) if (sourceStart == 0 || destinationRow == 0) { emit topChanged(); } emit changed(); } ); connect(m_model, &HistoryModel::rowsRemoved, this, [this](const QModelIndex &parent, int start) { Q_UNUSED(parent) if (start == 0) { emit topChanged(); } emit changed(); } ); connect(m_model, &HistoryModel::modelReset, this, &History::changed); connect(m_model, &HistoryModel::modelReset, this, &History::topChanged); connect(this, &History::topChanged, [this]() { m_topIsUserSelected = false; if (!CycleBlocker::isBlocked()) { m_cycleStartUuid = QByteArray(); } } ); } History::~History() { } void History::insert(HistoryItemPtr item) { if ( !item ) return; m_model->insert(item); } void History::forceInsert(HistoryItemPtr item) { if ( !item ) return; // TODO: do we need a force insert in HistoryModel m_model->insert(item); } void History::remove( const HistoryItemConstPtr &newItem ) { if ( !newItem ) return; m_model->remove(newItem->uuid()); } void History::slotClear() { m_model->clear(); } void History::slotMoveToTop(QAction* action) { QByteArray uuid = action->data().toByteArray(); if (uuid.isNull()) // not an action from popupproxy return; slotMoveToTop(uuid); } void History::slotMoveToTop(const QByteArray& uuid) { + const QModelIndex item = m_model->indexOf(uuid); + if (item.isValid() && item.row() == 0) { + // The item is already at the top, but it still may be not be set as the actual clipboard + // contents, normally this happens if the item is only in the X11 mouse selection but + // not in the Ctrl+V clipboard. + emit topChanged(); + m_topIsUserSelected = true; + emit topIsUserSelectedSet(); + return; + } m_model->moveToTop(uuid); m_topIsUserSelected = true; + emit topIsUserSelectedSet(); } void History::setMaxSize( unsigned max_size ) { m_model->setMaxSize(max_size); } void History::cycleNext() { if (m_model->rowCount() < 2) { return; } if (m_cycleStartUuid.isEmpty()) { m_cycleStartUuid = m_model->index(0).data(Qt::UserRole+1).toByteArray(); } else if (m_cycleStartUuid == m_model->index(1).data(Qt::UserRole+1).toByteArray()) { // end of cycle return; } CycleBlocker blocker; m_model->moveTopToBack(); } void History::cyclePrev() { if (m_cycleStartUuid.isEmpty()) { return; } CycleBlocker blocker; m_model->moveBackToTop(); if (m_cycleStartUuid == m_model->index(0).data(Qt::UserRole+1).toByteArray()) { m_cycleStartUuid = QByteArray(); } } HistoryItemConstPtr History::nextInCycle() const { if (m_model->hasIndex(1, 0)) { if (!m_cycleStartUuid.isEmpty()) { // check whether we are not at the end if (m_cycleStartUuid == m_model->index(1).data(Qt::UserRole+1).toByteArray()) { return HistoryItemConstPtr(); } } return m_model->index(1).data(Qt::UserRole).value(); } return HistoryItemConstPtr(); } HistoryItemConstPtr History::prevInCycle() const { if (m_cycleStartUuid.isEmpty()) { return HistoryItemConstPtr(); } return m_model->index(m_model->rowCount() - 1).data(Qt::UserRole).value(); } HistoryItemConstPtr History::find(const QByteArray& uuid) const { const QModelIndex index = m_model->indexOf(uuid); if (!index.isValid()) { return HistoryItemConstPtr(); } return index.data(Qt::UserRole).value(); } bool History::empty() const { return m_model->rowCount() == 0; } unsigned int History::maxSize() const { return m_model->maxSize(); } HistoryItemConstPtr History::first() const { const QModelIndex index = m_model->index(0); if (!index.isValid()) { return HistoryItemConstPtr(); } return index.data(Qt::UserRole).value(); } diff --git a/klipper/history.h b/klipper/history.h index 7690ffdc1..7001450fc 100644 --- a/klipper/history.h +++ b/klipper/history.h @@ -1,151 +1,152 @@ /* This file is part of the KDE project Copyright (C) 2004 Esben Mose Hansen Copyright (C) Andrew Stanley-Jones This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef HISTORY_H #define HISTORY_H #include #include #include class HistoryItem; class HistoryModel; class QAction; class History : public QObject { Q_OBJECT public: History( QObject* parent ); ~History() override; /** * Inserts item into clipboard history top * if duplicate entry exist, the older duplicate is deleted. * The duplicate concept is "deep", so that two text string * are considerd duplicate if identical. */ void insert(QSharedPointer item); /** * Inserts item into clipboard without any checks * Used when restoring a saved history and internally. * Don't use this unless you're reasonable certain * that no duplicates are introduced */ void forceInsert(QSharedPointer item); /** * Remove (first) history item equal to item from history */ void remove( const QSharedPointer &item ); /** * Traversal: Get first item */ QSharedPointer first() const; /** * Get item identified by uuid */ QSharedPointer find(const QByteArray& uuid) const; /** * @return next item in cycle, or null if at end */ QSharedPointer nextInCycle() const; /** * @return previous item in cycle, or null if at top */ QSharedPointer prevInCycle() const; /** * True if no history items */ bool empty() const; /** * Set maximum history size */ void setMaxSize( unsigned max_size ); /** * Get the maximum history size */ unsigned maxSize() const; /** * returns true if the user has selected the top item */ bool topIsUserSelected() { return m_topIsUserSelected; } /** * Cycle to next item */ void cycleNext(); /** * Cycle to prev item */ void cyclePrev(); HistoryModel *model() { return m_model; } public Q_SLOTS: /** * move the history in position pos to top */ void slotMoveToTop(QAction* action); /** * move the history in position pos to top */ void slotMoveToTop(const QByteArray& uuid); /** * Clear history */ void slotClear(); Q_SIGNALS: void changed(); /** * Emitted when the first history item has changed. */ void topChanged(); + void topIsUserSelectedSet(); private: /** * True if the top is selected by the user */ bool m_topIsUserSelected; HistoryModel *m_model; QByteArray m_cycleStartUuid; }; #endif diff --git a/klipper/klipper.cpp b/klipper/klipper.cpp index 80ca4b8c9..262a73920 100644 --- a/klipper/klipper.cpp +++ b/klipper/klipper.cpp @@ -1,1028 +1,1029 @@ /* This file is part of the KDE project Copyright (C) by Andrew Stanley-Jones Copyright (C) 2000 by Carsten Pfeiffer Copyright (C) 2004 Esben Mose Hansen Copyright (C) 2008 by Dmitry Suzdalev This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "klipper.h" #include #include "klipper_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "configdialog.h" #include "klippersettings.h" #include "urlgrabber.h" #include "history.h" #include "historyitem.h" #include "historymodel.h" #include "historystringitem.h" #include "klipperpopup.h" #ifdef HAVE_PRISON #include #endif #include #if HAVE_X11 #include #include #endif namespace { /** * Use this when manipulating the clipboard * from within clipboard-related signals. * * This avoids issues such as mouse-selections that immediately * disappear. * pattern: Resource Acqusition is Initialisation (RAII) * * (This is not threadsafe, so don't try to use such in threaded * applications). */ struct Ignore { Ignore(int& locklevel) : locklevelref(locklevel) { locklevelref++; } ~Ignore() { locklevelref--; } private: int& locklevelref; }; } // config == KGlobal::config for process, otherwise applet Klipper::Klipper(QObject* parent, const KSharedConfigPtr& config, KlipperMode mode) : QObject( parent ) , m_overflowCounter( 0 ) , m_locklevel( 0 ) , m_config( config ) , m_pendingContentsCheck( false ) , m_mode(mode) { if (m_mode == KlipperMode::Standalone) { setenv("KSNI_NO_DBUSMENU", "1", 1); QDBusConnection::sessionBus().registerObject(QStringLiteral("/klipper"), this, QDBusConnection::ExportScriptableSlots); } updateTimestamp(); // read initial X user time m_clip = qApp->clipboard(); connect( m_clip, &QClipboard::changed, this, &Klipper::newClipData ); connect( &m_overflowClearTimer, &QTimer::timeout, this, &Klipper::slotClearOverflow); m_pendingCheckTimer.setSingleShot( true ); connect( &m_pendingCheckTimer, &QTimer::timeout, this, &Klipper::slotCheckPending); m_history = new History( this ); m_popup = new KlipperPopup(m_history); m_popup->setShowHelp(m_mode == KlipperMode::Standalone); connect(m_history, &History::changed, m_popup, &KlipperPopup::slotHistoryChanged); + connect(m_history, &History::topIsUserSelectedSet, m_popup, &KlipperPopup::slotTopIsUserSelectedSet); // we need that collection, otherwise KToggleAction is not happy :} m_collection = new KActionCollection( this ); m_toggleURLGrabAction = new KToggleAction( this ); m_collection->addAction( QStringLiteral("clipboard_action"), m_toggleURLGrabAction ); m_toggleURLGrabAction->setText(i18n("Enable Clipboard Actions")); KGlobalAccel::setGlobalShortcut(m_toggleURLGrabAction, QKeySequence(Qt::ALT+Qt::CTRL+Qt::Key_X)); connect( m_toggleURLGrabAction, &QAction::toggled, this, &Klipper::setURLGrabberEnabled); /* * Create URL grabber */ m_myURLGrabber = new URLGrabber(m_history); connect( m_myURLGrabber, &URLGrabber::sigPopup, this, &Klipper::showPopupMenu ); connect( m_myURLGrabber, &URLGrabber::sigDisablePopup, this, &Klipper::disableURLGrabber ); /* * Load configuration settings */ loadSettings(); // load previous history if configured if (m_bKeepContents) { loadHistory(); } m_clearHistoryAction = m_collection->addAction( QStringLiteral("clear-history") ); m_clearHistoryAction->setIcon( QIcon::fromTheme(QStringLiteral("edit-clear-history")) ); m_clearHistoryAction->setText( i18n("C&lear Clipboard History") ); KGlobalAccel::setGlobalShortcut(m_clearHistoryAction, QKeySequence()); connect(m_clearHistoryAction, &QAction::triggered, this, &Klipper::slotAskClearHistory); QString CONFIGURE=QStringLiteral("configure"); m_configureAction = m_collection->addAction( CONFIGURE ); m_configureAction->setIcon( QIcon::fromTheme(CONFIGURE) ); m_configureAction->setText( i18n("&Configure Klipper...") ); connect(m_configureAction, &QAction::triggered, this, &Klipper::slotConfigure); m_quitAction = m_collection->addAction( QStringLiteral("quit") ); m_quitAction->setIcon( QIcon::fromTheme(QStringLiteral("application-exit")) ); m_quitAction->setText( i18nc("@item:inmenu Quit Klipper", "&Quit") ); connect(m_quitAction, &QAction::triggered, this, &Klipper::slotQuit); m_repeatAction = m_collection->addAction(QStringLiteral("repeat_action")); m_repeatAction->setText(i18n("Manually Invoke Action on Current Clipboard")); KGlobalAccel::setGlobalShortcut(m_repeatAction, QKeySequence(Qt::ALT+Qt::CTRL+Qt::Key_R)); connect(m_repeatAction, &QAction::triggered, this, &Klipper::slotRepeatAction); // add an edit-possibility m_editAction = m_collection->addAction(QStringLiteral("edit_clipboard")); m_editAction->setIcon(QIcon::fromTheme(QStringLiteral("document-properties"))); m_editAction->setText(i18n("&Edit Contents...")); KGlobalAccel::setGlobalShortcut(m_editAction, QKeySequence()); connect(m_editAction, &QAction::triggered, this, [this]() { editData(m_history->first()); } ); #ifdef HAVE_PRISON // add barcode for mobile phones m_showBarcodeAction = m_collection->addAction(QStringLiteral("show-barcode")); m_showBarcodeAction->setText(i18n("&Show Barcode...")); KGlobalAccel::setGlobalShortcut(m_showBarcodeAction, QKeySequence()); connect(m_showBarcodeAction, &QAction::triggered, this, [this]() { showBarcode(m_history->first()); } ); #endif // Cycle through history m_cycleNextAction = m_collection->addAction(QStringLiteral("cycleNextAction")); m_cycleNextAction->setText(i18n("Next History Item")); KGlobalAccel::setGlobalShortcut(m_cycleNextAction, QKeySequence()); connect(m_cycleNextAction, &QAction::triggered, this, &Klipper::slotCycleNext); m_cyclePrevAction = m_collection->addAction(QStringLiteral("cyclePrevAction")); m_cyclePrevAction->setText(i18n("Previous History Item")); KGlobalAccel::setGlobalShortcut(m_cyclePrevAction, QKeySequence()); connect(m_cyclePrevAction, &QAction::triggered, this, &Klipper::slotCyclePrev); // Action to show Klipper popup on mouse position m_showOnMousePos = m_collection->addAction(QStringLiteral("show-on-mouse-pos")); m_showOnMousePos->setText(i18n("Open Klipper at Mouse Position")); KGlobalAccel::setGlobalShortcut(m_showOnMousePos, QKeySequence()); connect(m_showOnMousePos, &QAction::triggered, this, &Klipper::slotPopupMenu); connect ( history(), &History::topChanged, this, &Klipper::slotHistoryTopChanged ); connect( m_popup, &QMenu::aboutToShow, this, &Klipper::slotStartShowTimer ); if (m_mode == KlipperMode::Standalone) { m_popup->plugAction( m_toggleURLGrabAction ); m_popup->plugAction( m_clearHistoryAction ); m_popup->plugAction( m_configureAction ); m_popup->plugAction( m_repeatAction ); m_popup->plugAction( m_editAction ); #ifdef HAVE_PRISON m_popup->plugAction( m_showBarcodeAction ); #endif m_popup->plugAction( m_quitAction ); } // session manager interaction if (m_mode == KlipperMode::Standalone) { connect(qApp, &QGuiApplication::commitDataRequest, this, &Klipper::saveSession); } connect(this, &Klipper::passivePopup, this, [this] (const QString &caption, const QString &text) { if (m_notification) { m_notification->setTitle(caption); m_notification->setText(text); } else { m_notification = KNotification::event(KNotification::Notification, caption, text, QStringLiteral("klipper")); } } ); } Klipper::~Klipper() { delete m_myURLGrabber; } // DBUS QString Klipper::getClipboardContents() { return getClipboardHistoryItem(0); } void Klipper::showKlipperPopupMenu() { slotPopupMenu(); } void Klipper::showKlipperManuallyInvokeActionMenu() { slotRepeatAction(); } // DBUS - don't call from Klipper itself void Klipper::setClipboardContents(QString s) { if (s.isEmpty()) return; Ignore lock( m_locklevel ); updateTimestamp(); HistoryItemPtr item(HistoryItemPtr(new HistoryStringItem(s))); setClipboard( *item, Clipboard | Selection); history()->insert( item ); } // DBUS - don't call from Klipper itself void Klipper::clearClipboardContents() { updateTimestamp(); slotClearClipboard(); } // DBUS - don't call from Klipper itself void Klipper::clearClipboardHistory() { updateTimestamp(); slotClearClipboard(); history()->slotClear(); saveSession(); } // DBUS - don't call from Klipper itself void Klipper::saveClipboardHistory() { if ( m_bKeepContents ) { // save the clipboard eventually saveHistory(); } } void Klipper::slotStartShowTimer() { m_showTimer.start(); } void Klipper::loadSettings() { // Security bug 142882: If user has save clipboard turned off, old data should be deleted from disk static bool firstrun = true; if (!firstrun && m_bKeepContents && !KlipperSettings::keepClipboardContents()) { saveHistory(true); } firstrun=false; m_bKeepContents = KlipperSettings::keepClipboardContents(); m_bReplayActionInHistory = KlipperSettings::replayActionInHistory(); m_bNoNullClipboard = KlipperSettings::preventEmptyClipboard(); // 0 is the id of "Ignore selection" radiobutton m_bIgnoreSelection = KlipperSettings::ignoreSelection(); m_bIgnoreImages = KlipperSettings::ignoreImages(); m_bSynchronize = KlipperSettings::syncClipboards(); // NOTE: not used atm - kregexpeditor is not ported to kde4 m_bUseGUIRegExpEditor = KlipperSettings::useGUIRegExpEditor(); m_bSelectionTextOnly = KlipperSettings::selectionTextOnly(); m_bURLGrabber = KlipperSettings::uRLGrabberEnabled(); // this will cause it to loadSettings too setURLGrabberEnabled(m_bURLGrabber); history()->setMaxSize( KlipperSettings::maxClipItems() ); // Convert 4.3 settings if (KlipperSettings::synchronize() != 3) { // 2 was the id of "Ignore selection" radiobutton m_bIgnoreSelection = KlipperSettings::synchronize() == 2; // 0 was the id of "Synchronize contents" radiobutton m_bSynchronize = KlipperSettings::synchronize() == 0; KConfigSkeletonItem* item = KlipperSettings::self()->findItem(QStringLiteral("SyncClipboards")); item->setProperty(m_bSynchronize); item = KlipperSettings::self()->findItem(QStringLiteral("IgnoreSelection")); item->setProperty(m_bIgnoreSelection); item = KlipperSettings::self()->findItem(QStringLiteral("Synchronize")); // Mark property as converted. item->setProperty(3); KlipperSettings::self()->save(); KlipperSettings::self()->load(); } if (m_bKeepContents && !m_saveFileTimer) { m_saveFileTimer = new QTimer(this); m_saveFileTimer->setSingleShot(true); m_saveFileTimer->setInterval(5000); connect(m_saveFileTimer, &QTimer::timeout, this, [this] { QtConcurrent::run(this, &Klipper::saveHistory, false); } ); connect(m_history, &History::changed, m_saveFileTimer, static_cast(&QTimer::start)); } else { delete m_saveFileTimer; m_saveFileTimer = nullptr; } } void Klipper::saveSettings() const { m_myURLGrabber->saveSettings(); KlipperSettings::self()->setVersion(QStringLiteral(KLIPPER_VERSION_STRING)); KlipperSettings::self()->save(); // other settings should be saved automatically by KConfigDialog } void Klipper::showPopupMenu( QMenu* menu ) { Q_ASSERT( menu != 0L ); QSize size = menu->sizeHint(); // geometry is not valid until it's shown QPoint pos = QCursor::pos(); // ### We can't know where the systray icon is (since it can be hidden or shown // in several places), so the cursor position is the only option. if ( size.height() < pos.y() ) pos.ry() -= size.height(); menu->popup(pos); } bool Klipper::loadHistory() { static const char failed_load_warning[] = "Failed to load history resource. Clipboard history cannot be read."; // don't use "appdata", klipper is also a kicker applet QFile history_file(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("klipper/history2.lst"))); if ( !history_file.exists() ) { qCWarning(KLIPPER_LOG) << failed_load_warning << ": " << "History file does not exist" ; return false; } if ( !history_file.open( QIODevice::ReadOnly ) ) { qCWarning(KLIPPER_LOG) << failed_load_warning << ": " << history_file.errorString() ; return false; } QDataStream file_stream( &history_file ); if( file_stream.atEnd()) { qCWarning(KLIPPER_LOG) << failed_load_warning << ": " << "Error in reading data" ; return false; } QByteArray data; quint32 crc; file_stream >> crc >> data; if( crc32( 0, reinterpret_cast( data.data() ), data.size() ) != crc ) { qCWarning(KLIPPER_LOG) << failed_load_warning << ": " << "CRC checksum does not match" ; return false; } QDataStream history_stream( &data, QIODevice::ReadOnly ); char* version; history_stream >> version; delete[] version; // The list needs to be reversed, as it is saved // youngest-first to keep the most important clipboard // items at the top, but the history is created oldest // first. QVector reverseList; for ( HistoryItemPtr item = HistoryItem::create( history_stream ); !item.isNull(); item = HistoryItem::create( history_stream ) ) { reverseList.prepend( item ); } history()->slotClear(); for ( auto it = reverseList.constBegin(); it != reverseList.constEnd(); ++it ) { history()->forceInsert(*it); } if ( !history()->empty() ) { setClipboard( *history()->first(), Clipboard | Selection ); } return true; } void Klipper::saveHistory(bool empty) { QMutexLocker lock(m_history->model()->mutex()); static const char failed_save_warning[] = "Failed to save history. Clipboard history cannot be saved."; // don't use "appdata", klipper is also a kicker applet QString history_file_name(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("klipper/history2.lst"))); if ( history_file_name.isNull() || history_file_name.isEmpty() ) { // try creating the file QDir dir(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation)); if (!dir.mkpath(QStringLiteral("klipper"))) { qCWarning(KLIPPER_LOG) << failed_save_warning ; return; } history_file_name = dir.absoluteFilePath(QStringLiteral("klipper/history2.lst")); } if ( history_file_name.isNull() || history_file_name.isEmpty() ) { qCWarning(KLIPPER_LOG) << failed_save_warning ; return; } QSaveFile history_file( history_file_name ); if (!history_file.open(QIODevice::WriteOnly)) { qCWarning(KLIPPER_LOG) << failed_save_warning ; return; } QByteArray data; QDataStream history_stream( &data, QIODevice::WriteOnly ); history_stream << KLIPPER_VERSION_STRING; // const char* if (!empty) { HistoryItemConstPtr item = history()->first(); if (item) { do { history_stream << item.data(); item = HistoryItemConstPtr(history()->find(item->next_uuid())); } while (item != history()->first()); } } quint32 crc = crc32( 0, reinterpret_cast( data.data() ), data.size() ); QDataStream ds ( &history_file ); ds << crc << data; if (!history_file.commit()) { qCWarning(KLIPPER_LOG) << failed_save_warning ; } } // save session on shutdown. Don't simply use the c'tor, as that may not be called. void Klipper::saveSession() { if ( m_bKeepContents ) { // save the clipboard eventually saveHistory(); } saveSettings(); } void Klipper::disableURLGrabber() { KMessageBox::information( 0L, i18n( "You can enable URL actions later by left-clicking on the " "Klipper icon and selecting 'Enable Clipboard Actions'" ) ); setURLGrabberEnabled( false ); } void Klipper::slotConfigure() { if (KConfigDialog::showDialog(QStringLiteral("preferences"))) { return; } ConfigDialog *dlg = new ConfigDialog( 0, KlipperSettings::self(), this, m_collection ); connect(dlg, &KConfigDialog::settingsChanged, this, &Klipper::loadSettings); dlg->show(); } void Klipper::slotQuit() { // If the menu was just opened, likely the user // selected quit by accident while attempting to // click the Klipper icon. if ( m_showTimer.elapsed() < 300 ) { return; } saveSession(); int autoStart = KMessageBox::questionYesNoCancel(0, i18n("Should Klipper start automatically when you login?"), i18n("Automatically Start Klipper?"), KGuiItem(i18n("Start")), KGuiItem(i18n("Do Not Start")), KStandardGuiItem::cancel(), QStringLiteral("StartAutomatically")); KConfigGroup config( KSharedConfig::openConfig(), "General"); if ( autoStart == KMessageBox::Yes ) { config.writeEntry("AutoStart", true); } else if ( autoStart == KMessageBox::No) { config.writeEntry("AutoStart", false); } else // cancel chosen don't quit return; config.sync(); qApp->quit(); } void Klipper::slotPopupMenu() { m_popup->ensureClean(); m_popup->slotSetTopActive(); showPopupMenu( m_popup ); } void Klipper::slotRepeatAction() { auto top = qSharedPointerCast( history()->first() ); if ( top ) { m_myURLGrabber->invokeAction( top ); } } void Klipper::setURLGrabberEnabled( bool enable ) { if (enable != m_bURLGrabber) { m_bURLGrabber = enable; m_lastURLGrabberTextSelection.clear(); m_lastURLGrabberTextClipboard.clear(); KlipperSettings::setURLGrabberEnabled(enable); } m_toggleURLGrabAction->setChecked( enable ); // make it update its settings m_myURLGrabber->loadSettings(); } void Klipper::slotHistoryTopChanged() { if ( m_locklevel ) { return; } auto topitem = history()->first(); if ( topitem ) { setClipboard( *topitem, Clipboard | Selection ); } if ( m_bReplayActionInHistory && m_bURLGrabber ) { slotRepeatAction(); } } void Klipper::slotClearClipboard() { Ignore lock( m_locklevel ); m_clip->clear(QClipboard::Selection); m_clip->clear(QClipboard::Clipboard); } HistoryItemPtr Klipper::applyClipChanges( const QMimeData* clipData ) { if ( m_locklevel ) { return HistoryItemPtr(); } Ignore lock( m_locklevel ); HistoryItemPtr item = HistoryItem::create( clipData ); history()->insert( item ); return item; } void Klipper::newClipData( QClipboard::Mode mode ) { if ( m_locklevel ) { return; } if( mode == QClipboard::Selection && blockFetchingNewData()) return; checkClipData( mode == QClipboard::Selection ? true : false ); } // Protection against too many clipboard data changes. Lyx responds to clipboard data // requests with setting new clipboard data, so if Lyx takes over clipboard, // Klipper notices, requests this data, this triggers "new" clipboard contents // from Lyx, so Klipper notices again, requests this data, ... you get the idea. const int MAX_CLIPBOARD_CHANGES = 10; // max changes per second bool Klipper::blockFetchingNewData() { #if HAVE_X11 // Hacks for #85198 and #80302. // #85198 - block fetching new clipboard contents if Shift is pressed and mouse is not, // this may mean the user is doing selection using the keyboard, in which case // it's possible the app sets new clipboard contents after every change - Klipper's // history would list them all. // #80302 - OOo (v1.1.3 at least) has a bug that if Klipper requests its clipboard contents // while the user is doing a selection using the mouse, OOo stops updating the clipboard // contents, so in practice it's like the user has selected only the part which was // selected when Klipper asked first. // Use XQueryPointer rather than QApplication::mouseButtons()/keyboardModifiers(), because // Klipper needs the very current state. if (!KWindowSystem::isPlatformX11()) { return false; } xcb_connection_t *c = QX11Info::connection(); const xcb_query_pointer_cookie_t cookie = xcb_query_pointer_unchecked(c, QX11Info::appRootWindow()); QScopedPointer queryPointer(xcb_query_pointer_reply(c, cookie, nullptr)); if (queryPointer.isNull()) { return false; } if (((queryPointer->mask & (XCB_KEY_BUT_MASK_SHIFT | XCB_KEY_BUT_MASK_BUTTON_1)) == XCB_KEY_BUT_MASK_SHIFT) // BUG: 85198 || ((queryPointer->mask & XCB_KEY_BUT_MASK_BUTTON_1) == XCB_KEY_BUT_MASK_BUTTON_1)) { // BUG: 80302 m_pendingContentsCheck = true; m_pendingCheckTimer.start( 100 ); return true; } m_pendingContentsCheck = false; if ( m_overflowCounter == 0 ) m_overflowClearTimer.start( 1000 ); if( ++m_overflowCounter > MAX_CLIPBOARD_CHANGES ) return true; #endif return false; } void Klipper::slotCheckPending() { if( !m_pendingContentsCheck ) return; m_pendingContentsCheck = false; // blockFetchingNewData() will be called again updateTimestamp(); newClipData( QClipboard::Selection ); // always selection } void Klipper::checkClipData( bool selectionMode ) { if ( ignoreClipboardChanges() ) // internal to klipper, ignoring QSpinBox selections { // keep our old clipboard, thanks // This won't quite work, but it's close enough for now. // The trouble is that the top selection =! top clipboard // but we don't track that yet. We will.... auto top = history()->first(); if ( top ) { setClipboard( *top, selectionMode ? Selection : Clipboard); } return; } qCDebug(KLIPPER_LOG) << "Checking clip data"; const QMimeData* data = m_clip->mimeData( selectionMode ? QClipboard::Selection : QClipboard::Clipboard ); if ( !data ) { qCWarning(KLIPPER_LOG) << "No data in clipboard. This not not supposed to happen."; return; } bool changed = true; // ### FIXME (only relevant under polling, might be better to simply remove polling and rely on XFixes) bool clipEmpty = data->formats().isEmpty(); if (clipEmpty) { // Might be a timeout. Try again clipEmpty = data->formats().isEmpty(); qCDebug(KLIPPER_LOG) << "was empty. Retried, now " << (clipEmpty?" still empty":" no longer empty"); } if ( changed && clipEmpty && m_bNoNullClipboard ) { auto top = history()->first(); if ( top ) { // keep old clipboard after someone set it to null qCDebug(KLIPPER_LOG) << "Resetting clipboard (Prevent empty clipboard)"; setClipboard( *top, selectionMode ? Selection : Clipboard ); } return; } // this must be below the "bNoNullClipboard" handling code! // XXX: I want a better handling of selection/clipboard in general. // XXX: Order sensitive code. Must die. if ( selectionMode && m_bIgnoreSelection ) return; if( selectionMode && m_bSelectionTextOnly && !data->hasText()) return; if( data->hasUrls() ) ; // ok else if( data->hasText() ) ; // ok else if( data->hasImage() ) { if( m_bIgnoreImages ) return; } else // unknown, ignore return; HistoryItemPtr item = applyClipChanges( data ); if (changed) { qCDebug(KLIPPER_LOG) << "Synchronize?" << m_bSynchronize; if ( m_bSynchronize && item ) { setClipboard( *item, selectionMode ? Clipboard : Selection ); } } QString& lastURLGrabberText = selectionMode ? m_lastURLGrabberTextSelection : m_lastURLGrabberTextClipboard; if( m_bURLGrabber && item && data->hasText()) { m_myURLGrabber->checkNewData( qSharedPointerConstCast(item) ); // Make sure URLGrabber doesn't repeat all the time if klipper reads the same // text all the time (e.g. because XFixes is not available and the application // has broken TIMESTAMP target). Using most recent history item may not always // work. if ( item->text() != lastURLGrabberText ) { lastURLGrabberText = item->text(); } } else { lastURLGrabberText.clear(); } } void Klipper::setClipboard( const HistoryItem& item, int mode ) { Ignore lock( m_locklevel ); Q_ASSERT( ( mode & 1 ) == 0 ); // Warn if trying to pass a boolean as a mode. if ( mode & Selection ) { qCDebug(KLIPPER_LOG) << "Setting selection to <" << item.text() << ">"; m_clip->setMimeData( item.mimeData(), QClipboard::Selection ); } if ( mode & Clipboard ) { qCDebug(KLIPPER_LOG) << "Setting clipboard to <" << item.text() << ">"; m_clip->setMimeData( item.mimeData(), QClipboard::Clipboard ); } } void Klipper::slotClearOverflow() { m_overflowClearTimer.stop(); if( m_overflowCounter > MAX_CLIPBOARD_CHANGES ) { qCDebug(KLIPPER_LOG) << "App owning the clipboard/selection is lame"; // update to the latest data - this unfortunately may trigger the problem again newClipData( QClipboard::Selection ); // Always the selection. } m_overflowCounter = 0; } QStringList Klipper::getClipboardHistoryMenu() { QStringList menu; auto item = history()->first(); if (item) { do { menu << item->text(); item = history()->find(item->next_uuid()); } while (item != history()->first()); } return menu; } QString Klipper::getClipboardHistoryItem(int i) { auto item = history()->first(); if (item) { do { if (i-- == 0) { return item->text(); } item = history()->find(item->next_uuid()); } while (item != history()->first()); } return QString(); } // // changing a spinbox in klipper's config-dialog causes the lineedit-contents // of the spinbox to be selected and hence the clipboard changes. But we don't // want all those items in klipper's history. See #41917 // bool Klipper::ignoreClipboardChanges() const { QWidget *focusWidget = qApp->focusWidget(); if ( focusWidget ) { if ( focusWidget->inherits( "QSpinBox" ) || (focusWidget->parentWidget() && focusWidget->inherits("QLineEdit") && focusWidget->parentWidget()->inherits("QSpinWidget")) ) { return true; } } return false; } void Klipper::updateTimestamp() { #if HAVE_X11 if (KWindowSystem::isPlatformX11()) { QX11Info::setAppTime(QX11Info::getTimestamp()); } #endif } void Klipper::editData(const QSharedPointer< const HistoryItem > &item) { QPointer dlg(new QDialog()); dlg->setWindowTitle( i18n("Edit Contents") ); QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, dlg); buttons->button(QDialogButtonBox::Ok)->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttons, &QDialogButtonBox::accepted, dlg.data(), &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, dlg.data(), &QDialog::reject); connect(dlg.data(), &QDialog::finished, dlg.data(), [this, dlg, item](int result) { emit editFinished(item, result); dlg->deleteLater(); } ); KTextEdit *edit = new KTextEdit( dlg ); if (item) { edit->setText( item->text() ); } edit->setFocus(); edit->setMinimumSize( 300, 40 ); QVBoxLayout *layout = new QVBoxLayout(dlg); layout->addWidget(edit); layout->addWidget(buttons); dlg->adjustSize(); connect(dlg.data(), &QDialog::accepted, this, [this, edit, item]() { QString text = edit->toPlainText(); if (item) { m_history->remove( item ); } m_history->insert(HistoryItemPtr(new HistoryStringItem(text))); if (m_myURLGrabber) { m_myURLGrabber->checkNewData(HistoryItemConstPtr(m_history->first())); } }); if (m_mode == KlipperMode::Standalone) { dlg->setModal(true); dlg->exec(); } else if (m_mode == KlipperMode::DataEngine) { dlg->open(); } } #ifdef HAVE_PRISON class BarcodeLabel : public QLabel { public: BarcodeLabel(Prison::AbstractBarcode *barcode, QWidget *parent = nullptr) : QLabel(parent) , m_barcode(barcode) { setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); setPixmap(QPixmap::fromImage(m_barcode->toImage(size()))); } protected: void resizeEvent(QResizeEvent *event) override { QLabel::resizeEvent(event); setPixmap(QPixmap::fromImage(m_barcode->toImage(event->size()))); } private: QScopedPointer m_barcode; }; void Klipper::showBarcode(const QSharedPointer< const HistoryItem > &item) { using namespace Prison; QPointer dlg(new QDialog()); dlg->setWindowTitle( i18n("Mobile Barcode") ); QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Ok, dlg); buttons->button(QDialogButtonBox::Ok)->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttons, &QDialogButtonBox::accepted, dlg.data(), &QDialog::accept); connect(dlg.data(), &QDialog::finished, dlg.data(), &QDialog::deleteLater); QWidget* mw = new QWidget(dlg); QHBoxLayout* layout = new QHBoxLayout(mw); AbstractBarcode *qrCode = createBarcode(QRCode); AbstractBarcode *dataMatrix = createBarcode(DataMatrix); if (item) { qrCode->setData(item->text()); dataMatrix->setData(item->text()); } BarcodeLabel *qrCodeLabel = new BarcodeLabel(qrCode, mw); BarcodeLabel *dataMatrixLabel = new BarcodeLabel(dataMatrix, mw); layout->addWidget(qrCodeLabel); layout->addWidget(dataMatrixLabel); mw->setFocus(); QVBoxLayout *vBox = new QVBoxLayout(dlg); vBox->addWidget(mw); vBox->addWidget(buttons); dlg->adjustSize(); if (m_mode == KlipperMode::Standalone) { dlg->setModal(true); dlg->exec(); } else if (m_mode == KlipperMode::DataEngine) { dlg->open(); } } #endif //HAVE_PRISON void Klipper::slotAskClearHistory() { int clearHist = KMessageBox::questionYesNo(0, i18n("Really delete entire clipboard history?"), i18n("Delete clipboard history?"), KStandardGuiItem::yes(), KStandardGuiItem::no(), QStringLiteral("really_clear_history"), KMessageBox::Dangerous); if (clearHist == KMessageBox::Yes) { history()->slotClear(); slotClearClipboard(); saveHistory(); } } void Klipper::slotCycleNext() { //do cycle and show popup only if we have something in clipboard if (m_history->first()) { m_history->cycleNext(); emit passivePopup(i18n("Clipboard history"), cycleText()); } } void Klipper::slotCyclePrev() { //do cycle and show popup only if we have something in clipboard if (m_history->first()) { m_history->cyclePrev(); emit passivePopup(i18n("Clipboard history"), cycleText()); } } QString Klipper::cycleText() const { const int WIDTH_IN_PIXEL = 400; auto itemprev = m_history->prevInCycle(); auto item = m_history->first(); auto itemnext = m_history->nextInCycle(); QFontMetrics font_metrics(m_popup->fontMetrics()); QString result(QStringLiteral("")); if (itemprev) { result += QLatin1String(""); } result += QLatin1String(""); if (itemnext) { result += QLatin1String(""); } result += QLatin1String("
"); result += i18n("up"); result += QLatin1String(""); result += font_metrics.elidedText(itemprev->text().simplified().toHtmlEscaped(), Qt::ElideMiddle, WIDTH_IN_PIXEL); result += QLatin1String("
"); result += i18n("current"); result += QLatin1String(""); result += font_metrics.elidedText(item->text().simplified().toHtmlEscaped(), Qt::ElideMiddle, WIDTH_IN_PIXEL); result += QLatin1String("
"); result += i18n("down"); result += QLatin1String(""); result += font_metrics.elidedText(itemnext->text().simplified().toHtmlEscaped(), Qt::ElideMiddle, WIDTH_IN_PIXEL); result += QLatin1String("
"); return result; } diff --git a/klipper/klipperpopup.cpp b/klipper/klipperpopup.cpp index 12d3205d9..e0d35e56c 100644 --- a/klipper/klipperpopup.cpp +++ b/klipper/klipperpopup.cpp @@ -1,274 +1,281 @@ /* This file is part of the KDE project Copyright (C) 2004 Esben Mose Hansen Copyright (C) by Andrew Stanley-Jones Copyright (C) 2000 by Carsten Pfeiffer This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "klipperpopup.h" #include #include "klipper_debug.h" #include #include #include #include #include #include #include #include "history.h" #include "klipper.h" #include "popupproxy.h" namespace { static const int TOP_HISTORY_ITEM_INDEX = 2; } // #define DEBUG_EVENTS__ #ifdef DEBUG_EVENTS__ kdbgstream& operator<<( kdbgstream& stream, const QKeyEvent& e ) { stream << "(QKeyEvent(text=" << e.text() << ",key=" << e.key() << ( e.isAccepted()?",accepted":",ignored)" ) << ",count=" << e.count(); if ( e.modifiers() & Qt::AltModifier ) { stream << ",ALT"; } if ( e.modifiers() & Qt::ControlModifier ) { stream << ",CTRL"; } if ( e.modifiers() & Qt::MetaModifier ) { stream << ",META"; } if ( e.modifiers() & Qt::ShiftModifier ) { stream << ",SHIFT"; } if ( e.isAutoRepeat() ) { stream << ",AUTOREPEAT"; } stream << ")"; return stream; } #endif KlipperPopup::KlipperPopup( History* history ) : m_dirty( true ), m_textForEmptyHistory( i18n( "" ) ), m_textForNoMatch( i18n( "" ) ), m_history( history ), m_helpMenu( new KHelpMenu( this, i18n("KDE cut & paste history utility"), false ) ), m_popupProxy( 0 ), m_filterWidget( 0 ), m_filterWidgetAction( 0 ), m_nHistoryItems( 0 ), m_showHelp(true), m_lastEvent(NULL) { ensurePolished(); KWindowInfo windowInfo( winId(), NET::WMGeometry ); QRect geometry = windowInfo.geometry(); QRect screen = qApp->desktop()->screenGeometry(geometry.center()); int menuHeight = ( screen.height() ) * 3/4; int menuWidth = ( screen.width() ) * 1/3; m_popupProxy = new PopupProxy( this, menuHeight, menuWidth ); connect(this, &KlipperPopup::aboutToShow, this, &KlipperPopup::slotAboutToShow); } KlipperPopup::~KlipperPopup() { } void KlipperPopup::slotAboutToShow() { if ( m_filterWidget ) { if ( !m_filterWidget->text().isEmpty() ) { m_dirty = true; m_filterWidget->clear(); } } ensureClean(); } void KlipperPopup::ensureClean() { // If the history is unchanged since last menu build, the is no reason // to rebuild it, if ( m_dirty ) { rebuild(); } } void KlipperPopup::buildFromScratch() { addSection(QIcon::fromTheme(QStringLiteral("klipper")), i18n("Klipper - Clipboard Tool")); m_filterWidget = new KLineEdit(this); m_filterWidget->setFocusPolicy( Qt::NoFocus ); m_filterWidget->setPlaceholderText(i18n("Search...")); m_filterWidgetAction = new QWidgetAction(this); m_filterWidgetAction->setDefaultWidget(m_filterWidget); addAction(m_filterWidgetAction); addSeparator(); for (int i = 0; i < m_actions.count(); i++) { if (i + 1 == m_actions.count() && m_showHelp) { addMenu(m_helpMenu->menu())->setIcon(QIcon::fromTheme(QStringLiteral("help-contents"))); addSeparator(); } addAction(m_actions.at(i)); } } void KlipperPopup::rebuild( const QString& filter ) { if (actions().isEmpty()) { buildFromScratch(); } else { for ( int i=0; ipalette(); if ( filterexp.isValid() ) { palette.setColor( m_filterWidget->foregroundRole(), palette.color(foregroundRole()) ); } else { palette.setColor( m_filterWidget->foregroundRole(), Qt::red ); } m_nHistoryItems = m_popupProxy->buildParent( TOP_HISTORY_ITEM_INDEX, filterexp ); if ( m_nHistoryItems == 0 ) { if ( m_history->empty() ) { insertAction(actions().at(TOP_HISTORY_ITEM_INDEX), new QAction(m_textForEmptyHistory, this)); } else { palette.setColor( m_filterWidget->foregroundRole(), Qt::red ); insertAction(actions().at(TOP_HISTORY_ITEM_INDEX), new QAction(m_textForNoMatch, this)); } m_nHistoryItems++; } else { if ( history()->topIsUserSelected() ) { actions().at(TOP_HISTORY_ITEM_INDEX)->setCheckable(true); actions().at(TOP_HISTORY_ITEM_INDEX)->setChecked(true); } } m_filterWidget->setPalette( palette ); m_dirty = false; } +void KlipperPopup::slotTopIsUserSelectedSet() { + if ( !m_dirty && m_nHistoryItems > 0 && history()->topIsUserSelected() ) { + actions().at(TOP_HISTORY_ITEM_INDEX)->setCheckable(true); + actions().at(TOP_HISTORY_ITEM_INDEX)->setChecked(true); + } +} + void KlipperPopup::plugAction( QAction* action ) { m_actions.append(action); } /* virtual */ void KlipperPopup::keyPressEvent( QKeyEvent* e ) { // Most events are send down directly to the m_filterWidget. // If the m_filterWidget does not handle the event, it will // come back to this method. Remembering the last event stops // the infinite event loop if (m_lastEvent == e) { m_lastEvent= NULL; return; } m_lastEvent= e; // If alt-something is pressed, select a shortcut // from the menu. Do this by sending a keyPress // without the alt-modifier to the superobject. if ( e->modifiers() & Qt::AltModifier ) { QKeyEvent ke( QEvent::KeyPress, e->key(), e->modifiers() ^ Qt::AltModifier, e->text(), e->isAutoRepeat(), e->count() ); QMenu::keyPressEvent( &ke ); #ifdef DEBUG_EVENTS__ qCDebug(KLIPPER_LOG) << "Passing this event to ancestor (KMenu): " << e << "->" << ke; #endif if (ke.isAccepted()) { e->accept(); return; } else { e->ignore(); } } // Otherwise, send most events to the search // widget, except a few used for navigation: // These go to the superobject. switch( e->key() ) { case Qt::Key_Up: case Qt::Key_Down: case Qt::Key_Right: case Qt::Key_Left: case Qt::Key_Tab: case Qt::Key_Backtab: case Qt::Key_Escape: { #ifdef DEBUG_EVENTS__ qCDebug(KLIPPER_LOG) << "Passing this event to ancestor (KMenu): " << e; #endif QMenu::keyPressEvent(e); break; } case Qt::Key_Return: case Qt::Key_Enter: { QMenu::keyPressEvent(e); this->hide(); if (activeAction() == m_filterWidgetAction) setActiveAction(actions().at(TOP_HISTORY_ITEM_INDEX)); break; } default: { #ifdef DEBUG_EVENTS__ qCDebug(KLIPPER_LOG) << "Passing this event down to child (KLineEdit): " << e; #endif setActiveAction(actions().at(actions().indexOf(m_filterWidgetAction))); QString lastString = m_filterWidget->text(); QApplication::sendEvent(m_filterWidget, e); if (m_filterWidget->text() != lastString) { m_dirty = true; rebuild(m_filterWidget->text()); } break; } //default: } //case m_lastEvent= NULL; } void KlipperPopup::slotSetTopActive() { if (actions().size() > TOP_HISTORY_ITEM_INDEX) { setActiveAction(actions().at(TOP_HISTORY_ITEM_INDEX)); } } diff --git a/klipper/klipperpopup.h b/klipper/klipperpopup.h index 3ba3d97b3..cf70ab977 100644 --- a/klipper/klipperpopup.h +++ b/klipper/klipperpopup.h @@ -1,135 +1,136 @@ /* This file is part of the KDE project Copyright (C) 2004 Esben Mose Hansen Copytight (C) by Andrew Stanley-Jones This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KLIPPERPOPUP_H #define KLIPPERPOPUP_H #include #include class QAction; class QWidgetAction; class QKeyEvent; class KHelpMenu; class KLineEdit; class PopupProxy; class History; /** * Default view of clipboard history. * */ class KlipperPopup : public QMenu { Q_OBJECT public: explicit KlipperPopup( History* history ); ~KlipperPopup() override; void plugAction( QAction* action ); /** * Normally, the popupmenu is only rebuilt just before showing. * If you need the pixel-size or similar of the this menu, call * this beforehand. */ void ensureClean(); History* history() { return m_history; } const History* history() const { return m_history; } void setShowHelp(bool show) { m_showHelp = show; } public Q_SLOTS: void slotHistoryChanged() { m_dirty = true; } + void slotTopIsUserSelectedSet(); void slotAboutToShow(); /** * set the top history item active, to easy kb navigation */ void slotSetTopActive(); private: void rebuild( const QString& filter = QString() ); void buildFromScratch(); protected: void keyPressEvent( QKeyEvent* e ) override; private: bool m_dirty : 1; // true if menu contents needs to be rebuild. /** * Contains the string shown if the menu is empty. */ QString m_textForEmptyHistory; /** * Contains the string shown if the search string has no * matches and the menu is not empty. */ QString m_textForNoMatch; /** * The "document" (clipboard history) */ History* m_history; /** * The help menu */ KHelpMenu* m_helpMenu; /** * (unowned) actions to plug into the primary popup menu */ QList m_actions; /** * Proxy helper object used to track history items */ PopupProxy* m_popupProxy; /** * search filter widget */ KLineEdit* m_filterWidget; /** * Action of search widget */ QWidgetAction* m_filterWidgetAction; /** * The current number of history items in the clipboard */ int m_nHistoryItems; bool m_showHelp; /** * The last event which was received. Used to avoid an infinite event loop */ QKeyEvent* m_lastEvent; }; #endif