diff --git a/app/folderviewcontextmanageritem.cpp b/app/folderviewcontextmanageritem.cpp index e56d67db..f94bb16d 100644 --- a/app/folderviewcontextmanageritem.cpp +++ b/app/folderviewcontextmanageritem.cpp @@ -1,273 +1,292 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2009 Aurélien Gâteau 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // Self #include "folderviewcontextmanageritem.h" // Qt #include #include #include #include #include #include +#include +#include // KDE #include // Local #include #include #include "sidebar.h" #include "fileoperations.h" +#include +#include "lib/touch/touch_helper.h" namespace Gwenview { /** * This treeview accepts url drops */ class UrlDropTreeView : public QTreeView { public: explicit UrlDropTreeView(QWidget* parent = nullptr) : QTreeView(parent) {} protected: void dragEnterEvent(QDragEnterEvent* event) override { QAbstractItemView::dragEnterEvent(event); setDirtyRegion(mDropRect); if (event->mimeData()->hasUrls()) { event->acceptProposedAction(); } } void dragMoveEvent(QDragMoveEvent* event) override { QAbstractItemView::dragMoveEvent(event); QModelIndex index = indexAt(event->pos()); // This code has been copied from Dolphin // (panels/folders/paneltreeview.cpp) setDirtyRegion(mDropRect); mDropRect = visualRect(index); setDirtyRegion(mDropRect); if (index.isValid()) { event->acceptProposedAction(); } else { event->ignore(); } } void dropEvent(QDropEvent* event) override { const QList urlList = KUrlMimeData::urlsFromMimeData(event->mimeData()); const QModelIndex index = indexAt(event->pos()); if (!index.isValid()) { qWarning() << "Invalid index!"; return; } const QUrl destUrl = static_cast(model())->urlForIndex(index); FileOperations::showMenuForDroppedUrls(this, urlList, destUrl); } + bool viewportEvent(QEvent* event) override + { + if (event->type() == QEvent::TouchBegin) { + return true; + } + const QPoint pos = Touch_Helper::simpleTapPosition(event); + if (pos != QPoint(-1, -1)) { + expand(indexAt(pos)); + emit activated(indexAt(pos)); + } + + return QTreeView::viewportEvent(event); + } private: QRect mDropRect; }; FolderViewContextManagerItem::FolderViewContextManagerItem(ContextManager* manager) : AbstractContextManagerItem(manager) { mModel = nullptr; setupView(); connect(contextManager(), &ContextManager::currentDirUrlChanged, this, &FolderViewContextManagerItem::slotCurrentDirUrlChanged); } void FolderViewContextManagerItem::slotCurrentDirUrlChanged(const QUrl &url) { if (url.isValid() && mUrlToSelect != url) { mUrlToSelect = url.adjusted(QUrl::StripTrailingSlash | QUrl::NormalizePathSegments); mExpandingIndex = QModelIndex(); } if (!mView->isVisible()) { return; } expandToSelectedUrl(); } void FolderViewContextManagerItem::expandToSelectedUrl() { if (!mUrlToSelect.isValid()) { return; } if (!mModel) { setupModel(); } QModelIndex index = findClosestIndex(mExpandingIndex, mUrlToSelect); if (!index.isValid()) { return; } mExpandingIndex = index; QUrl url = mModel->urlForIndex(mExpandingIndex); if (mUrlToSelect == url) { // We found our url QItemSelectionModel* selModel = mView->selectionModel(); selModel->setCurrentIndex(mExpandingIndex, QItemSelectionModel::ClearAndSelect); mView->scrollTo(mExpandingIndex); mUrlToSelect = QUrl(); mExpandingIndex = QModelIndex(); } else { // We found a parent of our url mView->setExpanded(mExpandingIndex, true); } } void FolderViewContextManagerItem::slotRowsInserted(const QModelIndex& parentIndex, int /*start*/, int /*end*/) { // Can't trigger the case where parentIndex is invalid, but it most // probably happen when root items are created. In this case we trigger // expandToSelectedUrl without checking the url. // See bug #191771 if (!parentIndex.isValid() || mModel->urlForIndex(parentIndex).isParentOf(mUrlToSelect)) { mExpandingIndex = parentIndex; // Hack because otherwise indexes are not in correct order! QMetaObject::invokeMethod(this, "expandToSelectedUrl", Qt::QueuedConnection); } } void FolderViewContextManagerItem::slotActivated(const QModelIndex& index) { if (!index.isValid()) { return; } QUrl url = mModel->urlForIndex(index); emit urlChanged(url); } void FolderViewContextManagerItem::setupModel() { mModel = new MODEL_CLASS(this); mView->setModel(mModel); #ifndef USE_PLACETREE for (int col = 1; col <= mModel->columnCount(); ++col) { mView->header()->setSectionHidden(col, true); } mModel->dirLister()->openUrl(QUrl("/")); #endif QObject::connect(mModel, &MODEL_CLASS::rowsInserted, this, &FolderViewContextManagerItem::slotRowsInserted); } void FolderViewContextManagerItem::setupView() { mView = new UrlDropTreeView; mView->setEditTriggers(QAbstractItemView::NoEditTriggers); mView->setAcceptDrops(true); mView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); mView->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel); // Necessary to get the drop target highlighted mView->viewport()->setAttribute(Qt::WA_Hover); mView->setHeaderHidden(true); // This is tricky: QTreeView header has stretchLastSection set to true. // In this configuration, the header gets quite wide and cause an // horizontal scrollbar to appear. // To avoid this, set stretchLastSection to false and resizeMode to // Stretch (we still want the column to take the full width of the // widget). mView->header()->setStretchLastSection(false); mView->header()->setSectionResizeMode(QHeaderView::ResizeToContents); + ScrollerUtils::setQScroller(mView->viewport()); + setWidget(mView); QObject::connect(mView, &QTreeView::activated, this, &FolderViewContextManagerItem::slotActivated); EventWatcher::install(mView, QEvent::Show, this, SLOT(expandToSelectedUrl())); } QModelIndex FolderViewContextManagerItem::findClosestIndex(const QModelIndex& parent, const QUrl& wantedUrl) { Q_ASSERT(mModel); QModelIndex index = parent; if (!index.isValid()) { index = findRootIndex(wantedUrl); if (!index.isValid()) { return QModelIndex(); } } QUrl url = mModel->urlForIndex(index); if (!url.isParentOf(wantedUrl)) { qWarning() << url << "is not a parent of" << wantedUrl << "!"; return QModelIndex(); } QString relativePath = QDir(url.path()).relativeFilePath(wantedUrl.path()); QModelIndex lastFoundIndex = index; Q_FOREACH(const QString & pathPart, relativePath.split(QDir::separator(), QString::SkipEmptyParts)) { bool found = false; for (int row = 0; row < mModel->rowCount(lastFoundIndex); ++row) { QModelIndex index = mModel->index(row, 0, lastFoundIndex); if (index.data().toString() == pathPart) { // FIXME: Check encoding found = true; lastFoundIndex = index; break; } } if (!found) { break; } } return lastFoundIndex; } QModelIndex FolderViewContextManagerItem::findRootIndex(const QUrl& wantedUrl) { QModelIndex matchIndex; int matchUrlLength = 0; for (int row = 0; row < mModel->rowCount(); ++row) { QModelIndex index = mModel->index(row, 0); QUrl url = mModel->urlForIndex(index); int urlLength = url.url().length(); if (url.isParentOf(wantedUrl) && urlLength > matchUrlLength) { matchIndex = index; matchUrlLength = urlLength; } } if (!matchIndex.isValid()) { qWarning() << "Found no root index for" << wantedUrl; } return matchIndex; } } // namespace diff --git a/app/startmainpage.cpp b/app/startmainpage.cpp index 95d385e0..db957b75 100644 --- a/app/startmainpage.cpp +++ b/app/startmainpage.cpp @@ -1,335 +1,350 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2008 Aurélien Gâteau 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. */ // Self #include "startmainpage.h" #include // Qt #include #include #ifdef GTK_WORKAROUND_BROKE_IN_KF5_PORT #include #endif #include #include // KDE #include #include // Local #include #include #include #include #include #include #include #include #include +#include #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE #include #endif namespace Gwenview { class HistoryThumbnailViewHelper : public AbstractThumbnailViewHelper { public: HistoryThumbnailViewHelper(QObject* parent) : AbstractThumbnailViewHelper(parent) {} void showContextMenu(QWidget*) override { } void showMenuForUrlDroppedOnViewport(QWidget*, const QList&) override { } void showMenuForUrlDroppedOnDir(QWidget*, const QList&, const QUrl&) override { } }; struct StartMainPagePrivate : public Ui_StartMainPage { StartMainPage* q; GvCore* mGvCore; KFilePlacesModel* mBookmarksModel; ThumbnailProvider *mRecentFilesThumbnailProvider; bool mSearchUiInitialized; void setupSearchUi() { #ifdef GWENVIEW_SEMANTICINFO_BACKEND_BALOO mTagView->setModel(TagModel::createAllTagsModel(mTagView, mGvCore->semanticInfoBackEnd())); mTagView->show(); mTagLabel->hide(); #else mTagView->hide(); mTagLabel->hide(); #endif } void updateHistoryTab() { mHistoryWidget->setVisible(GwenviewConfig::historyEnabled()); mHistoryDisabledLabel->setVisible(!GwenviewConfig::historyEnabled()); } void setupHistoryView(ThumbnailView *view) { view->setThumbnailViewHelper(new HistoryThumbnailViewHelper(view)); PreviewItemDelegate* delegate = new PreviewItemDelegate(view); delegate->setContextBarActions(PreviewItemDelegate::NoAction); delegate->setTextElideMode(Qt::ElideLeft); view->setItemDelegate(delegate); view->setThumbnailWidth(128); view->setCreateThumbnailsForRemoteUrls(false); QModelIndex index = view->model()->index(0, 0); if (index.isValid()) { view->setCurrentIndex(index); } } }; static void initViewPalette(QAbstractItemView* view, const QColor& fgColor) { QWidget* viewport = view->viewport(); QPalette palette = viewport->palette(); palette.setColor(viewport->backgroundRole(), Qt::transparent); palette.setColor(QPalette::WindowText, fgColor); palette.setColor(QPalette::Text, fgColor); // QListView uses QStyledItemDelegate, which uses the view palette for // foreground color, while KFilePlacesView uses the viewport palette. viewport->setPalette(palette); view->setPalette(palette); } static bool styleIsGtkBased() { const char* name = QApplication::style()->metaObject()->className(); return qstrcmp(name, "QGtkStyle") == 0; } StartMainPage::StartMainPage(QWidget* parent, GvCore* gvCore) : QFrame(parent) , d(new StartMainPagePrivate) { d->mRecentFilesThumbnailProvider = nullptr; d->q = this; d->mGvCore = gvCore; d->mSearchUiInitialized = false; d->setupUi(this); if (styleIsGtkBased()) { #ifdef GTK_WORKAROUND_BROKE_IN_KF5_PORT // Gtk-based styles do not apply the correct background color on tabs. // As a workaround, use the Plastique style instead. QStyle* fix = new QPlastiqueStyle(); fix->setParent(this); d->mHistoryWidget->tabBar()->setStyle(fix); d->mPlacesTagsWidget->tabBar()->setStyle(fix); #endif } setFrameStyle(QFrame::NoFrame); // Bookmark view d->mBookmarksModel = new KFilePlacesModel(this); d->mBookmarksView->setModel(d->mBookmarksModel); d->mBookmarksView->setAutoResizeItemsEnabled(false); connect(d->mBookmarksView, &KFilePlacesView::urlChanged, this, &StartMainPage::urlSelected); // Tag view connect(d->mTagView, &QListView::clicked, this, &StartMainPage::slotTagViewClicked); // Recent folders view connect(d->mRecentFoldersView, &Gwenview::ThumbnailView::indexActivated, this, &StartMainPage::slotListViewActivated); connect(d->mRecentFoldersView, &Gwenview::ThumbnailView::customContextMenuRequested, this, &StartMainPage::showContextMenu); // Recent files view connect(d->mRecentFilesView, &Gwenview::ThumbnailView::indexActivated, this, &StartMainPage::slotListViewActivated); connect(d->mRecentFilesView, &Gwenview::ThumbnailView::customContextMenuRequested, this, &StartMainPage::showContextMenu); d->updateHistoryTab(); connect(GwenviewConfig::self(), &GwenviewConfig::configChanged, this, &StartMainPage::loadConfig); d->mRecentFoldersView->setFocus(); + + ScrollerUtils::setQScroller(d->mBookmarksView->viewport()); + d->mBookmarksView->viewport()->installEventFilter(this); } StartMainPage::~StartMainPage() { delete d->mRecentFilesThumbnailProvider; delete d; } +bool StartMainPage::eventFilter(QObject*, QEvent* event) +{ + if (event->type() == QEvent::MouseMove) { + QMouseEvent* mouseEvent = static_cast(event); + if (mouseEvent->source() == Qt::MouseEventSynthesizedByQt) { + return true; + } + } + return false; +} + void StartMainPage::slotTagViewClicked(const QModelIndex& index) { #ifdef GWENVIEW_SEMANTICINFO_BACKEND_BALOO if (!index.isValid()) { return; } // FIXME: Check label encoding const QString tag = index.data().toString(); emit urlSelected(QUrl("tags:/" + tag)); #endif } void StartMainPage::applyPalette(const QPalette& newPalette) { QColor fgColor = newPalette.text().color(); QPalette pal = palette(); pal.setBrush(backgroundRole(), newPalette.base()); pal.setBrush(QPalette::Button, newPalette.base()); pal.setBrush(QPalette::WindowText, fgColor); pal.setBrush(QPalette::ButtonText, fgColor); pal.setBrush(QPalette::Text, fgColor); setPalette(pal); initViewPalette(d->mBookmarksView, fgColor); initViewPalette(d->mTagView, fgColor); initViewPalette(d->mRecentFoldersView, fgColor); initViewPalette(d->mRecentFilesView, fgColor); } void StartMainPage::slotListViewActivated(const QModelIndex& index) { if (!index.isValid()) { return; } QVariant data = index.data(KFilePlacesModel::UrlRole); QUrl url = data.toUrl(); // Prevent dir lister error if (!url.isValid()) { qCritical() << "Tried to open an invalid url"; return; } emit urlSelected(url); } void StartMainPage::showEvent(QShowEvent* event) { if (GwenviewConfig::historyEnabled()) { if (!d->mRecentFoldersView->model()) { d->mRecentFoldersView->setModel(d->mGvCore->recentFoldersModel()); d->setupHistoryView(d->mRecentFoldersView); } if (!d->mRecentFilesView->model()) { d->mRecentFilesView->setModel(d->mGvCore->recentFilesModel()); d->mRecentFilesThumbnailProvider = new ThumbnailProvider(); d->mRecentFilesView->setThumbnailProvider(d->mRecentFilesThumbnailProvider); d->setupHistoryView(d->mRecentFilesView); } } if (!d->mSearchUiInitialized) { d->mSearchUiInitialized = true; d->setupSearchUi(); } QFrame::showEvent(event); } void StartMainPage::showContextMenu(const QPoint& pos) { // Create menu DialogGuard menu(this); QAction* addAction = menu->addAction(QIcon::fromTheme("bookmark-new"), QString()); QAction* forgetAction = menu->addAction(QIcon::fromTheme("edit-delete"), QString()); menu->addSeparator(); QAction* forgetAllAction = menu->addAction(QIcon::fromTheme("edit-delete-all"), QString()); if (d->mHistoryWidget->currentWidget() == d->mRecentFoldersTab) { addAction->setText(i18nc("@action Recent Folders view", "Add Folder to Places")); forgetAction->setText(i18nc("@action Recent Folders view", "Forget This Folder")); forgetAllAction->setText(i18nc("@action Recent Folders view", "Forget All Folders")); } else if (d->mHistoryWidget->currentWidget() == d->mRecentFilesTab) { addAction->setText(i18nc("@action Recent Files view", "Add Containing Folder to Places")); forgetAction->setText(i18nc("@action Recent Files view", "Forget This File")); forgetAllAction->setText(i18nc("@action Recent Files view", "Forget All Files")); } else { GV_WARN_AND_RETURN("Context menu not implemented on this tab page"); } const QAbstractItemView* view = qobject_cast(sender()); const QModelIndex index = view->indexAt(pos); addAction->setEnabled(index.isValid()); forgetAction->setEnabled(index.isValid()); // Handle menu const QAction* action = menu->exec(view->mapToGlobal(pos)); if (!action) { return; } const QVariant data = index.data(KFilePlacesModel::UrlRole); QUrl url = data.toUrl(); if (action == addAction) { if (d->mHistoryWidget->currentWidget() == d->mRecentFilesTab) { url = url.adjusted(QUrl::RemoveFilename); } QString text = url.adjusted(QUrl::StripTrailingSlash).fileName(); if (text.isEmpty()) { text = url.toDisplayString(); } d->mBookmarksModel->addPlace(text, url); } else if (action == forgetAction) { view->model()->removeRow(index.row()); if (d->mHistoryWidget->currentWidget() == d->mRecentFilesTab) { emit recentFileRemoved(url); } } else if (action == forgetAllAction) { view->model()->removeRows(0, view->model()->rowCount()); if (d->mHistoryWidget->currentWidget() == d->mRecentFilesTab) { emit recentFilesCleared(); } } } void StartMainPage::loadConfig() { d->updateHistoryTab(); applyPalette(d->mGvCore->palette(GvCore::NormalViewPalette)); } ThumbnailView* StartMainPage::recentFoldersView() const { return d->mRecentFoldersView; } } // namespace diff --git a/app/startmainpage.h b/app/startmainpage.h index 765e927e..a10babf3 100644 --- a/app/startmainpage.h +++ b/app/startmainpage.h @@ -1,77 +1,79 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2008 Aurélien Gâteau 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. */ #ifndef STARTMAINPAGE_H #define STARTMAINPAGE_H // Qt #include // KDE // Local class QModelIndex; class QPalette; class QShowEvent; +class QEvent; class QUrl; namespace Gwenview { class GvCore; class ThumbnailView; struct StartMainPagePrivate; class StartMainPage : public QFrame { Q_OBJECT public: explicit StartMainPage(QWidget* parent, GvCore*); ~StartMainPage() override; void applyPalette(const QPalette&); ThumbnailView* recentFoldersView() const; Q_SIGNALS: void urlSelected(const QUrl &url); void recentFileRemoved(const QUrl& url); void recentFilesCleared(); public Q_SLOTS: void loadConfig(); protected: void showEvent(QShowEvent*) override; + bool eventFilter(QObject*, QEvent*) override; private Q_SLOTS: void slotListViewActivated(const QModelIndex& index); void slotTagViewClicked(const QModelIndex& index); private: StartMainPagePrivate* const d; void showContextMenu(const QPoint& pos); }; } // namespace #endif /* STARTMAINPAGE_H */ diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 229eac2a..29a784ef 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -1,296 +1,304 @@ project(gwenviewlib) add_definitions(-DTRANSLATION_DOMAIN="gwenview") set(LIBGWENVIEW_VERSION "4.97.0") # Extract version of libjpeg so that we can use the appropriate dir # See bug #227313 message(STATUS "Looking for libjpeg version in ${JPEG_INCLUDE_DIR}/jpeglib.h") # Due to the large variety of different headers the version data might be # found in (between libjpeg, libjpeg-turbo and various multilib header # forwarding schemes seen in distros), have a simple program print out the # right version. set(JPEGLIB_VERSION_CHECK_PATH "${CMAKE_CURRENT_BINARY_DIR}/jpeglib-version-check.c") file(WRITE ${JPEGLIB_VERSION_CHECK_PATH} " #include #include #include int main(void) { printf(\"%d\\\n\", JPEG_LIB_VERSION); } ") try_run(JPEGLIB_RUN_RESULT JPEGLIB_COMPILE_RESULT ${CMAKE_CURRENT_BINARY_DIR} ${JPEGLIB_VERSION_CHECK_PATH} CMAKE_FLAGS -DINCLUDE_DIRECTORIES:PATH=${JPEG_INCLUDE_DIR} RUN_OUTPUT_VARIABLE jpeglib_version) if ((${JPEGLIB_COMPILE_RESULT} EQUAL FALSE) OR ("${JPEGLIB_RUN_RESULT}" EQUAL FAILED_TO_RUN) OR "${jpeglib_version}" STREQUAL "") message(FATAL_ERROR "Could not find jpeglib.h. This file comes with libjpeg.") endif() if ("${jpeglib_version}" LESS 80) set(GV_JPEG_DIR libjpeg-62) endif() if ("${jpeglib_version}" EQUAL 80) set(GV_JPEG_DIR libjpeg-80) endif() if ("${jpeglib_version}" EQUAL 90) set(GV_JPEG_DIR libjpeg-90) endif() if ("${GV_JPEG_DIR}" STREQUAL "") message(FATAL_ERROR "Unknown libjpeg version: ${jpeglib_version}") endif() message(STATUS "libjpeg version: ${jpeglib_version}") add_definitions(-Dlibjpeg_EXPORTS) include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/${GV_JPEG_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/.. ${CMAKE_CURRENT_BINARY_DIR} ${EXIV2_INCLUDE_DIR} ${JPEG_INCLUDE_DIR} ${PNG_INCLUDE_DIRS} ) if(HAVE_FITS) include_directories( ${CFITSIO_INCLUDE_DIR} ) endif() # For config-gwenview.h include_directories( ${CMAKE_CURRENT_BINARY_DIR}/.. ) set(gwenviewlib_SRCS cms/iccjpeg.c cms/cmsprofile.cpp cms/cmsprofile_png.cpp contextmanager.cpp crop/cropwidget.cpp crop/cropimageoperation.cpp crop/croptool.cpp document/abstractdocumentimpl.cpp document/documentjob.cpp document/animateddocumentloadedimpl.cpp document/document.cpp document/documentfactory.cpp document/documentloadedimpl.cpp document/emptydocumentimpl.cpp document/jpegdocumentloadedimpl.cpp document/loadingdocumentimpl.cpp document/loadingjob.cpp document/savejob.cpp document/svgdocumentloadedimpl.cpp document/videodocumentloadedimpl.cpp documentview/abstractdocumentviewadapter.cpp documentview/abstractimageview.cpp documentview/abstractrasterimageviewtool.cpp documentview/birdeyeview.cpp documentview/documentview.cpp documentview/documentviewcontroller.cpp documentview/documentviewsynchronizer.cpp documentview/loadingindicator.cpp documentview/messageviewadapter.cpp documentview/rasterimageview.cpp documentview/rasterimageviewadapter.cpp documentview/svgviewadapter.cpp documentview/videoviewadapter.cpp about.cpp abstractimageoperation.cpp disabledactionshortcutmonitor.cpp documentonlyproxymodel.cpp documentview/documentviewcontainer.cpp binder.cpp eventwatcher.cpp historymodel.cpp recentfilesmodel.cpp archiveutils.cpp datewidget.cpp exiv2imageloader.cpp flowlayout.cpp fullscreenbar.cpp hud/hudbutton.cpp hud/hudbuttonbox.cpp hud/hudcountdown.cpp hud/hudlabel.cpp hud/hudmessagebubble.cpp hud/hudslider.cpp hud/hudtheme.cpp hud/hudwidget.cpp graphicswidgetfloater.cpp imagemetainfomodel.cpp imagescaler.cpp imageutils.cpp invisiblebuttongroup.cpp iodevicejpegsourcemanager.cpp jpegcontent.cpp kindproxymodel.cpp semanticinfo/sorteddirmodel.cpp memoryutils.cpp mimetypeutils.cpp paintutils.cpp placetreemodel.cpp preferredimagemetainfomodel.cpp print/printhelper.cpp print/printoptionspage.cpp recursivedirmodel.cpp shadowfilter.cpp slidecontainer.cpp slideshow.cpp statusbartoolbutton.cpp stylesheetutils.cpp redeyereduction/redeyereductionimageoperation.cpp redeyereduction/redeyereductiontool.cpp resize/resizeimageoperation.cpp resize/resizeimagedialog.cpp thumbnailprovider/thumbnailgenerator.cpp thumbnailprovider/thumbnailprovider.cpp thumbnailprovider/thumbnailwriter.cpp thumbnailview/abstractthumbnailviewhelper.cpp thumbnailview/abstractdocumentinfoprovider.cpp thumbnailview/contextbarbutton.cpp thumbnailview/dragpixmapgenerator.cpp thumbnailview/itemeditor.cpp thumbnailview/previewitemdelegate.cpp thumbnailview/thumbnailbarview.cpp thumbnailview/thumbnailslider.cpp thumbnailview/thumbnailview.cpp thumbnailview/tooltipwidget.cpp timeutils.cpp transformimageoperation.cpp urlutils.cpp widgetfloater.cpp zoomslider.cpp zoomwidget.cpp + scrollerutils.cpp + touch/touch.cpp + touch/tapholdandmoving.cpp + touch/twofingerpan.cpp + touch/oneandtwofingerswipe.cpp + touch/doubletap.cpp + touch/twofingertap.cpp + touch/touch_helper.cpp ${GV_JPEG_DIR}/transupp.c ) if(HAVE_QTDBUS) set(gwenviewlib_SRCS ${gwenviewlib_SRCS} mpris2/lockscreenwatcher.cpp mpris2/dbusabstractadaptor.cpp mpris2/mpris2service.cpp mpris2/mprismediaplayer2.cpp mpris2/mprismediaplayer2player.cpp ) qt5_add_dbus_interface(gwenviewlib_SRCS mpris2/org.freedesktop.ScreenSaver.xml screensaverdbusinterface) endif() if(HAVE_FITS) set(gwenviewlib_SRCS ${gwenviewlib_SRCS} imageformats/fitsplugin.cpp imageformats/fitshandler.cpp imageformats/fitsformat/bayer.c imageformats/fitsformat/fitsdata.cpp ) endif() if (NOT GWENVIEW_SEMANTICINFO_BACKEND_NONE) set(gwenviewlib_SRCS ${gwenviewlib_SRCS} semanticinfo/abstractsemanticinfobackend.cpp semanticinfo/semanticinfodirmodel.cpp semanticinfo/tagitemdelegate.cpp semanticinfo/tagmodel.cpp semanticinfo/tagwidget.cpp ) endif() if (GWENVIEW_SEMANTICINFO_BACKEND_FAKE) set(gwenviewlib_SRCS ${gwenviewlib_SRCS} semanticinfo/fakesemanticinfobackend.cpp ) endif() if (GWENVIEW_SEMANTICINFO_BACKEND_BALOO) set(gwenviewlib_SRCS ${gwenviewlib_SRCS} semanticinfo/baloosemanticinfobackend.cpp ) endif() kde_source_files_enable_exceptions( exiv2imageloader.cpp imagemetainfomodel.cpp timeutils.cpp cms/cmsprofile.cpp document/abstractdocumentimpl.cpp document/document.cpp document/loadingdocumentimpl.cpp jpegcontent.cpp ) ki18n_wrap_ui(gwenviewlib_SRCS documentview/messageview.ui print/printoptionspage.ui redeyereduction/redeyereductionwidget.ui resize/resizeimagewidget.ui ) kconfig_add_kcfg_files(gwenviewlib_SRCS gwenviewconfig.kcfgc ) add_library(gwenviewlib SHARED ${gwenviewlib_SRCS}) generate_export_header(gwenviewlib BASE_NAME gwenviewlib) set_target_properties(gwenviewlib PROPERTIES VERSION ${LIBGWENVIEW_VERSION} SOVERSION 5) if (WIN32) set_target_properties(gwenviewlib PROPERTIES COMPILE_FLAGS -DJPEG_STATIC) endif() target_link_libraries(gwenviewlib Qt5::Concurrent Qt5::Svg Qt5::OpenGL Qt5::PrintSupport KF5::KIOCore KF5::KIOWidgets KF5::KIOFileWidgets KF5::I18n KF5::WindowSystem KF5::IconThemes ${JPEG_LIBRARY} ${EXIV2_LIBRARIES} ${PNG_LIBRARIES} ${LCMS2_LIBRARIES} ${PHONON_LIBRARY} ) if(HAVE_QTDBUS) target_link_libraries(gwenviewlib Qt5::DBus) endif() if(HAVE_FITS) target_link_libraries(gwenviewlib ${CFITSIO_LIBRARIES}) endif() if (WIN32) target_link_libraries(gwenviewlib ${EXPAT_LIBRARIES}) endif() if (KF5KDcraw_FOUND) target_link_libraries(gwenviewlib KF5::KDcraw) endif() if (HAVE_X11) target_link_libraries(gwenviewlib Qt5::X11Extras ${X11_X11_LIB}) endif() if (GWENVIEW_SEMANTICINFO_BACKEND_BALOO) target_link_libraries(gwenviewlib KF5::Baloo KF5::FileMetaData ) endif() install(TARGETS gwenviewlib ${KDE_INSTALL_TARGETS_DEFAULT_ARGS} LIBRARY NAMELINK_SKIP) diff --git a/lib/documentview/documentview.cpp b/lib/documentview/documentview.cpp index ec007306..e78acdab 100644 --- a/lib/documentview/documentview.cpp +++ b/lib/documentview/documentview.cpp @@ -1,1022 +1,1101 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2008 Aurélien Gâteau 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. */ // Self #include "documentview.h" // C++ Standard library #include // Qt #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include // KDE #include #include #include // Local #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include +#include namespace Gwenview { #undef ENABLE_LOG #undef LOG //#define ENABLE_LOG #ifdef ENABLE_LOG #define LOG(x) //qDebug() << x #else #define LOG(x) ; #endif static const qreal REAL_DELTA = 0.001; static const qreal MAXIMUM_ZOOM_VALUE = qreal(DocumentView::MaximumZoom); static const auto MINSTEP = sqrt(0.5); static const auto MAXSTEP = sqrt(2.0); static const int COMPARE_MARGIN = 4; const int DocumentView::MaximumZoom = 16; const int DocumentView::AnimDuration = 250; struct DocumentViewPrivate { DocumentView* q; int mSortKey; // Used to sort views when displayed in compare mode HudWidget* mHud; BirdEyeView* mBirdEyeView; QPointer mMoveAnimation; QPointer mFadeAnimation; QGraphicsOpacityEffect* mOpacityEffect; LoadingIndicator* mLoadingIndicator; QScopedPointer mAdapter; QList mZoomSnapValues; Document::Ptr mDocument; DocumentView::Setup mSetup; bool mCurrent; bool mCompareMode; int controlWheelAccumulatedDelta; QPointF mDragStartPosition; QPointer mDragThumbnailProvider; QPointer mDrag; + Touch* mTouch; + void setCurrentAdapter(AbstractDocumentViewAdapter* adapter) { Q_ASSERT(adapter); mAdapter.reset(adapter); adapter->widget()->setParentItem(q); resizeAdapterWidget(); if (adapter->canZoom()) { QObject::connect(adapter, &AbstractDocumentViewAdapter::zoomChanged, q, &DocumentView::slotZoomChanged); QObject::connect(adapter, &AbstractDocumentViewAdapter::zoomInRequested, q, &DocumentView::zoomIn); QObject::connect(adapter, &AbstractDocumentViewAdapter::zoomOutRequested, q, &DocumentView::zoomOut); QObject::connect(adapter, &AbstractDocumentViewAdapter::zoomToFitChanged, q, &DocumentView::zoomToFitChanged); QObject::connect(adapter, &AbstractDocumentViewAdapter::zoomToFillChanged, q, &DocumentView::zoomToFillChanged); } QObject::connect(adapter, &AbstractDocumentViewAdapter::scrollPosChanged, q, &DocumentView::positionChanged); QObject::connect(adapter, &AbstractDocumentViewAdapter::previousImageRequested, q, &DocumentView::previousImageRequested); QObject::connect(adapter, &AbstractDocumentViewAdapter::nextImageRequested, q, &DocumentView::nextImageRequested); QObject::connect(adapter, &AbstractDocumentViewAdapter::toggleFullScreenRequested, q, &DocumentView::toggleFullScreenRequested); QObject::connect(adapter, &AbstractDocumentViewAdapter::completed, q, &DocumentView::slotCompleted); adapter->loadConfig(); adapter->widget()->installSceneEventFilter(q); if (mCurrent) { adapter->widget()->setFocus(); } if (mSetup.valid && adapter->canZoom()) { adapter->setZoomToFit(mSetup.zoomToFit); adapter->setZoomToFill(mSetup.zoomToFill); if (!mSetup.zoomToFit && !mSetup.zoomToFill) { adapter->setZoom(mSetup.zoom); adapter->setScrollPos(mSetup.position); } } emit q->adapterChanged(); emit q->positionChanged(); if (adapter->canZoom()) { if (adapter->zoomToFit()) { emit q->zoomToFitChanged(true); } else if (adapter->zoomToFill()) { emit q->zoomToFillChanged(true); } else { emit q->zoomChanged(adapter->zoom()); } } if (adapter->rasterImageView()) { QObject::connect(adapter->rasterImageView(), &RasterImageView::currentToolChanged, q, &DocumentView::currentToolChanged); } } void setupLoadingIndicator() { mLoadingIndicator = new LoadingIndicator(q); GraphicsWidgetFloater* floater = new GraphicsWidgetFloater(q); floater->setChildWidget(mLoadingIndicator); } HudButton* createHudButton(const QString& text, const QString &iconName, bool showText) { HudButton* button = new HudButton; if (showText) { button->setText(text); } else { button->setToolTip(text); } button->setIcon(QIcon::fromTheme(iconName)); return button; } void setupHud() { HudButton* trashButton = createHudButton(i18nc("@info:tooltip", "Trash"), QStringLiteral("user-trash"), false); HudButton* deselectButton = createHudButton(i18nc("@action:button", "Deselect"), QStringLiteral("list-remove"), true); QGraphicsWidget* content = new QGraphicsWidget; QGraphicsLinearLayout* layout = new QGraphicsLinearLayout(content); layout->addItem(trashButton); layout->addItem(deselectButton); mHud = new HudWidget(q); mHud->init(content, HudWidget::OptionNone); GraphicsWidgetFloater* floater = new GraphicsWidgetFloater(q); floater->setChildWidget(mHud); floater->setAlignment(Qt::AlignBottom | Qt::AlignHCenter); QObject::connect(trashButton, &HudButton::clicked, q, &DocumentView::emitHudTrashClicked); QObject::connect(deselectButton, &HudButton::clicked, q, &DocumentView::emitHudDeselectClicked); mHud->hide(); } void setupBirdEyeView() { if (mBirdEyeView) { delete mBirdEyeView; } mBirdEyeView = new BirdEyeView(q); mBirdEyeView->setZValue(1); } void updateCaption() { if (!mCurrent) { return; } QString caption; Document::Ptr doc = mAdapter->document(); if (!doc) { emit q->captionUpdateRequested(caption); return; } caption = doc->url().fileName(); QSize size = doc->size(); if (size.isValid()) { caption += QStringLiteral(" - %1x%2") .arg(size.width()) .arg(size.height()); if (mAdapter->canZoom()) { int intZoom = qRound(mAdapter->zoom() * 100); caption += QStringLiteral(" - %1%") .arg(intZoom); } } emit q->captionUpdateRequested(caption); } void uncheckZoomToFit() { if (mAdapter->zoomToFit()) { mAdapter->setZoomToFit(false); } } void uncheckZoomToFill() { if (mAdapter->zoomToFill()) { mAdapter->setZoomToFill(false); } } void setZoom(qreal zoom, const QPointF& center = QPointF(-1, -1)) { uncheckZoomToFit(); uncheckZoomToFill(); zoom = qBound(q->minimumZoom(), zoom, MAXIMUM_ZOOM_VALUE); mAdapter->setZoom(zoom, center); } void updateZoomSnapValues() { qreal min = q->minimumZoom(); mZoomSnapValues.clear(); for (qreal zoom = MINSTEP; zoom > min; zoom *= MINSTEP) { mZoomSnapValues << zoom; } mZoomSnapValues << min; std::reverse(mZoomSnapValues.begin(), mZoomSnapValues.end()); for (qreal zoom = 1; zoom < MAXIMUM_ZOOM_VALUE; zoom *= MAXSTEP) { mZoomSnapValues << zoom; } mZoomSnapValues << MAXIMUM_ZOOM_VALUE; emit q->minimumZoomChanged(min); } void showLoadingIndicator() { if (!mLoadingIndicator) { setupLoadingIndicator(); } mLoadingIndicator->show(); mLoadingIndicator->setZValue(1); } void hideLoadingIndicator() { if (!mLoadingIndicator) { return; } mLoadingIndicator->hide(); } void resizeAdapterWidget() { QRectF rect = QRectF(QPointF(0, 0), q->boundingRect().size()); if (mCompareMode) { rect.adjust(COMPARE_MARGIN, COMPARE_MARGIN, -COMPARE_MARGIN, -COMPARE_MARGIN); } mAdapter->widget()->setGeometry(rect); } void fadeTo(qreal value) { if (mFadeAnimation.data()) { qreal endValue = mFadeAnimation.data()->endValue().toReal(); if (qFuzzyCompare(value, endValue)) { // Same end value, don't change the actual animation return; } } // Create a new fade animation QPropertyAnimation* anim = new QPropertyAnimation(mOpacityEffect, "opacity"); anim->setStartValue(mOpacityEffect->opacity()); anim->setEndValue(value); if (qFuzzyCompare(value, 1)) { QObject::connect(anim, &QAbstractAnimation::finished, q, &DocumentView::slotFadeInFinished); } QObject::connect(anim, &QAbstractAnimation::finished, q, &DocumentView::isAnimatedChanged); anim->setDuration(DocumentView::AnimDuration); mFadeAnimation = anim; emit q->isAnimatedChanged(); anim->start(QAbstractAnimation::DeleteWhenStopped); } bool canPan() const { if (!q->canZoom()) { return false; } const QSize zoomedImageSize = mDocument->size() * q->zoom(); const QSize viewPortSize = q->boundingRect().size().toSize(); const bool imageWiderThanViewport = zoomedImageSize.width() > viewPortSize.width(); const bool imageTallerThanViewport = zoomedImageSize.height() > viewPortSize.height(); return (imageWiderThanViewport || imageTallerThanViewport); } void setDragPixmap(const QPixmap& pix) { if (mDrag) { DragPixmapGenerator::DragPixmap dragPixmap = DragPixmapGenerator::generate({pix}, 1); mDrag->setPixmap(dragPixmap.pix); mDrag->setHotSpot(dragPixmap.hotSpot); } } void executeDrag() { if (mDrag) { if (mAdapter->imageView()) { mAdapter->imageView()->resetDragCursor(); } mDrag->exec(Qt::MoveAction | Qt::CopyAction | Qt::LinkAction, Qt::CopyAction); } } void initDragThumbnailProvider() { mDragThumbnailProvider = new ThumbnailProvider(); QObject::connect(mDragThumbnailProvider, &ThumbnailProvider::thumbnailLoaded, q, &DocumentView::dragThumbnailLoaded); QObject::connect(mDragThumbnailProvider, &ThumbnailProvider::thumbnailLoadingFailed, q, &DocumentView::dragThumbnailLoadingFailed); } void startDragIfSensible() { if (q->document()->loadingState() == Document::LoadingFailed) { return; } if (q->currentTool()) { return; } if (mDrag) { mDrag->deleteLater(); } mDrag = new QDrag(q); const auto itemList = KFileItemList({q->document()->url()}); mDrag->setMimeData(MimeTypeUtils::selectionMimeData(itemList, MimeTypeUtils::DropTarget)); if (q->document()->isModified()) { setDragPixmap(QPixmap::fromImage(q->document()->image())); executeDrag(); } else { // Drag is triggered on success or failure of thumbnail generation if (mDragThumbnailProvider.isNull()) { initDragThumbnailProvider(); } mDragThumbnailProvider->appendItems(itemList); } } QPointF cursorPosition() { const QGraphicsScene* sc = q->scene(); if (sc) { const auto views = sc->views(); for (const QGraphicsView* view : views) { if (view->underMouse()) { return q->mapFromScene(view->mapFromGlobal(QCursor::pos())); } } } return QPointF(-1, -1); } }; DocumentView::DocumentView(QGraphicsScene* scene) : d(new DocumentViewPrivate) { setFlag(ItemIsFocusable); setFlag(ItemIsSelectable); setFlag(ItemClipsChildrenToShape); d->q = this; d->mLoadingIndicator = nullptr; d->mBirdEyeView = nullptr; d->mCurrent = false; d->mCompareMode = false; d->controlWheelAccumulatedDelta = 0; d->mDragStartPosition = QPointF(0, 0); d->mDrag = nullptr; + d->mTouch = new Touch(this); + setAcceptTouchEvents (true); + connect(d->mTouch, &Touch::doubleTapTriggered, this, &DocumentView::toggleFullScreenRequested); + connect(d->mTouch, &Touch::twoFingerTapTriggered, this, &DocumentView::contextMenuRequested); + connect(d->mTouch, &Touch::pinchGestureStarted, this, &DocumentView::setPinchParameter); + connect(d->mTouch, &Touch::pinchZoomTriggered, this, &DocumentView::zoomGesture); + connect(d->mTouch, &Touch::pinchRotateTriggered, this, &DocumentView::rotationsGesture); + connect(d->mTouch, &Touch::swipeRightTriggered, this, &DocumentView::swipeRight); + connect(d->mTouch, &Touch::swipeLeftTriggered, this, &DocumentView::swipeLeft); + connect(d->mTouch, &Touch::PanTriggered, this, &DocumentView::panGesture); + connect(d->mTouch, &Touch::tapHoldAndMovingTriggered, this, &DocumentView::startDragFromTouch); + // We use an opacity effect instead of using the opacity property directly, because the latter operates at // the painter level, which means if you draw multiple layers in paint(), all layers get the specified // opacity, resulting in all layers being visible when 0 < opacity < 1. // QGraphicsEffects on the other hand, operate after all painting is done, therefore 'flattening' all layers. // This is important for fade effects, where we don't want any background layers visible during the fade. d->mOpacityEffect = new QGraphicsOpacityEffect(this); d->mOpacityEffect->setOpacity(0); setGraphicsEffect(d->mOpacityEffect); scene->addItem(this); d->setupHud(); d->setCurrentAdapter(new EmptyAdapter); setAcceptDrops(true); connect(DocumentFactory::instance(), &DocumentFactory::documentChanged, this, [this]() { d->updateCaption(); }); } DocumentView::~DocumentView() { + delete d->mTouch; delete d->mDragThumbnailProvider; delete d->mDrag; delete d; } void DocumentView::createAdapterForDocument() { const MimeTypeUtils::Kind documentKind = d->mDocument->kind(); if (d->mAdapter && documentKind == d->mAdapter->kind() && documentKind != MimeTypeUtils::KIND_UNKNOWN) { // Do not reuse for KIND_UNKNOWN: we may need to change the message LOG("Reusing current adapter"); return; } AbstractDocumentViewAdapter* adapter = nullptr; switch (documentKind) { case MimeTypeUtils::KIND_RASTER_IMAGE: adapter = new RasterImageViewAdapter; break; case MimeTypeUtils::KIND_SVG_IMAGE: adapter = new SvgViewAdapter; break; case MimeTypeUtils::KIND_VIDEO: adapter = new VideoViewAdapter; connect(adapter, SIGNAL(videoFinished()), SIGNAL(videoFinished())); break; case MimeTypeUtils::KIND_UNKNOWN: adapter = new MessageViewAdapter; static_cast(adapter)->setErrorMessage(i18n("Gwenview does not know how to display this kind of document")); break; default: qWarning() << "should not be called for documentKind=" << documentKind; adapter = new MessageViewAdapter; break; } d->setCurrentAdapter(adapter); } void DocumentView::openUrl(const QUrl &url, const DocumentView::Setup& setup) { if (d->mDocument) { if (url == d->mDocument->url()) { return; } disconnect(d->mDocument.data(), nullptr, this, nullptr); } d->mSetup = setup; d->mDocument = DocumentFactory::instance()->load(url); connect(d->mDocument.data(), &Document::busyChanged, this, &DocumentView::slotBusyChanged); connect(d->mDocument.data(), &Document::modified, this, [this]() { d->updateZoomSnapValues(); }); if (d->mDocument->loadingState() < Document::KindDetermined) { MessageViewAdapter* messageViewAdapter = qobject_cast(d->mAdapter.data()); if (messageViewAdapter) { messageViewAdapter->setInfoMessage(QString()); } d->showLoadingIndicator(); connect(d->mDocument.data(), &Document::kindDetermined, this, &DocumentView::finishOpenUrl); } else { QMetaObject::invokeMethod(this, "finishOpenUrl", Qt::QueuedConnection); } d->setupBirdEyeView(); } void DocumentView::finishOpenUrl() { disconnect(d->mDocument.data(), &Document::kindDetermined, this, &DocumentView::finishOpenUrl); GV_RETURN_IF_FAIL(d->mDocument->loadingState() >= Document::KindDetermined); if (d->mDocument->loadingState() == Document::LoadingFailed) { slotLoadingFailed(); return; } createAdapterForDocument(); connect(d->mDocument.data(), &Document::loadingFailed, this, &DocumentView::slotLoadingFailed); d->mAdapter->setDocument(d->mDocument); d->updateCaption(); } void DocumentView::loadAdapterConfig() { d->mAdapter->loadConfig(); } RasterImageView* DocumentView::imageView() const { return d->mAdapter->rasterImageView(); } void DocumentView::slotCompleted() { d->hideLoadingIndicator(); d->updateCaption(); d->updateZoomSnapValues(); if (!d->mAdapter->zoomToFit() || !d->mAdapter->zoomToFill()) { qreal min = minimumZoom(); if (d->mAdapter->zoom() < min) { d->mAdapter->setZoom(min); } } emit completed(); } DocumentView::Setup DocumentView::setup() const { Setup setup; if (d->mAdapter->canZoom()) { setup.valid = true; setup.zoomToFit = zoomToFit(); setup.zoomToFill = zoomToFill(); if (!setup.zoomToFit && !setup.zoomToFill) { setup.zoom = zoom(); setup.position = position(); } } return setup; } void DocumentView::slotLoadingFailed() { d->hideLoadingIndicator(); MessageViewAdapter* adapter = new MessageViewAdapter; adapter->setDocument(d->mDocument); QString message = xi18n("Loading %1 failed", d->mDocument->url().fileName()); adapter->setErrorMessage(message, d->mDocument->errorString()); d->setCurrentAdapter(adapter); emit completed(); } bool DocumentView::canZoom() const { return d->mAdapter->canZoom(); } void DocumentView::setZoomToFit(bool on) { if (on == d->mAdapter->zoomToFit()) { return; } d->mAdapter->setZoomToFit(on); } void DocumentView::toggleZoomToFit() { const bool zoomToFitOn = d->mAdapter->zoomToFit(); d->mAdapter->setZoomToFit(!zoomToFitOn); if (zoomToFitOn) { d->setZoom(1., d->cursorPosition()); } } void DocumentView::setZoomToFill(bool on) { if (on == d->mAdapter->zoomToFill()) { return; } d->mAdapter->setZoomToFill(on, d->cursorPosition()); } void DocumentView::toggleZoomToFill() { const bool zoomToFillOn = d->mAdapter->zoomToFill(); d->mAdapter->setZoomToFill(!zoomToFillOn, d->cursorPosition()); if (zoomToFillOn) { d->setZoom(1., d->cursorPosition()); } } bool DocumentView::zoomToFit() const { return d->mAdapter->zoomToFit(); } bool DocumentView::zoomToFill() const { return d->mAdapter->zoomToFill(); } void DocumentView::zoomActualSize() { d->uncheckZoomToFit(); d->uncheckZoomToFill(); d->mAdapter->setZoom(1., d->cursorPosition()); } void DocumentView::zoomIn(QPointF center) { if (center == QPointF(-1, -1)) { center = d->cursorPosition(); } qreal currentZoom = d->mAdapter->zoom(); Q_FOREACH(qreal zoom, d->mZoomSnapValues) { if (zoom > currentZoom + REAL_DELTA) { d->setZoom(zoom, center); return; } } } void DocumentView::zoomOut(QPointF center) { if (center == QPointF(-1, -1)) { center = d->cursorPosition(); } qreal currentZoom = d->mAdapter->zoom(); QListIterator it(d->mZoomSnapValues); it.toBack(); while (it.hasPrevious()) { qreal zoom = it.previous(); if (zoom < currentZoom - REAL_DELTA) { d->setZoom(zoom, center); return; } } } void DocumentView::slotZoomChanged(qreal zoom) { d->updateCaption(); emit zoomChanged(zoom); } void DocumentView::setZoom(qreal zoom) { d->setZoom(zoom); } qreal DocumentView::zoom() const { return d->mAdapter->zoom(); } +void DocumentView::setPinchParameter() +{ + const qreal sensitivityModifier = 0.85; + const qreal rotationThreshold = 40; + d->mTouch->setZoomParameter(sensitivityModifier, zoom()); + d->mTouch->setRotationThreshold (rotationThreshold); +} + +void DocumentView::zoomGesture(qreal zoom, const QPoint& zoomCenter) +{ + if (zoom >= 0.0 && d->mAdapter->canZoom()) { + d->setZoom(zoom, zoomCenter); + } +} + +void DocumentView::rotationsGesture(qreal rotation) +{ + if (rotation > 0.0) { + TransformImageOperation* op = new TransformImageOperation(ROT_90); + op->applyToDocument(d->mDocument); + } else if (rotation < 0.0) { + TransformImageOperation* op = new TransformImageOperation(ROT_270); + op->applyToDocument(d->mDocument); + } +} + +void DocumentView::swipeRight() +{ + const QPoint scrollPos = d->mAdapter->scrollPos().toPoint(); + if (scrollPos.x() <= 1) { + emit d->mAdapter->previousImageRequested(); + } +} + +void DocumentView::swipeLeft() +{ + const QPoint scrollPos = d->mAdapter->scrollPos().toPoint(); + const int width = d->mAdapter->document()->width() * d->mAdapter->zoom(); + const QRect visibleRect = d->mAdapter->visibleDocumentRect().toRect(); + const int x = scrollPos.x() + visibleRect.width(); + if (x >= (width - 1)) { + emit d->mAdapter->nextImageRequested(); + } +} + +void DocumentView::panGesture(const QPointF& delta) +{ + d->mAdapter->setScrollPos(d->mAdapter->scrollPos() + delta); +} + +void DocumentView::startDragFromTouch(const QPoint&) +{ + d->startDragIfSensible(); +} + void DocumentView::resizeEvent(QGraphicsSceneResizeEvent *event) { d->resizeAdapterWidget(); d->updateZoomSnapValues(); QGraphicsWidget::resizeEvent(event); } void DocumentView::mousePressEvent(QGraphicsSceneMouseEvent* event) { QGraphicsWidget::mousePressEvent(event); if (d->mAdapter->canZoom() && event->button() == Qt::MiddleButton) { if (event->modifiers() == Qt::NoModifier) { toggleZoomToFit(); } else if (event->modifiers() == Qt::SHIFT) { toggleZoomToFill(); } } } void DocumentView::wheelEvent(QGraphicsSceneWheelEvent* event) { if (d->mAdapter->canZoom()) { if ((event->modifiers() & Qt::ControlModifier) || (GwenviewConfig::mouseWheelBehavior() == MouseWheelBehavior::Zoom && event->modifiers() == Qt::NoModifier)) { d->controlWheelAccumulatedDelta += event->delta(); // Ctrl + wheel => zoom in or out if (d->controlWheelAccumulatedDelta >= QWheelEvent::DefaultDeltasPerStep) { zoomIn(event->pos()); d->controlWheelAccumulatedDelta = 0; } else if (d->controlWheelAccumulatedDelta <= -QWheelEvent::DefaultDeltasPerStep) { zoomOut(event->pos()); d->controlWheelAccumulatedDelta = 0; } return; } } if (GwenviewConfig::mouseWheelBehavior() == MouseWheelBehavior::Browse && event->modifiers() == Qt::NoModifier) { d->controlWheelAccumulatedDelta += event->delta(); // Browse with mouse wheel if (d->controlWheelAccumulatedDelta >= QWheelEvent::DefaultDeltasPerStep) { emit previousImageRequested(); d->controlWheelAccumulatedDelta = 0; } else if (d->controlWheelAccumulatedDelta <= -QWheelEvent::DefaultDeltasPerStep) { emit nextImageRequested(); d->controlWheelAccumulatedDelta = 0; } return; } // Scroll qreal dx = 0; // 16 = pixels for one line // 120: see QWheelEvent::delta() doc qreal dy = -qApp->wheelScrollLines() * 16 * event->delta() / 120; if (event->orientation() == Qt::Horizontal) { qSwap(dx, dy); } d->mAdapter->setScrollPos(d->mAdapter->scrollPos() + QPointF(dx, dy)); } void DocumentView::contextMenuEvent(QGraphicsSceneContextMenuEvent* event) { // Filter out context menu if Ctrl is down to avoid showing it when // zooming out with Ctrl + Right button if (event->modifiers() != Qt::ControlModifier) { emit contextMenuRequested(); } } void DocumentView::paint(QPainter* painter, const QStyleOptionGraphicsItem* /*option*/, QWidget* /*widget*/) { // Fill background manually, because setAutoFillBackground(true) fill with QPalette::Window, // but our palettes use QPalette::Base for the background color/texture painter->fillRect(rect(), palette().base()); // Selection indicator/highlight if (d->mCompareMode && d->mCurrent) { painter->save(); painter->setBrush(Qt::NoBrush); painter->setPen(QPen(palette().highlight().color(), 2)); painter->setRenderHint(QPainter::Antialiasing); const QRectF visibleRectF = mapRectFromItem(d->mAdapter->widget(), d->mAdapter->visibleDocumentRect()); // Round the point and size independently. This is different than calling toRect(), // and is necessary to keep consistent rects, otherwise the selection rect can be // drawn 1 pixel too big or small. const QRect visibleRect = QRect(visibleRectF.topLeft().toPoint(), visibleRectF.size().toSize()); const QRect selectionRect = visibleRect.adjusted(-1, -1, 1, 1); painter->drawRoundedRect(selectionRect, 3, 3); painter->restore(); } } void DocumentView::slotBusyChanged(const QUrl&, bool busy) { if (busy) { d->showLoadingIndicator(); } else { d->hideLoadingIndicator(); } } qreal DocumentView::minimumZoom() const { // There is no point zooming out less than zoomToFit, but make sure it does // not get too small either return qBound(qreal(0.001), d->mAdapter->computeZoomToFit(), qreal(1.)); } void DocumentView::setCompareMode(bool compare) { d->mCompareMode = compare; if (compare) { d->mHud->show(); d->mHud->setZValue(1); } else { d->mHud->hide(); } } void DocumentView::setCurrent(bool value) { d->mCurrent = value; if (value) { d->mAdapter->widget()->setFocus(); d->updateCaption(); } update(); } bool DocumentView::isCurrent() const { return d->mCurrent; } QPoint DocumentView::position() const { return d->mAdapter->scrollPos().toPoint(); } void DocumentView::setPosition(const QPoint& pos) { d->mAdapter->setScrollPos(pos); } Document::Ptr DocumentView::document() const { return d->mDocument; } QUrl DocumentView::url() const { Document::Ptr doc = d->mDocument; return doc ? doc->url() : QUrl(); } void DocumentView::emitHudDeselectClicked() { emit hudDeselectClicked(this); } void DocumentView::emitHudTrashClicked() { emit hudTrashClicked(this); } void DocumentView::emitFocused() { emit focused(this); } void DocumentView::setGeometry(const QRectF& rect) { QGraphicsWidget::setGeometry(rect); if (d->mBirdEyeView) { d->mBirdEyeView->slotZoomOrSizeChanged(); } } void DocumentView::moveTo(const QRect& rect) { if (d->mMoveAnimation) { d->mMoveAnimation.data()->setEndValue(rect); } else { setGeometry(rect); } } void DocumentView::moveToAnimated(const QRect& rect) { QPropertyAnimation* anim = new QPropertyAnimation(this, "geometry"); anim->setStartValue(geometry()); anim->setEndValue(rect); anim->setDuration(DocumentView::AnimDuration); connect(anim, &QAbstractAnimation::finished, this, &DocumentView::isAnimatedChanged); d->mMoveAnimation = anim; emit isAnimatedChanged(); anim->start(QAbstractAnimation::DeleteWhenStopped); } QPropertyAnimation* DocumentView::fadeIn() { d->fadeTo(1); return d->mFadeAnimation.data(); } void DocumentView::fadeOut() { d->fadeTo(0); } void DocumentView::slotFadeInFinished() { emit fadeInFinished(this); } bool DocumentView::isAnimated() const { return d->mMoveAnimation || d->mFadeAnimation; } bool DocumentView::sceneEventFilter(QGraphicsItem*, QEvent* event) { if (event->type() == QEvent::GraphicsSceneMousePress) { const QGraphicsSceneMouseEvent* mouseEvent = static_cast(event); if (mouseEvent->button() == Qt::LeftButton) { d->mDragStartPosition = mouseEvent->pos(); } QMetaObject::invokeMethod(this, "emitFocused", Qt::QueuedConnection); } else if (event->type() == QEvent::GraphicsSceneHoverMove) { if (d->mBirdEyeView) { d->mBirdEyeView->onMouseMoved(); } } else if (event->type() == QEvent::GraphicsSceneMouseMove) { const QGraphicsSceneMouseEvent* mouseEvent = static_cast(event); + //in some older version of Qt, Qt synthesize a mouse event from the touch event + //we need to suppress this. + //I need this for my working system (OpenSUSE Leap 15.0, Qt 5.9.4) + if (mouseEvent->source() == Qt::MouseEventSynthesizedByQt) { + return true; + } const qreal dragDistance = (mouseEvent->pos() - d->mDragStartPosition).manhattanLength(); const qreal minDistanceToStartDrag = QGuiApplication::styleHints()->startDragDistance(); if (!d->canPan() && dragDistance >= minDistanceToStartDrag) { d->startDragIfSensible(); } } return false; } AbstractRasterImageViewTool* DocumentView::currentTool() const { return imageView() ? imageView()->currentTool() : nullptr; } int DocumentView::sortKey() const { return d->mSortKey; } void DocumentView::setSortKey(int sortKey) { d->mSortKey = sortKey; } void DocumentView::hideAndDeleteLater() { hide(); deleteLater(); } void DocumentView::setGraphicsEffectOpacity(qreal opacity) { d->mOpacityEffect->setOpacity(opacity); } void DocumentView::dragEnterEvent(QGraphicsSceneDragDropEvent* event) { QGraphicsWidget::dragEnterEvent(event); const auto urls = KUrlMimeData::urlsFromMimeData(event->mimeData()); bool acceptDrag = !urls.isEmpty(); if (urls.size() == 1 && urls.first() == url()) { // Do not allow dragging a single image onto itself acceptDrag = false; } event->setAccepted(acceptDrag); } void DocumentView::dropEvent(QGraphicsSceneDragDropEvent* event) { QGraphicsWidget::dropEvent(event); // Since we're capturing drops in View mode, we only support one url const QUrl url = event->mimeData()->urls().first(); if (UrlUtils::urlIsDirectory(url)) { emit openDirUrlRequested(url); } else { emit openUrlRequested(url); } } void DocumentView::dragThumbnailLoaded(const KFileItem& item, const QPixmap& pix) { d->setDragPixmap(pix); d->executeDrag(); d->mDragThumbnailProvider->removeItems(KFileItemList({item})); } void DocumentView::dragThumbnailLoadingFailed(const KFileItem& item) { d->executeDrag(); d->mDragThumbnailProvider->removeItems(KFileItemList({item})); } } // namespace diff --git a/lib/documentview/documentview.h b/lib/documentview/documentview.h index a2da3d8a..6088f00e 100644 --- a/lib/documentview/documentview.h +++ b/lib/documentview/documentview.h @@ -1,246 +1,257 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2008 Aurélien Gâteau 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. */ #ifndef DOCUMENTVIEW_H #define DOCUMENTVIEW_H #include // Qt #include // KDE // Local #include class QPropertyAnimation; class QUrl; +class QGestureEvent; +class QTapGesture; +class QPinchGesture; +class QGesture; namespace Gwenview { class AbstractRasterImageViewTool; class RasterImageView; struct DocumentViewPrivate; /** * This widget can display various documents, using an instance of * AbstractDocumentViewAdapter */ class GWENVIEWLIB_EXPORT DocumentView : public QGraphicsWidget { Q_OBJECT Q_PROPERTY(qreal zoom READ zoom WRITE setZoom NOTIFY zoomChanged) Q_PROPERTY(bool zoomToFit READ zoomToFit WRITE setZoomToFit NOTIFY zoomToFitChanged) Q_PROPERTY(bool zoomToFill READ zoomToFill WRITE setZoomToFill NOTIFY zoomToFillChanged) Q_PROPERTY(QPoint position READ position WRITE setPosition NOTIFY positionChanged) public: static const int MaximumZoom; static const int AnimDuration; struct Setup { Setup() : valid(false) , zoomToFit(true) , zoomToFill(false) , zoom(0) {} bool valid:1; bool zoomToFit:1; bool zoomToFill:1; qreal zoom; QPointF position; }; enum AnimationMethod { NoAnimation, SoftwareAnimation, GLAnimation }; /** * Create a new view attached to scene. We need the scene to be able to * install scene event filters. */ explicit DocumentView(QGraphicsScene* scene); ~DocumentView() override; Document::Ptr document() const; QUrl url() const; void openUrl(const QUrl&, const Setup&); Setup setup() const; /** * Tells the current adapter to load its config. Used when the user changed * the config while the view was visible. */ void loadAdapterConfig(); bool canZoom() const; qreal minimumZoom() const; qreal zoom() const; bool isCurrent() const; void setCurrent(bool); void setCompareMode(bool); bool zoomToFit() const; bool zoomToFill() const; QPoint position() const; /** * Returns the RasterImageView of the current adapter, if it has one */ RasterImageView* imageView() const; AbstractRasterImageViewTool* currentTool() const; void moveTo(const QRect&); void moveToAnimated(const QRect&); QPropertyAnimation* fadeIn(); void fadeOut(); void fakeFadeOut(); void setGeometry(const QRectF& rect) override; int sortKey() const; void setSortKey(int sortKey); bool isAnimated() const; /** * Sets the opacity on the installed QGraphicsOpacityEffect. * Use this instead of setOpacity(). */ void setGraphicsEffectOpacity(qreal opacity); public Q_SLOTS: void setZoom(qreal); void setZoomToFit(bool); void toggleZoomToFit(); void setZoomToFill(bool); void toggleZoomToFill(); void setPosition(const QPoint&); void hideAndDeleteLater(); Q_SIGNALS: /** * Emitted when the part has finished loading */ void completed(); void previousImageRequested(); void nextImageRequested(); void openUrlRequested(const QUrl&); void openDirUrlRequested(const QUrl&); void captionUpdateRequested(const QString&); void toggleFullScreenRequested(); void videoFinished(); void minimumZoomChanged(qreal); void zoomChanged(qreal); void adapterChanged(); void focused(DocumentView*); void zoomToFitChanged(bool); void zoomToFillChanged(bool); void positionChanged(); void hudTrashClicked(DocumentView*); void hudDeselectClicked(DocumentView*); void fadeInFinished(DocumentView*); void contextMenuRequested(); void currentToolChanged(AbstractRasterImageViewTool*); void isAnimatedChanged(); protected: void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget = nullptr) override; void resizeEvent(QGraphicsSceneResizeEvent* event) override; void mousePressEvent(QGraphicsSceneMouseEvent* event) override; void wheelEvent(QGraphicsSceneWheelEvent* event) override; void contextMenuEvent(QGraphicsSceneContextMenuEvent* event) override; bool sceneEventFilter(QGraphicsItem*, QEvent*) override; void dragEnterEvent(QGraphicsSceneDragDropEvent* event) override; void dropEvent(QGraphicsSceneDragDropEvent* event) override; private Q_SLOTS: void finishOpenUrl(); void slotCompleted(); void slotLoadingFailed(); void zoomActualSize(); void zoomIn(QPointF center = QPointF(-1, -1)); void zoomOut(QPointF center = QPointF(-1, -1)); void slotZoomChanged(qreal); void slotBusyChanged(const QUrl&, bool); void emitHudTrashClicked(); void emitHudDeselectClicked(); void emitFocused(); void slotFadeInFinished(); void dragThumbnailLoaded(const KFileItem&, const QPixmap&); void dragThumbnailLoadingFailed(const KFileItem&); + void setPinchParameter(); + void zoomGesture(qreal newZoom, const QPoint& pos); + void rotationsGesture(qreal); + void swipeRight(); + void swipeLeft(); + void panGesture(const QPointF& delta); + void startDragFromTouch(const QPoint& pos); private: friend struct DocumentViewPrivate; DocumentViewPrivate* const d; void createAdapterForDocument(); }; } // namespace #endif /* DOCUMENTVIEW_H */ diff --git a/lib/scrollerutils.cpp b/lib/scrollerutils.cpp new file mode 100644 index 00000000..995ee39f --- /dev/null +++ b/lib/scrollerutils.cpp @@ -0,0 +1,44 @@ +/* +Gwenview: an image viewer +Copyright 2019 Steffen Hartleib + +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, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +*/ +#include "scrollerutils.h" + +// Qt +#include + + +namespace Gwenview +{ +namespace ScrollerUtils +{ + +QScroller* setQScroller (QObject* viewport) +{ + + QScroller* scroller = QScroller::scroller(viewport); + QScrollerProperties scrollerProperties = scroller->scrollerProperties(); + scrollerProperties.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff); + scrollerProperties.setScrollMetric(QScrollerProperties::VerticalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff); + scroller->setScrollerProperties(scrollerProperties); + scroller->grabGesture(viewport); + return scroller; +} + +} // namespace +} // namespace diff --git a/lib/scrollerutils.h b/lib/scrollerutils.h new file mode 100644 index 00000000..59aa70d6 --- /dev/null +++ b/lib/scrollerutils.h @@ -0,0 +1,38 @@ +/* +Gwenview: an image viewer +Copyright 2019 Steffen Hartleib + +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, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +*/ +#ifndef SCROLLERUTILS_H +#define SCROLLERUTILS_H + +#include + +class QScroller; +class QObject; + +namespace Gwenview +{ +namespace ScrollerUtils +{ + +GWENVIEWLIB_EXPORT QScroller* setQScroller(QObject*); + +} // namespace +} // namespace + +#endif /* SCROLLERUTILS_H */ diff --git a/lib/thumbnailview/thumbnailview.cpp b/lib/thumbnailview/thumbnailview.cpp index 8c1d2fdc..a076dd1e 100644 --- a/lib/thumbnailview/thumbnailview.cpp +++ b/lib/thumbnailview/thumbnailview.cpp @@ -1,984 +1,1031 @@ /* Gwenview: an image viewer Copyright 2007 Aurélien Gâteau 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "thumbnailview.h" // Std #include // Qt #include #include #include #include #include #include #include #include #include #include #include #include #include +#include +#include // KDE #include #include #include #include // Local #include "abstractdocumentinfoprovider.h" #include "abstractthumbnailviewhelper.h" #include "archiveutils.h" #include "dragpixmapgenerator.h" #include "mimetypeutils.h" #include "urlutils.h" #include #include +#include +#include namespace Gwenview { #undef ENABLE_LOG #undef LOG //#define ENABLE_LOG #ifdef ENABLE_LOG #define LOG(x) //qDebug() << x #else #define LOG(x) ; #endif /** How many msec to wait before starting to smooth thumbnails */ const int SMOOTH_DELAY = 500; const int WHEEL_ZOOM_MULTIPLIER = 4; static KFileItem fileItemForIndex(const QModelIndex& index) { if (!index.isValid()) { LOG("Invalid index"); return KFileItem(); } QVariant data = index.data(KDirModel::FileItemRole); return qvariant_cast(data); } static QUrl urlForIndex(const QModelIndex& index) { KFileItem item = fileItemForIndex(index); return item.isNull() ? QUrl() : item.url(); } struct Thumbnail { Thumbnail(const QPersistentModelIndex& index_, const QDateTime& mtime) : mIndex(index_) , mModificationTime(mtime) , mFileSize(0) , mRough(true) , mWaitingForThumbnail(true) {} Thumbnail() : mFileSize(0) , mRough(true) , mWaitingForThumbnail(true) {} /** * Init the thumbnail based on a icon */ void initAsIcon(const QPixmap& pix) { mGroupPix = pix; int largeGroupSize = ThumbnailGroup::pixelSize(ThumbnailGroup::Large); mFullSize = QSize(largeGroupSize, largeGroupSize); } bool isGroupPixAdaptedForSize(int size) const { if (mWaitingForThumbnail) { return false; } if (mGroupPix.isNull()) { return false; } const int groupSize = qMax(mGroupPix.width(), mGroupPix.height()); if (groupSize >= size) { return true; } // groupSize is less than size, but this may be because the full image // is the same size as groupSize return groupSize == qMax(mFullSize.width(), mFullSize.height()); } void prepareForRefresh(const QDateTime& mtime) { mModificationTime = mtime; mFileSize = 0; mGroupPix = QPixmap(); mAdjustedPix = QPixmap(); mFullSize = QSize(); mRealFullSize = QSize(); mRough = true; mWaitingForThumbnail = true; } QPersistentModelIndex mIndex; QDateTime mModificationTime; /// The pix loaded from .thumbnails/{large,normal} QPixmap mGroupPix; /// Scaled version of mGroupPix, adjusted to ThumbnailView::thumbnailSize QPixmap mAdjustedPix; /// Size of the full image QSize mFullSize; /// Real size of the full image, invalid unless the thumbnail /// represents a raster image (not an icon) QSize mRealFullSize; /// File size of the full image KIO::filesize_t mFileSize; /// Whether mAdjustedPix represents has been scaled using fast or smooth /// transformation bool mRough; /// Set to true if mGroupPix should be replaced with a real thumbnail bool mWaitingForThumbnail; }; typedef QHash ThumbnailForUrl; typedef QQueue UrlQueue; typedef QSet PersistentModelIndexSet; struct ThumbnailViewPrivate { ThumbnailView* q; ThumbnailView::ThumbnailScaleMode mScaleMode; QSize mThumbnailSize; qreal mThumbnailAspectRatio; AbstractDocumentInfoProvider* mDocumentInfoProvider; AbstractThumbnailViewHelper* mThumbnailViewHelper; ThumbnailForUrl mThumbnailForUrl; QTimer mScheduledThumbnailGenerationTimer; UrlQueue mSmoothThumbnailQueue; QTimer mSmoothThumbnailTimer; QPixmap mWaitingThumbnail; QPointer mThumbnailProvider; PersistentModelIndexSet mBusyIndexSet; KPixmapSequence mBusySequence; QTimeLine* mBusyAnimationTimeLine; bool mCreateThumbnailsForRemoteUrls; + QScroller* mScroller; + Touch* mTouch; + void setupBusyAnimation() { mBusySequence = KIconLoader::global()->loadPixmapSequence(QStringLiteral("process-working"), 22); mBusyAnimationTimeLine = new QTimeLine(100 * mBusySequence.frameCount(), q); mBusyAnimationTimeLine->setCurveShape(QTimeLine::LinearCurve); mBusyAnimationTimeLine->setEndFrame(mBusySequence.frameCount() - 1); mBusyAnimationTimeLine->setLoopCount(0); QObject::connect(mBusyAnimationTimeLine, &QTimeLine::frameChanged, q, &ThumbnailView::updateBusyIndexes); } void scheduleThumbnailGeneration() { if (mThumbnailProvider) { mThumbnailProvider->removePendingItems(); } mSmoothThumbnailQueue.clear(); mScheduledThumbnailGenerationTimer.start(); } void updateThumbnailForModifiedDocument(const QModelIndex& index) { Q_ASSERT(mDocumentInfoProvider); KFileItem item = fileItemForIndex(index); QUrl url = item.url(); ThumbnailGroup::Enum group = ThumbnailGroup::fromPixelSize(mThumbnailSize.width()); QPixmap pix; QSize fullSize; mDocumentInfoProvider->thumbnailForDocument(url, group, &pix, &fullSize); mThumbnailForUrl[url] = Thumbnail(QPersistentModelIndex(index), QDateTime::currentDateTime()); q->setThumbnail(item, pix, fullSize, 0); } void appendItemsToThumbnailProvider(const KFileItemList& list) { if (mThumbnailProvider) { ThumbnailGroup::Enum group = ThumbnailGroup::fromPixelSize(mThumbnailSize.width()); mThumbnailProvider->setThumbnailGroup(group); mThumbnailProvider->appendItems(list); } } void roughAdjustThumbnail(Thumbnail* thumbnail) { const QPixmap& mGroupPix = thumbnail->mGroupPix; const int groupSize = qMax(mGroupPix.width(), mGroupPix.height()); const int fullSize = qMax(thumbnail->mFullSize.width(), thumbnail->mFullSize.height()); if (fullSize == groupSize && mGroupPix.height() <= mThumbnailSize.height() && mGroupPix.width() <= mThumbnailSize.width()) { thumbnail->mAdjustedPix = mGroupPix; thumbnail->mRough = false; } else { thumbnail->mAdjustedPix = scale(mGroupPix, Qt::FastTransformation); thumbnail->mRough = true; } } void initDragPixmap(QDrag* drag, const QModelIndexList& indexes) { const int thumbCount = qMin(indexes.count(), int(DragPixmapGenerator::MaxCount)); QList lst; for (int row = 0; row < thumbCount; ++row) { const QUrl url = urlForIndex(indexes[row]); lst << mThumbnailForUrl.value(url).mAdjustedPix; } DragPixmapGenerator::DragPixmap dragPixmap = DragPixmapGenerator::generate(lst, indexes.count()); drag->setPixmap(dragPixmap.pix); drag->setHotSpot(dragPixmap.hotSpot); } QPixmap scale(const QPixmap& pix, Qt::TransformationMode transformationMode) { switch (mScaleMode) { case ThumbnailView::ScaleToFit: return pix.scaled(mThumbnailSize.width(), mThumbnailSize.height(), Qt::KeepAspectRatio, transformationMode); case ThumbnailView::ScaleToSquare: { int minSize = qMin(pix.width(), pix.height()); QPixmap pix2 = pix.copy((pix.width() - minSize) / 2, (pix.height() - minSize) / 2, minSize, minSize); return pix2.scaled(mThumbnailSize.width(), mThumbnailSize.height(), Qt::KeepAspectRatio, transformationMode); } case ThumbnailView::ScaleToHeight: return pix.scaledToHeight(mThumbnailSize.height(), transformationMode); case ThumbnailView::ScaleToWidth: return pix.scaledToWidth(mThumbnailSize.width(), transformationMode); } // Keep compiler happy Q_ASSERT(0); return QPixmap(); } }; ThumbnailView::ThumbnailView(QWidget* parent) : QListView(parent) , d(new ThumbnailViewPrivate) { d->q = this; d->mScaleMode = ScaleToFit; d->mThumbnailViewHelper = nullptr; d->mDocumentInfoProvider = nullptr; d->mThumbnailProvider = nullptr; // Init to some stupid value so that the first call to setThumbnailSize() // is not ignored (do not use 0 in case someone try to divide by // mThumbnailSize...) d->mThumbnailSize = QSize(1, 1); d->mThumbnailAspectRatio = 1; d->mCreateThumbnailsForRemoteUrls = true; setFrameShape(QFrame::NoFrame); setViewMode(QListView::IconMode); setResizeMode(QListView::Adjust); setDragEnabled(true); setAcceptDrops(true); setDropIndicatorShown(true); setUniformItemSizes(true); setEditTriggers(QAbstractItemView::EditKeyPressed); d->setupBusyAnimation(); setVerticalScrollMode(ScrollPerPixel); setHorizontalScrollMode(ScrollPerPixel); d->mScheduledThumbnailGenerationTimer.setSingleShot(true); d->mScheduledThumbnailGenerationTimer.setInterval(500); connect(&d->mScheduledThumbnailGenerationTimer, &QTimer::timeout, this, &ThumbnailView::generateThumbnailsForItems); d->mSmoothThumbnailTimer.setSingleShot(true); connect(&d->mSmoothThumbnailTimer, &QTimer::timeout, this, &ThumbnailView::smoothNextThumbnail); setContextMenuPolicy(Qt::CustomContextMenu); connect(this, &ThumbnailView::customContextMenuRequested, this, &ThumbnailView::showContextMenu); connect(this, &ThumbnailView::activated, this, &ThumbnailView::emitIndexActivatedIfNoModifiers); + + d->mScroller = ScrollerUtils::setQScroller(this->viewport()); + d->mTouch = new Touch(viewport()); + connect(d->mTouch, &Touch::twoFingerTapTriggered, this, &ThumbnailView::showContextMenu); + connect(d->mTouch, &Touch::pinchZoomTriggered, this, &ThumbnailView::zoomGesture); + connect(d->mTouch, &Touch::pinchGestureStarted, this, &ThumbnailView::setZoomParameter); + connect(d->mTouch, &Touch::tapTriggered, this, &ThumbnailView::tapGesture); + connect(d->mTouch, &Touch::tapHoldAndMovingTriggered, this, &ThumbnailView::startDragFromTouch); } ThumbnailView::~ThumbnailView() { + delete d->mTouch; delete d; } ThumbnailView::ThumbnailScaleMode ThumbnailView::thumbnailScaleMode() const { return d->mScaleMode; } void ThumbnailView::setThumbnailScaleMode(ThumbnailScaleMode mode) { d->mScaleMode = mode; setUniformItemSizes(mode == ScaleToFit || mode == ScaleToSquare); } void ThumbnailView::setModel(QAbstractItemModel* newModel) { if (model()) { disconnect(model(), nullptr, this, nullptr); } QListView::setModel(newModel); connect(model(), &QAbstractItemModel::rowsRemoved, this, &ThumbnailView::rowsRemovedSignal); } void ThumbnailView::setThumbnailProvider(ThumbnailProvider* thumbnailProvider) { GV_RETURN_IF_FAIL(d->mThumbnailProvider != thumbnailProvider); if (thumbnailProvider) { connect(thumbnailProvider, &ThumbnailProvider::thumbnailLoaded, this, &ThumbnailView::setThumbnail); connect(thumbnailProvider, &ThumbnailProvider::thumbnailLoadingFailed, this, &ThumbnailView::setBrokenThumbnail); } else { disconnect(d->mThumbnailProvider, nullptr , this, nullptr); } d->mThumbnailProvider = thumbnailProvider; } void ThumbnailView::updateThumbnailSize() { QSize value = d->mThumbnailSize; // mWaitingThumbnail int waitingThumbnailSize; if (value.width() > 64) { waitingThumbnailSize = 48; } else { waitingThumbnailSize = 32; } QPixmap icon = DesktopIcon(QStringLiteral("chronometer"), waitingThumbnailSize); QPixmap pix(value); pix.fill(Qt::transparent); QPainter painter(&pix); painter.setOpacity(0.5); painter.drawPixmap((value.width() - icon.width()) / 2, (value.height() - icon.height()) / 2, icon); painter.end(); d->mWaitingThumbnail = pix; // Stop smoothing d->mSmoothThumbnailTimer.stop(); d->mSmoothThumbnailQueue.clear(); // Clear adjustedPixes ThumbnailForUrl::iterator it = d->mThumbnailForUrl.begin(), end = d->mThumbnailForUrl.end(); for (; it != end; ++it) { it.value().mAdjustedPix = QPixmap(); } emit thumbnailSizeChanged(value); emit thumbnailWidthChanged(value.width()); if (d->mScaleMode != ScaleToFit) { scheduleDelayedItemsLayout(); } d->scheduleThumbnailGeneration(); } void ThumbnailView::setThumbnailWidth(int width) { if(d->mThumbnailSize.width() == width) { return; } int height = round((qreal)width / d->mThumbnailAspectRatio); d->mThumbnailSize = QSize(width, height); updateThumbnailSize(); } void ThumbnailView::setThumbnailAspectRatio(qreal ratio) { if(d->mThumbnailAspectRatio == ratio) { return; } d->mThumbnailAspectRatio = ratio; int width = d->mThumbnailSize.width(); int height = round((qreal)width / d->mThumbnailAspectRatio); d->mThumbnailSize = QSize(width, height); updateThumbnailSize(); } qreal ThumbnailView::thumbnailAspectRatio() const { return d->mThumbnailAspectRatio; } QSize ThumbnailView::thumbnailSize() const { return d->mThumbnailSize; } void ThumbnailView::setThumbnailViewHelper(AbstractThumbnailViewHelper* helper) { d->mThumbnailViewHelper = helper; } AbstractThumbnailViewHelper* ThumbnailView::thumbnailViewHelper() const { return d->mThumbnailViewHelper; } void ThumbnailView::setDocumentInfoProvider(AbstractDocumentInfoProvider* provider) { d->mDocumentInfoProvider = provider; if (provider) { connect(provider, &AbstractDocumentInfoProvider::busyStateChanged, this, &ThumbnailView::updateThumbnailBusyState); connect(provider, &AbstractDocumentInfoProvider::documentChanged, this, &ThumbnailView::updateThumbnail); } } AbstractDocumentInfoProvider* ThumbnailView::documentInfoProvider() const { return d->mDocumentInfoProvider; } void ThumbnailView::rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) { QListView::rowsAboutToBeRemoved(parent, start, end); // Remove references to removed items KFileItemList itemList; for (int pos = start; pos <= end; ++pos) { QModelIndex index = model()->index(pos, 0, parent); KFileItem item = fileItemForIndex(index); if (item.isNull()) { //qDebug() << "Skipping invalid item!" << index.data().toString(); continue; } QUrl url = item.url(); d->mThumbnailForUrl.remove(url); d->mSmoothThumbnailQueue.removeAll(url); itemList.append(item); } if (d->mThumbnailProvider) { d->mThumbnailProvider->removeItems(itemList); } // Removing rows might make new images visible, make sure their thumbnail // is generated d->mScheduledThumbnailGenerationTimer.start(); } void ThumbnailView::rowsInserted(const QModelIndex& parent, int start, int end) { QListView::rowsInserted(parent, start, end); d->mScheduledThumbnailGenerationTimer.start(); emit rowsInsertedSignal(parent, start, end); } void ThumbnailView::dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QVector &roles) { QListView::dataChanged(topLeft, bottomRight, roles); bool thumbnailsNeedRefresh = false; for (int row = topLeft.row(); row <= bottomRight.row(); ++row) { QModelIndex index = model()->index(row, 0); KFileItem item = fileItemForIndex(index); if (item.isNull()) { qWarning() << "Invalid item for index" << index << ". This should not happen!"; GV_FATAL_FAILS; continue; } ThumbnailForUrl::Iterator it = d->mThumbnailForUrl.find(item.url()); if (it != d->mThumbnailForUrl.end()) { // All thumbnail views are connected to the model, so // ThumbnailView::dataChanged() is called for all of them. As a // result this method will also be called for views which are not // currently visible, and do not yet have a thumbnail for the // modified url. QDateTime mtime = item.time(KFileItem::ModificationTime); if (it->mModificationTime != mtime || it->mFileSize != item.size()) { // dataChanged() is called when the file changes but also when // the model fetched additional data such as semantic info. To // avoid needless refreshes, we only trigger a refresh if the // modification time changes. thumbnailsNeedRefresh = true; it->prepareForRefresh(mtime); } } } if (thumbnailsNeedRefresh) { d->mScheduledThumbnailGenerationTimer.start(); } } void ThumbnailView::showContextMenu() { d->mThumbnailViewHelper->showContextMenu(this); } void ThumbnailView::emitIndexActivatedIfNoModifiers(const QModelIndex& index) { if (QApplication::keyboardModifiers() == Qt::NoModifier) { emit indexActivated(index); } } void ThumbnailView::setThumbnail(const KFileItem& item, const QPixmap& pixmap, const QSize& size, qulonglong fileSize) { ThumbnailForUrl::iterator it = d->mThumbnailForUrl.find(item.url()); if (it == d->mThumbnailForUrl.end()) { return; } Thumbnail& thumbnail = it.value(); thumbnail.mGroupPix = pixmap; thumbnail.mAdjustedPix = QPixmap(); int largeGroupSize = ThumbnailGroup::pixelSize(ThumbnailGroup::Large2x); thumbnail.mFullSize = size.isValid() ? size : QSize(largeGroupSize, largeGroupSize); thumbnail.mRealFullSize = size; thumbnail.mWaitingForThumbnail = false; thumbnail.mFileSize = fileSize; update(thumbnail.mIndex); if (d->mScaleMode != ScaleToFit) { scheduleDelayedItemsLayout(); } } void ThumbnailView::setBrokenThumbnail(const KFileItem& item) { ThumbnailForUrl::iterator it = d->mThumbnailForUrl.find(item.url()); if (it == d->mThumbnailForUrl.end()) { return; } Thumbnail& thumbnail = it.value(); MimeTypeUtils::Kind kind = MimeTypeUtils::fileItemKind(item); if (kind == MimeTypeUtils::KIND_VIDEO) { // Special case for videos because our kde install may come without // support for video thumbnails so we show the mimetype icon instead of // a broken image icon const QPixmap pix = KIconLoader::global()->loadIcon(item.iconName(), KIconLoader::Desktop, d->mThumbnailSize.height()); thumbnail.initAsIcon(pix); } else if (kind == MimeTypeUtils::KIND_DIR) { // Special case for folders because ThumbnailProvider does not return a // thumbnail if there is no images thumbnail.mWaitingForThumbnail = false; return; } else { thumbnail.initAsIcon(DesktopIcon(QStringLiteral("image-missing"), 48)); thumbnail.mFullSize = thumbnail.mGroupPix.size(); } update(thumbnail.mIndex); } QPixmap ThumbnailView::thumbnailForIndex(const QModelIndex& index, QSize* fullSize) { KFileItem item = fileItemForIndex(index); if (item.isNull()) { LOG("Invalid item"); if (fullSize) { *fullSize = QSize(); } return QPixmap(); } QUrl url = item.url(); // Find or create Thumbnail instance ThumbnailForUrl::Iterator it = d->mThumbnailForUrl.find(url); if (it == d->mThumbnailForUrl.end()) { Thumbnail thumbnail = Thumbnail(QPersistentModelIndex(index), item.time(KFileItem::ModificationTime)); it = d->mThumbnailForUrl.insert(url, thumbnail); } Thumbnail& thumbnail = it.value(); // If dir or archive, generate a thumbnail from fileitem pixmap MimeTypeUtils::Kind kind = MimeTypeUtils::fileItemKind(item); if (kind == MimeTypeUtils::KIND_ARCHIVE || kind == MimeTypeUtils::KIND_DIR) { int groupSize = ThumbnailGroup::pixelSize(ThumbnailGroup::fromPixelSize(d->mThumbnailSize.height())); if (thumbnail.mGroupPix.isNull() || thumbnail.mGroupPix.height() < groupSize) { const QPixmap pix = KIconLoader::global()->loadIcon(item.iconName(), KIconLoader::Desktop, d->mThumbnailSize.height()); thumbnail.initAsIcon(pix); if (kind == MimeTypeUtils::KIND_ARCHIVE) { // No thumbnails for archives thumbnail.mWaitingForThumbnail = false; } else if (!d->mCreateThumbnailsForRemoteUrls && !UrlUtils::urlIsFastLocalFile(url)) { // If we don't want thumbnails for remote urls, use // "folder-remote" icon for remote folders, so that they do // not look like regular folders thumbnail.mWaitingForThumbnail = false; thumbnail.initAsIcon(DesktopIcon(QStringLiteral("folder-remote"), groupSize)); } else { // set mWaitingForThumbnail to true (necessary in the case // 'thumbnail' already existed before, but with a too small // mGroupPix) thumbnail.mWaitingForThumbnail = true; } } } if (thumbnail.mGroupPix.isNull()) { if (fullSize) { *fullSize = QSize(); } return d->mWaitingThumbnail; } // Adjust thumbnail if (thumbnail.mAdjustedPix.isNull()) { d->roughAdjustThumbnail(&thumbnail); } if (thumbnail.mRough && !d->mSmoothThumbnailQueue.contains(url)) { d->mSmoothThumbnailQueue.enqueue(url); if (!d->mSmoothThumbnailTimer.isActive()) { d->mSmoothThumbnailTimer.start(SMOOTH_DELAY); } } if (fullSize) { *fullSize = thumbnail.mRealFullSize; } return thumbnail.mAdjustedPix; } bool ThumbnailView::isModified(const QModelIndex& index) const { if (!d->mDocumentInfoProvider) { return false; } QUrl url = urlForIndex(index); return d->mDocumentInfoProvider->isModified(url); } bool ThumbnailView::isBusy(const QModelIndex& index) const { if (!d->mDocumentInfoProvider) { return false; } QUrl url = urlForIndex(index); return d->mDocumentInfoProvider->isBusy(url); } void ThumbnailView::startDrag(Qt::DropActions) { const QModelIndexList indexes = selectionModel()->selectedIndexes(); if (indexes.isEmpty()) { return; } KFileItemList selectedFiles; for (const auto &index : indexes) { selectedFiles << fileItemForIndex(index); } QDrag* drag = new QDrag(this); drag->setMimeData(MimeTypeUtils::selectionMimeData(selectedFiles, MimeTypeUtils::DropTarget)); d->initDragPixmap(drag, indexes); drag->exec(Qt::MoveAction | Qt::CopyAction | Qt::LinkAction, Qt::CopyAction); } +void ThumbnailView::setZoomParameter() +{ + const qreal sensitivityModifier = 0.25; + d->mTouch->setZoomParameter(sensitivityModifier, d->mThumbnailSize.width()); +} + +void ThumbnailView::zoomGesture(qreal newZoom, const QPoint&) +{ + if (newZoom >= 0.0) { + int width = qBound (int(MinThumbnailSize), static_cast(newZoom), int(MaxThumbnailSize)); + setThumbnailWidth(width); + } +} + +void ThumbnailView::tapGesture(const QPoint& pos) +{ + const QRect rect = QRect(pos, QSize(1, 1)); + setSelection(rect, QItemSelectionModel::ClearAndSelect); + emit activated(indexAt(pos)); +} + +void ThumbnailView::startDragFromTouch(const QPoint& pos) +{ + QModelIndex index = indexAt(pos); + if (index.isValid()) { + setCurrentIndex(index); + d->mScroller->stop(); + startDrag(Qt::CopyAction); + } +} + void ThumbnailView::dragEnterEvent(QDragEnterEvent* event) { QAbstractItemView::dragEnterEvent(event); if (event->mimeData()->hasUrls()) { event->acceptProposedAction(); } } void ThumbnailView::dragMoveEvent(QDragMoveEvent* event) { // Necessary, otherwise we don't reach dropEvent() QAbstractItemView::dragMoveEvent(event); event->acceptProposedAction(); } void ThumbnailView::dropEvent(QDropEvent* event) { const QList urlList = KUrlMimeData::urlsFromMimeData(event->mimeData()); if (urlList.isEmpty()) { return; } QModelIndex destIndex = indexAt(event->pos()); if (destIndex.isValid()) { KFileItem item = fileItemForIndex(destIndex); if (item.isDir()) { QUrl destUrl = item.url(); d->mThumbnailViewHelper->showMenuForUrlDroppedOnDir(this, urlList, destUrl); return; } } d->mThumbnailViewHelper->showMenuForUrlDroppedOnViewport(this, urlList); event->acceptProposedAction(); } void ThumbnailView::keyPressEvent(QKeyEvent* event) { QListView::keyPressEvent(event); if (event->key() == Qt::Key_Return) { const QModelIndex index = selectionModel()->currentIndex(); if (index.isValid() && selectionModel()->selectedIndexes().count() == 1) { emit indexActivated(index); } } } void ThumbnailView::resizeEvent(QResizeEvent* event) { QListView::resizeEvent(event); d->scheduleThumbnailGeneration(); } void ThumbnailView::showEvent(QShowEvent* event) { QListView::showEvent(event); d->scheduleThumbnailGeneration(); QTimer::singleShot(0, this, &ThumbnailView::scrollToSelectedIndex); } void ThumbnailView::wheelEvent(QWheelEvent* event) { // If we don't adjust the single step, the wheel scroll exactly one item up // and down, giving the impression that the items do not move but only // their label changes. // For some reason it is necessary to set the step here: setting it in // setThumbnailSize() does not work //verticalScrollBar()->setSingleStep(d->mThumbnailSize / 5); if (event->modifiers() == Qt::ControlModifier) { int width = d->mThumbnailSize.width() + (event->delta() > 0 ? 1 : -1) * WHEEL_ZOOM_MULTIPLIER; width = qMax(int(MinThumbnailSize), qMin(width, int(MaxThumbnailSize))); setThumbnailWidth(width); } else { QListView::wheelEvent(event); } } void ThumbnailView::mousePressEvent(QMouseEvent* event) { switch (event->button()) { case Qt::ForwardButton: case Qt::BackButton: return; default: QListView::mousePressEvent(event); } } void ThumbnailView::scrollToSelectedIndex() { QModelIndexList list = selectedIndexes(); if (list.count() >= 1) { scrollTo(list.first(), PositionAtCenter); } } void ThumbnailView::selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) { QListView::selectionChanged(selected, deselected); emit selectionChangedSignal(selected, deselected); } void ThumbnailView::scrollContentsBy(int dx, int dy) { QListView::scrollContentsBy(dx, dy); d->scheduleThumbnailGeneration(); } void ThumbnailView::generateThumbnailsForItems() { if (!isVisible() || !model()) { return; } const QRect visibleRect = viewport()->rect(); const int visibleSurface = visibleRect.width() * visibleRect.height(); const QPoint origin = visibleRect.center(); // distance => item QMultiMap itemMap; for (int row = 0; row < model()->rowCount(); ++row) { QModelIndex index = model()->index(row, 0); KFileItem item = fileItemForIndex(index); QUrl url = item.url(); // Filter out remote items if necessary if (!d->mCreateThumbnailsForRemoteUrls && !url.isLocalFile()) { continue; } // Filter out archives MimeTypeUtils::Kind kind = MimeTypeUtils::fileItemKind(item); if (kind == MimeTypeUtils::KIND_ARCHIVE) { continue; } // Immediately update modified items if (d->mDocumentInfoProvider && d->mDocumentInfoProvider->isModified(url)) { d->updateThumbnailForModifiedDocument(index); continue; } // Filter out items which already have a thumbnail ThumbnailForUrl::ConstIterator it = d->mThumbnailForUrl.constFind(url); if (it != d->mThumbnailForUrl.constEnd() && it.value().isGroupPixAdaptedForSize(d->mThumbnailSize.height())) { continue; } // Compute distance int distance; const QRect itemRect = visualRect(index); const qreal itemSurface = itemRect.width() * itemRect.height(); const QRect visibleItemRect = visibleRect.intersected(itemRect); qreal visibleItemFract = 0; if (itemSurface > 0) { visibleItemFract = visibleItemRect.width() * visibleItemRect.height() / itemSurface; } if (visibleItemFract > 0.7) { // Item is visible, order thumbnails from left to right, top to bottom // Distance is computed so that it is between 0 and visibleSurface distance = itemRect.top() * visibleRect.width() + itemRect.left(); // Make sure directory thumbnails are generated after image thumbnails: // Distance is between visibleSurface and 2 * visibleSurface if (kind == MimeTypeUtils::KIND_DIR) { distance = distance + visibleSurface; } } else { // Item is not visible, order thumbnails according to distance // Start at 2 * visibleSurface to ensure invisible thumbnails are // generated *after* visible thumbnails distance = 2 * visibleSurface + (itemRect.center() - origin).manhattanLength(); } // Add the item to our map itemMap.insert(distance, item); // Insert the thumbnail in mThumbnailForUrl, so that // setThumbnail() can find the item to update if (it == d->mThumbnailForUrl.constEnd()) { Thumbnail thumbnail = Thumbnail(QPersistentModelIndex(index), item.time(KFileItem::ModificationTime)); d->mThumbnailForUrl.insert(url, thumbnail); } } if (!itemMap.isEmpty()) { d->appendItemsToThumbnailProvider(itemMap.values()); } } void ThumbnailView::updateThumbnail(const QUrl& url) { const ThumbnailForUrl::Iterator it = d->mThumbnailForUrl.find(url); if (it == d->mThumbnailForUrl.end()) { return; } if (d->mDocumentInfoProvider) { d->updateThumbnailForModifiedDocument(it->mIndex); } else { const KFileItem item = fileItemForIndex(it->mIndex); d->appendItemsToThumbnailProvider(KFileItemList({ item })); } } void ThumbnailView::updateThumbnailBusyState(const QUrl& url, bool busy) { const ThumbnailForUrl::Iterator it = d->mThumbnailForUrl.find(url); if (it == d->mThumbnailForUrl.end()) { return; } QPersistentModelIndex index(it->mIndex); if (busy && !d->mBusyIndexSet.contains(index)) { d->mBusyIndexSet << index; update(index); if (d->mBusyAnimationTimeLine->state() != QTimeLine::Running) { d->mBusyAnimationTimeLine->start(); } } else if (!busy && d->mBusyIndexSet.remove(index)) { update(index); if (d->mBusyIndexSet.isEmpty()) { d->mBusyAnimationTimeLine->stop(); } } } void ThumbnailView::updateBusyIndexes() { Q_FOREACH(const QPersistentModelIndex & index, d->mBusyIndexSet) { update(index); } } QPixmap ThumbnailView::busySequenceCurrentPixmap() const { return d->mBusySequence.frameAt(d->mBusyAnimationTimeLine->currentFrame()); } void ThumbnailView::smoothNextThumbnail() { if (d->mSmoothThumbnailQueue.isEmpty()) { return; } if (d->mThumbnailProvider && d->mThumbnailProvider->isRunning()) { // give mThumbnailProvider priority over smoothing d->mSmoothThumbnailTimer.start(SMOOTH_DELAY); return; } QUrl url = d->mSmoothThumbnailQueue.dequeue(); ThumbnailForUrl::Iterator it = d->mThumbnailForUrl.find(url); GV_RETURN_IF_FAIL2(it != d->mThumbnailForUrl.end(), url << "not in mThumbnailForUrl."); Thumbnail& thumbnail = it.value(); thumbnail.mAdjustedPix = d->scale(thumbnail.mGroupPix, Qt::SmoothTransformation); thumbnail.mRough = false; GV_RETURN_IF_FAIL2(thumbnail.mIndex.isValid(), "index for" << url << "is invalid."); update(thumbnail.mIndex); if (!d->mSmoothThumbnailQueue.isEmpty()) { d->mSmoothThumbnailTimer.start(0); } } void ThumbnailView::reloadThumbnail(const QModelIndex& index) { QUrl url = urlForIndex(index); if (!url.isValid()) { qWarning() << "Invalid url for index" << index; return; } ThumbnailProvider::deleteImageThumbnail(url); ThumbnailForUrl::Iterator it = d->mThumbnailForUrl.find(url); if (it == d->mThumbnailForUrl.end()) { return; } d->mThumbnailForUrl.erase(it); generateThumbnailsForItems(); } void ThumbnailView::setCreateThumbnailsForRemoteUrls(bool createRemoteThumbs) { d->mCreateThumbnailsForRemoteUrls = createRemoteThumbs; } } // namespace diff --git a/lib/thumbnailview/thumbnailview.h b/lib/thumbnailview/thumbnailview.h index 174df877..afee75a2 100644 --- a/lib/thumbnailview/thumbnailview.h +++ b/lib/thumbnailview/thumbnailview.h @@ -1,218 +1,226 @@ /* Gwenview: an image viewer Copyright 2007 Aurélien Gâteau 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef THUMBNAILVIEW_H #define THUMBNAILVIEW_H #include // Qt #include // KDE #include class KFileItem; class QDragEnterEvent; class QDragMoveEvent; class QDropEvent; class QPixmap; +class QGestureEvent; +class QTapGesture; +class QPinchGesture; +class QGesture; namespace Gwenview { class AbstractDocumentInfoProvider; class AbstractThumbnailViewHelper; class ThumbnailProvider; struct ThumbnailViewPrivate; class GWENVIEWLIB_EXPORT ThumbnailView : public QListView { Q_OBJECT public: enum { MinThumbnailSize = 48, MaxThumbnailSize = 512 }; enum ThumbnailScaleMode { ScaleToSquare, ScaleToHeight, ScaleToWidth, ScaleToFit }; explicit ThumbnailView(QWidget* parent); ~ThumbnailView() override; void setThumbnailViewHelper(AbstractThumbnailViewHelper* helper); AbstractThumbnailViewHelper* thumbnailViewHelper() const; void setDocumentInfoProvider(AbstractDocumentInfoProvider* provider); AbstractDocumentInfoProvider* documentInfoProvider() const; ThumbnailScaleMode thumbnailScaleMode() const; void setThumbnailScaleMode(ThumbnailScaleMode); /** * Returns the thumbnail size. */ QSize thumbnailSize() const; /** * Returns the aspect ratio of the thumbnail. */ qreal thumbnailAspectRatio() const; QPixmap thumbnailForIndex(const QModelIndex&, QSize* fullSize = nullptr); /** * Returns true if the document pointed by the index has been modified * inside Gwenview. */ bool isModified(const QModelIndex&) const; /** * Returns true if the document pointed by the index is currently busy * (loading, saving, rotating...) */ bool isBusy(const QModelIndex& index) const; void setModel(QAbstractItemModel* model) override; void setThumbnailProvider(ThumbnailProvider* thumbnailProvider); /** * Publish this method so that delegates can call it. */ using QListView::scheduleDelayedItemsLayout; /** * Returns the current pixmap to paint when drawing a busy index. */ QPixmap busySequenceCurrentPixmap() const; void reloadThumbnail(const QModelIndex&); void updateThumbnailSize(); void setCreateThumbnailsForRemoteUrls(bool createRemoteThumbs); Q_SIGNALS: /** * It seems we can't use the 'activated()' signal for now because it does * not know about KDE single vs doubleclick settings. The indexActivated() * signal replaces it. */ void indexActivated(const QModelIndex&); void urlListDropped(const QList& lst, const QUrl &destination); void thumbnailSizeChanged(const QSize&); void thumbnailWidthChanged(int); /** * Emitted whenever selectionChanged() is called. * This signal is suffixed with "Signal" because * QAbstractItemView::selectionChanged() is a slot. */ void selectionChangedSignal(const QItemSelection&, const QItemSelection&); /** * Forward some signals from model, so that the delegate can use them */ void rowsRemovedSignal(const QModelIndex& parent, int start, int end); void rowsInsertedSignal(const QModelIndex& parent, int start, int end); public Q_SLOTS: /** * Sets the thumbnail's width, in pixels. Keeps aspect ratio unchanged. */ void setThumbnailWidth(int width); /** * Sets the thumbnail's aspect ratio. Keeps width unchanged. */ void setThumbnailAspectRatio(qreal ratio); void scrollToSelectedIndex(); void generateThumbnailsForItems(); protected: void dragEnterEvent(QDragEnterEvent*) override; void dragMoveEvent(QDragMoveEvent*) override; void dropEvent(QDropEvent*) override; void keyPressEvent(QKeyEvent*) override; void resizeEvent(QResizeEvent*) override; void scrollContentsBy(int dx, int dy) override; void showEvent(QShowEvent*) override; void wheelEvent(QWheelEvent*) override; void startDrag(Qt::DropActions) override; void mousePressEvent(QMouseEvent*) override; protected Q_SLOTS: void rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) override; void rowsInserted(const QModelIndex& parent, int start, int end) override; void selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) override; void dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QVector &roles = QVector()) override; private Q_SLOTS: + void startDragFromTouch(const QPoint& pos); + void tapGesture(const QPoint& pos); + void setZoomParameter(); + void zoomGesture(qreal newZoom, const QPoint& pos); void showContextMenu(); void emitIndexActivatedIfNoModifiers(const QModelIndex&); void setThumbnail(const KFileItem&, const QPixmap&, const QSize&, qulonglong fileSize); void setBrokenThumbnail(const KFileItem&); /** * Generate thumbnail for url. */ void updateThumbnail(const QUrl& url); /** * Tells the view the busy state of the document pointed by the url has changed. */ void updateThumbnailBusyState(const QUrl& url, bool); /* * Cause a repaint of all busy indexes */ void updateBusyIndexes(); void smoothNextThumbnail(); private: friend struct ThumbnailViewPrivate; ThumbnailViewPrivate * const d; }; } // namespace #endif /* THUMBNAILVIEW_H */ diff --git a/lib/touch/doubletap.cpp b/lib/touch/doubletap.cpp new file mode 100644 index 00000000..6b838ca5 --- /dev/null +++ b/lib/touch/doubletap.cpp @@ -0,0 +1,127 @@ +/* +Gwenview: an image viewer +Copyright 2019 Steffen Hartleib + +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, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. + +*/ +// Self +#include "doubletap.h" + +// Qt +#include +#include +#include +#include + +// KDE + +// Local +#include "lib/touch/touch_helper.h" + + +namespace Gwenview +{ + +struct DoubleTapRecognizerPrivate +{ + DoubleTapRecognizer* q; + bool mTargetIsGrapicsWidget = false; + qint64 mTouchBeginnTimestamp; + bool mIsOnlyTap; + qint64 mLastTapTimestamp = 0; + qint64 mLastDoupleTapTimestamp = 0; +}; + +DoubleTapRecognizer::DoubleTapRecognizer() : QGestureRecognizer() +, d (new DoubleTapRecognizerPrivate) +{ + d->q = this; +} + +DoubleTapRecognizer::~DoubleTapRecognizer() +{ + delete d; +} + +QGesture* DoubleTapRecognizer::create(QObject*) +{ + return static_cast(new DoubleTap()); +} + +QGestureRecognizer::Result DoubleTapRecognizer::recognize(QGesture* state, QObject* watched, QEvent* event) +{ + //Because of a bug in Qt in a gesture event in a graphicsview, all gestures are trigger twice + //https://bugreports.qt.io/browse/QTBUG-13103 + if (qobject_cast(watched)) d->mTargetIsGrapicsWidget = true; + if (d->mTargetIsGrapicsWidget && watched->isWidgetType()) return Ignore; + + switch (event->type()) { + case QEvent::TouchBegin: { + QTouchEvent* touchEvent = static_cast(event); + d->mTouchBeginnTimestamp = touchEvent->timestamp(); + d->mIsOnlyTap = true; + if (d->mLastDoupleTapTimestamp == 0) d->mLastDoupleTapTimestamp = touchEvent->timestamp() - Touch_Helper::Touch::doubleTapInterval; + state->setHotSpot(touchEvent->touchPoints().first().screenPos()); + return MayBeGesture; + } + + case QEvent::TouchUpdate: { + QTouchEvent* touchEvent = static_cast(event); + const qint64 now = touchEvent->timestamp(); + state->setHotSpot(touchEvent->touchPoints().first().screenPos()); + + if (d->mIsOnlyTap && now - d->mTouchBeginnTimestamp < Touch_Helper::Touch::maxTimeForTap && Touch_Helper::touchStationary(event)) { + d->mIsOnlyTap = true; + return MayBeGesture; + } else { + d->mIsOnlyTap = false; + return CancelGesture; + } + break; + } + + case QEvent::TouchEnd: { + QTouchEvent* touchEvent = static_cast(event); + const qint64 now = touchEvent->timestamp(); + + if (now - d->mLastTapTimestamp <= Touch_Helper::Touch::doubleTapInterval && d->mIsOnlyTap) { + //Interval between two double tap gesture need to be bigger than Touch_Helper::Touch::doupleTapIntervall, + //to suppress fast successively double tap gestures + if (now - d->mLastDoupleTapTimestamp > Touch_Helper::Touch::doubleTapInterval) { + d->mLastTapTimestamp = 0; + state->setHotSpot(touchEvent->touchPoints().first().screenPos()); + d->mLastDoupleTapTimestamp = now; + return FinishGesture; + } + } + + if (d->mIsOnlyTap) d->mLastTapTimestamp = now; + + break; + } + + default: + return Ignore; + } + return Ignore; +} + +DoubleTap::DoubleTap(QObject* parent) +: QGesture(parent) +{ +} + +} // namespace diff --git a/lib/touch/doubletap.h b/lib/touch/doubletap.h new file mode 100644 index 00000000..4541b553 --- /dev/null +++ b/lib/touch/doubletap.h @@ -0,0 +1,59 @@ +/* +Gwenview: an image viewer +Copyright 2019 Steffen Hartleib + +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, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. + +*/ +#ifndef DOUBLETAP_H +#define DOUBLETAP_H + +#include +// Qt +#include +#include + +// KDE + +// Local + +namespace Gwenview +{ +struct DoubleTapRecognizerPrivate; + +class GWENVIEWLIB_EXPORT DoubleTap : public QGesture +{ + Q_PROPERTY(QPointF pos READ pos WRITE pos) +public: + explicit DoubleTap(QObject* parent = 0); +private: + QPointF pos; +}; + +class GWENVIEWLIB_EXPORT DoubleTapRecognizer : public QGestureRecognizer +{ +public: + explicit DoubleTapRecognizer(); + ~DoubleTapRecognizer(); +private: + DoubleTapRecognizerPrivate* d; + + virtual QGesture* create(QObject*) override; + virtual Result recognize(QGesture*, QObject*, QEvent*) override; + +}; + +} // namespace +#endif /* DOUBLETAP_H */ diff --git a/lib/touch/oneandtwofingerswipe.cpp b/lib/touch/oneandtwofingerswipe.cpp new file mode 100644 index 00000000..74d9dc87 --- /dev/null +++ b/lib/touch/oneandtwofingerswipe.cpp @@ -0,0 +1,123 @@ +/* +Gwenview: an image viewer +Copyright 2019 Steffen Hartleib + +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, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. + +*/ +// Self +#include "oneandtwofingerswipe.h" + +// Qt +#include +#include +#include + +// KDE + +// Local +#include "lib/touch/touch_helper.h" + +namespace Gwenview +{ + +struct OneAndTwoFingerSwipeRecognizerPrivate +{ + OneAndTwoFingerSwipeRecognizer* q; + bool mTargetIsGrapicsWidget = false; + qint64 mTouchBeginnTimestamp; + bool mGestureAlreadyTriggered; +}; + +OneAndTwoFingerSwipeRecognizer::OneAndTwoFingerSwipeRecognizer() : QGestureRecognizer() +, d (new OneAndTwoFingerSwipeRecognizerPrivate) +{ + d->q = this; +} + +OneAndTwoFingerSwipeRecognizer::~OneAndTwoFingerSwipeRecognizer() +{ + delete d; +} + +QGesture* OneAndTwoFingerSwipeRecognizer::create(QObject*) +{ + return static_cast(new OneAndTwoFingerSwipe()); +} + +QGestureRecognizer::Result OneAndTwoFingerSwipeRecognizer::recognize(QGesture* state, QObject* watched, QEvent* event) +{ + //Because of a bug in Qt in a gesture event in a graphicsview, all gestures are trigger twice + //https://bugreports.qt.io/browse/QTBUG-13103 + if (qobject_cast(watched)) d->mTargetIsGrapicsWidget = true; + if (d->mTargetIsGrapicsWidget && watched->isWidgetType()) return Ignore; + + switch (event->type()) { + case QEvent::TouchBegin: { + QTouchEvent* touchEvent = static_cast(event); + d->mTouchBeginnTimestamp = touchEvent->timestamp(); + d->mGestureAlreadyTriggered = false; + state->setHotSpot(touchEvent->touchPoints().first().screenPos()); + return MayBeGesture; + } + + case QEvent::TouchUpdate: { + QTouchEvent* touchEvent = static_cast(event); + const qint64 now = touchEvent->timestamp(); + const QPointF distance = touchEvent->touchPoints().first().startPos() - touchEvent->touchPoints().first().pos(); + state->setHotSpot(touchEvent->touchPoints().first().screenPos()); + + if (touchEvent->touchPoints().size() >> 2) { + d->mGestureAlreadyTriggered = false; + return CancelGesture; + } + + if (distance.manhattanLength() >= Touch_Helper::Touch::minDistanceForSwipe && + (now - d->mTouchBeginnTimestamp) <= Touch_Helper::Touch::maxTimeFrameForSwipe && + !d->mGestureAlreadyTriggered) { + if (distance.x() < 0 && abs(distance.x()) >= abs(distance.y()) * 2) { + state->setProperty("right", true); + state->setProperty("left", false); + d->mGestureAlreadyTriggered = true; + return FinishGesture; + } + if (distance.x() > 0 && abs(distance.x()) >= abs(distance.y()) * 2) { + state->setProperty("right", false); + state->setProperty("left", true); + d->mGestureAlreadyTriggered = true; + return FinishGesture; + } + if ((now - d->mTouchBeginnTimestamp) <= Touch_Helper::Touch::maxTimeFrameForSwipe && !d->mGestureAlreadyTriggered) { + return MayBeGesture; + } else { + d->mGestureAlreadyTriggered = false; + return CancelGesture; + } + } + break; + } + + default: + return Ignore; + } + return Ignore; +} + +OneAndTwoFingerSwipe::OneAndTwoFingerSwipe(QObject* parent) +: QGesture(parent) +{ +} + +} // namespace diff --git a/lib/touch/oneandtwofingerswipe.h b/lib/touch/oneandtwofingerswipe.h new file mode 100644 index 00000000..91aadb27 --- /dev/null +++ b/lib/touch/oneandtwofingerswipe.h @@ -0,0 +1,62 @@ +/* +Gwenview: an image viewer +Copyright 2019 Steffen Hartleib + +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, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. + +*/ +#ifndef ONEANDTWOFINGERSWIPE_H +#define ONEANDTWOFINGERSWIPE_H + +#include +// Qt +#include +#include + +// KDE + +// Local + +namespace Gwenview +{ +struct OneAndTwoFingerSwipeRecognizerPrivate; + +class GWENVIEWLIB_EXPORT OneAndTwoFingerSwipe : public QGesture +{ + Q_PROPERTY(bool left READ left WRITE left) + Q_PROPERTY(bool right READ right WRITE right) + +public: + explicit OneAndTwoFingerSwipe(QObject* parent = 0); + +private: + bool left; + bool right; +}; + +class GWENVIEWLIB_EXPORT OneAndTwoFingerSwipeRecognizer : public QGestureRecognizer +{ +public: + explicit OneAndTwoFingerSwipeRecognizer(); + ~OneAndTwoFingerSwipeRecognizer(); +private: + OneAndTwoFingerSwipeRecognizerPrivate* d; + + virtual QGesture* create(QObject*) override; + virtual Result recognize(QGesture*, QObject*, QEvent*) override; +}; + +} // namespace +#endif /* ONEANDTWOFINGERSWIPE_H */ diff --git a/lib/touch/tapholdandmoving.cpp b/lib/touch/tapholdandmoving.cpp new file mode 100644 index 00000000..50d0e2aa --- /dev/null +++ b/lib/touch/tapholdandmoving.cpp @@ -0,0 +1,129 @@ +/* +Gwenview: an image viewer +Copyright 2019 Steffen Hartleib + +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, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. + +*/ +// Self +#include "tapholdandmoving.h" + +// Qt +#include +#include +#include + +// KDE + +// Local +#include "lib/touch/touch_helper.h" + +namespace Gwenview +{ + +struct TapHoldAndMovingRecognizerPrivate +{ + TapHoldAndMovingRecognizer* q; + bool mTargetIsGrapicsWidget = false; + qint64 mTouchBeginnTimestamp; + bool mTouchPointStationary; + bool mGestureTriggered; + Qt::GestureState mLastGestureState = Qt::NoGesture; +}; + +TapHoldAndMovingRecognizer::TapHoldAndMovingRecognizer() : QGestureRecognizer() +, d (new TapHoldAndMovingRecognizerPrivate) +{ + d->q = this; +} + +TapHoldAndMovingRecognizer::~TapHoldAndMovingRecognizer() +{ + delete d; +} + +QGesture* TapHoldAndMovingRecognizer::create(QObject*) +{ + return static_cast(new TapHoldAndMoving()); +} + +QGestureRecognizer::Result TapHoldAndMovingRecognizer::recognize(QGesture* state, QObject* watched, QEvent* event) +{ + //Because of a bug in Qt in a gesture event in a graphicsview, all gestures are trigger twice + //https://bugreports.qt.io/browse/QTBUG-13103 + if (qobject_cast(watched)) d->mTargetIsGrapicsWidget = true; + if (d->mTargetIsGrapicsWidget && watched->isWidgetType()) return Ignore; + + switch (event->type()) { + case QEvent::TouchBegin: { + QTouchEvent* touchEvent = static_cast(event); + d->mTouchBeginnTimestamp = touchEvent->timestamp(); + d->mGestureTriggered = false; + d->mTouchPointStationary = true; + state->setHotSpot(touchEvent->touchPoints().first().screenPos()); + d->mLastGestureState = Qt::NoGesture; + return MayBeGesture; + } + + case QEvent::TouchUpdate: { + QTouchEvent* touchEvent = static_cast(event); + const qint64 now = touchEvent->timestamp(); + const QPoint pos = touchEvent->touchPoints().first().pos().toPoint(); + state->setHotSpot(touchEvent->touchPoints().first().screenPos()); + + if (touchEvent->touchPoints().size() >> 1) { + d->mGestureTriggered = false; + d->mLastGestureState = Qt::GestureCanceled; + return CancelGesture; + } + + if (touchEvent->touchPoints().size() == 1 && d->mLastGestureState != Qt::GestureCanceled) { + if (!d->mGestureTriggered && + d->mTouchPointStationary && + now - d->mTouchBeginnTimestamp >= Touch_Helper::Touch::durationForTapHold) { + d->mGestureTriggered = true; + } + } + d->mTouchPointStationary = Touch_Helper::touchStationary(event); + + if (d->mGestureTriggered && d->mLastGestureState != Qt::GestureCanceled) { + state->setProperty("pos", pos); + d->mLastGestureState = Qt::GestureStarted; + return TriggerGesture; + } + break; + } + + case QEvent::TouchEnd: { + QTouchEvent* touchEvent = static_cast(event); + state->setHotSpot(touchEvent->touchPoints().first().screenPos()); + if (d->mGestureTriggered) { + d->mLastGestureState = Qt::GestureFinished; + return FinishGesture; + } + break; + } + default: + return Ignore; + } + return Ignore; +} + +TapHoldAndMoving::TapHoldAndMoving(QObject* parent) +: QGesture(parent) +{ +} + +} // namespace diff --git a/lib/touch/tapholdandmoving.h b/lib/touch/tapholdandmoving.h new file mode 100644 index 00000000..c70e5b8c --- /dev/null +++ b/lib/touch/tapholdandmoving.h @@ -0,0 +1,63 @@ +/* +Gwenview: an image viewer +Copyright 2019 Steffen Hartleib + +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, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. + +*/ +#ifndef TAPHOLDANDMOVING_H +#define TAPHOLDANDMOVING_H + +#include +// Qt +#include +#include + +// KDE + +// Local + +namespace Gwenview +{ +struct TapHoldAndMovingRecognizerPrivate; + +class GWENVIEWLIB_EXPORT TapHoldAndMoving : public QGesture +{ + Q_PROPERTY(QPoint pos READ getPos WRITE setPos) + +public: + explicit TapHoldAndMoving(QObject* parent = 0); + QPoint getPos() {return pos;}; + void setPos(QPoint _pos) {pos = _pos;}; + +private: + QPoint pos; +}; + +class GWENVIEWLIB_EXPORT TapHoldAndMovingRecognizer : public QGestureRecognizer +{ +public: + explicit TapHoldAndMovingRecognizer(); + ~TapHoldAndMovingRecognizer(); +private: + TapHoldAndMovingRecognizerPrivate* d; + + virtual QGesture* create(QObject* target) override; + virtual Result recognize(QGesture* state, QObject* watched, QEvent* event) override; + +}; + +} // namespace +#endif /* TAPHOLDANDMOVING_H */ diff --git a/lib/touch/touch.cpp b/lib/touch/touch.cpp new file mode 100644 index 00000000..69f2630a --- /dev/null +++ b/lib/touch/touch.cpp @@ -0,0 +1,464 @@ +/* +Gwenview: an image viewer +Copyright 2019 Steffen Hartleib + +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, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. + +*/ +// Self +#include "touch.h" + +// Qt +#include +#include +#include +#include +#include +#include +#include +#include "touch_helper.h" + + +// KDE + +// Local + +namespace Gwenview +{ + +struct TouchPrivate +{ + Touch* q; + QObject* mTarget; + Qt::GestureState mLastPanGestureState; + QPointF mLastTapPos; + bool mTabHoldandMovingGestureActive; + qreal mStartZoom; + qreal mZoomModifier; + qreal mRotationThreshold; + + TapHoldAndMovingRecognizer* mTapHoldAndMovingRecognizer; + Qt::GestureType mTapHoldAndMoving; + TwoFingerPanRecognizer* mTwoFingerPanRecognizer; + Qt::GestureType mTwoFingerPan; + OneAndTwoFingerSwipeRecognizer* mOneAndTwoFingerSwipeRecognizer; + Qt::GestureType mOneAndTwoFingerSwipe; + DoubleTapRecognizer* mDoubleTapRecognizer; + Qt::GestureType mDoubleTap; + TwoFingerTapRecognizer* mTwoFingerTapRecognizer; + Qt::GestureType mTwoFingerTap; +}; + +Touch::Touch(QObject* target) +: QObject () +, d(new TouchPrivate) +{ + d->q = this; + d->mTarget = target; + + d->mTapHoldAndMovingRecognizer = new TapHoldAndMovingRecognizer(); + d->mTapHoldAndMoving = QGestureRecognizer::registerRecognizer(d->mTapHoldAndMovingRecognizer); + + d->mTwoFingerPanRecognizer = new TwoFingerPanRecognizer(); + d->mTwoFingerPan = QGestureRecognizer::registerRecognizer(d->mTwoFingerPanRecognizer); + + d->mTwoFingerTapRecognizer = new TwoFingerTapRecognizer(); + d->mTwoFingerTap = QGestureRecognizer::registerRecognizer(d->mTwoFingerTapRecognizer); + + d->mOneAndTwoFingerSwipeRecognizer = new OneAndTwoFingerSwipeRecognizer(); + d->mOneAndTwoFingerSwipe = QGestureRecognizer::registerRecognizer(d->mOneAndTwoFingerSwipeRecognizer); + + d->mDoubleTapRecognizer = new DoubleTapRecognizer(); + d->mDoubleTap = QGestureRecognizer::registerRecognizer(d->mDoubleTapRecognizer); + + if (qobject_cast(target)) { + QGraphicsWidget* widgetTarget = qobject_cast(target); + widgetTarget->grabGesture(d->mOneAndTwoFingerSwipe); + widgetTarget->grabGesture(d->mDoubleTap); + widgetTarget->grabGesture(Qt::TapGesture); + widgetTarget->grabGesture(Qt::PinchGesture); + widgetTarget->grabGesture(d->mTwoFingerTap); + widgetTarget->grabGesture(d->mTwoFingerPan); + widgetTarget->grabGesture(d->mTapHoldAndMoving); + } else if (qobject_cast(target)) { + QWidget* widgetTarget = qobject_cast(target); + widgetTarget->grabGesture(Qt::TapGesture); + widgetTarget->grabGesture(Qt::PinchGesture); + widgetTarget->grabGesture(d->mTwoFingerTap); + widgetTarget->grabGesture(d->mTwoFingerPan); + widgetTarget->grabGesture(d->mTapHoldAndMoving); + } + target->installEventFilter(this); +} + +Touch::~Touch() +{ + delete d; +} + +bool Touch::eventFilter(QObject*, QEvent* event) +{ + if (event->type() == QEvent::TouchBegin) { + //move mouse cursor to touchpoint + const QPoint pos = Touch_Helper::simpleTouchPosition(event); + touchToMouseMove(pos, event, Qt::NoButton); + return true; + } + if (event->type() == QEvent::TouchUpdate) { + QTouchEvent* touchEvent = static_cast(event); + //because we suppress the making of mouse event through Qt, we need to make our own one finger panning + //but only if no TapHoldandMovingGesture is active (Drag and Drop action) + if (touchEvent->touchPoints().size() == 1 && !getTapHoldandMovingGestureActive()) { + const QPointF delta = touchEvent->touchPoints().first().lastPos() - touchEvent->touchPoints().first().pos(); + emit PanTriggered(delta); + } + return true; + } + if (event->type() == QEvent::Gesture) { + gestureEvent(static_cast(event)); + } + return false; +} + +bool Touch::gestureEvent(QGestureEvent* event) +{ + bool ret = false; + + if (checkTwoFingerTapGesture(event)) { + ret = true; + } + + if (checkPinchGesture(event)) { + ret = true; + emit pinchZoomTriggered(getZoomFromPinchGesture(event), positionGesture(event)); + emit pinchRotateTriggered(getRotationFromPinchGesture(event)); + } + + if (checkTapGesture(event)) { + ret = true; + if (event->widget()) { + touchToMouseClick(positionGesture(event), event->widget()); + } + emit tapTriggered(positionGesture(event)); + } + + if (checkTapHoldAndMovingGesture(event, d->mTarget)) { + ret = true; + emit tapHoldAndMovingTriggered(positionGesture(event)); + } + + if (checkDoubleTapGesture(event)) { + ret = true; + } + + if (checkOneAndTwoFingerSwipeGesture(event)) { + ret = true; + } + + checkTwoFingerPanGesture(event); + + return ret; +} + +void Touch::setZoomParameter(qreal modifier, qreal startZoom) +{ + d->mZoomModifier = modifier; + d->mStartZoom = startZoom; +} + +void Touch::setRotationThreshold(qreal rotationThreshold) +{ + d->mRotationThreshold = rotationThreshold; +} + +qreal Touch::getRotationFromPinchGesture(QGestureEvent* event) +{ + const QPinchGesture* pinch = static_cast(event->gesture(Qt::PinchGesture)); + static qreal lastRotationAngel; + if (pinch) { + if (pinch->state() == Qt::GestureStarted) { + lastRotationAngel = 0; + return 0.0; + } + if (pinch->state() == Qt::GestureUpdated) { + const qreal rotationDelta = pinch->rotationAngle() - pinch->lastRotationAngle(); + //very low and high changes in the rotation are suspect, so we ignore them + if (abs(rotationDelta) <= 1.5 || abs(rotationDelta) >= 30) { + return 0.0; + } + lastRotationAngel += rotationDelta; + const qreal ret = lastRotationAngel; + if (abs(lastRotationAngel) > d->mRotationThreshold) { + lastRotationAngel = 0; + return ret; + } else { + return 0.0; + } + } + } + return 0.0; +} + +qreal Touch::getZoomFromPinchGesture(QGestureEvent* event) +{ + static qreal lastZoom; + const QPinchGesture* pinch = static_cast(event->gesture(Qt::PinchGesture)); + if (pinch) { + if (pinch->state() == Qt::GestureStarted) { + lastZoom = d->mStartZoom; + return -1; + } + if (pinch->state() == Qt::GestureUpdated) { + lastZoom = calculateZoom(pinch->scaleFactor(), d->mZoomModifier) * lastZoom; + return lastZoom; + } + } + return -1; +} + +qreal Touch::calculateZoom(qreal scale, qreal modifier) +{ + return ((scale - 1.0) * modifier) + 1.0; +} + +QPoint Touch::positionGesture(QGestureEvent* event) +{ + //return the position or the center point for follow gestures: QTapGesture, TabHoldAndMovingGesture and PinchGesture; + QPoint position = QPoint(-1, -1); + if (QTapGesture* tap = static_cast(event->gesture(Qt::TapGesture))) { + position = tap->position().toPoint(); + } else if (QGesture* gesture = event->gesture(getTapHoldandMovingGesture())) { + position = gesture->property("pos").toPoint(); + } else if (QPinchGesture* pinch = static_cast(event->gesture(Qt::PinchGesture))) { + if (qobject_cast(d->mTarget)) { + QGraphicsWidget* widget = qobject_cast(d->mTarget); + position = widget->mapFromScene(event->mapToGraphicsScene(pinch->centerPoint())).toPoint(); + } else { + position = pinch->centerPoint().toPoint(); + } + } + return position; +} + +bool Touch::checkTwoFingerPanGesture(QGestureEvent* event) +{ + if (QGesture* gesture = event->gesture(getTwoFingerPanGesture())) { + event->accept(); + setPanGestureState(event); + if (gesture->state() == Qt::GestureUpdated) { + const QPoint diff = gesture->property("delta").toPoint(); + emit PanTriggered(diff); + return true; + } + } + return false; +} + +bool Touch::checkOneAndTwoFingerSwipeGesture(QGestureEvent* event) +{ + if (QGesture* gesture = event->gesture(getOneAndTwoFingerSwipeGesture())) { + event->accept(); + if (gesture->state() == Qt::GestureFinished) { + if (gesture->property("right").toBool()) { + emit swipeRightTriggered(); + return true; + } else if (gesture->property("left").toBool()) { + emit swipeLeftTriggered(); + return true; + } + } + } + return false; +} + + +bool Touch::checkTapGesture(QGestureEvent* event) +{ + const QTapGesture* tap = static_cast(event->gesture(Qt::TapGesture)); + if (tap) { + event->accept(); + if (tap->state() == Qt::GestureFinished) return true; + } + return false; +} + +bool Touch::checkDoubleTapGesture(QGestureEvent* event) +{ + if (QGesture* gesture = event->gesture(getDoubleTapGesture())) { + event->accept(); + if (gesture->state() == Qt::GestureFinished) { + emit doubleTapTriggered(); + return true; + } + } + return false; +} + +bool Touch::checkTwoFingerTapGesture(QGestureEvent* event) +{ + if (QGesture* twoFingerTap = event->gesture(getTwoFingerTapGesture())) { + event->accept(); + if (twoFingerTap->state() == Qt::GestureFinished) { + emit twoFingerTapTriggered(); + return true; + } + } + return false; +} + +bool Touch::checkTapHoldAndMovingGesture(QGestureEvent* event, QObject* target) +{ + if (QGesture* tapHoldAndMoving = event->gesture(getTapHoldandMovingGesture())) { + event->accept(); + const QPoint pos = tapHoldAndMoving->property("pos").toPoint(); + switch (tapHoldAndMoving->state()) { + case Qt::GestureStarted: { + setTapHoldandMovingGestureActive(true); + return true; + } + case Qt::GestureUpdated: { + touchToMouseMove(pos, target, Qt::LeftButton); + break; + } + case Qt::GestureCanceled: + case Qt::GestureFinished: { + touchToMouseRelease(pos, target); + setTapHoldandMovingGestureActive(false); + break; + } + default: + break; + } + } + return false; +} + +bool Touch::checkPinchGesture(QGestureEvent* event) +{ + static qreal lastScaleFactor; + const QPinchGesture* pinch = static_cast(event->gesture(Qt::PinchGesture)); + if (pinch) { + //we don't want a pinch gesture, if a pan gesture is active + //only exception is, if the pinch gesture state is Qt::GestureStarted + if (getLastPanGestureState() == Qt::GestureCanceled || pinch->state() == Qt::GestureStarted) { + event->accept(); + if (pinch->state() == Qt::GestureStarted) { + lastScaleFactor = 0; + emit pinchGestureStarted(); + } else if (pinch->state() == Qt::GestureUpdated) { + //Because of a bug in Qt in a gesture event in a graphicsview, all gestures are trigger twice + //https://bugreports.qt.io/browse/QTBUG-13103 + //the duplicate events have the same scaleFactor, so I ignore them + if (lastScaleFactor == pinch->scaleFactor()) { + return false; + } else { + lastScaleFactor = pinch->scaleFactor(); + } + } + return true; + } + } + return false; +} + +void Touch::touchToMouseRelease(QPoint pos, QObject* receiver) +{ + touchToMouseEvent(pos, receiver, QEvent::MouseButtonRelease, Qt::LeftButton, Qt::LeftButton); +} + +void Touch::touchToMouseMove(QPoint pos, QEvent* event, Qt::MouseButton button) +{ + if (QTouchEvent* touchEvent = static_cast(event)) { + touchToMouseEvent(pos, touchEvent->target(), QEvent::MouseMove, button, button); + } +} + +void Touch::touchToMouseMove(QPoint pos, QObject* receiver, Qt::MouseButton button) +{ + touchToMouseEvent(pos, receiver, QEvent::MouseMove, button, button); +} + + +void Touch::touchToMouseClick (QPoint pos, QObject* receiver) +{ + touchToMouseEvent(pos, receiver, QEvent::MouseButtonPress, Qt::LeftButton, Qt::LeftButton); + touchToMouseEvent(pos, receiver, QEvent::MouseButtonRelease, Qt::LeftButton, Qt::LeftButton); +} + +void Touch::touchToMouseEvent (QPoint pos, QObject* receiver, QEvent::Type type, Qt::MouseButton button, Qt::MouseButtons buttons) +{ + QMouseEvent* evt = new QMouseEvent(type, pos, button, buttons, Qt::NoModifier); + QCoreApplication::postEvent(receiver, evt); +} + +Qt::GestureState Touch::getLastPanGestureState() +{ + return d->mLastPanGestureState;; +} + +void Touch::setPanGestureState(QGestureEvent* event) +{ + if (QGesture* panGesture = event->gesture(getTwoFingerPanGesture())) { + d->mLastPanGestureState = panGesture->state(); + } + return; +} + +QPointF Touch::getLastTapPos() +{ + return d->mLastTapPos; +} + +void Touch::setLastTapPos(QPointF pos) +{ + d->mLastTapPos = pos; +} + +Qt::GestureType Touch::getTapHoldandMovingGesture() +{ + return d->mTapHoldAndMoving; +} + +Qt::GestureType Touch::getTwoFingerPanGesture() +{ + return d->mTwoFingerPan; +} + +Qt::GestureType Touch::getOneAndTwoFingerSwipeGesture() +{ + return d->mOneAndTwoFingerSwipe; +} + +Qt::GestureType Touch::getDoubleTapGesture() +{ + return d->mDoubleTap; +} + +Qt::GestureType Touch::getTwoFingerTapGesture() +{ + return d->mTwoFingerTap; +} + +bool Touch::getTapHoldandMovingGestureActive() +{ + return d->mTabHoldandMovingGestureActive; +} + +void Touch::setTapHoldandMovingGestureActive(bool active) +{ + d->mTabHoldandMovingGestureActive = active; +} + +} // namespace diff --git a/lib/touch/touch.h b/lib/touch/touch.h new file mode 100644 index 00000000..670dacb2 --- /dev/null +++ b/lib/touch/touch.h @@ -0,0 +1,99 @@ +/* +Gwenview: an image viewer +Copyright 2019 Steffen Hartleib + +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, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. + +*/ +#ifndef TOUCH_H +#define TOUCH_H + +#include +// Qt +#include + +// KDE + +// Local +#include "lib/touch/tapholdandmoving.h" +#include "lib/touch/twofingerpan.h" +#include "lib/touch/oneandtwofingerswipe.h" +#include "lib/touch/doubletap.h" +#include "lib/touch/twofingertap.h" + +namespace Gwenview +{ + +struct TouchPrivate; +class GWENVIEWLIB_EXPORT Touch : public QObject +{ + Q_OBJECT +public: + Touch(QObject* target); + ~Touch() override; + void setZoomParameter (qreal, qreal); + void setRotationThreshold (qreal); + qreal getRotationFromPinchGesture(QGestureEvent*); + qreal getZoomFromPinchGesture(QGestureEvent*); + QPoint positionGesture(QGestureEvent*); + bool checkTwoFingerPanGesture(QGestureEvent*); + bool checkOneAndTwoFingerSwipeGesture(QGestureEvent*); + bool checkTapGesture(QGestureEvent*); + bool checkDoubleTapGesture(QGestureEvent*); + bool checkTwoFingerTapGesture(QGestureEvent*); + bool checkTapHoldAndMovingGesture(QGestureEvent*, QObject*); + bool checkPinchGesture(QGestureEvent*); + void touchToMouseRelease(QPoint, QObject*); + void touchToMouseMove(QPoint, QEvent*, Qt::MouseButton); + void touchToMouseMove(QPoint, QObject*, Qt::MouseButton); + void touchToMouseClick(QPoint, QObject*); + void setPanGestureState ( QGestureEvent* event ); + Qt::GestureState getLastPanGestureState(); + QPointF getLastTapPos(); + void setLastTapPos(QPointF); + bool getTapHoldandMovingGestureActive(); + void setTapHoldandMovingGestureActive(bool); + + Qt::GestureType getTapHoldandMovingGesture(); + Qt::GestureType getTwoFingerPanGesture(); + Qt::GestureType getOneAndTwoFingerSwipeGesture(); + Qt::GestureType getDoubleTapGesture(); + Qt::GestureType getTwoFingerTapGesture(); + +protected: + bool eventFilter(QObject*, QEvent*) override; + + +signals: + void PanTriggered(const QPointF&); + void swipeLeftTriggered(); + void swipeRightTriggered(); + void doubleTapTriggered(); + void tapHoldAndMovingTriggered(const QPoint&); + void tapTriggered(const QPoint&); + void pinchGestureStarted(); + void twoFingerTapTriggered(); + void pinchZoomTriggered(qreal, const QPoint&); + void pinchRotateTriggered(qreal); + +private: + qreal calculateZoom (qreal, qreal); + void touchToMouseEvent(QPoint, QObject*, QEvent::Type, Qt::MouseButton, Qt::MouseButtons); + bool gestureEvent(QGestureEvent*); + TouchPrivate* const d; +}; + +} // namespace +#endif /* TOUCH_H */ diff --git a/lib/touch/touch_helper.cpp b/lib/touch/touch_helper.cpp new file mode 100644 index 00000000..8b226a35 --- /dev/null +++ b/lib/touch/touch_helper.cpp @@ -0,0 +1,69 @@ +/* +Gwenview: an image viewer +Copyright 2019 Steffen Hartleib + +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, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +*/ +#include "touch_helper.h" + +// Qt +#include +#include +#include +#include +#include +#include + + +namespace Gwenview +{ +namespace Touch_Helper +{ + +QPoint simpleTapPosition (QEvent* event) +{ + if (event->type() == QEvent::TouchEnd) { + event->accept(); + if (touchStationary(event)) { + return simpleTouchPosition(event); + } + } + return QPoint(-1, -1); +} + +QPoint simpleTouchPosition(QEvent* event, int at) +{ + if (QTouchEvent* touchEvent = static_cast(event)) { + if (touchEvent->touchPoints().size() > at) { + return touchEvent->touchPoints().at(at).pos().toPoint(); + } + } + return QPoint(-1, -1); +} + +bool touchStationary(QEvent* event) +{ + if (QTouchEvent* touchEvent = static_cast(event)) { + const QPointF distance = touchEvent->touchPoints().first().startPos() - touchEvent->touchPoints().first().pos(); + if (distance.manhattanLength() <= Touch::wiggleRoomForTap) { + return true; + } + } + return false; +} + +} // namespace +} // namespace diff --git a/lib/touch/touch_helper.h b/lib/touch/touch_helper.h new file mode 100644 index 00000000..eca3a21d --- /dev/null +++ b/lib/touch/touch_helper.h @@ -0,0 +1,67 @@ +/* +Gwenview: an image viewer +Copyright 2019 Steffen Hartleib + +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 detai +ls. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. + +*/ +#ifndef TOUCH_HELPER_H +#define TOUCH_HELPER_H + +// Qt +#include +// KDE + +// Local +#include + +class QPoint; +class QPointF; +class QEvent; + +namespace Gwenview +{ + +namespace Touch_Helper +{ + +//constant variables that define touch behavior +struct Touch +{ + //a little delay in the begin of the gesture, to get more data of the touch points moving so the recognizing + //of the pan gesture is better + static const int gestureDelay = 110; + //this defines how much a touch point can move, for a single tap gesture + static const int wiggleRoomForTap = 10; + //how long must a touch point be stationary, before he can move for a TabHoldAndMoving gesture + static const int durationForTapHold = 400; + //in what time and how far must the touch point moving to trigger a swipe gesture + static const int maxTimeFrameForSwipe = 100; + static const int minDistanceForSwipe = 70; + //How long is the duration for a simple tap gesture + static const int maxTimeForTap = 100; + //max interval for a double tap gesture + static const int doubleTapInterval = 400; +}; + +GWENVIEWLIB_EXPORT QPoint simpleTapPosition(QEvent*); +GWENVIEWLIB_EXPORT QPoint simpleTouchPosition(QEvent*,int = 0); +GWENVIEWLIB_EXPORT bool touchStationary(QEvent*); + +} // namespace +} // namespace + +#endif /* TOUCH_HELPER_H */ diff --git a/lib/touch/twofingerpan.cpp b/lib/touch/twofingerpan.cpp new file mode 100644 index 00000000..72652c7a --- /dev/null +++ b/lib/touch/twofingerpan.cpp @@ -0,0 +1,165 @@ +/* +Gwenview: an image viewer +Copyright 2019 Steffen Hartleib + +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, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. + +*/ +// Self +#include "twofingerpan.h" + +// Qt +#include +#include +#include +#include + +// KDE + +// Local +#include "lib/touch/touch_helper.h" + +namespace Gwenview +{ + +struct TwoFingerPanRecognizerPrivate +{ + TwoFingerPanRecognizer* q; + bool mTargetIsGrapicsWidget = false; + qint64 mTouchBeginnTimestamp; + bool mGestureTriggered; + qint64 mLastTouchTimestamp; +}; + +TwoFingerPanRecognizer::TwoFingerPanRecognizer() : QGestureRecognizer() + , d (new TwoFingerPanRecognizerPrivate) +{ + d->q = this; +} + +TwoFingerPanRecognizer::~TwoFingerPanRecognizer() +{ + delete d; +} + +QGesture* TwoFingerPanRecognizer::create(QObject*) +{ + return static_cast(new TwoFingerPan()); +} + +QGestureRecognizer::Result TwoFingerPanRecognizer::recognize(QGesture* state, QObject* watched, QEvent* event) +{ + //Because of a bug in Qt in a gesture event in a graphicsview, all gestures are trigger twice + //https://bugreports.qt.io/browse/QTBUG-13103 + if (qobject_cast(watched)) d->mTargetIsGrapicsWidget = true; + if (d->mTargetIsGrapicsWidget && watched->isWidgetType()) return Ignore; + + switch (event->type()) { + case QEvent::TouchBegin: { + QTouchEvent* touchEvent = static_cast(event); + d->mTouchBeginnTimestamp = touchEvent->timestamp(); + d->mLastTouchTimestamp = touchEvent->timestamp(); + d->mGestureTriggered = false; + state->setProperty("delayActive", true); + state->setHotSpot(touchEvent->touchPoints().first().screenPos()); + return TriggerGesture; + } + + case QEvent::TouchUpdate: { + QTouchEvent* touchEvent = static_cast(event); + const qint64 now = touchEvent->timestamp(); + const QPointF pos = touchEvent->touchPoints().first().pos(); + state->setHotSpot(touchEvent->touchPoints().first().screenPos()); + + if (touchEvent->touchPoints().size() >> 2) { + if (d->mGestureTriggered) { + d->mGestureTriggered = false; + } + return CancelGesture; + } + + if (touchEvent->touchPoints().size() == 2) { + if (touchEvent->touchPointStates() & Qt::TouchPointReleased) { + if (d->mGestureTriggered) { + d->mGestureTriggered = false; + return FinishGesture; + } + } + + if (now - d->mTouchBeginnTimestamp >= Touch_Helper::Touch::gestureDelay) { + state ->setProperty("delayActive", false); + + //Check if both touch points moving in the same direction + const QVector2D vectorTouchPoint1 = QVector2D (touchEvent->touchPoints().at(0).startPos() - touchEvent->touchPoints().at(0).pos()); + const QVector2D vectorTouchPoint2 = QVector2D (touchEvent->touchPoints().at(1).startPos() - touchEvent->touchPoints().at(1).pos()); + QVector2D dotProduct = vectorTouchPoint1 * vectorTouchPoint2; + + //The dot product is greater than zero if both touch points moving in the same direction + //special case if the touch point moving exact or almost exact in x or y axis + //one value of the dot product is zero or a little bit less than zero and + //the other value is bigger than zero + if (dotProduct.x() >= -500 && dotProduct.x() <= 0 && dotProduct.y() > 0) dotProduct.setX(1); + if (dotProduct.y() >= -500 && dotProduct.y() <= 0 && dotProduct.x() > 0) dotProduct.setY(1); + + if (dotProduct.x() > 0 && dotProduct.y() > 0) { + const QPointF diff = (touchEvent->touchPoints().first().lastPos() - pos); + state->setProperty("delta", diff); + d->mGestureTriggered = true; + return TriggerGesture; + } else { + //special case if the user makes a very slow pan gesture, the vectors a very short. Sometimes the + //dot product is then zero, so we only want to cancel the gesture if the user makes a bigger wrong gesture + if (vectorTouchPoint1.toPoint().manhattanLength() > 50 || vectorTouchPoint2.toPoint().manhattanLength() > 50 ) { + d->mGestureTriggered = false; + return CancelGesture; + } else { + const QPointF diff = (touchEvent->touchPoints().first().lastPos() - pos); + state->setProperty("delta", diff); + d->mGestureTriggered = true; + return TriggerGesture; + } + } + } else { + state->setProperty("delta", QPointF (0, 0)); + state->setProperty("delayActive", true); + d->mGestureTriggered = false; + return TriggerGesture; + } + } + break; + } + + case QEvent::TouchEnd: { + QTouchEvent* touchEvent = static_cast(event); + if (d->mGestureTriggered) { + d->mGestureTriggered = false; + state->setHotSpot(touchEvent->touchPoints().first().screenPos()); + return FinishGesture; + } + break; + } + + default: + return Ignore; + } + return Ignore; +} + +TwoFingerPan::TwoFingerPan(QObject* parent) + : QGesture(parent) +{ +} + +} // namespace diff --git a/lib/touch/twofingerpan.h b/lib/touch/twofingerpan.h new file mode 100644 index 00000000..aa70a59c --- /dev/null +++ b/lib/touch/twofingerpan.h @@ -0,0 +1,66 @@ +/* +Gwenview: an image viewer +Copyright 2019 Steffen Hartleib + +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, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. + +*/ +#ifndef TWOFINGERPAN_H +#define TWOFINGERPAN_H + +#include +// Qt +#include +#include + +// KDE + +// Local + +namespace Gwenview +{ +struct TwoFingerPanRecognizerPrivate; + +class GWENVIEWLIB_EXPORT TwoFingerPan : public QGesture +{ + Q_PROPERTY(QPointF delta READ getDelta WRITE setDelta) + Q_PROPERTY(bool delayActive READ getDelayActive WRITE setDelayActive) + +public: + explicit TwoFingerPan(QObject* parent = 0); + QPointF getDelta() {return delta;}; + void setDelta(QPointF _delta) {delta = _delta;}; + bool getDelayActive() {return delayActive;}; + void setDelayActive (bool _delay) {delayActive = _delay;}; +private: + QPointF delta; + bool delayActive; +}; + +class GWENVIEWLIB_EXPORT TwoFingerPanRecognizer : public QGestureRecognizer +{ +public: + explicit TwoFingerPanRecognizer(); + ~TwoFingerPanRecognizer(); +private: + TwoFingerPanRecognizerPrivate* d; + + virtual QGesture* create(QObject*) override; + virtual Result recognize(QGesture*, QObject*, QEvent*) override; + +}; + +} // namespace +#endif /* TWOFINGERPAN_H */ diff --git a/lib/touch/twofingertap.cpp b/lib/touch/twofingertap.cpp new file mode 100644 index 00000000..8bba093f --- /dev/null +++ b/lib/touch/twofingertap.cpp @@ -0,0 +1,117 @@ +/* +Gwenview: an image viewer +Copyright 2019 Steffen Hartleib + +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, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. + +*/ +// Self +#include "twofingertap.h" + +// Qt +#include +#include +#include + +// KDE + +// Local +#include "lib/touch/touch_helper.h" + +namespace Gwenview +{ + +struct TwoFingerTapRecognizerPrivate +{ + TwoFingerTapRecognizer* q; + bool mTargetIsGrapicsWidget = false; + qint64 mTouchBeginnTimestamp; + bool mGestureTriggered; +}; + +TwoFingerTapRecognizer::TwoFingerTapRecognizer() : QGestureRecognizer() +, d (new TwoFingerTapRecognizerPrivate) +{ + d->q = this; +} + +TwoFingerTapRecognizer::~TwoFingerTapRecognizer() +{ + delete d; +} + +QGesture* TwoFingerTapRecognizer::create(QObject*) +{ + return static_cast(new TwoFingerTap()); +} + +QGestureRecognizer::Result TwoFingerTapRecognizer::recognize(QGesture* state, QObject* watched, QEvent* event) +{ + //Because of a bug in Qt in a gesture event in a graphicsview, all gestures are trigger twice + //https://bugreports.qt.io/browse/QTBUG-13103 + if (qobject_cast(watched)) d->mTargetIsGrapicsWidget = true; + if (d->mTargetIsGrapicsWidget && watched->isWidgetType()) return Ignore; + + + switch (event->type()) { + case QEvent::TouchBegin: { + QTouchEvent* touchEvent = static_cast(event); + d->mTouchBeginnTimestamp = touchEvent->timestamp(); + state->setHotSpot(touchEvent->touchPoints().first().screenPos()); + d->mGestureTriggered = false; + return MayBeGesture; + } + + case QEvent::TouchUpdate: { + QTouchEvent* touchEvent = static_cast(event); + state->setHotSpot(touchEvent->touchPoints().first().screenPos()); + + if (touchEvent->touchPoints().size() >> 2) { + d->mGestureTriggered = false; + return CancelGesture; + } + + if (touchEvent->touchPoints().size() == 2) { + if ((touchEvent->touchPoints().first().startPos() - touchEvent->touchPoints().first().pos()).manhattanLength() > Touch_Helper::Touch::wiggleRoomForTap) { + d->mGestureTriggered = false; + return CancelGesture; + } + if ((touchEvent->touchPoints().at(1).startPos() - touchEvent->touchPoints().at(1).pos()).manhattanLength() > Touch_Helper::Touch::wiggleRoomForTap) { + d->mGestureTriggered = false; + return CancelGesture; + } + if (touchEvent->touchPointStates() & Qt::TouchPointPressed) { + d->mGestureTriggered = true; + } + if (touchEvent->touchPointStates() & Qt::TouchPointReleased && d->mGestureTriggered) { + d->mGestureTriggered = false; + return FinishGesture; + } + } + break; + } + + default: + return Ignore; + } + return Ignore; +} + +TwoFingerTap::TwoFingerTap(QObject* parent) +: QGesture(parent) +{ +} + +} // namespace diff --git a/lib/touch/twofingertap.h b/lib/touch/twofingertap.h new file mode 100644 index 00000000..4d2e9a15 --- /dev/null +++ b/lib/touch/twofingertap.h @@ -0,0 +1,59 @@ +/* +Gwenview: an image viewer +Copyright 2019 Steffen Hartleib + +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, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. + +*/ +#ifndef TWOFINGERTAP_H +#define TWOFINGERTAP_H + +#include +// Qt +#include +#include + +// KDE + +// Local + +namespace Gwenview +{ +struct TwoFingerTapRecognizerPrivate; + +class GWENVIEWLIB_EXPORT TwoFingerTap : public QGesture +{ + +public: + explicit TwoFingerTap(QObject* parent = 0); + +private: +}; + +class GWENVIEWLIB_EXPORT TwoFingerTapRecognizer : public QGestureRecognizer +{ +public: + explicit TwoFingerTapRecognizer(); + ~TwoFingerTapRecognizer(); +private: + TwoFingerTapRecognizerPrivate* d; + + virtual QGesture* create(QObject*) override; + virtual Result recognize(QGesture*, QObject*, QEvent*) override; + +}; + +} // namespace +#endif /* TWOFINGERTAP_H */