diff --git a/applets/clipboard/contents/ui/ClipboardPage.qml b/applets/clipboard/contents/ui/ClipboardPage.qml index 2623d0146..54061274c 100644 --- a/applets/clipboard/contents/ui/ClipboardPage.qml +++ b/applets/clipboard/contents/ui/ClipboardPage.qml @@ -1,126 +1,126 @@ /******************************************************************** This file is part of the KDE project. Copyright (C) 2014 Martin Gräßlin Copyright (C) 2014 Kai Uwe Broulik 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. If not, see . *********************************************************************/ import QtQuick 2.4 import QtQuick.Layouts 1.1 import org.kde.plasma.plasmoid 2.0 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 2.0 as PlasmaComponents import org.kde.plasma.extras 2.0 as PlasmaExtras ColumnLayout { Keys.onPressed: { switch(event.key) { case Qt.Key_Up: { clipboardMenu.view.decrementCurrentIndex(); event.accepted = true; break; } case Qt.Key_Down: { clipboardMenu.view.incrementCurrentIndex(); event.accepted = true; break; } case Qt.Key_Enter: case Qt.Key_Return: { if (clipboardMenu.view.currentIndex >= 0) { var uuid = clipboardMenu.model.get(clipboardMenu.view.currentIndex).UuidRole if (uuid) { clipboardSource.service(uuid, "select") clipboardMenu.view.currentIndex = 0 } } break; } case Qt.Key_Escape: { if (filter.text != "") { filter.text = ""; event.accepted = true; } break; } default: { // forward key to filter // filter.text += event.text wil break if the key is backspace if (event.key === Qt.Key_Backspace && filter.text == "") { return; } if (event.text !== "" && !filter.activeFocus) { clipboardMenu.view.currentIndex = -1 if (event.matches(StandardKey.Paste)) { filter.paste(); } else { filter.text = ""; filter.text += event.text; } filter.forceActiveFocus(); event.accepted = true; } } } } PlasmaExtras.Heading { id: emptyHint Layout.fillWidth: true level: 3 opacity: 0.6 visible: clipboardMenu.model.count === 0 && filter.length === 0 - text: i18n("Clipboard history is empty.") + text: i18n("Clipboard is empty") } RowLayout { Layout.fillWidth: true visible: !emptyHint.visible PlasmaComponents.TextField { id: filter placeholderText: i18n("Search...") clearButtonShown: true Layout.fillWidth: true } PlasmaComponents.ToolButton { iconSource: "edit-clear-history" tooltip: i18n("Clear history") onClicked: clipboardSource.service("", "clearHistory") } } Menu { id: clipboardMenu model: PlasmaCore.SortFilterModel { sourceModel: clipboardSource.models.clipboard filterRole: "DisplayRole" filterRegExp: filter.text } supportsBarcodes: clipboardSource.data["clipboard"]["supportsBarcodes"] Layout.fillWidth: true Layout.fillHeight: true onItemSelected: clipboardSource.service(uuid, "select") onRemove: clipboardSource.service(uuid, "remove") onEdit: clipboardSource.edit(uuid) onBarcode: { var page = stack.push(barcodePage); page.show(uuid); } onAction: { clipboardSource.service(uuid, "action") clipboardMenu.view.currentIndex = 0 } } } diff --git a/applets/clipboard/contents/ui/ImageItemDelegate.qml b/applets/clipboard/contents/ui/ImageItemDelegate.qml index c8c95666f..ae936b659 100644 --- a/applets/clipboard/contents/ui/ImageItemDelegate.qml +++ b/applets/clipboard/contents/ui/ImageItemDelegate.qml @@ -1,30 +1,31 @@ /******************************************************************** This file is part of the KDE project. Copyright (C) 2014 Martin Gräßlin Copyright 2014 Sebastian Kügler 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. If not, see . *********************************************************************/ import QtQuick 2.0 import org.kde.kquickcontrolsaddons 2.0 as KQuickControlsAddons KQuickControlsAddons.QPixmapItem { id: previewPixmap - height: Math.round(width * (nativeHeight/nativeWidth) + units.smallSpacing * 2) + width: Math.min(nativeWidth, width) + height: Math.min(nativeHeight, Math.round(width * (nativeHeight/nativeWidth) + units.smallSpacing * 2)) pixmap: DecorationRole fillMode: KQuickControlsAddons.QPixmapItem.PreserveAspectFit } diff --git a/klipper/autotests/CMakeLists.txt b/klipper/autotests/CMakeLists.txt index e7d42039d..07966faed 100644 --- a/klipper/autotests/CMakeLists.txt +++ b/klipper/autotests/CMakeLists.txt @@ -1,48 +1,50 @@ add_definitions(-DKLIPPER_UNIT_TEST) include(ECMMarkAsTest) ######################################################## # Test History ######################################################## set(libklipper_test_SRCS) ecm_qt_declare_logging_category(libklipper_test_SRCS HEADER klipper_debug.h IDENTIFIER KLIPPER_LOG CATEGORY_NAME org.kde.klipper) set(testHistory_SRCS ${libklipper_test_SRCS} historytest.cpp ../history.cpp ../historyimageitem.cpp ../historyitem.cpp ../historystringitem.cpp ../historyurlitem.cpp ../historymodel.cpp ) add_executable(testHistory ${testHistory_SRCS}) target_link_libraries(testHistory Qt5::Test Qt5::Widgets # QAction KF5::CoreAddons # KUrlMimeData + KF5::I18n ) add_test(NAME klipper-testHistory COMMAND testHistory) ecm_mark_as_test(testHistory) ######################################################## # Test History Model ######################################################## set(testHistoryModel_SRCS historymodeltest.cpp modeltest.cpp ../historymodel.cpp ../historyimageitem.cpp ../historyitem.cpp ../historystringitem.cpp ../historyurlitem.cpp ${libklipper_test_SRCS} ) add_executable(testHistoryModel ${testHistoryModel_SRCS}) target_link_libraries(testHistoryModel Qt5::Test Qt5::Widgets # QAction KF5::CoreAddons # KUrlMimeData + KF5::I18n ) add_test(NAME klipper-testHistoryModel COMMAND testHistoryModel) ecm_mark_as_test(testHistoryModel) diff --git a/klipper/historyimageitem.cpp b/klipper/historyimageitem.cpp index 1cc3d6726..3ebf1b56f 100644 --- a/klipper/historyimageitem.cpp +++ b/klipper/historyimageitem.cpp @@ -1,62 +1,78 @@ /* This file is part of the KDE project Copyright (C) 2004 Esben Mose Hansen 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 "historyimageitem.h" -#include +#include "historymodel.h" + #include +#include +#include + +#include namespace { QByteArray compute_uuid(const QPixmap& data) { QByteArray buffer; QDataStream out(&buffer, QIODevice::WriteOnly); out << data; return QCryptographicHash::hash(buffer, QCryptographicHash::Sha1); } } HistoryImageItem::HistoryImageItem( const QPixmap& data ) : HistoryItem(compute_uuid(data)) , m_data( data ) { } QString HistoryImageItem::text() const { - if ( m_text.isNull() ) { - m_text = QStringLiteral( "%1x%2x%3 %4" ) - .arg( m_data.width() ) - .arg( m_data.height() ) - .arg( m_data.depth() ); + if (m_text.isNull()) { + m_text = + QStringLiteral("▨ ") + + i18n("%1x%2 %3bpp") + .arg(m_data.width()) + .arg(m_data.height()) + .arg(m_data.depth()); } return m_text; - } /* virtual */ void HistoryImageItem::write( QDataStream& stream ) const { stream << QStringLiteral( "image" ) << m_data; } QMimeData* HistoryImageItem::mimeData() const { QMimeData *data = new QMimeData(); data->setImageData(m_data.toImage()); return data; } +const QPixmap& HistoryImageItem::image() const { + if (m_model->displayImages()) { + return m_data; + } + static QPixmap imageIcon( + QIcon::fromTheme(QStringLiteral("view-preview")).pixmap(QSize(48, 48)) + ); + return imageIcon; +} diff --git a/klipper/historyimageitem.h b/klipper/historyimageitem.h index 8ba76de74..ec0814d9a 100644 --- a/klipper/historyimageitem.h +++ b/klipper/historyimageitem.h @@ -1,58 +1,58 @@ /* This file is part of the KDE project Copyright (C) 2004 Esben Mose Hansen 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 HISTORYIMAGEITEM_H #define HISTORYIMAGEITEM_H #include "historyitem.h" /** * A image entry in the clipboard history. */ class HistoryImageItem : public HistoryItem { public: explicit HistoryImageItem( const QPixmap& data ); ~HistoryImageItem() override {} QString text() const override; bool operator==( const HistoryItem& rhs) const override { if ( const HistoryImageItem* casted_rhs = dynamic_cast( &rhs ) ) { return &casted_rhs->m_data == &m_data; // Not perfect, but better than nothing. } return false; } - const QPixmap& image() const override { return m_data; } + const QPixmap& image() const override; QMimeData* mimeData() const override; void write( QDataStream& stream ) const override; private: /** * */ const QPixmap m_data; /** * Cache for m_data's string representation */ mutable QString m_text; }; #endif diff --git a/klipper/historyitem.cpp b/klipper/historyitem.cpp index 6135feb2a..b60f701fd 100644 --- a/klipper/historyitem.cpp +++ b/klipper/historyitem.cpp @@ -1,134 +1,134 @@ /* This file is part of the KDE project Copyright (C) 2004 Esben Mose Hansen 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 "historyitem.h" #include "klipper_debug.h" #include #include #include #include "historystringitem.h" #include "historyimageitem.h" #include "historyurlitem.h" #include "historymodel.h" HistoryItem::HistoryItem(const QByteArray& uuid) - : m_uuid(uuid) - , m_model(nullptr) + : m_model(nullptr) + , m_uuid(uuid) { } HistoryItem::~HistoryItem() { } HistoryItemPtr HistoryItem::create( const QMimeData* data ) { #if 0 int i=0; foreach ( QString format, data->formats() ) { qCDebug(KLIPPER_LOG) << "format(" << i++ <<"): " << format; } #endif if (data->hasUrls()) { KUrlMimeData::MetaDataMap metaData; QList urls = KUrlMimeData::urlsFromMimeData(data, KUrlMimeData::PreferKdeUrls, &metaData); QByteArray bytes = data->data(QStringLiteral("application/x-kde-cutselection")); bool cut = !bytes.isEmpty() && (bytes.at(0) == '1'); // true if 1 return HistoryItemPtr(new HistoryURLItem(urls, metaData, cut)); } if (data->hasText()) { return HistoryItemPtr(new HistoryStringItem(data->text())); } if (data->hasImage()) { QImage image = qvariant_cast(data->imageData()); return HistoryItemPtr(new HistoryImageItem(QPixmap::fromImage(image))); } return HistoryItemPtr(); // Failed. } HistoryItemPtr HistoryItem::create( QDataStream& dataStream ) { if ( dataStream.atEnd() ) { return HistoryItemPtr(); } QString type; dataStream >> type; if ( type == QLatin1String("url") ) { QList urls; QMap< QString, QString > metaData; int cut; dataStream >> urls; dataStream >> metaData; dataStream >> cut; return HistoryItemPtr(new HistoryURLItem( urls, metaData, cut )); } if ( type == QLatin1String("string") ) { QString text; dataStream >> text; return HistoryItemPtr(new HistoryStringItem( text )); } if ( type == QLatin1String("image") ) { QPixmap image; dataStream >> image; return HistoryItemPtr(new HistoryImageItem( image )); } qCWarning(KLIPPER_LOG) << "Failed to restore history item: Unknown type \"" << type << "\"" ; return HistoryItemPtr(); } QByteArray HistoryItem::next_uuid() const { if (!m_model) { return m_uuid; } // go via the model to the next const QModelIndex ownIndex = m_model->indexOf(m_uuid); if (!ownIndex.isValid()) { // that was wrong, model doesn't contain our item, so there is no chain return m_uuid; } const int nextRow = (ownIndex.row() +1) % m_model->rowCount(); return m_model->index(nextRow, 0).data(Qt::UserRole+1).toByteArray(); } QByteArray HistoryItem::previous_uuid() const { if (!m_model) { return m_uuid; } // go via the model to the next const QModelIndex ownIndex = m_model->indexOf(m_uuid); if (!ownIndex.isValid()) { // that was wrong, model doesn't contain our item, so there is no chain return m_uuid; } const int nextRow = ((ownIndex.row() == 0) ? m_model->rowCount() : ownIndex.row()) - 1; return m_model->index(nextRow, 0).data(Qt::UserRole+1).toByteArray(); } void HistoryItem::setModel(HistoryModel *model) { m_model = model; } diff --git a/klipper/historyitem.h b/klipper/historyitem.h index f7a2cd50b..83c2a700f 100644 --- a/klipper/historyitem.h +++ b/klipper/historyitem.h @@ -1,127 +1,130 @@ /* This file is part of the KDE project Copyright (C) 2004 Esben Mose Hansen 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 HISTORYITEM_H #define HISTORYITEM_H #include class HistoryModel; class QString; class QMimeData; class QDataStream; class HistoryItem; typedef QSharedPointer HistoryItemPtr; typedef QSharedPointer HistoryItemConstPtr; /** * An entry in the clipboard history. */ class HistoryItem { public: explicit HistoryItem(const QByteArray& uuid); virtual ~HistoryItem(); /** * Return the current item as text * An image would be returned as a descriptive * text, such as 32x43 image. */ virtual QString text() const = 0; /** * @return uuid of current item. */ const QByteArray& uuid() const { return m_uuid; } /** * Return the current item as pixmap * A text would be returned as a null pixmap, * which is also the default implementation */ inline virtual const QPixmap& image() const; /** * Returns a pointer to a QMimeData suitable for QClipboard::setMimeData(). */ virtual QMimeData* mimeData() const = 0; /** * Write object on datastream */ virtual void write( QDataStream& stream ) const = 0; /** * Equality. */ virtual bool operator==(const HistoryItem& rhs) const = 0; /** * Create an HistoryItem from MimeSources (i.e., clipboard data) * returns null if create fails (e.g, unsupported mimetype) */ static HistoryItemPtr create( const QMimeData* data ); /** * Create an HistoryItem from data stream (i.e., disk file) * returns null if creation fails. In this case, the datastream * is left in an undefined state. */ static HistoryItemPtr create( QDataStream& dataStream ); /** * previous item's uuid * TODO: drop, only used in unit test now */ QByteArray previous_uuid() const; /** * next item's uuid * TODO: drop, only used in unit test now */ QByteArray next_uuid() const; void setModel(HistoryModel *model); + +protected: + HistoryModel *m_model; + private: QByteArray m_uuid; - HistoryModel *m_model; }; inline const QPixmap& HistoryItem::image() const { static QPixmap nullPixmap; return nullPixmap; } inline QDataStream& operator<<( QDataStream& lhs, HistoryItem const * const rhs ) { if ( rhs ) { rhs->write( lhs ); } return lhs; } Q_DECLARE_METATYPE(HistoryItem*) Q_DECLARE_METATYPE(HistoryItemPtr) Q_DECLARE_METATYPE(HistoryItemConstPtr) #endif diff --git a/klipper/historymodel.cpp b/klipper/historymodel.cpp index 5ed372296..156f73d84 100644 --- a/klipper/historymodel.cpp +++ b/klipper/historymodel.cpp @@ -1,219 +1,220 @@ /******************************************************************** This file is part of the KDE project. Copyright (C) 2014 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. If not, see . *********************************************************************/ #include "historymodel.h" #include "historyimageitem.h" #include "historystringitem.h" #include "historyurlitem.h" HistoryModel::HistoryModel(QObject *parent) : QAbstractListModel(parent) , m_maxSize(0) + , m_displayImages(true) , m_mutex(QMutex::Recursive) { } HistoryModel::~HistoryModel() { clear(); } void HistoryModel::clear() { QMutexLocker lock(&m_mutex); beginResetModel(); m_items.clear(); endResetModel(); } void HistoryModel::setMaxSize(int size) { if (m_maxSize == size) { return; } QMutexLocker lock(&m_mutex); m_maxSize = size; if (m_items.count() > m_maxSize) { removeRows(m_maxSize, m_items.count() - m_maxSize); } } int HistoryModel::rowCount(const QModelIndex &parent) const { if (parent.isValid()) { return 0; } return m_items.count(); } QVariant HistoryModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() >= m_items.count() || index.column() != 0) { return QVariant(); } QSharedPointer item = m_items.at(index.row()); HistoryItemType type = HistoryItemType::Text; if (dynamic_cast(item.data())) { type = HistoryItemType::Text; } else if (dynamic_cast(item.data())) { type = HistoryItemType::Image; } else if (dynamic_cast(item.data())) { type = HistoryItemType::Url; } switch (role) { case Qt::DisplayRole: return item->text(); case Qt::DecorationRole: return item->image(); case Qt::UserRole: return QVariant::fromValue(qSharedPointerConstCast(item)); case Qt::UserRole+1: return item->uuid(); case Qt::UserRole+2: return QVariant::fromValue(type); case Qt::UserRole+3: return item->uuid().toBase64(); case Qt::UserRole+4: return int(type); } return QVariant(); } bool HistoryModel::removeRows(int row, int count, const QModelIndex &parent) { if (parent.isValid()) { return false; } if ((row + count) > m_items.count()) { return false; } QMutexLocker lock(&m_mutex); beginRemoveRows(QModelIndex(), row, row + count - 1); for (int i = 0; i < count; ++i) { m_items.removeAt(row); } endRemoveRows(); return true; } bool HistoryModel::remove(const QByteArray &uuid) { QModelIndex index = indexOf(uuid); if (!index.isValid()) { return false; } return removeRow(index.row(), QModelIndex()); } QModelIndex HistoryModel::indexOf(const QByteArray &uuid) const { for (int i = 0; i < m_items.count(); ++i) { if (m_items.at(i)->uuid() == uuid) { return index(i); } } return QModelIndex(); } QModelIndex HistoryModel::indexOf(const HistoryItem *item) const { if (!item) { return QModelIndex(); } return indexOf(item->uuid()); } void HistoryModel::insert(QSharedPointer item) { if (item.isNull()) { return; } const QModelIndex existingItem = indexOf(item.data()); if (existingItem.isValid()) { // move to top moveToTop(existingItem.row()); return; } QMutexLocker lock(&m_mutex); if (m_items.count() == m_maxSize) { // remove last item if (m_maxSize == 0) { // special case - cannot insert any items return; } beginRemoveRows(QModelIndex(), m_items.count() - 1, m_items.count() - 1); m_items.removeLast(); endRemoveRows(); } beginInsertRows(QModelIndex(), 0, 0); item->setModel(this); m_items.prepend(item); endInsertRows(); } void HistoryModel::moveToTop(const QByteArray &uuid) { const QModelIndex existingItem = indexOf(uuid); if (!existingItem.isValid()) { return; } moveToTop(existingItem.row()); } void HistoryModel::moveToTop(int row) { if (row == 0 || row >= m_items.count()) { return; } QMutexLocker lock(&m_mutex); beginMoveRows(QModelIndex(), row, row, QModelIndex(), 0); m_items.move(row, 0); endMoveRows(); } void HistoryModel::moveTopToBack() { if (m_items.count() < 2) { return; } QMutexLocker lock(&m_mutex); beginMoveRows(QModelIndex(), 0, 0, QModelIndex(), m_items.count()); auto item = m_items.takeFirst(); m_items.append(item); endMoveRows(); } void HistoryModel::moveBackToTop() { moveToTop(m_items.count() - 1); } QHash< int, QByteArray > HistoryModel::roleNames() const { QHash hash; hash.insert(Qt::DisplayRole, QByteArrayLiteral("DisplayRole")); hash.insert(Qt::DecorationRole, QByteArrayLiteral("DecorationRole")); hash.insert(Qt::UserRole+3, QByteArrayLiteral("UuidRole")); hash.insert(Qt::UserRole+4, QByteArrayLiteral("TypeRole")); return hash; } diff --git a/klipper/historymodel.h b/klipper/historymodel.h index 256da1ab5..88ad5d76f 100644 --- a/klipper/historymodel.h +++ b/klipper/historymodel.h @@ -1,77 +1,89 @@ /******************************************************************** This file is part of the KDE project. Copyright (C) 2014 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. If not, see . *********************************************************************/ #ifndef KLIPPER_HISTORYMODEL_H #define KLIPPER_HISTORYMODEL_H #include #include class HistoryItem; enum class HistoryItemType { Text, Image, Url }; class HistoryModel : public QAbstractListModel { Q_OBJECT public: explicit HistoryModel(QObject *parent = nullptr); ~HistoryModel() override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override; QHash< int, QByteArray > roleNames() const override; bool remove(const QByteArray &uuid); int maxSize() const; void setMaxSize(int size); + bool displayImages() const; + void setDisplayImages(bool show); + void clear(); void moveToTop(const QByteArray &uuid); void moveTopToBack(); void moveBackToTop(); QModelIndex indexOf(const QByteArray &uuid) const; QModelIndex indexOf(const HistoryItem *item) const; void insert(QSharedPointer item); QMutex *mutex() { return &m_mutex; } private: void moveToTop(int row); QList> m_items; int m_maxSize; + bool m_displayImages; QMutex m_mutex; }; inline int HistoryModel::maxSize() const { return m_maxSize; } +inline bool HistoryModel::displayImages() const { + return m_displayImages; +} + +inline void HistoryModel::setDisplayImages(bool show) { + m_displayImages = show; +} + Q_DECLARE_METATYPE(HistoryItemType) #endif diff --git a/klipper/klipper.cpp b/klipper/klipper.cpp index 4e138e725..d022c4a43 100644 --- a/klipper/klipper.cpp +++ b/klipper/klipper.cpp @@ -1,1055 +1,1064 @@ /* 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 #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 Acquisition 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().registerService(QStringLiteral("org.kde.klipper")); 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, this, &Klipper::slotHistoryChanged); 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")); // When Klipper is run as part of plasma, we still need to pretend to be it for notification settings to work m_notification->setHint(QStringLiteral("desktop-entry"), QStringLiteral("org.kde.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(const 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() ); + history()->model()->setDisplayImages(!m_bIgnoreImages); + // 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 != nullptr ); menu->popup(QCursor::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() { QMessageBox *message = new QMessageBox(QMessageBox::Information, QString(), i18n("You can enable URL actions later by left-clicking on the " "Klipper icon and selecting 'Enable Clipboard Actions'")); message->setAttribute(Qt::WA_DeleteOnClose); message->setModal(false); message->show(); setURLGrabberEnabled( false ); } void Klipper::slotConfigure() { if (KConfigDialog::showDialog(QStringLiteral("preferences"))) { return; } ConfigDialog *dlg = new ConfigDialog( nullptr, 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(nullptr, 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 ); - bool saveHistory = true; - if (clipData->data(QStringLiteral("x-kde-passwordManagerHint")) == QByteArrayLiteral("secret")) { - saveHistory = false; - } - if (clipData->hasImage() && m_bIgnoreImages) { - saveHistory = false; + if (!(history()->empty())) { + if (m_bIgnoreImages && history()->first()->mimeData()->hasImage()) { + history()->remove(history()->first()); + } } - m_last = item; + HistoryItemPtr item = HistoryItem::create( clipData ); - if (saveHistory) { + bool saveToHistory = true; + if (clipData->data(QStringLiteral("x-kde-passwordManagerHint")) == QByteArrayLiteral("secret")) { + saveToHistory = false; + } + if (saveToHistory) { history()->insert( item ); } - return 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 ); } +void Klipper::slotHistoryChanged() +{ + if (history()->empty()) { + slotClearClipboard(); + } +} + // 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 = m_last; + 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 = m_last; + 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 && !data->hasFormat(QStringLiteral("x-kde-force-image-copy"))) 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 ); edit->setAcceptRichText(false); if (item) { edit->setPlainText( 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); if (qrCode) { if(item) { qrCode->setData(item->text()); } BarcodeLabel *qrCodeLabel = new BarcodeLabel(qrCode, mw); layout->addWidget(qrCodeLabel); } } { AbstractBarcode *dataMatrix = createBarcode(DataMatrix); if (dataMatrix) { if (item) { dataMatrix->setData(item->text()); } BarcodeLabel *dataMatrixLabel = new BarcodeLabel(dataMatrix, mw); 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(nullptr, 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/klipper.h b/klipper/klipper.h index 2ae39a475..2651b0fd7 100644 --- a/klipper/klipper.h +++ b/klipper/klipper.h @@ -1,217 +1,217 @@ /* This file is part of the KDE project Copyright (C) by Andrew Stanley-Jones 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. */ #ifndef KLIPPER_H #define KLIPPER_H #include "config-klipper.h" #include #include #include #include #include "urlgrabber.h" class KToggleAction; class KActionCollection; class KlipperPopup; class URLGrabber; class QTime; class History; class QAction; class QMenu; class QMimeData; class HistoryItem; class KNotification; enum class KlipperMode { Standalone, DataEngine }; class Klipper : public QObject { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.klipper.klipper") public Q_SLOTS: Q_SCRIPTABLE QString getClipboardContents(); Q_SCRIPTABLE void setClipboardContents(const QString &s); Q_SCRIPTABLE void clearClipboardContents(); Q_SCRIPTABLE void clearClipboardHistory(); Q_SCRIPTABLE void saveClipboardHistory(); Q_SCRIPTABLE QStringList getClipboardHistoryMenu(); Q_SCRIPTABLE QString getClipboardHistoryItem(int i); Q_SCRIPTABLE void showKlipperPopupMenu(); Q_SCRIPTABLE void showKlipperManuallyInvokeActionMenu(); public: Klipper(QObject* parent, const KSharedConfigPtr& config, KlipperMode mode = KlipperMode::Standalone); ~Klipper() override; /** * Get clipboard history (the "document") */ History* history() { return m_history; } URLGrabber* urlGrabber() const { return m_myURLGrabber; } void saveSettings() const; KlipperPopup *popup() { return m_popup; } void editData(const QSharedPointer &item); #ifdef HAVE_PRISON void showBarcode(const QSharedPointer &item); #endif public Q_SLOTS: void saveSession(); void slotHistoryTopChanged(); void slotConfigure(); void slotCycleNext(); void slotCyclePrev(); protected: /** * The selection modes * * Don't use 1, as I use that as a guard against passing * a boolean true as a mode. */ enum SelectionMode { Clipboard = 2, Selection = 4 }; /** * Loads history from disk. */ bool loadHistory(); /** * Save history to disk * @param empty save empty history instead of actual history */ void saveHistory(bool empty = false); /** * Check data in clipboard, and if it passes these checks, * store the data in the clipboard history. */ void checkClipData( bool selectionMode ); /** * Enter clipboard data in the history. */ QSharedPointer applyClipChanges( const QMimeData* data ); void setClipboard( const HistoryItem& item, int mode ); bool ignoreClipboardChanges() const; KSharedConfigPtr config() const { return m_config; } Q_SIGNALS: void passivePopup(const QString& caption, const QString& text); void editFinished(QSharedPointer< const HistoryItem > item, int result); public Q_SLOTS: void slotPopupMenu(); void slotAskClearHistory(); protected Q_SLOTS: void showPopupMenu( QMenu * ); void slotRepeatAction(); void setURLGrabberEnabled( bool ); void disableURLGrabber(); private Q_SLOTS: void newClipData( QClipboard::Mode ); void slotClearClipboard(); + void slotHistoryChanged(); + void slotQuit(); void slotStartShowTimer(); void slotClearOverflow(); void slotCheckPending(); void loadSettings(); private: static void updateTimestamp(); QClipboard* m_clip; - QSharedPointer m_last; - QElapsedTimer m_showTimer; History* m_history; KlipperPopup *m_popup; int m_overflowCounter; KToggleAction* m_toggleURLGrabAction; QAction* m_clearHistoryAction; QAction* m_repeatAction; QAction* m_editAction; #ifdef HAVE_PRISON QAction* m_showBarcodeAction; #endif QAction* m_configureAction; QAction* m_quitAction; QAction* m_cycleNextAction; QAction* m_cyclePrevAction; QAction* m_showOnMousePos; bool m_bKeepContents :1; bool m_bURLGrabber :1; bool m_bReplayActionInHistory :1; bool m_bUseGUIRegExpEditor :1; bool m_bNoNullClipboard :1; bool m_bIgnoreSelection :1; bool m_bSynchronize :1; bool m_bSelectionTextOnly :1; bool m_bIgnoreImages :1; /** * Avoid reacting to our own changes, using this * lock. * Don't manupulate this object directly... use the Ignore struct * instead */ int m_locklevel; URLGrabber* m_myURLGrabber; QString m_lastURLGrabberTextSelection; QString m_lastURLGrabberTextClipboard; KSharedConfigPtr m_config; QTimer m_overflowClearTimer; QTimer m_pendingCheckTimer; bool m_pendingContentsCheck; bool blockFetchingNewData(); QString cycleText() const; KActionCollection* m_collection; KlipperMode m_mode; QTimer *m_saveFileTimer = nullptr; QPointer m_notification; }; #endif