diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2daaa46ca..10064ff18 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,54 +1,54 @@ include: - https://invent.kde.org/sysadmin/ci-tooling/raw/master/invent/ci-before.yml - https://invent.kde.org/sysadmin/ci-tooling/raw/master/invent/ci-applications-linux.yml build_ubuntu_18_04: stage: build image: ubuntu:bionic only: - merge_requests before_script: - sed -i -e 's/# deb-src/deb-src/g' /etc/apt/sources.list - apt-get update - apt-get build-dep --yes --no-install-recommends okular - apt-get install --yes --no-install-recommends ninja-build libkf5crash-dev script: - mkdir -p build && cd build - cmake -G Ninja .. - ninja build_ubuntu_20_04: stage: build image: ubuntu:focal only: - merge_requests before_script: - sed -i -e 's/# deb-src/deb-src/g' /etc/apt/sources.list - apt-get update - apt-get build-dep --yes --no-install-recommends okular - apt-get install --yes --no-install-recommends ninja-build script: - mkdir -p build && cd build - cmake -DOKULAR_UI=desktop -G Ninja .. - ninja - rm -rf * - cmake -DOKULAR_UI=mobile -G Ninja .. - ninja build_clazy_clang_tidy: stage: build image: debian:unstable only: - merge_requests before_script: - echo 'deb-src http://deb.debian.org/debian unstable main' >> /etc/apt/sources.list - apt-get update - apt-get build-dep --yes --no-install-recommends okular - apt-get install --yes --no-install-recommends ninja-build clazy clang clang-tidy python python-yaml libkf5crash-dev libkf5purpose-dev libegl-dev jq script: - srcdir=`pwd` && mkdir -p /tmp/okular_build && cd /tmp/okular_build && CC=clang CXX=clazy CXXFLAGS="-Werror -Wno-deprecated-declarations" cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -G Ninja $srcdir && cat compile_commands.json | jq '[.[] | select(.file | contains("'"$srcdir"'"))]' > compile_commands.aux.json && cat compile_commands.aux.json | jq '[.[] | select(.file | contains("/synctex/")| not)]' > compile_commands.json - - CLAZY_CHECKS="level0,level1,old-style-connect" ninja + - CLAZY_IGNORE_DIRS="settings_core.cpp|settings.cpp" CLAZY_CHECKS="level0,level1,level2,no-ctor-missing-parent-argument,no-copyable-polymorphic,no-qstring-allocations,no-missing-qobject-macro" ninja # Fix the poppler header, remove when debian:unstable ships poppler 0.82 or later - sed -i "N;N;N;N; s#class MediaRendition\;\nclass MovieAnnotation\;\nclass ScreenAnnotation;#class MediaRendition\;#g" /usr/include/poppler/qt5/poppler-link.h - "run-clang-tidy -header-filter='.*/okular/.*' -checks='-*,performance-*,bugprone-*,readability-inconsistent-declaration-parameter-name,readability-string-compare,modernize-redundant-void-arg,modernize-use-bool-literals,modernize-make-unique,modernize-make-shared,modernize-use-override,modernize-use-equals-delete,modernize-use-emplace,modernize-loop-convert,modernize-use-nullptr,-bugprone-macro-parentheses,-bugprone-narrowing-conversions,-bugprone-branch-clone,-bugprone-incorrect-roundings' -config=\"{WarningsAsErrors: '*'}\"" diff --git a/core/area.cpp b/core/area.cpp index 11803e447..6d92b595e 100644 --- a/core/area.cpp +++ b/core/area.cpp @@ -1,504 +1,504 @@ /*************************************************************************** * Copyright (C) 2004-05 by Enrico Ros * * Copyright (C) 2005 by Piotr Szymanski * * 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. * ***************************************************************************/ #include "area.h" #include #include #include #include #include "action.h" #include "annotations.h" #include "annotations_p.h" #include "debug_p.h" #include "sourcereference.h" using namespace Okular; /** class NormalizedPoint **/ NormalizedPoint::NormalizedPoint() : x( 0.0 ), y( 0.0 ) {} NormalizedPoint::NormalizedPoint( double dX, double dY ) : x( dX ), y( dY ) {} NormalizedPoint::NormalizedPoint( int iX, int iY, int xScale, int yScale ) : x( (double)iX / (double)xScale ), y( (double)iY / (double)yScale ) {} NormalizedPoint& NormalizedPoint::operator=( const NormalizedPoint & p ) = default; NormalizedPoint::NormalizedPoint( const NormalizedPoint& ) = default; NormalizedPoint::~NormalizedPoint() = default; void NormalizedPoint::transform( const QTransform &matrix ) { qreal tmp_x = (qreal)x; qreal tmp_y = (qreal)y; matrix.map( tmp_x, tmp_y, &tmp_x, &tmp_y ); x = tmp_x; y = tmp_y; } double NormalizedPoint::distanceSqr( double x, double y, double xScale, double yScale ) const { return pow( (this->x - x) * xScale, 2 ) + pow( (this->y - y) * yScale, 2 ); } /** * Returns a vector from the given points @p a and @p b * @internal */ NormalizedPoint operator-( const NormalizedPoint& a, const NormalizedPoint& b ) { return NormalizedPoint( a.x - b.x, a.y - b.y ); } /** * @brief Calculates distance of the point @p x @p y @p xScale @p yScale to the line segment from @p start to @p end */ double NormalizedPoint::distanceSqr( double x, double y, double xScale, double yScale, const NormalizedPoint& start, const NormalizedPoint& end ) { NormalizedPoint point( x, y ); double thisDistance; NormalizedPoint lineSegment( end - start ); const double lengthSqr = pow( lineSegment.x, 2 ) + pow( lineSegment.y, 2 ); //if the length of the current segment is null, we can just //measure the distance to either end point if ( lengthSqr == 0.0 ) { thisDistance = end.distanceSqr( x, y, xScale, yScale ); } else { //vector from the start point of the current line segment to the measurement point NormalizedPoint a = point - start; //vector from the same start point to the end point of the current line segment NormalizedPoint b = end - start; //we're using a * b (dot product) := |a| * |b| * cos(phi) and the knowledge //that cos(phi) is adjacent side / hypotenuse (hypotenuse = |b|) //therefore, t becomes the length of the vector that represents the projection of //the point p onto the current line segment //(hint: if this is still unclear, draw it!) float t = (a.x * b.x + a.y * b.y) / lengthSqr; if ( t < 0 ) { //projection falls outside the line segment on the side of "start" thisDistance = point.distanceSqr( start.x, start.y, xScale, yScale ); } else if ( t > 1 ) { //projection falls outside the line segment on the side of the current point thisDistance = point.distanceSqr( end.x, end.y, xScale, yScale ); } else { //projection is within [start, *i]; //determine the length of the perpendicular distance from the projection to the actual point NormalizedPoint direction = end - start; NormalizedPoint projection = start - NormalizedPoint( -t * direction.x, -t * direction.y ); thisDistance = projection.distanceSqr( x, y, xScale, yScale ); } } return thisDistance; } QDebug operator<<( QDebug str, const Okular::NormalizedPoint& p ) { str.nospace() << "NormPt(" << p.x << "," << p.y << ")"; return str.space(); } /** class NormalizedRect **/ NormalizedRect::NormalizedRect() : left( 0.0 ), top( 0.0 ), right( 0.0 ), bottom( 0.0 ) {} NormalizedRect::NormalizedRect( double l, double t, double r, double b ) // note: check for swapping coords? : left( l ), top( t ), right( r ), bottom( b ) {} -NormalizedRect::NormalizedRect( const QRect & r, double xScale, double yScale ) +NormalizedRect::NormalizedRect( const QRect & r, double xScale, double yScale ) // clazy:exclude=function-args-by-value TODO when BIC changes are allowed : left( (double)r.left() / xScale ), top( (double)r.top() / yScale ), right( (double)r.right() / xScale ), bottom( (double)r.bottom() / yScale ) {} NormalizedRect::NormalizedRect( const NormalizedRect & rect ) = default; NormalizedRect NormalizedRect::fromQRectF( const QRectF &rect ) { QRectF nrect = rect.normalized(); NormalizedRect ret; ret.left = nrect.left(); ret.top = nrect.top(); ret.right = nrect.right(); ret.bottom = nrect.bottom(); return ret; } bool NormalizedRect::isNull() const { return left == 0 && top== 0 && right == 0 && bottom == 0; } bool NormalizedRect::contains( double x, double y ) const { return x >= left && x <= right && y >= top && y <= bottom; } bool NormalizedRect::intersects( const NormalizedRect & r ) const { return (r.left <= right) && (r.right >= left) && (r.top <= bottom) && (r.bottom >= top); } bool NormalizedRect::intersects( const NormalizedRect * r ) const { return (r->left <= right) && (r->right >= left) && (r->top <= bottom) && (r->bottom >= top); } bool NormalizedRect::intersects( double l, double t, double r, double b ) const { return (l <= right) && (r >= left) && (t <= bottom) && (b >= top); } NormalizedRect NormalizedRect::operator| (const NormalizedRect & r) const { NormalizedRect ret; // todo ! ret.left=qMin(left,r.left); ret.top=qMin(top,r.top); ret.bottom=qMax(bottom,r.bottom); ret.right=qMax(right,r.right); return ret; } NormalizedRect& NormalizedRect::operator|= (const NormalizedRect & r) { left = qMin( left, r.left ); top = qMin( top, r.top ); bottom = qMax( bottom, r.bottom ); right = qMax( right, r.right ); return *this; } NormalizedRect NormalizedRect::operator&( const NormalizedRect & r ) const { if ( isNull() || r.isNull() ) return NormalizedRect(); NormalizedRect ret; ret.left = qMax( left, r.left ); ret.top = qMax( top, r.top ); ret.bottom = qMin( bottom, r.bottom ); ret.right = qMin( right, r.right ); return ret; } NormalizedRect & NormalizedRect::operator=( const NormalizedRect & r ) = default; NormalizedRect::~NormalizedRect() = default; bool NormalizedRect::operator==( const NormalizedRect & r ) const { return ( isNull() && r.isNull() ) || ( fabs( left - r.left ) < 1e-4 && fabs( right - r.right ) < 1e-4 && fabs( top - r.top ) < 1e-4 && fabs( bottom - r.bottom ) < 1e-4 ); } NormalizedPoint NormalizedRect::center() const { return NormalizedPoint((left+right)/2.0, (top+bottom)/2.0); } /* QDebug operator << (QDebug str , const NormalizedRect &r) { str << "[" <(), d( nullptr ) { } RegularAreaRect::RegularAreaRect( const RegularAreaRect& rar ) : RegularArea< NormalizedRect, QRect >( rar ), d( nullptr ) { } RegularAreaRect::~RegularAreaRect() { } RegularAreaRect& RegularAreaRect::operator=( const RegularAreaRect& rar ) { if (this != &rar) { RegularArea< NormalizedRect, QRect >::operator=( rar ); } return *this; } HighlightAreaRect::HighlightAreaRect( const RegularAreaRect *area ) : RegularAreaRect(), s_id( -1 ) { if ( area ) { RegularAreaRect::ConstIterator it = area->begin(); RegularAreaRect::ConstIterator itEnd = area->end(); for ( ; it != itEnd; ++it ) { append( NormalizedRect( *it ) ); } } } /** class ObjectRect **/ ObjectRect::ObjectRect( double l, double t, double r, double b, bool ellipse, ObjectType type, void * object ) : m_objectType( type ), m_object( object ) { // assign coordinates swapping them if negative width or height QRectF rect( r > l ? l : r, b > t ? t : b, fabs( r - l ), fabs( b - t ) ); if ( ellipse ) m_path.addEllipse( rect ); else m_path.addRect( rect ); m_transformedPath = m_path; } ObjectRect::ObjectRect( const NormalizedRect& r, bool ellipse, ObjectType type, void * object ) : m_objectType( type ), m_object( object ) { QRectF rect( r.left, r.top, fabs( r.right - r.left ), fabs( r.bottom - r.top ) ); if ( ellipse ) m_path.addEllipse( rect ); else m_path.addRect( rect ); m_transformedPath = m_path; } ObjectRect::ObjectRect( const QPolygonF &poly, ObjectType type, void * object ) : m_objectType( type ), m_object( object ) { m_path.addPolygon( poly ); m_transformedPath = m_path; } ObjectRect::ObjectType ObjectRect::objectType() const { return m_objectType; } const void * ObjectRect::object() const { return m_object; } const QPainterPath &ObjectRect::region() const { return m_transformedPath; } QRect ObjectRect::boundingRect( double xScale, double yScale ) const { const QRectF &br = m_transformedPath.boundingRect(); return QRect( (int)( br.left() * xScale ), (int)( br.top() * yScale ), (int)( br.width() * xScale ), (int)( br.height() * yScale ) ); } bool ObjectRect::contains( double x, double y, double, double ) const { return m_transformedPath.contains( QPointF( x, y ) ); } void ObjectRect::transform( const QTransform &matrix ) { m_transformedPath = matrix.map( m_path ); } double ObjectRect::distanceSqr( double x, double y, double xScale, double yScale ) const { switch ( m_objectType ) { case Action: case Image: { const QRectF& rect( m_transformedPath.boundingRect() ); return NormalizedRect( rect.x(), rect.y(), rect.right(), rect.bottom() ).distanceSqr( x, y, xScale, yScale ); } case OAnnotation: { return static_cast(m_object)->d_func()->distanceSqr( x, y, xScale, yScale ); } case SourceRef: { const SourceRefObjectRect * sr = static_cast< const SourceRefObjectRect * >( this ); const NormalizedPoint& point = sr->m_point; if ( point.x == -1.0 ) { return pow( ( y - point.y ) * yScale, 2 ); } else if ( point.y == -1.0 ) { return pow( ( x - point.x ) * xScale, 2 ); } else { return pow( ( x - point.x ) * xScale, 2 ) + pow( ( y - point.y ) * yScale, 2 ); } } } return 0.0; } ObjectRect::~ObjectRect() { if ( !m_object ) return; if ( m_objectType == Action ) delete static_cast( m_object ); else if ( m_objectType == SourceRef ) delete static_cast( m_object ); else qCDebug(OkularCoreDebug).nospace() << "Object deletion not implemented for type '" << m_objectType << "'."; } /** class AnnotationObjectRect **/ AnnotationObjectRect::AnnotationObjectRect( Annotation * annotation ) : ObjectRect( QPolygonF(), OAnnotation, annotation ), m_annotation( annotation ) { } Annotation *AnnotationObjectRect::annotation() const { return m_annotation; } QRect AnnotationObjectRect::boundingRect( double xScale, double yScale ) const { const QRect annotRect = AnnotationUtils::annotationGeometry( m_annotation, xScale, yScale ); const QPoint center = annotRect.center(); // Make sure that the rectangle has a minimum size, so that it's possible // to click on it const int minSize = 14; const QRect minRect( center.x()-minSize/2, center.y()-minSize/2, minSize, minSize ); return annotRect | minRect; } bool AnnotationObjectRect::contains( double x, double y, double xScale, double yScale ) const { return boundingRect( xScale, yScale ).contains( (int)( x * xScale ), (int)( y * yScale ), false ); } AnnotationObjectRect::~AnnotationObjectRect() { // the annotation pointer is kept elsewehere (in Page, most probably), // so just release its pointer m_object = nullptr; } void AnnotationObjectRect::transform( const QTransform &matrix ) { m_annotation->d_func()->annotationTransform( matrix ); } /** class SourceRefObjectRect **/ SourceRefObjectRect::SourceRefObjectRect( const NormalizedPoint& point, void * srcRef ) : ObjectRect( point.x, point.y, .0, .0, false, SourceRef, srcRef ), m_point( point ) { const double x = m_point.x < 0.0 ? 0.5 : m_point.x; const double y = m_point.y < 0.0 ? 0.5 : m_point.y; const QRectF rect( x - 2, y - 2, 5, 5 ); m_path.addRect( rect ); m_transformedPath = m_path; } QRect SourceRefObjectRect::boundingRect( double xScale, double yScale ) const { const double x = m_point.x < 0.0 ? 0.5 : m_point.x; const double y = m_point.y < 0.0 ? 0.5 : m_point.y; return QRect( x * xScale, y * yScale, 1, 1 ); } bool SourceRefObjectRect::contains( double x, double y, double xScale, double yScale ) const { return distanceSqr( x, y, xScale, yScale ) < ( pow( 7.0 / xScale, 2 ) + pow( 7.0 / yScale, 2 ) ); } /** class NonOwningObjectRect **/ NonOwningObjectRect::NonOwningObjectRect( double left, double top, double right, double bottom, bool ellipse, ObjectType type, void *object ) : ObjectRect( left, top, right, bottom, ellipse, type, object ) { } NonOwningObjectRect::~NonOwningObjectRect() { // Set m_object so that ~ObjectRect() doesn't delete it m_object = nullptr; } diff --git a/core/document.cpp b/core/document.cpp index 86135adf7..1b9bab41d 100644 --- a/core/document.cpp +++ b/core/document.cpp @@ -1,5936 +1,5936 @@ /*************************************************************************** * Copyright (C) 2004-2005 by Enrico Ros * * Copyright (C) 2004-2008 by Albert Astals Cid * * Copyright (C) 2017, 2018 Klarälvdalens Datakonsult AB, a KDAB Group * * company, info@kdab.com. Work sponsored by the * * LiMux project of the city of Munich * * * * 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. * ***************************************************************************/ #include "document.h" #include "document_p.h" #include "documentcommands_p.h" #include #include #ifdef Q_OS_WIN #define _WIN32_WINNT 0x0500 #include #elif defined(Q_OS_FREEBSD) #include #include #include #endif // qt/kde/system includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // local includes #include "action.h" #include "annotations.h" #include "annotations_p.h" #include "audioplayer.h" #include "audioplayer_p.h" #include "bookmarkmanager.h" #include "chooseenginedialog_p.h" #include "debug_p.h" #include "generator_p.h" #include "interfaces/configinterface.h" #include "interfaces/guiinterface.h" #include "interfaces/printinterface.h" #include "interfaces/saveinterface.h" #include "observer.h" #include "misc.h" #include "page.h" #include "page_p.h" #include "pagecontroller_p.h" #include "scripter.h" #include "script/event_p.h" #include "settings_core.h" #include "sourcereference.h" #include "sourcereference_p.h" #include "texteditors_p.h" #include "tile.h" #include "tilesmanager_p.h" #include "utils_p.h" #include "view.h" #include "view_p.h" #include "form.h" #include "utils.h" #include #if HAVE_MALLOC_TRIM #include "malloc.h" #endif using namespace Okular; struct AllocatedPixmap { // owner of the page DocumentObserver *observer; int page; qulonglong memory; // public constructor: initialize data AllocatedPixmap( DocumentObserver *o, int p, qulonglong m ) : observer( o ), page( p ), memory( m ) {} }; struct ArchiveData { ArchiveData() { } QString originalFileName; QTemporaryFile document; QTemporaryFile metadataFile; }; struct RunningSearch { // store search properties int continueOnPage; RegularAreaRect continueOnMatch; QSet< int > highlightedPages; // fields related to previous searches (used for 'continueSearch') QString cachedString; Document::SearchType cachedType; Qt::CaseSensitivity cachedCaseSensitivity; bool cachedViewportMove : 1; bool isCurrentlySearching : 1; QColor cachedColor; int pagesDone; }; #define foreachObserver( cmd ) {\ QSet< DocumentObserver * >::const_iterator it=d->m_observers.constBegin(), end=d->m_observers.constEnd();\ for ( ; it != end ; ++ it ) { (*it)-> cmd ; } } #define foreachObserverD( cmd ) {\ QSet< DocumentObserver * >::const_iterator it = m_observers.constBegin(), end = m_observers.constEnd();\ for ( ; it != end ; ++ it ) { (*it)-> cmd ; } } #define OKULAR_HISTORY_MAXSTEPS 100 #define OKULAR_HISTORY_SAVEDSTEPS 10 /***** Document ******/ QString DocumentPrivate::pagesSizeString() const { if (m_generator) { if (m_generator->pagesSizeMetric() != Generator::None) { QSizeF size = m_parent->allPagesSize(); if (size.isValid()) return localizedSize(size); else return QString(); } else return QString(); } else return QString(); } QString DocumentPrivate::namePaperSize(double inchesWidth, double inchesHeight) const { const QPrinter::Orientation orientation = inchesWidth > inchesHeight ? QPrinter::Landscape : QPrinter::Portrait; const QSize pointsSize(inchesWidth *72.0, inchesHeight*72.0); const QPageSize::PageSizeId paperSize = QPageSize::id(pointsSize, QPageSize::FuzzyOrientationMatch); const QString paperName = QPageSize::name(paperSize); if (orientation == QPrinter::Portrait) { return i18nc("paper type and orientation (eg: Portrait A4)", "Portrait %1", paperName); } else { return i18nc("paper type and orientation (eg: Portrait A4)", "Landscape %1", paperName); } } -QString DocumentPrivate::localizedSize(const QSizeF &size) const +QString DocumentPrivate::localizedSize(const QSizeF size) const { double inchesWidth = 0, inchesHeight = 0; switch (m_generator->pagesSizeMetric()) { case Generator::Points: inchesWidth = size.width() / 72.0; inchesHeight = size.height() / 72.0; break; case Generator::Pixels: { const QSizeF dpi = m_generator->dpi(); inchesWidth = size.width() / dpi.width(); inchesHeight = size.height() / dpi.height(); } break; case Generator::None: break; } if (QLocale::system().measurementSystem() == QLocale::ImperialSystem) { return i18nc("%1 is width, %2 is height, %3 is paper size name", "%1 x %2 in (%3)", inchesWidth, inchesHeight, namePaperSize(inchesWidth, inchesHeight)); } else { return i18nc("%1 is width, %2 is height, %3 is paper size name", "%1 x %2 mm (%3)", QString::number(inchesWidth * 25.4, 'd', 0), QString::number(inchesHeight * 25.4, 'd', 0), namePaperSize(inchesWidth, inchesHeight)); } } qulonglong DocumentPrivate::calculateMemoryToFree() { // [MEM] choose memory parameters based on configuration profile qulonglong clipValue = 0; qulonglong memoryToFree = 0; switch ( SettingsCore::memoryLevel() ) { case SettingsCore::EnumMemoryLevel::Low: memoryToFree = m_allocatedPixmapsTotalMemory; break; case SettingsCore::EnumMemoryLevel::Normal: { qulonglong thirdTotalMemory = getTotalMemory() / 3; qulonglong freeMemory = getFreeMemory(); if (m_allocatedPixmapsTotalMemory > thirdTotalMemory) memoryToFree = m_allocatedPixmapsTotalMemory - thirdTotalMemory; if (m_allocatedPixmapsTotalMemory > freeMemory) clipValue = (m_allocatedPixmapsTotalMemory - freeMemory) / 2; } break; case SettingsCore::EnumMemoryLevel::Aggressive: { qulonglong freeMemory = getFreeMemory(); if (m_allocatedPixmapsTotalMemory > freeMemory) clipValue = (m_allocatedPixmapsTotalMemory - freeMemory) / 2; } break; case SettingsCore::EnumMemoryLevel::Greedy: { qulonglong freeSwap; qulonglong freeMemory = getFreeMemory( &freeSwap ); const qulonglong memoryLimit = qMin( qMax( freeMemory, getTotalMemory()/2 ), freeMemory+freeSwap ); if (m_allocatedPixmapsTotalMemory > memoryLimit) clipValue = (m_allocatedPixmapsTotalMemory - memoryLimit) / 2; } break; } if ( clipValue > memoryToFree ) memoryToFree = clipValue; return memoryToFree; } void DocumentPrivate::cleanupPixmapMemory() { cleanupPixmapMemory( calculateMemoryToFree() ); } void DocumentPrivate::cleanupPixmapMemory( qulonglong memoryToFree ) { if ( memoryToFree < 1 ) return; const int currentViewportPage = (*m_viewportIterator).pageNumber; // Create a QMap of visible rects, indexed by page number QMap< int, VisiblePageRect * > visibleRects; QVector< Okular::VisiblePageRect * >::const_iterator vIt = m_pageRects.constBegin(), vEnd = m_pageRects.constEnd(); for ( ; vIt != vEnd; ++vIt ) visibleRects.insert( (*vIt)->pageNumber, (*vIt) ); // Free memory starting from pages that are farthest from the current one int pagesFreed = 0; while ( memoryToFree > 0 ) { AllocatedPixmap * p = searchLowestPriorityPixmap( true, true ); if ( !p ) // No pixmap to remove break; qCDebug(OkularCoreDebug).nospace() << "Evicting cache pixmap observer=" << p->observer << " page=" << p->page; // m_allocatedPixmapsTotalMemory can't underflow because we always add or remove // the memory used by the AllocatedPixmap so at most it can reach zero m_allocatedPixmapsTotalMemory -= p->memory; // Make sure memoryToFree does not underflow if ( p->memory > memoryToFree ) memoryToFree = 0; else memoryToFree -= p->memory; pagesFreed++; // delete pixmap m_pagesVector.at( p->page )->deletePixmap( p->observer ); // delete allocation descriptor delete p; } // If we're still on low memory, try to free individual tiles // Store pages that weren't completely removed QLinkedList< AllocatedPixmap * > pixmapsToKeep; while (memoryToFree > 0) { int clean_hits = 0; for (DocumentObserver *observer : qAsConst(m_observers)) { AllocatedPixmap * p = searchLowestPriorityPixmap( false, true, observer ); if ( !p ) // No pixmap to remove continue; clean_hits++; TilesManager *tilesManager = m_pagesVector.at( p->page )->d->tilesManager( observer ); if ( tilesManager && tilesManager->totalMemory() > 0 ) { qulonglong memoryDiff = p->memory; NormalizedRect visibleRect; if ( visibleRects.contains( p->page ) ) visibleRect = visibleRects[ p->page ]->rect; // Free non visible tiles tilesManager->cleanupPixmapMemory( memoryToFree, visibleRect, currentViewportPage ); p->memory = tilesManager->totalMemory(); memoryDiff -= p->memory; memoryToFree = (memoryDiff < memoryToFree) ? (memoryToFree - memoryDiff) : 0; m_allocatedPixmapsTotalMemory -= memoryDiff; if ( p->memory > 0 ) pixmapsToKeep.append( p ); else delete p; } else pixmapsToKeep.append( p ); } if (clean_hits == 0) break; } m_allocatedPixmaps += pixmapsToKeep; //p--rintf("freeMemory A:[%d -%d = %d] \n", m_allocatedPixmaps.count() + pagesFreed, pagesFreed, m_allocatedPixmaps.count() ); } /* Returns the next pixmap to evict from cache, or NULL if no suitable pixmap * if found. If unloadableOnly is set, only unloadable pixmaps are returned. If * thenRemoveIt is set, the pixmap is removed from m_allocatedPixmaps before * returning it */ AllocatedPixmap * DocumentPrivate::searchLowestPriorityPixmap( bool unloadableOnly, bool thenRemoveIt, DocumentObserver *observer ) { QLinkedList< AllocatedPixmap * >::iterator pIt = m_allocatedPixmaps.begin(); QLinkedList< AllocatedPixmap * >::iterator pEnd = m_allocatedPixmaps.end(); QLinkedList< AllocatedPixmap * >::iterator farthestPixmap = pEnd; const int currentViewportPage = (*m_viewportIterator).pageNumber; /* Find the pixmap that is farthest from the current viewport */ int maxDistance = -1; while ( pIt != pEnd ) { const AllocatedPixmap * p = *pIt; // Filter by observer if ( observer == nullptr || p->observer == observer ) { const int distance = qAbs( p->page - currentViewportPage ); if ( maxDistance < distance && ( !unloadableOnly || p->observer->canUnloadPixmap( p->page ) ) ) { maxDistance = distance; farthestPixmap = pIt; } } ++pIt; } /* No pixmap to remove */ if ( farthestPixmap == pEnd ) return nullptr; AllocatedPixmap * selectedPixmap = *farthestPixmap; if ( thenRemoveIt ) m_allocatedPixmaps.erase( farthestPixmap ); return selectedPixmap; } qulonglong DocumentPrivate::getTotalMemory() { static qulonglong cachedValue = 0; if ( cachedValue ) return cachedValue; #if defined(Q_OS_LINUX) // if /proc/meminfo doesn't exist, return 128MB QFile memFile( QStringLiteral("/proc/meminfo") ); if ( !memFile.open( QIODevice::ReadOnly ) ) return (cachedValue = 134217728); QTextStream readStream( &memFile ); while ( true ) { QString entry = readStream.readLine(); if ( entry.isNull() ) break; if ( entry.startsWith( QLatin1String("MemTotal:") ) ) return (cachedValue = (Q_UINT64_C(1024) * entry.section( QLatin1Char ( ' ' ), -2, -2 ).toULongLong())); } #elif defined(Q_OS_FREEBSD) qulonglong physmem; int mib[] = {CTL_HW, HW_PHYSMEM}; size_t len = sizeof( physmem ); if ( sysctl( mib, 2, &physmem, &len, NULL, 0 ) == 0 ) return (cachedValue = physmem); #elif defined(Q_OS_WIN) MEMORYSTATUSEX stat; stat.dwLength = sizeof(stat); GlobalMemoryStatusEx (&stat); return ( cachedValue = stat.ullTotalPhys ); #endif return (cachedValue = 134217728); } qulonglong DocumentPrivate::getFreeMemory( qulonglong *freeSwap ) { static QTime lastUpdate = QTime::currentTime().addSecs(-3); static qulonglong cachedValue = 0; static qulonglong cachedFreeSwap = 0; if ( qAbs( lastUpdate.secsTo( QTime::currentTime() ) ) <= 2 ) { if (freeSwap) *freeSwap = cachedFreeSwap; return cachedValue; } /* Initialize the returned free swap value to 0. It is overwritten if the * actual value is available */ if (freeSwap) *freeSwap = 0; #if defined(Q_OS_LINUX) // if /proc/meminfo doesn't exist, return MEMORY FULL QFile memFile( QStringLiteral("/proc/meminfo") ); if ( !memFile.open( QIODevice::ReadOnly ) ) return 0; // read /proc/meminfo and sum up the contents of 'MemFree', 'Buffers' // and 'Cached' fields. consider swapped memory as used memory. qulonglong memoryFree = 0; QString entry; QTextStream readStream( &memFile ); static const int nElems = 5; QString names[nElems] = { QStringLiteral("MemFree:"), QStringLiteral("Buffers:"), QStringLiteral("Cached:"), QStringLiteral("SwapFree:"), QStringLiteral("SwapTotal:") }; qulonglong values[nElems] = { 0, 0, 0, 0, 0 }; bool foundValues[nElems] = { false, false, false, false, false }; while ( true ) { entry = readStream.readLine(); if ( entry.isNull() ) break; for ( int i = 0; i < nElems; ++i ) { if ( entry.startsWith( names[i] ) ) { values[i] = entry.section( QLatin1Char ( ' ' ), -2, -2 ).toULongLong( &foundValues[i] ); } } } memFile.close(); bool found = true; for ( int i = 0; found && i < nElems; ++i ) found = found && foundValues[i]; if ( found ) { /* MemFree + Buffers + Cached - SwapUsed = * = MemFree + Buffers + Cached - (SwapTotal - SwapFree) = * = MemFree + Buffers + Cached + SwapFree - SwapTotal */ memoryFree = values[0] + values[1] + values[2] + values[3]; if ( values[4] > memoryFree ) memoryFree = 0; else memoryFree -= values[4]; } else { return 0; } lastUpdate = QTime::currentTime(); if (freeSwap) *freeSwap = ( cachedFreeSwap = (Q_UINT64_C(1024) * values[3]) ); return ( cachedValue = (Q_UINT64_C(1024) * memoryFree) ); #elif defined(Q_OS_FREEBSD) qulonglong cache, inact, free, psize; size_t cachelen, inactlen, freelen, psizelen; cachelen = sizeof( cache ); inactlen = sizeof( inact ); freelen = sizeof( free ); psizelen = sizeof( psize ); // sum up inactive, cached and free memory if ( sysctlbyname( "vm.stats.vm.v_cache_count", &cache, &cachelen, NULL, 0 ) == 0 && sysctlbyname( "vm.stats.vm.v_inactive_count", &inact, &inactlen, NULL, 0 ) == 0 && sysctlbyname( "vm.stats.vm.v_free_count", &free, &freelen, NULL, 0 ) == 0 && sysctlbyname( "vm.stats.vm.v_page_size", &psize, &psizelen, NULL, 0 ) == 0 ) { lastUpdate = QTime::currentTime(); return (cachedValue = (cache + inact + free) * psize); } else { return 0; } #elif defined(Q_OS_WIN) MEMORYSTATUSEX stat; stat.dwLength = sizeof(stat); GlobalMemoryStatusEx (&stat); lastUpdate = QTime::currentTime(); if (freeSwap) *freeSwap = ( cachedFreeSwap = stat.ullAvailPageFile ); return ( cachedValue = stat.ullAvailPhys ); #else // tell the memory is full.. will act as in LOW profile return 0; #endif } bool DocumentPrivate::loadDocumentInfo( LoadDocumentInfoFlags loadWhat ) // note: load data and stores it internally (document or pages). observers // are still uninitialized at this point so don't access them { //qCDebug(OkularCoreDebug).nospace() << "Using '" << d->m_xmlFileName << "' as document info file."; if ( m_xmlFileName.isEmpty() ) return false; QFile infoFile( m_xmlFileName ); return loadDocumentInfo( infoFile, loadWhat ); } bool DocumentPrivate::loadDocumentInfo( QFile &infoFile, LoadDocumentInfoFlags loadWhat ) { if ( !infoFile.exists() || !infoFile.open( QIODevice::ReadOnly ) ) return false; // Load DOM from XML file QDomDocument doc( QStringLiteral("documentInfo") ); if ( !doc.setContent( &infoFile ) ) { qCDebug(OkularCoreDebug) << "Can't load XML pair! Check for broken xml."; infoFile.close(); return false; } infoFile.close(); QDomElement root = doc.documentElement(); if ( root.tagName() != QLatin1String("documentInfo") ) return false; bool loadedAnything = false; // set if something gets actually loaded // Parse the DOM tree QDomNode topLevelNode = root.firstChild(); while ( topLevelNode.isElement() ) { QString catName = topLevelNode.toElement().tagName(); // Restore page attributes (bookmark, annotations, ...) from the DOM if ( catName == QLatin1String("pageList") && ( loadWhat & LoadPageInfo ) ) { QDomNode pageNode = topLevelNode.firstChild(); while ( pageNode.isElement() ) { QDomElement pageElement = pageNode.toElement(); if ( pageElement.hasAttribute( QStringLiteral("number") ) ) { // get page number (node's attribute) bool ok; int pageNumber = pageElement.attribute( QStringLiteral("number") ).toInt( &ok ); // pass the domElement to the right page, to read config data from if ( ok && pageNumber >= 0 && pageNumber < (int)m_pagesVector.count() ) { if ( m_pagesVector[ pageNumber ]->d->restoreLocalContents( pageElement ) ) loadedAnything = true; } } pageNode = pageNode.nextSibling(); } } // Restore 'general info' from the DOM else if ( catName == QLatin1String("generalInfo") && ( loadWhat & LoadGeneralInfo ) ) { QDomNode infoNode = topLevelNode.firstChild(); while ( infoNode.isElement() ) { QDomElement infoElement = infoNode.toElement(); // restore viewports history if ( infoElement.tagName() == QLatin1String("history") ) { // clear history m_viewportHistory.clear(); // append old viewports QDomNode historyNode = infoNode.firstChild(); while ( historyNode.isElement() ) { QDomElement historyElement = historyNode.toElement(); if ( historyElement.hasAttribute( QStringLiteral("viewport") ) ) { QString vpString = historyElement.attribute( QStringLiteral("viewport") ); m_viewportIterator = m_viewportHistory.insert( m_viewportHistory.end(), DocumentViewport( vpString ) ); loadedAnything = true; } historyNode = historyNode.nextSibling(); } // consistency check if ( m_viewportHistory.isEmpty() ) m_viewportIterator = m_viewportHistory.insert( m_viewportHistory.end(), DocumentViewport() ); } else if ( infoElement.tagName() == QLatin1String("rotation") ) { QString str = infoElement.text(); bool ok = true; int newrotation = !str.isEmpty() ? ( str.toInt( &ok ) % 4 ) : 0; if ( ok && newrotation != 0 ) { setRotationInternal( newrotation, false ); loadedAnything = true; } } else if ( infoElement.tagName() == QLatin1String("views") ) { QDomNode viewNode = infoNode.firstChild(); while ( viewNode.isElement() ) { QDomElement viewElement = viewNode.toElement(); if ( viewElement.tagName() == QLatin1String("view") ) { const QString viewName = viewElement.attribute( QStringLiteral("name") ); for ( View *view : qAsConst(m_views) ) { if ( view->name() == viewName ) { loadViewsInfo( view, viewElement ); loadedAnything = true; break; } } } viewNode = viewNode.nextSibling(); } } infoNode = infoNode.nextSibling(); } } topLevelNode = topLevelNode.nextSibling(); } // return loadedAnything; } void DocumentPrivate::loadViewsInfo( View *view, const QDomElement &e ) { QDomNode viewNode = e.firstChild(); while ( viewNode.isElement() ) { QDomElement viewElement = viewNode.toElement(); if ( viewElement.tagName() == QLatin1String("zoom") ) { const QString valueString = viewElement.attribute( QStringLiteral("value") ); bool newzoom_ok = true; const double newzoom = !valueString.isEmpty() ? valueString.toDouble( &newzoom_ok ) : 1.0; if ( newzoom_ok && newzoom != 0 && view->supportsCapability( View::Zoom ) && ( view->capabilityFlags( View::Zoom ) & ( View::CapabilityRead | View::CapabilitySerializable ) ) ) { view->setCapability( View::Zoom, newzoom ); } const QString modeString = viewElement.attribute( QStringLiteral("mode") ); bool newmode_ok = true; const int newmode = !modeString.isEmpty() ? modeString.toInt( &newmode_ok ) : 2; if ( newmode_ok && view->supportsCapability( View::ZoomModality ) && ( view->capabilityFlags( View::ZoomModality ) & ( View::CapabilityRead | View::CapabilitySerializable ) ) ) { view->setCapability( View::ZoomModality, newmode ); } } else if ( viewElement.tagName() == "viewMode" ) { const QString modeString = viewElement.attribute( "mode" ); bool newmode_ok = true; const int newmode = !modeString.isEmpty() ? modeString.toInt( &newmode_ok ) : 2; if ( newmode_ok && view->supportsCapability( View::ViewModeModality ) && ( view->capabilityFlags( View::ViewModeModality ) & ( View::CapabilityRead | View::CapabilitySerializable ) ) ) { view->setCapability( View::ViewModeModality, newmode ); } } else if ( viewElement.tagName() == "continuous" ) { const QString modeString = viewElement.attribute( "mode" ); bool newmode_ok = true; const int newmode = !modeString.isEmpty() ? modeString.toInt( &newmode_ok ) : 2; if ( newmode_ok && view->supportsCapability( View::Continuous ) && ( view->capabilityFlags( View::Continuous ) & ( View::CapabilityRead | View::CapabilitySerializable ) ) ) { view->setCapability( View::Continuous, newmode ); } } else if ( viewElement.tagName() == "trimMargins" ) { const QString valueString = viewElement.attribute( "value" ); bool newmode_ok = true; const int newmode = !valueString.isEmpty() ? valueString.toInt( &newmode_ok ) : 2; if ( newmode_ok && view->supportsCapability( View::TrimMargins ) && ( view->capabilityFlags( View::TrimMargins ) & ( View::CapabilityRead | View::CapabilitySerializable ) ) ) { view->setCapability( View::TrimMargins, newmode ); } } viewNode = viewNode.nextSibling(); } } void DocumentPrivate::saveViewsInfo( View *view, QDomElement &e ) const { if ( view->supportsCapability( View::Zoom ) && ( view->capabilityFlags( View::Zoom ) & ( View::CapabilityRead | View::CapabilitySerializable ) ) && view->supportsCapability( View::ZoomModality ) && ( view->capabilityFlags( View::ZoomModality ) & ( View::CapabilityRead | View::CapabilitySerializable ) ) ) { QDomElement zoomEl = e.ownerDocument().createElement( QStringLiteral("zoom") ); e.appendChild( zoomEl ); bool ok = true; const double zoom = view->capability( View::Zoom ).toDouble( &ok ); if ( ok && zoom != 0 ) { zoomEl.setAttribute( QStringLiteral("value"), QString::number(zoom) ); } const int mode = view->capability( View::ZoomModality ).toInt( &ok ); if ( ok ) { zoomEl.setAttribute( QStringLiteral("mode"), mode ); } } if ( view->supportsCapability( View::Continuous ) && ( view->capabilityFlags( View::Continuous ) & ( View::CapabilityRead | View::CapabilitySerializable ) ) ) { QDomElement contEl = e.ownerDocument().createElement( "continuous" ); e.appendChild( contEl ); const bool mode = view->capability( View::Continuous ).toBool(); contEl.setAttribute( "mode", mode ); } if ( view->supportsCapability( View::ViewModeModality ) && ( view->capabilityFlags( View::ViewModeModality ) & ( View::CapabilityRead | View::CapabilitySerializable ) ) ) { QDomElement viewEl = e.ownerDocument().createElement( "viewMode" ); e.appendChild( viewEl ); bool ok = true; const int mode = view->capability( View::ViewModeModality ).toInt( &ok ); if ( ok ) { viewEl.setAttribute( "mode", mode ); } } if ( view->supportsCapability( View::TrimMargins ) && ( view->capabilityFlags( View::TrimMargins ) & ( View::CapabilityRead | View::CapabilitySerializable ) ) ) { QDomElement contEl = e.ownerDocument().createElement( "trimMargins" ); e.appendChild( contEl ); const bool value = view->capability( View::TrimMargins ).toBool(); contEl.setAttribute( "value", value ); } } QUrl DocumentPrivate::giveAbsoluteUrl( const QString & fileName ) const { if ( !QDir::isRelativePath( fileName ) ) return QUrl::fromLocalFile(fileName); if ( !m_url.isValid() ) return QUrl(); return QUrl(KIO::upUrl(m_url).toString() + fileName); } bool DocumentPrivate::openRelativeFile( const QString & fileName ) { QUrl url = giveAbsoluteUrl( fileName ); if ( url.isEmpty() ) return false; qCDebug(OkularCoreDebug).nospace() << "openRelativeFile: '" << url << "'"; emit m_parent->openUrl( url ); return true; } Generator * DocumentPrivate::loadGeneratorLibrary( const KPluginMetaData &service ) { KPluginLoader loader( service.fileName() ); qCDebug(OkularCoreDebug) << service.fileName(); KPluginFactory *factory = loader.factory(); if ( !factory ) { qCWarning(OkularCoreDebug).nospace() << "Invalid plugin factory for " << service.fileName() << ":" << loader.errorString(); return nullptr; } Generator * plugin = factory->create(); GeneratorInfo info( plugin, service ); m_loadedGenerators.insert( service.pluginId(), info ); return plugin; } void DocumentPrivate::loadAllGeneratorLibraries() { if ( m_generatorsLoaded ) return; loadServiceList( availableGenerators() ); m_generatorsLoaded = true; } void DocumentPrivate::loadServiceList( const QVector& offers ) { int count = offers.count(); if ( count <= 0 ) return; for ( int i = 0; i < count; ++i ) { QString id = offers.at(i).pluginId(); // don't load already loaded generators QHash< QString, GeneratorInfo >::const_iterator genIt = m_loadedGenerators.constFind( id ); if ( !m_loadedGenerators.isEmpty() && genIt != m_loadedGenerators.constEnd() ) continue; Generator * g = loadGeneratorLibrary( offers.at(i) ); (void)g; } } void DocumentPrivate::unloadGenerator( const GeneratorInfo& info ) { delete info.generator; } void DocumentPrivate::cacheExportFormats() { if ( m_exportCached ) return; const ExportFormat::List formats = m_generator->exportFormats(); for ( int i = 0; i < formats.count(); ++i ) { if ( formats.at( i ).mimeType().name() == QLatin1String( "text/plain" ) ) m_exportToText = formats.at( i ); else m_exportFormats.append( formats.at( i ) ); } m_exportCached = true; } ConfigInterface* DocumentPrivate::generatorConfig( GeneratorInfo& info ) { if ( info.configChecked ) return info.config; info.config = qobject_cast< Okular::ConfigInterface * >( info.generator ); info.configChecked = true; return info.config; } SaveInterface* DocumentPrivate::generatorSave( GeneratorInfo& info ) { if ( info.saveChecked ) return info.save; info.save = qobject_cast< Okular::SaveInterface * >( info.generator ); info.saveChecked = true; return info.save; } Document::OpenResult DocumentPrivate::openDocumentInternal( const KPluginMetaData& offer, bool isstdin, const QString& docFile, const QByteArray& filedata, const QString& password ) { QString propName = offer.pluginId(); QHash< QString, GeneratorInfo >::const_iterator genIt = m_loadedGenerators.constFind( propName ); m_walletGenerator = nullptr; if ( genIt != m_loadedGenerators.constEnd() ) { m_generator = genIt.value().generator; } else { m_generator = loadGeneratorLibrary( offer ); if ( !m_generator ) return Document::OpenError; genIt = m_loadedGenerators.constFind( propName ); Q_ASSERT( genIt != m_loadedGenerators.constEnd() ); } Q_ASSERT_X( m_generator, "Document::load()", "null generator?!" ); m_generator->d_func()->m_document = this; // connect error reporting signals m_openError.clear(); QMetaObject::Connection errorToOpenErrorConnection = QObject::connect( m_generator, &Generator::error, m_parent, [this](const QString &message) { m_openError = message; } ); QObject::connect( m_generator, &Generator::warning, m_parent, &Document::warning ); QObject::connect( m_generator, &Generator::notice, m_parent, &Document::notice ); QApplication::setOverrideCursor( Qt::WaitCursor ); const QSizeF dpi = Utils::realDpi(m_widget); qCDebug(OkularCoreDebug) << "Output DPI:" << dpi; m_generator->setDPI(dpi); Document::OpenResult openResult = Document::OpenError; if ( !isstdin ) { openResult = m_generator->loadDocumentWithPassword( docFile, m_pagesVector, password ); } else if ( !filedata.isEmpty() ) { if ( m_generator->hasFeature( Generator::ReadRawData ) ) { openResult = m_generator->loadDocumentFromDataWithPassword( filedata, m_pagesVector, password ); } else { m_tempFile = new QTemporaryFile(); if ( !m_tempFile->open() ) { delete m_tempFile; m_tempFile = nullptr; } else { m_tempFile->write( filedata ); QString tmpFileName = m_tempFile->fileName(); m_tempFile->close(); openResult = m_generator->loadDocumentWithPassword( tmpFileName, m_pagesVector, password ); } } } QApplication::restoreOverrideCursor(); if ( openResult != Document::OpenSuccess || m_pagesVector.size() <= 0 ) { m_generator->d_func()->m_document = nullptr; QObject::disconnect( m_generator, nullptr, m_parent, nullptr ); // TODO this is a bit of a hack, since basically means that // you can only call walletDataForFile after calling openDocument // but since in reality it's what happens I've decided not to refactor/break API // One solution is just kill walletDataForFile and make OpenResult be an object // where the wallet data is also returned when OpenNeedsPassword m_walletGenerator = m_generator; m_generator = nullptr; qDeleteAll( m_pagesVector ); m_pagesVector.clear(); delete m_tempFile; m_tempFile = nullptr; // TODO: emit a message telling the document is empty if ( openResult == Document::OpenSuccess ) openResult = Document::OpenError; } else { /* * Now that the documen is opened, the tab (if using tabs) is visible, which mean that * we can now connect the error reporting signal directly to the parent */ QObject::disconnect(errorToOpenErrorConnection); QObject::connect( m_generator, &Generator::error, m_parent, &Document::error ); } return openResult; } bool DocumentPrivate::savePageDocumentInfo( QTemporaryFile *infoFile, int what ) const { if ( infoFile->open() ) { // 1. Create DOM QDomDocument doc( QStringLiteral("documentInfo") ); QDomProcessingInstruction xmlPi = doc.createProcessingInstruction( QStringLiteral( "xml" ), QStringLiteral( "version=\"1.0\" encoding=\"utf-8\"" ) ); doc.appendChild( xmlPi ); QDomElement root = doc.createElement( QStringLiteral("documentInfo") ); doc.appendChild( root ); // 2.1. Save page attributes (bookmark state, annotations, ... ) to DOM QDomElement pageList = doc.createElement( QStringLiteral("pageList") ); root.appendChild( pageList ); // .... save pages that hold data QVector< Page * >::const_iterator pIt = m_pagesVector.constBegin(), pEnd = m_pagesVector.constEnd(); for ( ; pIt != pEnd; ++pIt ) (*pIt)->d->saveLocalContents( pageList, doc, PageItems( what ) ); // 3. Save DOM to XML file QString xml = doc.toString(); QTextStream os( infoFile ); os.setCodec( "UTF-8" ); os << xml; return true; } return false; } DocumentViewport DocumentPrivate::nextDocumentViewport() const { DocumentViewport ret = m_nextDocumentViewport; if ( !m_nextDocumentDestination.isEmpty() && m_generator ) { DocumentViewport vp( m_parent->metaData( QStringLiteral("NamedViewport"), m_nextDocumentDestination ).toString() ); if ( vp.isValid() ) { ret = vp; } } return ret; } void DocumentPrivate::performAddPageAnnotation( int page, Annotation * annotation ) { Okular::SaveInterface * iface = qobject_cast< Okular::SaveInterface * >( m_generator ); AnnotationProxy *proxy = iface ? iface->annotationProxy() : nullptr; // find out the page to attach annotation Page * kp = m_pagesVector[ page ]; if ( !m_generator || !kp ) return; // the annotation belongs already to a page if ( annotation->d_ptr->m_page ) return; // add annotation to the page kp->addAnnotation( annotation ); // tell the annotation proxy if ( proxy && proxy->supports(AnnotationProxy::Addition) ) proxy->notifyAddition( annotation, page ); // notify observers about the change notifyAnnotationChanges( page ); if ( annotation->flags() & Annotation::ExternallyDrawn ) { // Redraw everything, including ExternallyDrawn annotations refreshPixmaps( page ); } } void DocumentPrivate::performRemovePageAnnotation( int page, Annotation * annotation ) { Okular::SaveInterface * iface = qobject_cast< Okular::SaveInterface * >( m_generator ); AnnotationProxy *proxy = iface ? iface->annotationProxy() : nullptr; bool isExternallyDrawn; // find out the page Page * kp = m_pagesVector[ page ]; if ( !m_generator || !kp ) return; if ( annotation->flags() & Annotation::ExternallyDrawn ) isExternallyDrawn = true; else isExternallyDrawn = false; // try to remove the annotation if ( m_parent->canRemovePageAnnotation( annotation ) ) { // tell the annotation proxy if ( proxy && proxy->supports(AnnotationProxy::Removal) ) proxy->notifyRemoval( annotation, page ); kp->removeAnnotation( annotation ); // Also destroys the object // in case of success, notify observers about the change notifyAnnotationChanges( page ); if ( isExternallyDrawn ) { // Redraw everything, including ExternallyDrawn annotations refreshPixmaps( page ); } } } void DocumentPrivate::performModifyPageAnnotation( int page, Annotation * annotation, bool appearanceChanged ) { Okular::SaveInterface * iface = qobject_cast< Okular::SaveInterface * >( m_generator ); AnnotationProxy *proxy = iface ? iface->annotationProxy() : nullptr; // find out the page Page * kp = m_pagesVector[ page ]; if ( !m_generator || !kp ) return; // tell the annotation proxy if ( proxy && proxy->supports(AnnotationProxy::Modification) ) { proxy->notifyModification( annotation, page, appearanceChanged ); } // notify observers about the change notifyAnnotationChanges( page ); if ( appearanceChanged && (annotation->flags() & Annotation::ExternallyDrawn) ) { /* When an annotation is being moved, the generator will not render it. * Therefore there's no need to refresh pixmaps after the first time */ if ( annotation->flags() & (Annotation::BeingMoved | Annotation::BeingResized) ) { if ( m_annotationBeingModified ) return; else // First time: take note m_annotationBeingModified = true; } else { m_annotationBeingModified = false; } // Redraw everything, including ExternallyDrawn annotations qCDebug(OkularCoreDebug) << "Refreshing Pixmaps"; refreshPixmaps( page ); } } void DocumentPrivate::performSetAnnotationContents( const QString & newContents, Annotation *annot, int pageNumber ) { bool appearanceChanged = false; // Check if appearanceChanged should be true switch ( annot->subType() ) { // If it's an in-place TextAnnotation, set the inplace text case Okular::Annotation::AText: { Okular::TextAnnotation * txtann = static_cast< Okular::TextAnnotation * >( annot ); if ( txtann->textType() == Okular::TextAnnotation::InPlace ) { appearanceChanged = true; } break; } // If it's a LineAnnotation, check if caption text is visible case Okular::Annotation::ALine: { Okular::LineAnnotation * lineann = static_cast< Okular::LineAnnotation * >( annot ); if ( lineann->showCaption() ) appearanceChanged = true; break; } default: break; } // Set contents annot->setContents( newContents ); // Tell the document the annotation has been modified performModifyPageAnnotation( pageNumber, annot, appearanceChanged ); } void DocumentPrivate::recalculateForms() { const QVariant fco = m_parent->metaData(QStringLiteral("FormCalculateOrder")); const QVector formCalculateOrder = fco.value>(); foreach(int formId, formCalculateOrder) { for ( uint pageIdx = 0; pageIdx < m_parent->pages(); pageIdx++ ) { const Page *p = m_parent->page( pageIdx ); if (p) { bool pageNeedsRefresh = false; foreach( FormField *form, p->formFields() ) { if ( form->id() == formId ) { Action *action = form->additionalAction( FormField::CalculateField ); if (action) { FormFieldText *fft = dynamic_cast< FormFieldText * >( form ); std::shared_ptr event; QString oldVal; if ( fft ) { // Prepare text calculate event event = Event::createFormCalculateEvent( fft, m_pagesVector[pageIdx] ); if ( !m_scripter ) m_scripter = new Scripter( this ); m_scripter->setEvent( event.get() ); // The value maybe changed in javascript so save it first. oldVal = fft->text(); } m_parent->processAction( action ); if ( event && fft ) { // Update text field from calculate m_scripter->setEvent( nullptr ); const QString newVal = event->value().toString(); if ( newVal != oldVal ) { fft->setText( newVal ); if ( const Okular::Action *action = fft->additionalAction( Okular::FormField::FormatField ) ) { // The format action handles the refresh. m_parent->processFormatAction( action, fft ); } else { emit m_parent->refreshFormWidget( fft ); pageNeedsRefresh = true; } } } } else { qWarning() << "Form that is part of calculate order doesn't have a calculate action"; } } } if ( pageNeedsRefresh ) { refreshPixmaps( p->number() ); } } } } } void DocumentPrivate::saveDocumentInfo() const { if ( m_xmlFileName.isEmpty() ) return; QFile infoFile( m_xmlFileName ); qCDebug(OkularCoreDebug) << "About to save document info to" << m_xmlFileName; if (!infoFile.open( QIODevice::WriteOnly | QIODevice::Truncate)) { qCWarning(OkularCoreDebug) << "Failed to open docdata file" << m_xmlFileName; return; } // 1. Create DOM QDomDocument doc( QStringLiteral("documentInfo") ); QDomProcessingInstruction xmlPi = doc.createProcessingInstruction( QStringLiteral( "xml" ), QStringLiteral( "version=\"1.0\" encoding=\"utf-8\"" ) ); doc.appendChild( xmlPi ); QDomElement root = doc.createElement( QStringLiteral("documentInfo") ); root.setAttribute( QStringLiteral("url"), m_url.toDisplayString(QUrl::PreferLocalFile) ); doc.appendChild( root ); // 2.1. Save page attributes (bookmark state, annotations, ... ) to DOM // -> do this if there are not-yet-migrated annots or forms in docdata/ if ( m_docdataMigrationNeeded ) { QDomElement pageList = doc.createElement( QStringLiteral("pageList") ); root.appendChild( pageList ); // OriginalAnnotationPageItems and OriginalFormFieldPageItems tell to // store the same unmodified annotation list and form contents that we // read when we opened the file and ignore any change made by the user. // Since we don't store annotations and forms in docdata/ any more, this is // necessary to preserve annotations/forms that previous Okular version // had stored there. const PageItems saveWhat = AllPageItems | OriginalAnnotationPageItems | OriginalFormFieldPageItems; // .... save pages that hold data QVector< Page * >::const_iterator pIt = m_pagesVector.constBegin(), pEnd = m_pagesVector.constEnd(); for ( ; pIt != pEnd; ++pIt ) (*pIt)->d->saveLocalContents( pageList, doc, saveWhat ); } // 2.2. Save document info (current viewport, history, ... ) to DOM QDomElement generalInfo = doc.createElement( QStringLiteral("generalInfo") ); root.appendChild( generalInfo ); // create rotation node if ( m_rotation != Rotation0 ) { QDomElement rotationNode = doc.createElement( QStringLiteral("rotation") ); generalInfo.appendChild( rotationNode ); rotationNode.appendChild( doc.createTextNode( QString::number( (int)m_rotation ) ) ); } // ... save history up to OKULAR_HISTORY_SAVEDSTEPS viewports const auto currentViewportIterator = QLinkedList< DocumentViewport >::const_iterator(m_viewportIterator); QLinkedList< DocumentViewport >::const_iterator backIterator = currentViewportIterator; if ( backIterator != m_viewportHistory.constEnd() ) { // go back up to OKULAR_HISTORY_SAVEDSTEPS steps from the current viewportIterator int backSteps = OKULAR_HISTORY_SAVEDSTEPS; while ( backSteps-- && backIterator != m_viewportHistory.constBegin() ) --backIterator; // create history root node QDomElement historyNode = doc.createElement( QStringLiteral("history") ); generalInfo.appendChild( historyNode ); // add old[backIterator] and present[viewportIterator] items QLinkedList< DocumentViewport >::const_iterator endIt = currentViewportIterator; ++endIt; while ( backIterator != endIt ) { QString name = (backIterator == currentViewportIterator) ? QStringLiteral ("current") : QStringLiteral ("oldPage"); QDomElement historyEntry = doc.createElement( name ); historyEntry.setAttribute( QStringLiteral("viewport"), (*backIterator).toString() ); historyNode.appendChild( historyEntry ); ++backIterator; } } // create views root node QDomElement viewsNode = doc.createElement( QStringLiteral("views") ); generalInfo.appendChild( viewsNode ); for ( View *view : qAsConst(m_views) ) { QDomElement viewEntry = doc.createElement( QStringLiteral("view") ); viewEntry.setAttribute( QStringLiteral("name"), view->name() ); viewsNode.appendChild( viewEntry ); saveViewsInfo( view, viewEntry ); } // 3. Save DOM to XML file QString xml = doc.toString(); QTextStream os( &infoFile ); os.setCodec( "UTF-8" ); os << xml; infoFile.close(); } void DocumentPrivate::slotTimedMemoryCheck() { // [MEM] clean memory (for 'free mem dependent' profiles only) if ( SettingsCore::memoryLevel() != SettingsCore::EnumMemoryLevel::Low && m_allocatedPixmapsTotalMemory > 1024*1024 ) cleanupPixmapMemory(); } void DocumentPrivate::sendGeneratorPixmapRequest() { /* If the pixmap cache will have to be cleaned in order to make room for the * next request, get the distance from the current viewport of the page * whose pixmap will be removed. We will ignore preload requests for pages * that are at the same distance or farther */ const qulonglong memoryToFree = calculateMemoryToFree(); const int currentViewportPage = (*m_viewportIterator).pageNumber; int maxDistance = INT_MAX; // Default: No maximum if ( memoryToFree ) { AllocatedPixmap *pixmapToReplace = searchLowestPriorityPixmap( true ); if ( pixmapToReplace ) maxDistance = qAbs( pixmapToReplace->page - currentViewportPage ); } // find a request PixmapRequest * request = nullptr; m_pixmapRequestsMutex.lock(); while ( !m_pixmapRequestsStack.isEmpty() && !request ) { PixmapRequest * r = m_pixmapRequestsStack.last(); if (!r) { m_pixmapRequestsStack.pop_back(); continue; } QRect requestRect = r->isTile() ? r->normalizedRect().geometry( r->width(), r->height() ) : QRect( 0, 0, r->width(), r->height() ); TilesManager *tilesManager = r->d->tilesManager(); const double normalizedArea = r->normalizedRect().width() * r->normalizedRect().height(); // If it's a preload but the generator is not threaded no point in trying to preload if ( r->preload() && !m_generator->hasFeature( Generator::Threaded ) ) { m_pixmapRequestsStack.pop_back(); delete r; } // request only if page isn't already present and request has valid id else if ( ( !r->d->mForce && r->page()->hasPixmap( r->observer(), r->width(), r->height(), r->normalizedRect() ) ) || !m_observers.contains(r->observer()) ) { m_pixmapRequestsStack.pop_back(); delete r; } else if ( !r->d->mForce && r->preload() && qAbs( r->pageNumber() - currentViewportPage ) >= maxDistance ) { m_pixmapRequestsStack.pop_back(); //qCDebug(OkularCoreDebug) << "Ignoring request that doesn't fit in cache"; delete r; } // Ignore requests for pixmaps that are already being generated else if ( tilesManager && tilesManager->isRequesting( r->normalizedRect(), r->width(), r->height() ) ) { m_pixmapRequestsStack.pop_back(); delete r; } // If the requested area is above 8000000 pixels, and we're not rendering most of the page, switch on the tile manager else if ( !tilesManager && m_generator->hasFeature( Generator::TiledRendering ) && (long)r->width() * (long)r->height() > 8000000L && normalizedArea < 0.75 && normalizedArea != 0 ) { // if the image is too big. start using tiles qCDebug(OkularCoreDebug).nospace() << "Start using tiles on page " << r->pageNumber() << " (" << r->width() << "x" << r->height() << " px);"; // fill the tiles manager with the last rendered pixmap const QPixmap *pixmap = r->page()->_o_nearestPixmap( r->observer(), r->width(), r->height() ); if ( pixmap ) { tilesManager = new TilesManager( r->pageNumber(), pixmap->width(), pixmap->height(), r->page()->rotation() ); tilesManager->setPixmap( pixmap, NormalizedRect( 0, 0, 1, 1 ), true /*isPartialPixmap*/ ); tilesManager->setSize( r->width(), r->height() ); } else { // create new tiles manager tilesManager = new TilesManager( r->pageNumber(), r->width(), r->height(), r->page()->rotation() ); } tilesManager->setRequest( r->normalizedRect(), r->width(), r->height() ); r->page()->deletePixmap( r->observer() ); r->page()->d->setTilesManager( r->observer(), tilesManager ); r->setTile( true ); // Change normalizedRect to the smallest rect that contains all // visible tiles. if ( !r->normalizedRect().isNull() ) { NormalizedRect tilesRect; const QList tiles = tilesManager->tilesAt( r->normalizedRect(), TilesManager::TerminalTile ); QList::const_iterator tIt = tiles.constBegin(), tEnd = tiles.constEnd(); while ( tIt != tEnd ) { Tile tile = *tIt; if ( tilesRect.isNull() ) tilesRect = tile.rect(); else tilesRect |= tile.rect(); ++tIt; } r->setNormalizedRect( tilesRect ); request = r; } else { // Discard request if normalizedRect is null. This happens in // preload requests issued by PageView if the requested page is // not visible and the user has just switched from a non-tiled // zoom level to a tiled one m_pixmapRequestsStack.pop_back(); delete r; } } // If the requested area is below 6000000 pixels, switch off the tile manager else if ( tilesManager && (long)r->width() * (long)r->height() < 6000000L ) { qCDebug(OkularCoreDebug).nospace() << "Stop using tiles on page " << r->pageNumber() << " (" << r->width() << "x" << r->height() << " px);"; // page is too small. stop using tiles. r->page()->deletePixmap( r->observer() ); r->setTile( false ); request = r; } else if ( (long)requestRect.width() * (long)requestRect.height() > 200000000L && (SettingsCore::memoryLevel() != SettingsCore::EnumMemoryLevel::Greedy ) ) { m_pixmapRequestsStack.pop_back(); if ( !m_warnedOutOfMemory ) { qCWarning(OkularCoreDebug).nospace() << "Running out of memory on page " << r->pageNumber() << " (" << r->width() << "x" << r->height() << " px);"; qCWarning(OkularCoreDebug) << "this message will be reported only once."; m_warnedOutOfMemory = true; } delete r; } else { request = r; } } // if no request found (or already generated), return if ( !request ) { m_pixmapRequestsMutex.unlock(); return; } // [MEM] preventive memory freeing qulonglong pixmapBytes = 0; TilesManager * tm = request->d->tilesManager(); if ( tm ) pixmapBytes = tm->totalMemory(); else pixmapBytes = 4 * request->width() * request->height(); if ( pixmapBytes > (1024 * 1024) ) cleanupPixmapMemory( memoryToFree /* previously calculated value */ ); // submit the request to the generator if ( m_generator->canGeneratePixmap() ) { QRect requestRect = !request->isTile() ? QRect(0, 0, request->width(), request->height() ) : request->normalizedRect().geometry( request->width(), request->height() ); qCDebug(OkularCoreDebug).nospace() << "sending request observer=" << request->observer() << " " <pageNumber() << " async == " << request->asynchronous() << " isTile == " << request->isTile(); m_pixmapRequestsStack.removeAll ( request ); if ( tm ) tm->setRequest( request->normalizedRect(), request->width(), request->height() ); if ( (int)m_rotation % 2 ) request->d->swap(); if ( m_rotation != Rotation0 && !request->normalizedRect().isNull() ) request->setNormalizedRect( TilesManager::fromRotatedRect( request->normalizedRect(), m_rotation ) ); // If set elsewhere we already know we want it to be partial if ( !request->partialUpdatesWanted() ) { request->setPartialUpdatesWanted( request->asynchronous() && !request->page()->hasPixmap( request->observer() ) ); } // we always have to unlock _before_ the generatePixmap() because // a sync generation would end with requestDone() -> deadlock, and // we can not really know if the generator can do async requests m_executingPixmapRequests.push_back( request ); m_pixmapRequestsMutex.unlock(); m_generator->generatePixmap( request ); } else { m_pixmapRequestsMutex.unlock(); // pino (7/4/2006): set the polling interval from 10 to 30 QTimer::singleShot( 30, m_parent, [this] { sendGeneratorPixmapRequest(); } ); } } void DocumentPrivate::rotationFinished( int page, Okular::Page *okularPage ) { Okular::Page *wantedPage = m_pagesVector.value( page, 0 ); if ( !wantedPage || wantedPage != okularPage ) return; foreach(DocumentObserver *o, m_observers) o->notifyPageChanged( page, DocumentObserver::Pixmap | DocumentObserver::Annotations ); } void DocumentPrivate::slotFontReadingProgress( int page ) { emit m_parent->fontReadingProgress( page ); if ( page >= (int)m_parent->pages() - 1 ) { emit m_parent->fontReadingEnded(); m_fontThread = nullptr; m_fontsCached = true; } } void DocumentPrivate::fontReadingGotFont( const Okular::FontInfo& font ) { // Try to avoid duplicate fonts if (m_fontsCache.indexOf(font) == -1) { m_fontsCache.append( font ); emit m_parent->gotFont( font ); } } void DocumentPrivate::slotGeneratorConfigChanged() { if ( !m_generator ) return; // reparse generator config and if something changed clear Pages bool configchanged = false; QHash< QString, GeneratorInfo >::iterator it = m_loadedGenerators.begin(), itEnd = m_loadedGenerators.end(); for ( ; it != itEnd; ++it ) { Okular::ConfigInterface * iface = generatorConfig( it.value() ); if ( iface ) { bool it_changed = iface->reparseConfig(); if ( it_changed && ( m_generator == it.value().generator ) ) configchanged = true; } } if ( configchanged ) { // invalidate pixmaps QVector::const_iterator it = m_pagesVector.constBegin(), end = m_pagesVector.constEnd(); for ( ; it != end; ++it ) { (*it)->deletePixmaps(); } // [MEM] remove allocation descriptors qDeleteAll( m_allocatedPixmaps ); m_allocatedPixmaps.clear(); m_allocatedPixmapsTotalMemory = 0; // send reload signals to observers foreachObserverD( notifyContentsCleared( DocumentObserver::Pixmap ) ); } // free memory if in 'low' profile if ( SettingsCore::memoryLevel() == SettingsCore::EnumMemoryLevel::Low && !m_allocatedPixmaps.isEmpty() && !m_pagesVector.isEmpty() ) cleanupPixmapMemory(); } void DocumentPrivate::refreshPixmaps( int pageNumber ) { Page* page = m_pagesVector.value( pageNumber, 0 ); if ( !page ) return; QMap< DocumentObserver*, PagePrivate::PixmapObject >::ConstIterator it = page->d->m_pixmaps.constBegin(), itEnd = page->d->m_pixmaps.constEnd(); QVector< Okular::PixmapRequest * > pixmapsToRequest; for ( ; it != itEnd; ++it ) { const QSize size = (*it).m_pixmap->size(); PixmapRequest * p = new PixmapRequest( it.key(), pageNumber, size.width() / qApp->devicePixelRatio(), size.height() / qApp->devicePixelRatio(), 1, PixmapRequest::Asynchronous ); p->d->mForce = true; pixmapsToRequest << p; } // Need to do this ↑↓ in two steps since requestPixmaps can end up calling cancelRenderingBecauseOf // which changes m_pixmaps and thus breaks the loop above for ( PixmapRequest *pr : qAsConst( pixmapsToRequest ) ) { QLinkedList< Okular::PixmapRequest * > requestedPixmaps; requestedPixmaps.push_back( pr ); m_parent->requestPixmaps( requestedPixmaps, Okular::Document::NoOption ); } for (DocumentObserver *observer : qAsConst(m_observers)) { QLinkedList< Okular::PixmapRequest * > requestedPixmaps; TilesManager *tilesManager = page->d->tilesManager( observer ); if ( tilesManager ) { tilesManager->markDirty(); PixmapRequest * p = new PixmapRequest( observer, pageNumber, tilesManager->width() / qApp->devicePixelRatio(), tilesManager->height() / qApp->devicePixelRatio(), 1, PixmapRequest::Asynchronous ); // Get the visible page rect NormalizedRect visibleRect; QVector< Okular::VisiblePageRect * >::const_iterator vIt = m_pageRects.constBegin(), vEnd = m_pageRects.constEnd(); for ( ; vIt != vEnd; ++vIt ) { if ( (*vIt)->pageNumber == pageNumber ) { visibleRect = (*vIt)->rect; break; } } if ( !visibleRect.isNull() ) { p->setNormalizedRect( visibleRect ); p->setTile( true ); p->d->mForce = true; requestedPixmaps.push_back( p ); } else { delete p; } } m_parent->requestPixmaps( requestedPixmaps, Okular::Document::NoOption ); } } void DocumentPrivate::_o_configChanged() { // free text pages if needed calculateMaxTextPages(); while (m_allocatedTextPagesFifo.count() > m_maxAllocatedTextPages) { int pageToKick = m_allocatedTextPagesFifo.takeFirst(); m_pagesVector.at(pageToKick)->setTextPage( nullptr ); // deletes the textpage } } void DocumentPrivate::doContinueDirectionMatchSearch(void *doContinueDirectionMatchSearchStruct) { DoContinueDirectionMatchSearchStruct *searchStruct = static_cast(doContinueDirectionMatchSearchStruct); RunningSearch *search = m_searches.value(searchStruct->searchID); if ((m_searchCancelled && !searchStruct->match) || !search) { // if the user cancelled but he just got a match, give him the match! QApplication::restoreOverrideCursor(); if (search) search->isCurrentlySearching = false; emit m_parent->searchFinished( searchStruct->searchID, Document::SearchCancelled ); delete searchStruct->pagesToNotify; delete searchStruct; return; } const bool forward = search->cachedType == Document::NextMatch; bool doContinue = false; // if no match found, loop through the whole doc, starting from currentPage if ( !searchStruct->match ) { const int pageCount = m_pagesVector.count(); if (search->pagesDone < pageCount) { doContinue = true; if ( searchStruct->currentPage >= pageCount ) { searchStruct->currentPage = 0; emit m_parent->notice(i18n("Continuing search from beginning"), 3000); } else if ( searchStruct->currentPage < 0 ) { searchStruct->currentPage = pageCount - 1; emit m_parent->notice(i18n("Continuing search from bottom"), 3000); } } } if (doContinue) { // get page Page * page = m_pagesVector[ searchStruct->currentPage ]; // request search page if needed if ( !page->hasTextPage() ) m_parent->requestTextPage( page->number() ); // if found a match on the current page, end the loop searchStruct->match = page->findText( searchStruct->searchID, search->cachedString, forward ? FromTop : FromBottom, search->cachedCaseSensitivity ); if ( !searchStruct->match ) { if (forward) searchStruct->currentPage++; else searchStruct->currentPage--; search->pagesDone++; } else { search->pagesDone = 1; } // Both of the previous if branches need to call doContinueDirectionMatchSearch QTimer::singleShot(0, m_parent, [this, searchStruct] { doContinueDirectionMatchSearch(searchStruct); }); } else { doProcessSearchMatch( searchStruct->match, search, searchStruct->pagesToNotify, searchStruct->currentPage, searchStruct->searchID, search->cachedViewportMove, search->cachedColor ); delete searchStruct; } } void DocumentPrivate::doProcessSearchMatch( RegularAreaRect *match, RunningSearch *search, QSet< int > *pagesToNotify, int currentPage, int searchID, bool moveViewport, const QColor & color ) { // reset cursor to previous shape QApplication::restoreOverrideCursor(); bool foundAMatch = false; search->isCurrentlySearching = false; // if a match has been found.. if ( match ) { // update the RunningSearch structure adding this match.. foundAMatch = true; search->continueOnPage = currentPage; search->continueOnMatch = *match; search->highlightedPages.insert( currentPage ); // ..add highlight to the page.. m_pagesVector[ currentPage ]->d->setHighlight( searchID, match, color ); // ..queue page for notifying changes.. pagesToNotify->insert( currentPage ); // Create a normalized rectangle around the search match that includes a 5% buffer on all sides. const Okular::NormalizedRect matchRectWithBuffer = Okular::NormalizedRect( match->first().left - 0.05, match->first().top - 0.05, match->first().right + 0.05, match->first().bottom + 0.05 ); const bool matchRectFullyVisible = isNormalizedRectangleFullyVisible( matchRectWithBuffer, currentPage ); // ..move the viewport to show the first of the searched word sequence centered if ( moveViewport && !matchRectFullyVisible ) { DocumentViewport searchViewport( currentPage ); searchViewport.rePos.enabled = true; searchViewport.rePos.normalizedX = (match->first().left + match->first().right) / 2.0; searchViewport.rePos.normalizedY = (match->first().top + match->first().bottom) / 2.0; m_parent->setViewport( searchViewport, nullptr, true ); } delete match; } // notify observers about highlights changes foreach(int pageNumber, *pagesToNotify) foreach(DocumentObserver *observer, m_observers) observer->notifyPageChanged( pageNumber, DocumentObserver::Highlights ); if (foundAMatch) emit m_parent->searchFinished( searchID, Document::MatchFound ); else emit m_parent->searchFinished( searchID, Document::NoMatchFound ); delete pagesToNotify; } void DocumentPrivate::doContinueAllDocumentSearch(void *pagesToNotifySet, void *pageMatchesMap, int currentPage, int searchID) { QMap< Page *, QVector > *pageMatches = static_cast< QMap< Page *, QVector > * >(pageMatchesMap); QSet< int > *pagesToNotify = static_cast< QSet< int > * >( pagesToNotifySet ); RunningSearch *search = m_searches.value(searchID); if (m_searchCancelled || !search) { typedef QVector MatchesVector; QApplication::restoreOverrideCursor(); if (search) search->isCurrentlySearching = false; emit m_parent->searchFinished( searchID, Document::SearchCancelled ); foreach(const MatchesVector &mv, *pageMatches) qDeleteAll(mv); delete pageMatches; delete pagesToNotify; return; } if (currentPage < m_pagesVector.count()) { // get page (from the first to the last) Page *page = m_pagesVector.at(currentPage); int pageNumber = page->number(); // redundant? is it == currentPage ? // request search page if needed if ( !page->hasTextPage() ) m_parent->requestTextPage( pageNumber ); // loop on a page adding highlights for all found items RegularAreaRect * lastMatch = nullptr; while ( true ) { if ( lastMatch ) lastMatch = page->findText( searchID, search->cachedString, NextResult, search->cachedCaseSensitivity, lastMatch ); else lastMatch = page->findText( searchID, search->cachedString, FromTop, search->cachedCaseSensitivity ); if ( !lastMatch ) break; // add highlight rect to the matches map (*pageMatches)[page].append(lastMatch); } delete lastMatch; QTimer::singleShot(0, m_parent, [this, pagesToNotifySet, pageMatches, currentPage, searchID] { doContinueAllDocumentSearch(pagesToNotifySet, pageMatches, currentPage + 1, searchID); }); } else { // reset cursor to previous shape QApplication::restoreOverrideCursor(); search->isCurrentlySearching = false; bool foundAMatch = pageMatches->count() != 0; QMap< Page *, QVector >::const_iterator it, itEnd; it = pageMatches->constBegin(); itEnd = pageMatches->constEnd(); for ( ; it != itEnd; ++it) { foreach(RegularAreaRect *match, it.value()) { it.key()->d->setHighlight( searchID, match, search->cachedColor ); delete match; } search->highlightedPages.insert( it.key()->number() ); pagesToNotify->insert( it.key()->number() ); } foreach(DocumentObserver *observer, m_observers) observer->notifySetup( m_pagesVector, 0 ); // notify observers about highlights changes foreach(int pageNumber, *pagesToNotify) foreach(DocumentObserver *observer, m_observers) observer->notifyPageChanged( pageNumber, DocumentObserver::Highlights ); if (foundAMatch) emit m_parent->searchFinished(searchID, Document::MatchFound ); else emit m_parent->searchFinished( searchID, Document::NoMatchFound ); delete pageMatches; delete pagesToNotify; } } void DocumentPrivate::doContinueGooglesDocumentSearch(void *pagesToNotifySet, void *pageMatchesMap, int currentPage, int searchID, const QStringList & words) { typedef QPair MatchColor; QMap< Page *, QVector > *pageMatches = static_cast< QMap< Page *, QVector > * >(pageMatchesMap); QSet< int > *pagesToNotify = static_cast< QSet< int > * >( pagesToNotifySet ); RunningSearch *search = m_searches.value(searchID); if (m_searchCancelled || !search) { typedef QVector MatchesVector; QApplication::restoreOverrideCursor(); if (search) search->isCurrentlySearching = false; emit m_parent->searchFinished( searchID, Document::SearchCancelled ); foreach(const MatchesVector &mv, *pageMatches) { foreach(const MatchColor &mc, mv) delete mc.first; } delete pageMatches; delete pagesToNotify; return; } const int wordCount = words.count(); const int hueStep = (wordCount > 1) ? (60 / (wordCount - 1)) : 60; int baseHue, baseSat, baseVal; search->cachedColor.getHsv( &baseHue, &baseSat, &baseVal ); if (currentPage < m_pagesVector.count()) { // get page (from the first to the last) Page *page = m_pagesVector.at(currentPage); int pageNumber = page->number(); // redundant? is it == currentPage ? // request search page if needed if ( !page->hasTextPage() ) m_parent->requestTextPage( pageNumber ); // loop on a page adding highlights for all found items bool allMatched = wordCount > 0, anyMatched = false; for ( int w = 0; w < wordCount; w++ ) { const QString &word = words[ w ]; int newHue = baseHue - w * hueStep; if ( newHue < 0 ) newHue += 360; QColor wordColor = QColor::fromHsv( newHue, baseSat, baseVal ); RegularAreaRect * lastMatch = nullptr; // add all highlights for current word bool wordMatched = false; while ( true ) { if ( lastMatch ) lastMatch = page->findText( searchID, word, NextResult, search->cachedCaseSensitivity, lastMatch ); else lastMatch = page->findText( searchID, word, FromTop, search->cachedCaseSensitivity); if ( !lastMatch ) break; // add highligh rect to the matches map (*pageMatches)[page].append(MatchColor(lastMatch, wordColor)); wordMatched = true; } allMatched = allMatched && wordMatched; anyMatched = anyMatched || wordMatched; } // if not all words are present in page, remove partial highlights const bool matchAll = search->cachedType == Document::GoogleAll; if ( !allMatched && matchAll ) { QVector &matches = (*pageMatches)[page]; foreach(const MatchColor &mc, matches) delete mc.first; pageMatches->remove(page); } QTimer::singleShot(0, m_parent, [this, pagesToNotifySet, pageMatches, currentPage, searchID, words] { doContinueGooglesDocumentSearch(pagesToNotifySet, pageMatches, currentPage + 1, searchID, words); }); } else { // reset cursor to previous shape QApplication::restoreOverrideCursor(); search->isCurrentlySearching = false; bool foundAMatch = pageMatches->count() != 0; QMap< Page *, QVector >::const_iterator it, itEnd; it = pageMatches->constBegin(); itEnd = pageMatches->constEnd(); for ( ; it != itEnd; ++it) { foreach(const MatchColor &mc, it.value()) { it.key()->d->setHighlight( searchID, mc.first, mc.second ); delete mc.first; } search->highlightedPages.insert( it.key()->number() ); pagesToNotify->insert( it.key()->number() ); } // send page lists to update observers (since some filter on bookmarks) foreach(DocumentObserver *observer, m_observers) observer->notifySetup( m_pagesVector, 0 ); // notify observers about highlights changes foreach(int pageNumber, *pagesToNotify) foreach(DocumentObserver *observer, m_observers) observer->notifyPageChanged( pageNumber, DocumentObserver::Highlights ); if (foundAMatch) emit m_parent->searchFinished( searchID, Document::MatchFound ); else emit m_parent->searchFinished( searchID, Document::NoMatchFound ); delete pageMatches; delete pagesToNotify; } } QVariant DocumentPrivate::documentMetaData( const Generator::DocumentMetaDataKey key, const QVariant &option ) const { switch ( key ) { case Generator::PaperColorMetaData: { bool giveDefault = option.toBool(); QColor color; if ( ( SettingsCore::renderMode() == SettingsCore::EnumRenderMode::Paper ) && SettingsCore::changeColors() ) { color = SettingsCore::paperColor(); } else if ( giveDefault ) { color = Qt::white; } return color; } break; case Generator::TextAntialiasMetaData: switch ( SettingsCore::textAntialias() ) { case SettingsCore::EnumTextAntialias::Enabled: return true; break; #if 0 case Settings::EnumTextAntialias::UseKDESettings: // TODO: read the KDE configuration return true; break; #endif case SettingsCore::EnumTextAntialias::Disabled: return false; break; } break; case Generator::GraphicsAntialiasMetaData: switch ( SettingsCore::graphicsAntialias() ) { case SettingsCore::EnumGraphicsAntialias::Enabled: return true; break; case SettingsCore::EnumGraphicsAntialias::Disabled: return false; break; } break; case Generator::TextHintingMetaData: switch ( SettingsCore::textHinting() ) { case SettingsCore::EnumTextHinting::Enabled: return true; break; case SettingsCore::EnumTextHinting::Disabled: return false; break; } break; } return QVariant(); } bool DocumentPrivate::isNormalizedRectangleFullyVisible( const Okular::NormalizedRect & rectOfInterest, int rectPage ) { bool rectFullyVisible = false; const QVector & visibleRects = m_parent->visiblePageRects(); QVector::const_iterator vEnd = visibleRects.end(); QVector::const_iterator vIt = visibleRects.begin(); for ( ; ( vIt != vEnd ) && !rectFullyVisible; ++vIt ) { if ( (*vIt)->pageNumber == rectPage && (*vIt)->rect.contains( rectOfInterest.left, rectOfInterest.top ) && (*vIt)->rect.contains( rectOfInterest.right, rectOfInterest.bottom ) ) { rectFullyVisible = true; } } return rectFullyVisible; } struct pdfsyncpoint { QString file; qlonglong x; qlonglong y; int row; int column; int page; }; void DocumentPrivate::loadSyncFile( const QString & filePath ) { QFile f( filePath + QLatin1String( "sync" ) ); if ( !f.open( QIODevice::ReadOnly ) ) return; QTextStream ts( &f ); // first row: core name of the pdf output const QString coreName = ts.readLine(); // second row: version string, in the form 'Version %u' const QString versionstr = ts.readLine(); // anchor the pattern with \A and \z to match the entire subject string // TODO: with Qt 5.12 QRegularExpression::anchoredPattern() can be used instead QRegularExpression versionre( QStringLiteral("\\AVersion \\d+\\z") , QRegularExpression::CaseInsensitiveOption ); QRegularExpressionMatch match = versionre.match( versionstr ); if ( !match.hasMatch() ) { return; } QHash points; QStack fileStack; int currentpage = -1; const QLatin1String texStr( ".tex" ); const QChar spaceChar = QChar::fromLatin1( ' ' ); fileStack.push( coreName + texStr ); const QSizeF dpi = m_generator->dpi(); QString line; while ( !ts.atEnd() ) { line = ts.readLine(); const QStringList tokens = line.split( spaceChar, QString::SkipEmptyParts ); const int tokenSize = tokens.count(); if ( tokenSize < 1 ) continue; if ( tokens.first() == QLatin1String( "l" ) && tokenSize >= 3 ) { int id = tokens.at( 1 ).toInt(); QHash::const_iterator it = points.constFind( id ); if ( it == points.constEnd() ) { pdfsyncpoint pt; pt.x = 0; pt.y = 0; pt.row = tokens.at( 2 ).toInt(); pt.column = 0; // TODO pt.page = -1; pt.file = fileStack.top(); points[ id ] = pt; } } else if ( tokens.first() == QLatin1String( "s" ) && tokenSize >= 2 ) { currentpage = tokens.at( 1 ).toInt() - 1; } else if ( tokens.first() == QLatin1String( "p*" ) && tokenSize >= 4 ) { // TODO qCDebug(OkularCoreDebug) << "PdfSync: 'p*' line ignored"; } else if ( tokens.first() == QLatin1String( "p" ) && tokenSize >= 4 ) { int id = tokens.at( 1 ).toInt(); QHash::iterator it = points.find( id ); if ( it != points.end() ) { it->x = tokens.at( 2 ).toInt(); it->y = tokens.at( 3 ).toInt(); it->page = currentpage; } } else if ( line.startsWith( QLatin1Char( '(' ) ) && tokenSize == 1 ) { QString newfile = line; // chop the leading '(' newfile.remove( 0, 1 ); if ( !newfile.endsWith( texStr ) ) { newfile += texStr; } fileStack.push( newfile ); } else if ( line == QLatin1String( ")" ) ) { if ( !fileStack.isEmpty() ) { fileStack.pop(); } else qCDebug(OkularCoreDebug) << "PdfSync: going one level down too much"; } else qCDebug(OkularCoreDebug).nospace() << "PdfSync: unknown line format: '" << line << "'"; } QVector< QLinkedList< Okular::SourceRefObjectRect * > > refRects( m_pagesVector.size() ); for ( const pdfsyncpoint &pt : qAsConst(points) ) { // drop pdfsync points not completely valid if ( pt.page < 0 || pt.page >= m_pagesVector.size() ) continue; // magic numbers for TeX's RSU's (Ridiculously Small Units) conversion to pixels Okular::NormalizedPoint p( ( pt.x * dpi.width() ) / ( 72.27 * 65536.0 * m_pagesVector[pt.page]->width() ), ( pt.y * dpi.height() ) / ( 72.27 * 65536.0 * m_pagesVector[pt.page]->height() ) ); QString file = pt.file; Okular::SourceReference * sourceRef = new Okular::SourceReference( file, pt.row, pt.column ); refRects[ pt.page ].append( new Okular::SourceRefObjectRect( p, sourceRef ) ); } for ( int i = 0; i < refRects.size(); ++i ) if ( !refRects.at(i).isEmpty() ) m_pagesVector[i]->setSourceReferences( refRects.at(i) ); } void DocumentPrivate::clearAndWaitForRequests() { m_pixmapRequestsMutex.lock(); QLinkedList< PixmapRequest * >::const_iterator sIt = m_pixmapRequestsStack.constBegin(); QLinkedList< PixmapRequest * >::const_iterator sEnd = m_pixmapRequestsStack.constEnd(); for ( ; sIt != sEnd; ++sIt ) delete *sIt; m_pixmapRequestsStack.clear(); m_pixmapRequestsMutex.unlock(); QEventLoop loop; bool startEventLoop = false; do { m_pixmapRequestsMutex.lock(); startEventLoop = !m_executingPixmapRequests.isEmpty(); if ( m_generator->hasFeature( Generator::SupportsCancelling ) ) { for ( PixmapRequest *executingRequest : qAsConst( m_executingPixmapRequests ) ) executingRequest->d->mShouldAbortRender = 1; if ( m_generator->d_ptr->mTextPageGenerationThread ) m_generator->d_ptr->mTextPageGenerationThread->abortExtraction(); } m_pixmapRequestsMutex.unlock(); if ( startEventLoop ) { m_closingLoop = &loop; loop.exec(); m_closingLoop = nullptr; } } while ( startEventLoop ); } int DocumentPrivate::findFieldPageNumber( Okular::FormField *field ) { // Lookup the page of the FormField int foundPage = -1; for ( uint pageIdx = 0, nPages = m_parent->pages(); pageIdx < nPages; pageIdx++ ) { const Page *p = m_parent->page( pageIdx ); if ( p && p->formFields().contains( field ) ) { foundPage = static_cast< int >( pageIdx ); break; } } return foundPage; } void DocumentPrivate::executeScriptEvent( const std::shared_ptr< Event > &event, const Okular::ScriptAction * linkscript ) { if ( !m_scripter ) { m_scripter = new Scripter( this ); } m_scripter->setEvent( event.get() ); m_scripter->execute( linkscript->scriptType(), linkscript->script() ); // Clear out the event after execution m_scripter->setEvent( nullptr ); } Document::Document( QWidget *widget ) : QObject( nullptr ), d( new DocumentPrivate( this ) ) { d->m_widget = widget; d->m_bookmarkManager = new BookmarkManager( d ); d->m_viewportIterator = d->m_viewportHistory.insert( d->m_viewportHistory.end(), DocumentViewport() ); d->m_undoStack = new QUndoStack(this); connect( SettingsCore::self(), &SettingsCore::configChanged, this, [this] { d->_o_configChanged(); } ); connect(d->m_undoStack, &QUndoStack::canUndoChanged, this, &Document::canUndoChanged); connect(d->m_undoStack, &QUndoStack::canRedoChanged, this, &Document::canRedoChanged); connect(d->m_undoStack, &QUndoStack::cleanChanged, this, &Document::undoHistoryCleanChanged); qRegisterMetaType(); } Document::~Document() { // delete generator, pages, and related stuff closeDocument(); QSet< View * >::const_iterator viewIt = d->m_views.constBegin(), viewEnd = d->m_views.constEnd(); for ( ; viewIt != viewEnd; ++viewIt ) { View *v = *viewIt; v->d_func()->document = nullptr; } // delete the bookmark manager delete d->m_bookmarkManager; // delete the loaded generators QHash< QString, GeneratorInfo >::const_iterator it = d->m_loadedGenerators.constBegin(), itEnd = d->m_loadedGenerators.constEnd(); for ( ; it != itEnd; ++it ) d->unloadGenerator( it.value() ); d->m_loadedGenerators.clear(); // delete the private structure delete d; } QString DocumentPrivate::docDataFileName(const QUrl &url, qint64 document_size) { QString fn = url.fileName(); fn = QString::number( document_size ) + QLatin1Char('.') + fn + QStringLiteral(".xml"); QString docdataDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/okular/docdata"); // make sure that the okular/docdata/ directory exists (probably this used to be handled by KStandardDirs) if (!QFileInfo::exists(docdataDir)) { qCDebug(OkularCoreDebug) << "creating docdata folder" << docdataDir; QDir().mkpath(docdataDir); } QString newokularfile = docdataDir + QLatin1Char('/') + fn; // we don't want to accidentally migrate old files when running unit tests if (!QFile::exists( newokularfile ) && !QStandardPaths::isTestModeEnabled()) { // see if an KDE4 file still exists static Kdelibs4Migration k4migration; QString oldfile = k4migration.locateLocal("data", QStringLiteral("okular/docdata/") + fn); if (oldfile.isEmpty()) { oldfile = k4migration.locateLocal("data", QStringLiteral("kpdf/") + fn); } if ( !oldfile.isEmpty() && QFile::exists( oldfile ) ) { // ### copy or move? if ( !QFile::copy( oldfile, newokularfile ) ) return QString(); } } return newokularfile; } QVector DocumentPrivate::availableGenerators() { static QVector result; if (result.isEmpty()) { result = KPluginLoader::findPlugins( QStringLiteral ( "okular/generators" ) ); } return result; } KPluginMetaData DocumentPrivate::generatorForMimeType(const QMimeType& type, QWidget* widget, const QVector &triedOffers) { // First try to find an exact match, and then look for more general ones (e. g. the plain text one) // Ideally we would rank these by "closeness", but that might be overdoing it const QVector available = availableGenerators(); QVector offers; QVector exactMatches; QMimeDatabase mimeDatabase; for (const KPluginMetaData& md : available) { if (triedOffers.contains(md)) continue; const QStringList mimetypes = md.mimeTypes(); for (const QString &supported : mimetypes) { QMimeType mimeType = mimeDatabase.mimeTypeForName(supported); if (mimeType == type && !exactMatches.contains(md)) { exactMatches << md; } if (type.inherits(supported) && !offers.contains(md)) { offers << md; } } } if (!exactMatches.isEmpty()) { offers = exactMatches; } if (offers.isEmpty()) { return KPluginMetaData(); } int hRank=0; // best ranked offer search int offercount = offers.size(); if (offercount > 1) { // sort the offers: the offers with an higher priority come before auto cmp = [](const KPluginMetaData& s1, const KPluginMetaData& s2) { const QString property = QStringLiteral("X-KDE-Priority"); return s1.rawData()[property].toInt() > s2.rawData()[property].toInt(); }; std::stable_sort(offers.begin(), offers.end(), cmp); if (SettingsCore::chooseGenerators()) { QStringList list; for (int i = 0; i < offercount; ++i) { list << offers.at(i).pluginId(); } ChooseEngineDialog choose(list, type, widget); if (choose.exec() == QDialog::Rejected) return KPluginMetaData(); hRank = choose.selectedGenerator(); } } Q_ASSERT(hRank < offers.size()); return offers.at(hRank); } Document::OpenResult Document::openDocument(const QString & docFile, const QUrl &url, const QMimeType &_mime, const QString & password ) { QMimeDatabase db; QMimeType mime = _mime; QByteArray filedata; int fd = -1; if (url.scheme() == QLatin1String("fd")) { bool ok; fd = url.path().midRef(1).toInt(&ok); if (!ok) { return OpenError; } } else if (url.fileName() == QLatin1String( "-" )) { fd = 0; } bool triedMimeFromFileContent = false; if ( fd < 0 ) { if ( !mime.isValid() ) return OpenError; d->m_url = url; d->m_docFileName = docFile; if ( !d->updateMetadataXmlNameAndDocSize() ) return OpenError; } else { QFile qstdin; const bool ret = qstdin.open( fd, QIODevice::ReadOnly, QFileDevice::AutoCloseHandle ); if (!ret) { qWarning() << "failed to read" << url << filedata; return OpenError; } filedata = qstdin.readAll(); mime = db.mimeTypeForData( filedata ); if ( !mime.isValid() || mime.isDefault() ) return OpenError; d->m_docSize = filedata.size(); triedMimeFromFileContent = true; } const bool fromFileDescriptor = fd >= 0; // 0. load Generator // request only valid non-disabled plugins suitable for the mimetype KPluginMetaData offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget); if ( !offer.isValid() && !triedMimeFromFileContent ) { QMimeType newmime = db.mimeTypeForFile(docFile, QMimeDatabase::MatchContent); triedMimeFromFileContent = true; if ( newmime != mime ) { mime = newmime; offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget); } if ( !offer.isValid() ) { // There's still no offers, do a final mime search based on the filename // We need this because sometimes (e.g. when downloading from a webserver) the mimetype we // use is the one fed by the server, that may be wrong newmime = db.mimeTypeForUrl( url ); if ( !newmime.isDefault() && newmime != mime ) { mime = newmime; offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget); } } } if (!offer.isValid()) { d->m_openError = i18n( "Can not find a plugin which is able to handle the document being passed." ); emit error( d->m_openError, -1 ); qCWarning(OkularCoreDebug).nospace() << "No plugin for mimetype '" << mime.name() << "'."; return OpenError; } // 1. load Document OpenResult openResult = d->openDocumentInternal( offer, fromFileDescriptor, docFile, filedata, password ); if ( openResult == OpenError ) { QVector triedOffers; triedOffers << offer; offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget, triedOffers); while ( offer.isValid() ) { openResult = d->openDocumentInternal( offer, fromFileDescriptor, docFile, filedata, password ); if ( openResult == OpenError ) { triedOffers << offer; offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget, triedOffers); } else break; } if (openResult == OpenError && !triedMimeFromFileContent ) { QMimeType newmime = db.mimeTypeForFile(docFile, QMimeDatabase::MatchContent); triedMimeFromFileContent = true; if ( newmime != mime ) { mime = newmime; offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget, triedOffers); while ( offer.isValid() ) { openResult = d->openDocumentInternal( offer, fromFileDescriptor, docFile, filedata, password ); if ( openResult == OpenError ) { triedOffers << offer; offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget, triedOffers); } else break; } } } if ( openResult == OpenSuccess ) { // Clear errors, since we're trying various generators, maybe one of them errored out // but we finally succeeded // TODO one can still see the error message animating out but since this is a very rare // condition we can leave this for future work emit error( QString(), -1 ); } } if ( openResult != OpenSuccess ) { return openResult; } // no need to check for the existence of a synctex file, no parser will be // created if none exists d->m_synctex_scanner = synctex_scanner_new_with_output_file( QFile::encodeName( docFile ).constData(), nullptr, 1); if ( !d->m_synctex_scanner && QFile::exists(docFile + QLatin1String( "sync" ) ) ) { d->loadSyncFile(docFile); } d->m_generatorName = offer.pluginId(); d->m_pageController = new PageController(); connect( d->m_pageController, &PageController::rotationFinished, this, [this](int p, Okular::Page *op) { d->rotationFinished(p, op); } ); for ( Page *p : qAsConst(d->m_pagesVector) ) p->d->m_doc = d; d->m_metadataLoadingCompleted = false; d->m_docdataMigrationNeeded = false; // 2. load Additional Data (bookmarks, local annotations and metadata) about the document if ( d->m_archiveData ) { // QTemporaryFile is weird and will return false in exists if fileName wasn't called before d->m_archiveData->metadataFile.fileName(); d->loadDocumentInfo( d->m_archiveData->metadataFile, LoadPageInfo ); d->loadDocumentInfo( LoadGeneralInfo ); } else { if ( d->loadDocumentInfo( LoadPageInfo ) ) d->m_docdataMigrationNeeded = true; d->loadDocumentInfo( LoadGeneralInfo ); } d->m_metadataLoadingCompleted = true; d->m_bookmarkManager->setUrl( d->m_url ); // 3. setup observers internal lists and data foreachObserver( notifySetup( d->m_pagesVector, DocumentObserver::DocumentChanged | DocumentObserver::UrlChanged ) ); // 4. set initial page (restoring the page saved in xml if loaded) DocumentViewport loadedViewport = (*d->m_viewportIterator); if ( loadedViewport.isValid() ) { (*d->m_viewportIterator) = DocumentViewport(); if ( loadedViewport.pageNumber >= (int)d->m_pagesVector.size() ) loadedViewport.pageNumber = d->m_pagesVector.size() - 1; } else loadedViewport.pageNumber = 0; setViewport( loadedViewport ); // start bookmark saver timer if ( !d->m_saveBookmarksTimer ) { d->m_saveBookmarksTimer = new QTimer( this ); connect( d->m_saveBookmarksTimer, &QTimer::timeout, this, [this] { d->saveDocumentInfo(); } ); } d->m_saveBookmarksTimer->start( 5 * 60 * 1000 ); // start memory check timer if ( !d->m_memCheckTimer ) { d->m_memCheckTimer = new QTimer( this ); connect( d->m_memCheckTimer, &QTimer::timeout, this, [this] { d->slotTimedMemoryCheck(); } ); } d->m_memCheckTimer->start( 2000 ); const DocumentViewport nextViewport = d->nextDocumentViewport(); if ( nextViewport.isValid() ) { setViewport( nextViewport ); d->m_nextDocumentViewport = DocumentViewport(); d->m_nextDocumentDestination = QString(); } AudioPlayer::instance()->d->m_currentDocument = fromFileDescriptor ? QUrl() : d->m_url; const QStringList docScripts = d->m_generator->metaData( QStringLiteral("DocumentScripts"), QStringLiteral ( "JavaScript" ) ).toStringList(); if ( !docScripts.isEmpty() ) { d->m_scripter = new Scripter( d ); for ( const QString &docscript : docScripts ) { d->m_scripter->execute( JavaScript, docscript ); } } return OpenSuccess; } bool DocumentPrivate::updateMetadataXmlNameAndDocSize() { // m_docFileName is always local so we can use QFileInfo on it QFileInfo fileReadTest( m_docFileName ); if ( !fileReadTest.isFile() && !fileReadTest.isReadable() ) return false; m_docSize = fileReadTest.size(); // determine the related "xml document-info" filename if ( m_url.isLocalFile() ) { const QString filePath = docDataFileName( m_url, m_docSize ); qCDebug(OkularCoreDebug) << "Metadata file is now:" << filePath; m_xmlFileName = filePath; } else { qCDebug(OkularCoreDebug) << "Metadata file: disabled"; m_xmlFileName = QString(); } return true; } KXMLGUIClient* Document::guiClient() { if ( d->m_generator ) { Okular::GuiInterface * iface = qobject_cast< Okular::GuiInterface * >( d->m_generator ); if ( iface ) return iface->guiClient(); } return nullptr; } void Document::closeDocument() { // check if there's anything to close... if ( !d->m_generator ) return; emit aboutToClose(); delete d->m_pageController; d->m_pageController = nullptr; delete d->m_scripter; d->m_scripter = nullptr; // remove requests left in queue d->clearAndWaitForRequests(); if ( d->m_fontThread ) { disconnect( d->m_fontThread, nullptr, this, nullptr ); d->m_fontThread->stopExtraction(); d->m_fontThread->wait(); d->m_fontThread = nullptr; } // stop any audio playback AudioPlayer::instance()->stopPlaybacks(); // close the current document and save document info if a document is still opened if ( d->m_generator && d->m_pagesVector.size() > 0 ) { d->saveDocumentInfo(); d->m_generator->closeDocument(); } if ( d->m_synctex_scanner ) { synctex_scanner_free( d->m_synctex_scanner ); d->m_synctex_scanner = nullptr; } // stop timers if ( d->m_memCheckTimer ) d->m_memCheckTimer->stop(); if ( d->m_saveBookmarksTimer ) d->m_saveBookmarksTimer->stop(); if ( d->m_generator ) { // disconnect the generator from this document ... d->m_generator->d_func()->m_document = nullptr; // .. and this document from the generator signals disconnect( d->m_generator, nullptr, this, nullptr ); QHash< QString, GeneratorInfo >::const_iterator genIt = d->m_loadedGenerators.constFind( d->m_generatorName ); Q_ASSERT( genIt != d->m_loadedGenerators.constEnd() ); } d->m_generator = nullptr; d->m_generatorName = QString(); d->m_url = QUrl(); d->m_walletGenerator = nullptr; d->m_docFileName = QString(); d->m_xmlFileName = QString(); delete d->m_tempFile; d->m_tempFile = nullptr; delete d->m_archiveData; d->m_archiveData = nullptr; d->m_docSize = -1; d->m_exportCached = false; d->m_exportFormats.clear(); d->m_exportToText = ExportFormat(); d->m_fontsCached = false; d->m_fontsCache.clear(); d->m_rotation = Rotation0; // send an empty list to observers (to free their data) foreachObserver( notifySetup( QVector< Page * >(), DocumentObserver::DocumentChanged | DocumentObserver::UrlChanged ) ); // delete pages and clear 'd->m_pagesVector' container QVector< Page * >::const_iterator pIt = d->m_pagesVector.constBegin(); QVector< Page * >::const_iterator pEnd = d->m_pagesVector.constEnd(); for ( ; pIt != pEnd; ++pIt ) delete *pIt; d->m_pagesVector.clear(); // clear 'memory allocation' descriptors qDeleteAll( d->m_allocatedPixmaps ); d->m_allocatedPixmaps.clear(); // clear 'running searches' descriptors QMap< int, RunningSearch * >::const_iterator rIt = d->m_searches.constBegin(); QMap< int, RunningSearch * >::const_iterator rEnd = d->m_searches.constEnd(); for ( ; rIt != rEnd; ++rIt ) delete *rIt; d->m_searches.clear(); // clear the visible areas and notify the observers QVector< VisiblePageRect * >::const_iterator vIt = d->m_pageRects.constBegin(); QVector< VisiblePageRect * >::const_iterator vEnd = d->m_pageRects.constEnd(); for ( ; vIt != vEnd; ++vIt ) delete *vIt; d->m_pageRects.clear(); foreachObserver( notifyVisibleRectsChanged() ); // reset internal variables d->m_viewportHistory.clear(); d->m_viewportHistory.append( DocumentViewport() ); d->m_viewportIterator = d->m_viewportHistory.begin(); d->m_allocatedPixmapsTotalMemory = 0; d->m_allocatedTextPagesFifo.clear(); d->m_pageSize = PageSize(); d->m_pageSizes.clear(); d->m_documentInfo = DocumentInfo(); d->m_documentInfoAskedKeys.clear(); AudioPlayer::instance()->d->m_currentDocument = QUrl(); d->m_undoStack->clear(); d->m_docdataMigrationNeeded = false; #if HAVE_MALLOC_TRIM // trim unused memory, glibc should do this but it seems it does not // this can greatly decrease the [perceived] memory consumption of okular // see: https://sourceware.org/bugzilla/show_bug.cgi?id=14827 malloc_trim(0); #endif } void Document::addObserver( DocumentObserver * pObserver ) { Q_ASSERT( !d->m_observers.contains( pObserver ) ); d->m_observers << pObserver; // if the observer is added while a document is already opened, tell it if ( !d->m_pagesVector.isEmpty() ) { pObserver->notifySetup( d->m_pagesVector, DocumentObserver::DocumentChanged | DocumentObserver::UrlChanged ); pObserver->notifyViewportChanged( false /*disables smoothMove*/ ); } } void Document::removeObserver( DocumentObserver * pObserver ) { // remove observer from the set. it won't receive notifications anymore if ( d->m_observers.contains( pObserver ) ) { // free observer's pixmap data QVector::const_iterator it = d->m_pagesVector.constBegin(), end = d->m_pagesVector.constEnd(); for ( ; it != end; ++it ) (*it)->deletePixmap( pObserver ); // [MEM] free observer's allocation descriptors QLinkedList< AllocatedPixmap * >::iterator aIt = d->m_allocatedPixmaps.begin(); QLinkedList< AllocatedPixmap * >::iterator aEnd = d->m_allocatedPixmaps.end(); while ( aIt != aEnd ) { AllocatedPixmap * p = *aIt; if ( p->observer == pObserver ) { aIt = d->m_allocatedPixmaps.erase( aIt ); delete p; } else ++aIt; } for ( PixmapRequest *executingRequest : qAsConst( d->m_executingPixmapRequests ) ) { if ( executingRequest->observer() == pObserver ) { d->cancelRenderingBecauseOf( executingRequest, nullptr ); } } // remove observer entry from the set d->m_observers.remove( pObserver ); } } void Document::reparseConfig() { // reparse generator config and if something changed clear Pages bool configchanged = false; if ( d->m_generator ) { Okular::ConfigInterface * iface = qobject_cast< Okular::ConfigInterface * >( d->m_generator ); if ( iface ) configchanged = iface->reparseConfig(); } if ( configchanged ) { // invalidate pixmaps QVector::const_iterator it = d->m_pagesVector.constBegin(), end = d->m_pagesVector.constEnd(); for ( ; it != end; ++it ) { (*it)->deletePixmaps(); } // [MEM] remove allocation descriptors qDeleteAll( d->m_allocatedPixmaps ); d->m_allocatedPixmaps.clear(); d->m_allocatedPixmapsTotalMemory = 0; // send reload signals to observers foreachObserver( notifyContentsCleared( DocumentObserver::Pixmap ) ); } // free memory if in 'low' profile if ( SettingsCore::memoryLevel() == SettingsCore::EnumMemoryLevel::Low && !d->m_allocatedPixmaps.isEmpty() && !d->m_pagesVector.isEmpty() ) d->cleanupPixmapMemory(); } bool Document::isOpened() const { return d->m_generator; } bool Document::canConfigurePrinter( ) const { if ( d->m_generator ) { Okular::PrintInterface * iface = qobject_cast< Okular::PrintInterface * >( d->m_generator ); return iface ? true : false; } else return false; } DocumentInfo Document::documentInfo() const { QSet keys; for (Okular::DocumentInfo::Key ks = Okular::DocumentInfo::Title; ks < Okular::DocumentInfo::Invalid; ks = Okular::DocumentInfo::Key( ks+1 ) ) { keys << ks; } return documentInfo( keys ); } DocumentInfo Document::documentInfo( const QSet &keys ) const { DocumentInfo result = d->m_documentInfo; const QSet missingKeys = keys - d->m_documentInfoAskedKeys; if ( d->m_generator && !missingKeys.isEmpty() ) { DocumentInfo info = d->m_generator->generateDocumentInfo( missingKeys ); if ( missingKeys.contains( DocumentInfo::FilePath ) ) { info.set( DocumentInfo::FilePath, currentDocument().toDisplayString() ); } if ( d->m_docSize != -1 && missingKeys.contains( DocumentInfo::DocumentSize ) ) { const QString sizeString = KFormat().formatByteSize( d->m_docSize ); info.set( DocumentInfo::DocumentSize, sizeString ); } if ( missingKeys.contains( DocumentInfo::PagesSize ) ) { const QString pagesSize = d->pagesSizeString(); if ( !pagesSize.isEmpty() ) { info.set( DocumentInfo::PagesSize, pagesSize ); } } if ( missingKeys.contains( DocumentInfo::Pages ) && info.get( DocumentInfo::Pages ).isEmpty() ) { info.set( DocumentInfo::Pages, QString::number( this->pages() ) ); } d->m_documentInfo.d->values.unite(info.d->values); d->m_documentInfo.d->titles.unite(info.d->titles); result.d->values.unite(info.d->values); result.d->titles.unite(info.d->titles); } d->m_documentInfoAskedKeys += keys; return result; } const DocumentSynopsis * Document::documentSynopsis() const { return d->m_generator ? d->m_generator->generateDocumentSynopsis() : nullptr; } void Document::startFontReading() { if ( !d->m_generator || !d->m_generator->hasFeature( Generator::FontInfo ) || d->m_fontThread ) return; if ( d->m_fontsCached ) { // in case we have cached fonts, simulate a reading // this way the API is the same, and users no need to care about the // internal caching for ( int i = 0; i < d->m_fontsCache.count(); ++i ) { emit gotFont( d->m_fontsCache.at( i ) ); emit fontReadingProgress( i / pages() ); } emit fontReadingEnded(); return; } d->m_fontThread = new FontExtractionThread( d->m_generator, pages() ); connect( d->m_fontThread, &FontExtractionThread::gotFont, this, [this](const Okular::FontInfo &f) { d->fontReadingGotFont(f); } ); connect( d->m_fontThread.data(), &FontExtractionThread::progress, this, [this](int p) { d->slotFontReadingProgress(p); } ); d->m_fontThread->startExtraction( /*d->m_generator->hasFeature( Generator::Threaded )*/true ); } void Document::stopFontReading() { if ( !d->m_fontThread ) return; disconnect( d->m_fontThread, nullptr, this, nullptr ); d->m_fontThread->stopExtraction(); d->m_fontThread = nullptr; d->m_fontsCache.clear(); } bool Document::canProvideFontInformation() const { return d->m_generator ? d->m_generator->hasFeature( Generator::FontInfo ) : false; } const QList *Document::embeddedFiles() const { return d->m_generator ? d->m_generator->embeddedFiles() : nullptr; } const Page * Document::page( int n ) const { return ( n >= 0 && n < d->m_pagesVector.count() ) ? d->m_pagesVector.at(n) : nullptr; } const DocumentViewport & Document::viewport() const { return (*d->m_viewportIterator); } const QVector< VisiblePageRect * > & Document::visiblePageRects() const { return d->m_pageRects; } void Document::setVisiblePageRects( const QVector< VisiblePageRect * > & visiblePageRects, DocumentObserver *excludeObserver ) { QVector< VisiblePageRect * >::const_iterator vIt = d->m_pageRects.constBegin(); QVector< VisiblePageRect * >::const_iterator vEnd = d->m_pageRects.constEnd(); for ( ; vIt != vEnd; ++vIt ) delete *vIt; d->m_pageRects = visiblePageRects; // notify change to all other (different from id) observers foreach(DocumentObserver *o, d->m_observers) if ( o != excludeObserver ) o->notifyVisibleRectsChanged(); } uint Document::currentPage() const { return (*d->m_viewportIterator).pageNumber; } uint Document::pages() const { return d->m_pagesVector.size(); } QUrl Document::currentDocument() const { return d->m_url; } bool Document::isAllowed( Permission action ) const { if ( action == Okular::AllowNotes && ( d->m_docdataMigrationNeeded || !d->m_annotationEditingEnabled ) ) return false; if ( action == Okular::AllowFillForms && d->m_docdataMigrationNeeded ) return false; #if !OKULAR_FORCE_DRM if ( KAuthorized::authorize( QStringLiteral("skip_drm") ) && !SettingsCore::obeyDRM() ) return true; #endif return d->m_generator ? d->m_generator->isAllowed( action ) : false; } bool Document::supportsSearching() const { return d->m_generator ? d->m_generator->hasFeature( Generator::TextExtraction ) : false; } bool Document::supportsPageSizes() const { return d->m_generator ? d->m_generator->hasFeature( Generator::PageSizes ) : false; } bool Document::supportsTiles() const { return d->m_generator ? d->m_generator->hasFeature( Generator::TiledRendering ) : false; } PageSize::List Document::pageSizes() const { if ( d->m_generator ) { if ( d->m_pageSizes.isEmpty() ) d->m_pageSizes = d->m_generator->pageSizes(); return d->m_pageSizes; } return PageSize::List(); } bool Document::canExportToText() const { if ( !d->m_generator ) return false; d->cacheExportFormats(); return !d->m_exportToText.isNull(); } bool Document::exportToText( const QString& fileName ) const { if ( !d->m_generator ) return false; d->cacheExportFormats(); if ( d->m_exportToText.isNull() ) return false; return d->m_generator->exportTo( fileName, d->m_exportToText ); } ExportFormat::List Document::exportFormats() const { if ( !d->m_generator ) return ExportFormat::List(); d->cacheExportFormats(); return d->m_exportFormats; } bool Document::exportTo( const QString& fileName, const ExportFormat& format ) const { return d->m_generator ? d->m_generator->exportTo( fileName, format ) : false; } bool Document::historyAtBegin() const { return d->m_viewportIterator == d->m_viewportHistory.begin(); } bool Document::historyAtEnd() const { return d->m_viewportIterator == --(d->m_viewportHistory.end()); } QVariant Document::metaData( const QString & key, const QVariant & option ) const { // if option starts with "src:" assume that we are handling a // source reference if ( key == QLatin1String("NamedViewport") && option.toString().startsWith( QLatin1String("src:"), Qt::CaseInsensitive ) && d->m_synctex_scanner) { const QString reference = option.toString(); // The reference is of form "src:1111Filename", where "1111" // points to line number 1111 in the file "Filename". // Extract the file name and the numeral part from the reference string. // This will fail if Filename starts with a digit. QString name, lineString; // Remove "src:". Presence of substring has been checked before this // function is called. name = reference.mid( 4 ); // split int nameLength = name.length(); int i = 0; for( i = 0; i < nameLength; ++i ) { if ( !name[i].isDigit() ) break; } lineString = name.left( i ); name = name.mid( i ); // Remove spaces. name = name.trimmed(); lineString = lineString.trimmed(); // Convert line to integer. bool ok; int line = lineString.toInt( &ok ); if (!ok) line = -1; // Use column == -1 for now. if( synctex_display_query( d->m_synctex_scanner, QFile::encodeName(name).constData(), line, -1, 0 ) > 0 ) { synctex_node_p node; // For now use the first hit. Could possibly be made smarter // in case there are multiple hits. while( ( node = synctex_scanner_next_result( d->m_synctex_scanner ) ) ) { Okular::DocumentViewport viewport; // TeX pages start at 1. viewport.pageNumber = synctex_node_page( node ) - 1; if ( viewport.pageNumber >= 0 ) { const QSizeF dpi = d->m_generator->dpi(); // TeX small points ... double px = (synctex_node_visible_h( node ) * dpi.width()) / 72.27; double py = (synctex_node_visible_v( node ) * dpi.height()) / 72.27; viewport.rePos.normalizedX = px / page(viewport.pageNumber)->width(); viewport.rePos.normalizedY = ( py + 0.5 ) / page(viewport.pageNumber)->height(); viewport.rePos.enabled = true; viewport.rePos.pos = Okular::DocumentViewport::Center; return viewport.toString(); } } } } return d->m_generator ? d->m_generator->metaData( key, option ) : QVariant(); } Rotation Document::rotation() const { return d->m_rotation; } QSizeF Document::allPagesSize() const { bool allPagesSameSize = true; QSizeF size; for (int i = 0; allPagesSameSize && i < d->m_pagesVector.count(); ++i) { const Page *p = d->m_pagesVector.at(i); if (i == 0) size = QSizeF(p->width(), p->height()); else { allPagesSameSize = (size == QSizeF(p->width(), p->height())); } } if (allPagesSameSize) return size; else return QSizeF(); } QString Document::pageSizeString(int page) const { if (d->m_generator) { if (d->m_generator->pagesSizeMetric() != Generator::None) { const Page *p = d->m_pagesVector.at( page ); return d->localizedSize(QSizeF(p->width(), p->height())); } } return QString(); } static bool shouldCancelRenderingBecauseOf( const PixmapRequest & executingRequest, const PixmapRequest & otherRequest ) { // New request has higher priority -> cancel if ( executingRequest.priority() > otherRequest.priority() ) return true; // New request has lower priority -> don't cancel if ( executingRequest.priority() < otherRequest.priority() ) return false; // New request has same priority and is from a different observer -> don't cancel // AFAIK this never happens since all observers have different priorities if ( executingRequest.observer() != otherRequest.observer() ) return false; // Same priority and observer, different page number -> don't cancel // may still end up cancelled later in the parent caller if none of the requests // is of the executingRequest page and RemoveAllPrevious is specified if ( executingRequest.pageNumber() != otherRequest.pageNumber() ) return false; // Same priority, observer, page, different size -> cancel if ( executingRequest.width() != otherRequest.width() ) return true; // Same priority, observer, page, different size -> cancel if ( executingRequest.height() != otherRequest.height() ) return true; // Same priority, observer, page, different tiling -> cancel if ( executingRequest.isTile() != otherRequest.isTile() ) return true; // Same priority, observer, page, different tiling -> cancel if ( executingRequest.isTile() ) { const NormalizedRect bothRequestsRect = executingRequest.normalizedRect() | otherRequest.normalizedRect(); if ( !( bothRequestsRect == executingRequest.normalizedRect() ) ) return true; } return false; } bool DocumentPrivate::cancelRenderingBecauseOf( PixmapRequest *executingRequest, PixmapRequest *newRequest ) { // No point in aborting the rendering already finished, let it go through if ( !executingRequest->d->mResultImage.isNull() ) return false; if ( newRequest && newRequest->asynchronous() && executingRequest->partialUpdatesWanted() ) { newRequest->setPartialUpdatesWanted( true ); } TilesManager *tm = executingRequest->d->tilesManager(); if ( tm ) { tm->setPixmap( nullptr, executingRequest->normalizedRect(), true /*isPartialPixmap*/ ); tm->setRequest( NormalizedRect(), 0, 0 ); } PagePrivate::PixmapObject object = executingRequest->page()->d->m_pixmaps.take( executingRequest->observer() ); delete object.m_pixmap; if ( executingRequest->d->mShouldAbortRender != 0) return false; executingRequest->d->mShouldAbortRender = 1; if ( m_generator->d_ptr->mTextPageGenerationThread && m_generator->d_ptr->mTextPageGenerationThread->page() == executingRequest->page() ) { m_generator->d_ptr->mTextPageGenerationThread->abortExtraction(); } return true; } void Document::requestPixmaps( const QLinkedList< PixmapRequest * > & requests ) { requestPixmaps( requests, RemoveAllPrevious ); } void Document::requestPixmaps( const QLinkedList< PixmapRequest * > & requests, PixmapRequestFlags reqOptions ) { if ( requests.isEmpty() ) return; if ( !d->m_pageController ) { // delete requests.. QLinkedList< PixmapRequest * >::const_iterator rIt = requests.constBegin(), rEnd = requests.constEnd(); for ( ; rIt != rEnd; ++rIt ) delete *rIt; // ..and return return; } QSet< DocumentObserver * > observersPixmapCleared; // 1. [CLEAN STACK] remove previous requests of requesterID DocumentObserver *requesterObserver = requests.first()->observer(); QSet< int > requestedPages; { QLinkedList< PixmapRequest * >::const_iterator rIt = requests.constBegin(), rEnd = requests.constEnd(); for ( ; rIt != rEnd; ++rIt ) { Q_ASSERT( (*rIt)->observer() == requesterObserver ); requestedPages.insert( (*rIt)->pageNumber() ); } } const bool removeAllPrevious = reqOptions & RemoveAllPrevious; d->m_pixmapRequestsMutex.lock(); QLinkedList< PixmapRequest * >::iterator sIt = d->m_pixmapRequestsStack.begin(), sEnd = d->m_pixmapRequestsStack.end(); while ( sIt != sEnd ) { if ( (*sIt)->observer() == requesterObserver && ( removeAllPrevious || requestedPages.contains( (*sIt)->pageNumber() ) ) ) { // delete request and remove it from stack delete *sIt; sIt = d->m_pixmapRequestsStack.erase( sIt ); } else ++sIt; } // 1.B [PREPROCESS REQUESTS] tweak some values of the requests for ( PixmapRequest *request : requests ) { // set the 'page field' (see PixmapRequest) and check if it is valid qCDebug(OkularCoreDebug).nospace() << "request observer=" << request->observer() << " " <width() << "x" << request->height() << "@" << request->pageNumber(); if ( d->m_pagesVector.value( request->pageNumber() ) == 0 ) { // skip requests referencing an invalid page (must not happen) delete request; continue; } request->d->mPage = d->m_pagesVector.value( request->pageNumber() ); if ( request->isTile() ) { // Change the current request rect so that only invalid tiles are // requested. Also make sure the rect is tile-aligned. NormalizedRect tilesRect; const QList tiles = request->d->tilesManager()->tilesAt( request->normalizedRect(), TilesManager::TerminalTile ); QList::const_iterator tIt = tiles.constBegin(), tEnd = tiles.constEnd(); while ( tIt != tEnd ) { const Tile &tile = *tIt; if ( !tile.isValid() ) { if ( tilesRect.isNull() ) tilesRect = tile.rect(); else tilesRect |= tile.rect(); } tIt++; } request->setNormalizedRect( tilesRect ); } if ( !request->asynchronous() ) request->d->mPriority = 0; } // 1.C [CANCEL REQUESTS] cancel those requests that are running and should be cancelled because of the new requests coming in if ( d->m_generator->hasFeature( Generator::SupportsCancelling ) ) { for ( PixmapRequest *executingRequest : qAsConst( d->m_executingPixmapRequests ) ) { bool newRequestsContainExecutingRequestPage = false; bool requestCancelled = false; for ( PixmapRequest *newRequest : requests ) { if ( newRequest->pageNumber() == executingRequest->pageNumber() && requesterObserver == executingRequest->observer()) { newRequestsContainExecutingRequestPage = true; } if ( shouldCancelRenderingBecauseOf( *executingRequest, *newRequest ) ) { requestCancelled = d->cancelRenderingBecauseOf( executingRequest, newRequest ); } } // If we were told to remove all the previous requests and the executing request page is not part of the new requests, cancel it if ( !requestCancelled && removeAllPrevious && requesterObserver == executingRequest->observer() && !newRequestsContainExecutingRequestPage ) { requestCancelled = d->cancelRenderingBecauseOf( executingRequest, nullptr ); } if ( requestCancelled ) { observersPixmapCleared << executingRequest->observer(); } } } // 2. [ADD TO STACK] add requests to stack for ( PixmapRequest *request : requests ) { // add request to the 'stack' at the right place if ( !request->priority() ) // add priority zero requests to the top of the stack d->m_pixmapRequestsStack.append( request ); else { // insert in stack sorted by priority sIt = d->m_pixmapRequestsStack.begin(); sEnd = d->m_pixmapRequestsStack.end(); while ( sIt != sEnd && (*sIt)->priority() > request->priority() ) ++sIt; d->m_pixmapRequestsStack.insert( sIt, request ); } } d->m_pixmapRequestsMutex.unlock(); // 3. [START FIRST GENERATION] if generator is ready, start a new generation, // or else (if gen is running) it will be started when the new contents will //come from generator (in requestDone()) // all handling of requests put into sendGeneratorPixmapRequest // if ( generator->canRequestPixmap() ) d->sendGeneratorPixmapRequest(); for ( DocumentObserver *o : qAsConst( observersPixmapCleared ) ) o->notifyContentsCleared( Okular::DocumentObserver::Pixmap ); } void Document::requestTextPage( uint pageNumber ) { Page * kp = d->m_pagesVector[ pageNumber ]; if ( !d->m_generator || !kp ) return; // Memory management for TextPages d->m_generator->generateTextPage( kp ); } void DocumentPrivate::notifyAnnotationChanges( int page ) { foreachObserverD( notifyPageChanged( page, DocumentObserver::Annotations ) ); } void DocumentPrivate::notifyFormChanges( int /*page*/ ) { recalculateForms(); } void Document::addPageAnnotation( int page, Annotation * annotation ) { // Transform annotation's base boundary rectangle into unrotated coordinates Page *p = d->m_pagesVector[page]; QTransform t = p->d->rotationMatrix(); annotation->d_ptr->baseTransform(t.inverted()); QUndoCommand *uc = new AddAnnotationCommand(this->d, annotation, page); d->m_undoStack->push(uc); } bool Document::canModifyPageAnnotation( const Annotation * annotation ) const { if ( !annotation || ( annotation->flags() & Annotation::DenyWrite ) ) return false; if ( !isAllowed(Okular::AllowNotes) ) return false; if ( ( annotation->flags() & Annotation::External ) && !d->canModifyExternalAnnotations() ) return false; switch ( annotation->subType() ) { case Annotation::AText: case Annotation::ALine: case Annotation::AGeom: case Annotation::AHighlight: case Annotation::AStamp: case Annotation::AInk: return true; default: return false; } } void Document::prepareToModifyAnnotationProperties( Annotation * annotation ) { Q_ASSERT(d->m_prevPropsOfAnnotBeingModified.isNull()); if (!d->m_prevPropsOfAnnotBeingModified.isNull()) { qCCritical(OkularCoreDebug) << "Error: Document::prepareToModifyAnnotationProperties has already been called since last call to Document::modifyPageAnnotationProperties"; return; } d->m_prevPropsOfAnnotBeingModified = annotation->getAnnotationPropertiesDomNode(); } void Document::modifyPageAnnotationProperties( int page, Annotation * annotation ) { Q_ASSERT(!d->m_prevPropsOfAnnotBeingModified.isNull()); if (d->m_prevPropsOfAnnotBeingModified.isNull()) { qCCritical(OkularCoreDebug) << "Error: Document::prepareToModifyAnnotationProperties must be called before Annotation is modified"; return; } QDomNode prevProps = d->m_prevPropsOfAnnotBeingModified; QUndoCommand *uc = new Okular::ModifyAnnotationPropertiesCommand( d, annotation, page, prevProps, annotation->getAnnotationPropertiesDomNode() ); d->m_undoStack->push( uc ); d->m_prevPropsOfAnnotBeingModified.clear(); } void Document::translatePageAnnotation(int page, Annotation* annotation, const NormalizedPoint & delta ) { int complete = (annotation->flags() & Okular::Annotation::BeingMoved) == 0; QUndoCommand *uc = new Okular::TranslateAnnotationCommand( d, annotation, page, delta, complete ); d->m_undoStack->push(uc); } void Document::adjustPageAnnotation( int page, Annotation *annotation, const Okular::NormalizedPoint & delta1, const Okular::NormalizedPoint & delta2 ) { const bool complete = (annotation->flags() & Okular::Annotation::BeingResized) == 0; QUndoCommand *uc = new Okular::AdjustAnnotationCommand( d, annotation, page, delta1, delta2, complete ); d->m_undoStack->push(uc); } void Document::editPageAnnotationContents( int page, Annotation* annotation, const QString & newContents, int newCursorPos, int prevCursorPos, int prevAnchorPos ) { QString prevContents = annotation->contents(); QUndoCommand *uc = new EditAnnotationContentsCommand( d, annotation, page, newContents, newCursorPos, prevContents, prevCursorPos, prevAnchorPos ); d->m_undoStack->push( uc ); } bool Document::canRemovePageAnnotation( const Annotation * annotation ) const { if ( !annotation || ( annotation->flags() & Annotation::DenyDelete ) ) return false; if ( ( annotation->flags() & Annotation::External ) && !d->canRemoveExternalAnnotations() ) return false; switch ( annotation->subType() ) { case Annotation::AText: case Annotation::ALine: case Annotation::AGeom: case Annotation::AHighlight: case Annotation::AStamp: case Annotation::AInk: case Annotation::ACaret: return true; default: return false; } } void Document::removePageAnnotation( int page, Annotation * annotation ) { QUndoCommand *uc = new RemoveAnnotationCommand(this->d, annotation, page); d->m_undoStack->push(uc); } void Document::removePageAnnotations( int page, const QList &annotations ) { d->m_undoStack->beginMacro(i18nc("remove a collection of annotations from the page", "remove annotations")); foreach(Annotation* annotation, annotations) { QUndoCommand *uc = new RemoveAnnotationCommand(this->d, annotation, page); d->m_undoStack->push(uc); } d->m_undoStack->endMacro(); } bool DocumentPrivate::canAddAnnotationsNatively() const { Okular::SaveInterface * iface = qobject_cast< Okular::SaveInterface * >( m_generator ); if ( iface && iface->supportsOption(Okular::SaveInterface::SaveChanges) && iface->annotationProxy() && iface->annotationProxy()->supports(AnnotationProxy::Addition) ) return true; return false; } bool DocumentPrivate::canModifyExternalAnnotations() const { Okular::SaveInterface * iface = qobject_cast< Okular::SaveInterface * >( m_generator ); if ( iface && iface->supportsOption(Okular::SaveInterface::SaveChanges) && iface->annotationProxy() && iface->annotationProxy()->supports(AnnotationProxy::Modification) ) return true; return false; } bool DocumentPrivate::canRemoveExternalAnnotations() const { Okular::SaveInterface * iface = qobject_cast< Okular::SaveInterface * >( m_generator ); if ( iface && iface->supportsOption(Okular::SaveInterface::SaveChanges) && iface->annotationProxy() && iface->annotationProxy()->supports(AnnotationProxy::Removal) ) return true; return false; } void Document::setPageTextSelection( int page, RegularAreaRect * rect, const QColor & color ) { Page * kp = d->m_pagesVector[ page ]; if ( !d->m_generator || !kp ) return; // add or remove the selection basing whether rect is null or not if ( rect ) kp->d->setTextSelections( rect, color ); else kp->d->deleteTextSelections(); // notify observers about the change foreachObserver( notifyPageChanged( page, DocumentObserver::TextSelection ) ); } bool Document::canUndo() const { return d->m_undoStack->canUndo(); } bool Document::canRedo() const { return d->m_undoStack->canRedo(); } /* REFERENCE IMPLEMENTATION: better calling setViewport from other code void Document::setNextPage() { // advance page and set viewport on observers if ( (*d->m_viewportIterator).pageNumber < (int)d->m_pagesVector.count() - 1 ) setViewport( DocumentViewport( (*d->m_viewportIterator).pageNumber + 1 ) ); } void Document::setPrevPage() { // go to previous page and set viewport on observers if ( (*d->m_viewportIterator).pageNumber > 0 ) setViewport( DocumentViewport( (*d->m_viewportIterator).pageNumber - 1 ) ); } */ void Document::setViewportPage( int page, DocumentObserver *excludeObserver, bool smoothMove ) { // clamp page in range [0 ... numPages-1] if ( page < 0 ) page = 0; else if ( page > (int)d->m_pagesVector.count() ) page = d->m_pagesVector.count() - 1; // make a viewport from the page and broadcast it setViewport( DocumentViewport( page ), excludeObserver, smoothMove ); } void Document::setViewport( const DocumentViewport & viewport, DocumentObserver *excludeObserver, bool smoothMove ) { if ( !viewport.isValid() ) { qCDebug(OkularCoreDebug) << "invalid viewport:" << viewport.toString(); return; } if ( viewport.pageNumber >= int(d->m_pagesVector.count()) ) { //qCDebug(OkularCoreDebug) << "viewport out of document:" << viewport.toString(); return; } // if already broadcasted, don't redo it DocumentViewport & oldViewport = *d->m_viewportIterator; // disabled by enrico on 2005-03-18 (less debug output) //if ( viewport == oldViewport ) // qCDebug(OkularCoreDebug) << "setViewport with the same viewport."; const int oldPageNumber = oldViewport.pageNumber; // set internal viewport taking care of history if ( oldViewport.pageNumber == viewport.pageNumber || !oldViewport.isValid() ) { // if page is unchanged save the viewport at current position in queue oldViewport = viewport; } else { // remove elements after viewportIterator in queue d->m_viewportHistory.erase( ++d->m_viewportIterator, d->m_viewportHistory.end() ); // keep the list to a reasonable size by removing head when needed if ( d->m_viewportHistory.count() >= OKULAR_HISTORY_MAXSTEPS ) d->m_viewportHistory.pop_front(); // add the item at the end of the queue d->m_viewportIterator = d->m_viewportHistory.insert( d->m_viewportHistory.end(), viewport ); } const int currentViewportPage = (*d->m_viewportIterator).pageNumber; const bool currentPageChanged = (oldPageNumber != currentViewportPage); // notify change to all other (different from id) observers for (DocumentObserver *o : qAsConst(d->m_observers)) { if ( o != excludeObserver ) o->notifyViewportChanged( smoothMove ); if ( currentPageChanged ) o->notifyCurrentPageChanged( oldPageNumber, currentViewportPage ); } } void Document::setZoom(int factor, DocumentObserver *excludeObserver) { // notify change to all other (different from id) observers for (DocumentObserver *o : qAsConst(d->m_observers)) if (o != excludeObserver) o->notifyZoom( factor ); } void Document::setPrevViewport() // restore viewport from the history { if ( d->m_viewportIterator != d->m_viewportHistory.begin() ) { const int oldViewportPage = (*d->m_viewportIterator).pageNumber; // restore previous viewport and notify it to observers --d->m_viewportIterator; foreachObserver( notifyViewportChanged( true ) ); const int currentViewportPage = (*d->m_viewportIterator).pageNumber; if (oldViewportPage != currentViewportPage) foreachObserver( notifyCurrentPageChanged( oldViewportPage, currentViewportPage ) ); } } void Document::setNextViewport() // restore next viewport from the history { auto nextIterator = QLinkedList< DocumentViewport >::const_iterator(d->m_viewportIterator); ++nextIterator; if ( nextIterator != d->m_viewportHistory.constEnd() ) { const int oldViewportPage = (*d->m_viewportIterator).pageNumber; // restore next viewport and notify it to observers ++d->m_viewportIterator; foreachObserver( notifyViewportChanged( true ) ); const int currentViewportPage = (*d->m_viewportIterator).pageNumber; if (oldViewportPage != currentViewportPage) foreachObserver( notifyCurrentPageChanged( oldViewportPage, currentViewportPage ) ); } } void Document::setNextDocumentViewport( const DocumentViewport & viewport ) { d->m_nextDocumentViewport = viewport; } void Document::setNextDocumentDestination( const QString &namedDestination ) { d->m_nextDocumentDestination = namedDestination; } void Document::searchText( int searchID, const QString & text, bool fromStart, Qt::CaseSensitivity caseSensitivity, SearchType type, bool moveViewport, const QColor & color ) { d->m_searchCancelled = false; // safety checks: don't perform searches on empty or unsearchable docs if ( !d->m_generator || !d->m_generator->hasFeature( Generator::TextExtraction ) || d->m_pagesVector.isEmpty() ) { emit searchFinished( searchID, NoMatchFound ); return; } // if searchID search not recorded, create new descriptor and init params QMap< int, RunningSearch * >::iterator searchIt = d->m_searches.find( searchID ); if ( searchIt == d->m_searches.end() ) { RunningSearch * search = new RunningSearch(); search->continueOnPage = -1; searchIt = d->m_searches.insert( searchID, search ); } RunningSearch * s = *searchIt; // update search structure bool newText = text != s->cachedString; s->cachedString = text; s->cachedType = type; s->cachedCaseSensitivity = caseSensitivity; s->cachedViewportMove = moveViewport; s->cachedColor = color; s->isCurrentlySearching = true; // global data for search QSet< int > *pagesToNotify = new QSet< int >; // remove highlights from pages and queue them for notifying changes *pagesToNotify += s->highlightedPages; for (const int pageNumber : qAsConst(s->highlightedPages)) { d->m_pagesVector.at(pageNumber)->d->deleteHighlights( searchID ); } s->highlightedPages.clear(); // set hourglass cursor QApplication::setOverrideCursor( Qt::WaitCursor ); // 1. ALLDOC - process all document marking pages if ( type == AllDocument ) { QMap< Page *, QVector > *pageMatches = new QMap< Page *, QVector >; // search and highlight 'text' (as a solid phrase) on all pages QTimer::singleShot(0, this, [this, pagesToNotify, pageMatches, searchID] { d->doContinueAllDocumentSearch(pagesToNotify, pageMatches, 0, searchID); }); } // 2. NEXTMATCH - find next matching item (or start from top) // 3. PREVMATCH - find previous matching item (or start from bottom) else if ( type == NextMatch || type == PreviousMatch ) { // find out from where to start/resume search from const bool forward = type == NextMatch; const int viewportPage = (*d->m_viewportIterator).pageNumber; const int fromStartSearchPage = forward ? 0 : d->m_pagesVector.count() - 1; int currentPage = fromStart ? fromStartSearchPage : ((s->continueOnPage != -1) ? s->continueOnPage : viewportPage); Page * lastPage = fromStart ? 0 : d->m_pagesVector[ currentPage ]; int pagesDone = 0; // continue checking last TextPage first (if it is the current page) RegularAreaRect * match = nullptr; if ( lastPage && lastPage->number() == s->continueOnPage ) { if ( newText ) match = lastPage->findText( searchID, text, forward ? FromTop : FromBottom, caseSensitivity ); else match = lastPage->findText( searchID, text, forward ? NextResult : PreviousResult, caseSensitivity, &s->continueOnMatch ); if ( !match ) { if (forward) currentPage++; else currentPage--; pagesDone++; } } s->pagesDone = pagesDone; DoContinueDirectionMatchSearchStruct *searchStruct = new DoContinueDirectionMatchSearchStruct(); searchStruct->pagesToNotify = pagesToNotify; searchStruct->match = match; searchStruct->currentPage = currentPage; searchStruct->searchID = searchID; QTimer::singleShot(0, this, [this, searchStruct] { d->doContinueDirectionMatchSearch(searchStruct); }); } // 4. GOOGLE* - process all document marking pages else if ( type == GoogleAll || type == GoogleAny ) { QMap< Page *, QVector< QPair > > *pageMatches = new QMap< Page *, QVector > >; const QStringList words = text.split( QLatin1Char ( ' ' ), QString::SkipEmptyParts ); // search and highlight every word in 'text' on all pages QTimer::singleShot(0, this, [this, pagesToNotify, pageMatches, searchID, words] { d->doContinueGooglesDocumentSearch(pagesToNotify, pageMatches, 0, searchID, words); }); } } void Document::continueSearch( int searchID ) { // check if searchID is present in runningSearches QMap< int, RunningSearch * >::const_iterator it = d->m_searches.constFind( searchID ); if ( it == d->m_searches.constEnd() ) { emit searchFinished( searchID, NoMatchFound ); return; } // start search with cached parameters from last search by searchID RunningSearch * p = *it; if ( !p->isCurrentlySearching ) searchText( searchID, p->cachedString, false, p->cachedCaseSensitivity, p->cachedType, p->cachedViewportMove, p->cachedColor ); } void Document::continueSearch( int searchID, SearchType type ) { // check if searchID is present in runningSearches QMap< int, RunningSearch * >::const_iterator it = d->m_searches.constFind( searchID ); if ( it == d->m_searches.constEnd() ) { emit searchFinished( searchID, NoMatchFound ); return; } // start search with cached parameters from last search by searchID RunningSearch * p = *it; if ( !p->isCurrentlySearching ) searchText( searchID, p->cachedString, false, p->cachedCaseSensitivity, type, p->cachedViewportMove, p->cachedColor ); } void Document::resetSearch( int searchID ) { // if we are closing down, don't bother doing anything if ( !d->m_generator ) return; // check if searchID is present in runningSearches QMap< int, RunningSearch * >::iterator searchIt = d->m_searches.find( searchID ); if ( searchIt == d->m_searches.end() ) return; // get previous parameters for search RunningSearch * s = *searchIt; // unhighlight pages and inform observers about that for (const int pageNumber : qAsConst(s->highlightedPages)) { d->m_pagesVector.at(pageNumber)->d->deleteHighlights( searchID ); foreachObserver( notifyPageChanged( pageNumber, DocumentObserver::Highlights ) ); } // send the setup signal too (to update views that filter on matches) foreachObserver( notifySetup( d->m_pagesVector, 0 ) ); // remove search from the runningSearches list and delete it d->m_searches.erase( searchIt ); delete s; } void Document::cancelSearch() { d->m_searchCancelled = true; } void Document::undo() { d->m_undoStack->undo(); } void Document::redo() { d->m_undoStack->redo(); } void Document::editFormText( int pageNumber, Okular::FormFieldText* form, const QString & newContents, int newCursorPos, int prevCursorPos, int prevAnchorPos ) { QUndoCommand *uc = new EditFormTextCommand( this->d, form, pageNumber, newContents, newCursorPos, form->text(), prevCursorPos, prevAnchorPos ); d->m_undoStack->push( uc ); } void Document::editFormList( int pageNumber, FormFieldChoice* form, const QList< int > & newChoices ) { const QList< int > prevChoices = form->currentChoices(); QUndoCommand *uc = new EditFormListCommand( this->d, form, pageNumber, newChoices, prevChoices ); d->m_undoStack->push( uc ); } void Document::editFormCombo( int pageNumber, FormFieldChoice* form, const QString & newText, int newCursorPos, int prevCursorPos, int prevAnchorPos ) { QString prevText; if ( form->currentChoices().isEmpty() ) { prevText = form->editChoice(); } else { prevText = form->choices().at(form->currentChoices().constFirst()); } QUndoCommand *uc = new EditFormComboCommand( this->d, form, pageNumber, newText, newCursorPos, prevText, prevCursorPos, prevAnchorPos ); d->m_undoStack->push( uc ); } void Document::editFormButtons( int pageNumber, const QList< FormFieldButton* >& formButtons, const QList< bool >& newButtonStates ) { QUndoCommand *uc = new EditFormButtonsCommand( this->d, pageNumber, formButtons, newButtonStates ); d->m_undoStack->push( uc ); } void Document::reloadDocument() const { const int numOfPages = pages(); for( int i = currentPage(); i >= 0; i -- ) d->refreshPixmaps( i ); for( int i = currentPage() + 1; i < numOfPages; i ++ ) d->refreshPixmaps( i ); } BookmarkManager * Document::bookmarkManager() const { return d->m_bookmarkManager; } QList Document::bookmarkedPageList() const { QList list; uint docPages = pages(); //pages are 0-indexed internally, but 1-indexed externally for ( uint i = 0; i < docPages; i++ ) { if ( bookmarkManager()->isBookmarked( i ) ) { list << i + 1; } } return list; } QString Document::bookmarkedPageRange() const { // Code formerly in Part::slotPrint() // range detecting QString range; uint docPages = pages(); int startId = -1; int endId = -1; for ( uint i = 0; i < docPages; ++i ) { if ( bookmarkManager()->isBookmarked( i ) ) { if ( startId < 0 ) startId = i; if ( endId < 0 ) endId = startId; else ++endId; } else if ( startId >= 0 && endId >= 0 ) { if ( !range.isEmpty() ) range += QLatin1Char ( ',' ); if ( endId - startId > 0 ) range += QStringLiteral( "%1-%2" ).arg( startId + 1 ).arg( endId + 1 ); else range += QString::number( startId + 1 ); startId = -1; endId = -1; } } if ( startId >= 0 && endId >= 0 ) { if ( !range.isEmpty() ) range += QLatin1Char ( ',' ); if ( endId - startId > 0 ) range += QStringLiteral( "%1-%2" ).arg( startId + 1 ).arg( endId + 1 ); else range += QString::number( startId + 1 ); } return range; } void Document::processAction( const Action * action ) { if ( !action ) return; // Don't execute next actions if the action itself caused the closing of the document bool executeNextActions = true; QObject disconnectHelper; // guarantees the connect below will be disconnected on finishing the function connect( this, &Document::aboutToClose, &disconnectHelper, [&executeNextActions] { executeNextActions = false; } ); switch( action->actionType() ) { case Action::Goto: { const GotoAction * go = static_cast< const GotoAction * >( action ); d->m_nextDocumentViewport = go->destViewport(); d->m_nextDocumentDestination = go->destinationName(); // Explanation of why d->m_nextDocumentViewport is needed: // all openRelativeFile does is launch a signal telling we // want to open another URL, the problem is that when the file is // non local, the loading is done asynchronously so you can't // do a setViewport after the if as it was because you are doing the setViewport // on the old file and when the new arrives there is no setViewport for it and // it does not show anything // first open filename if link is pointing outside this document if ( go->isExternal() && !d->openRelativeFile( go->fileName() ) ) { qCWarning(OkularCoreDebug).nospace() << "Action: Error opening '" << go->fileName() << "'."; break; } else { const DocumentViewport nextViewport = d->nextDocumentViewport(); // skip local links that point to nowhere (broken ones) if ( !nextViewport.isValid() ) break; setViewport( nextViewport, nullptr, true ); d->m_nextDocumentViewport = DocumentViewport(); d->m_nextDocumentDestination = QString(); } } break; case Action::Execute: { const ExecuteAction * exe = static_cast< const ExecuteAction * >( action ); const QString fileName = exe->fileName(); if ( fileName.endsWith( QLatin1String(".pdf"), Qt::CaseInsensitive ) ) { d->openRelativeFile( fileName ); break; } // Albert: the only pdf i have that has that kind of link don't define // an application and use the fileName as the file to open QUrl url = d->giveAbsoluteUrl( fileName ); QMimeDatabase db; QMimeType mime = db.mimeTypeForUrl( url ); // Check executables if ( KRun::isExecutableFile( url, mime.name() ) ) { // Don't have any pdf that uses this code path, just a guess on how it should work if ( !exe->parameters().isEmpty() ) { url = d->giveAbsoluteUrl( exe->parameters() ); mime = db.mimeTypeForUrl( url ); if ( KRun::isExecutableFile( url, mime.name() ) ) { // this case is a link pointing to an executable with a parameter // that also is an executable, possibly a hand-crafted pdf KMessageBox::information( d->m_widget, i18n("The document is trying to execute an external application and, for your safety, Okular does not allow that.") ); break; } } else { // this case is a link pointing to an executable with no parameters // core developers find unacceptable executing it even after asking the user KMessageBox::information( d->m_widget, i18n("The document is trying to execute an external application and, for your safety, Okular does not allow that.") ); break; } } KService::Ptr ptr = KMimeTypeTrader::self()->preferredService( mime.name(), QStringLiteral("Application") ); if ( ptr ) { QList lst; lst.append( url ); KRun::runService( *ptr, lst, nullptr ); } else KMessageBox::information( d->m_widget, i18n( "No application found for opening file of mimetype %1.", mime.name() ) ); } break; case Action::DocAction: { const DocumentAction * docaction = static_cast< const DocumentAction * >( action ); switch( docaction->documentActionType() ) { case DocumentAction::PageFirst: setViewportPage( 0 ); break; case DocumentAction::PagePrev: if ( (*d->m_viewportIterator).pageNumber > 0 ) setViewportPage( (*d->m_viewportIterator).pageNumber - 1 ); break; case DocumentAction::PageNext: if ( (*d->m_viewportIterator).pageNumber < (int)d->m_pagesVector.count() - 1 ) setViewportPage( (*d->m_viewportIterator).pageNumber + 1 ); break; case DocumentAction::PageLast: setViewportPage( d->m_pagesVector.count() - 1 ); break; case DocumentAction::HistoryBack: setPrevViewport(); break; case DocumentAction::HistoryForward: setNextViewport(); break; case DocumentAction::Quit: emit quit(); break; case DocumentAction::Presentation: emit linkPresentation(); break; case DocumentAction::EndPresentation: emit linkEndPresentation(); break; case DocumentAction::Find: emit linkFind(); break; case DocumentAction::GoToPage: emit linkGoToPage(); break; case DocumentAction::Close: emit close(); break; } } break; case Action::Browse: { const BrowseAction * browse = static_cast< const BrowseAction * >( action ); QString lilySource; int lilyRow = 0, lilyCol = 0; // if the url is a mailto one, invoke mailer if ( browse->url().scheme() == QLatin1String("mailto") ) { QDesktopServices::openUrl( browse->url() ); } else if ( extractLilyPondSourceReference( browse->url(), &lilySource, &lilyRow, &lilyCol ) ) { const SourceReference ref( lilySource, lilyRow, lilyCol ); processSourceReference( &ref ); } else { const QUrl url = browse->url(); // fix for #100366, documents with relative links that are the form of http:foo.pdf if ((url.scheme() == QLatin1String("http")) && url.host().isEmpty() && url.fileName().endsWith(QLatin1String("pdf"))) { d->openRelativeFile(url.fileName()); break; } // handle documents with relative path if ( d->m_url.isValid() ) { const QUrl realUrl = KIO::upUrl(d->m_url).resolved(url); // KRun autodeletes new KRun( realUrl, d->m_widget ); } } } break; case Action::Sound: { const SoundAction * linksound = static_cast< const SoundAction * >( action ); AudioPlayer::instance()->playSound( linksound->sound(), linksound ); } break; case Action::Script: { const ScriptAction * linkscript = static_cast< const ScriptAction * >( action ); if ( !d->m_scripter ) d->m_scripter = new Scripter( d ); d->m_scripter->execute( linkscript->scriptType(), linkscript->script() ); } break; case Action::Movie: emit processMovieAction( static_cast< const MovieAction * >( action ) ); break; case Action::Rendition: { const RenditionAction * linkrendition = static_cast< const RenditionAction * >( action ); if ( !linkrendition->script().isEmpty() ) { if ( !d->m_scripter ) d->m_scripter = new Scripter( d ); d->m_scripter->execute( linkrendition->scriptType(), linkrendition->script() ); } emit processRenditionAction( static_cast< const RenditionAction * >( action ) ); } break; case Action::BackendOpaque: { d->m_generator->opaqueAction( static_cast< const BackendOpaqueAction * >( action ) ); } break; } if ( executeNextActions ) { const QVector nextActions = action->nextActions(); for ( const Action *a : nextActions ) { processAction( a ); } } } void Document::processFormatAction( const Action * action, Okular::FormFieldText *fft ) { if ( action->actionType() != Action::Script ) { qCDebug( OkularCoreDebug ) << "Unsupported action type" << action->actionType() << "for formatting."; return; } // Lookup the page of the FormFieldText int foundPage = d->findFieldPageNumber( fft ); if ( foundPage == -1 ) { qCDebug( OkularCoreDebug ) << "Could not find page for formfield!"; return; } const QString unformattedText = fft->text(); std::shared_ptr< Event > event = Event::createFormatEvent( fft, d->m_pagesVector[foundPage] ); const ScriptAction * linkscript = static_cast< const ScriptAction * >( action ); d->executeScriptEvent( event, linkscript ); const QString formattedText = event->value().toString(); if ( formattedText != unformattedText ) { // We set the formattedText, because when we call refreshFormWidget // It will set the QLineEdit to this formattedText fft->setText( formattedText ); fft->setAppearanceText( formattedText ); emit refreshFormWidget( fft ); d->refreshPixmaps( foundPage ); // Then we make the form have the unformatted text, to use // in calculations and other things. fft->setText( unformattedText ); } else if ( fft->additionalAction( FormField::CalculateField ) ) { // When the field was calculated we need to refresh even // if the format script changed nothing. e.g. on error. // This is because the recalculateForms function delegated // the responsiblity for the refresh to us. emit refreshFormWidget( fft ); d->refreshPixmaps( foundPage ); } } void Document::processKeystrokeAction( const Action * action, Okular::FormFieldText *fft, bool &returnCode ) { if ( action->actionType() != Action::Script ) { qCDebug( OkularCoreDebug ) << "Unsupported action type" << action->actionType() << "for keystroke."; return; } // Lookup the page of the FormFieldText int foundPage = d->findFieldPageNumber( fft ); if ( foundPage == -1 ) { qCDebug( OkularCoreDebug ) << "Could not find page for formfield!"; return; } std::shared_ptr< Event > event = Event::createKeystrokeEvent( fft, d->m_pagesVector[foundPage] ); const ScriptAction * linkscript = static_cast< const ScriptAction * >( action ); d->executeScriptEvent( event, linkscript ); returnCode = event->returnCode(); } void Document::processFocusAction( const Action * action, Okular::FormField *field ) { if ( !action || action->actionType() != Action::Script ) return; // Lookup the page of the FormFieldText int foundPage = d->findFieldPageNumber( field ); if ( foundPage == -1 ) { qCDebug( OkularCoreDebug ) << "Could not find page for formfield!"; return; } std::shared_ptr< Event > event = Event::createFormFocusEvent( field, d->m_pagesVector[foundPage] ); const ScriptAction * linkscript = static_cast< const ScriptAction * >( action ); d->executeScriptEvent( event, linkscript ); } void Document::processValidateAction( const Action * action, Okular::FormFieldText *fft, bool &returnCode ) { if ( !action || action->actionType() != Action::Script ) return; // Lookup the page of the FormFieldText int foundPage = d->findFieldPageNumber( fft ); if ( foundPage == -1 ) { qCDebug( OkularCoreDebug ) << "Could not find page for formfield!"; return; } std::shared_ptr< Event > event = Event::createFormValidateEvent( fft, d->m_pagesVector[foundPage] ); const ScriptAction * linkscript = static_cast< const ScriptAction * >( action ); d->executeScriptEvent( event, linkscript ); returnCode = event->returnCode(); } void Document::processSourceReference( const SourceReference * ref ) { if ( !ref ) return; const QUrl url = d->giveAbsoluteUrl( ref->fileName() ); if ( !url.isLocalFile() ) { qCDebug(OkularCoreDebug) << url.url() << "is not a local file."; return; } const QString absFileName = url.toLocalFile(); if ( !QFile::exists( absFileName ) ) { qCDebug(OkularCoreDebug) << "No such file:" << absFileName; return; } bool handled = false; emit sourceReferenceActivated(absFileName, ref->row(), ref->column(), &handled); if(handled) { return; } static QHash< int, QString > editors; // init the editors table if empty (on first run, usually) if ( editors.isEmpty() ) { editors = buildEditorsMap(); } QHash< int, QString >::const_iterator it = editors.constFind( SettingsCore::externalEditor() ); QString p; if ( it != editors.constEnd() ) p = *it; else p = SettingsCore::externalEditorCommand(); // custom editor not yet configured if ( p.isEmpty() ) return; // manually append the %f placeholder if not specified if ( p.indexOf( QLatin1String( "%f" ) ) == -1 ) p.append( QLatin1String( " %f" ) ); // replacing the placeholders QHash< QChar, QString > map; map.insert( QLatin1Char ( 'f' ), absFileName ); map.insert( QLatin1Char ( 'c' ), QString::number( ref->column() ) ); map.insert( QLatin1Char ( 'l' ), QString::number( ref->row() ) ); const QString cmd = KMacroExpander::expandMacrosShellQuote( p, map ); if ( cmd.isEmpty() ) return; const QStringList args = KShell::splitArgs( cmd ); if ( args.isEmpty() ) return; KProcess::startDetached( args ); } const SourceReference * Document::dynamicSourceReference( int pageNr, double absX, double absY ) { if ( !d->m_synctex_scanner ) return nullptr; const QSizeF dpi = d->m_generator->dpi(); if (synctex_edit_query(d->m_synctex_scanner, pageNr + 1, absX * 72. / dpi.width(), absY * 72. / dpi.height()) > 0) { synctex_node_p node; // TODO what should we do if there is really more than one node? while (( node = synctex_scanner_next_result( d->m_synctex_scanner ) )) { int line = synctex_node_line(node); int col = synctex_node_column(node); // column extraction does not seem to be implemented in synctex so far. set the SourceReference default value. if ( col == -1 ) { col = 0; } const char *name = synctex_scanner_get_name( d->m_synctex_scanner, synctex_node_tag( node ) ); return new Okular::SourceReference( QFile::decodeName( name ), line, col ); } } return nullptr; } Document::PrintingType Document::printingSupport() const { if ( d->m_generator ) { if ( d->m_generator->hasFeature( Generator::PrintNative ) ) { return NativePrinting; } #ifndef Q_OS_WIN if ( d->m_generator->hasFeature( Generator::PrintPostscript ) ) { return PostscriptPrinting; } #endif } return NoPrinting; } bool Document::supportsPrintToFile() const { return d->m_generator ? d->m_generator->hasFeature( Generator::PrintToFile ) : false; } bool Document::print( QPrinter &printer ) { return d->m_generator ? d->m_generator->print( printer ) : false; } QString Document::printError() const { Okular::Generator::PrintError err = Generator::UnknownPrintError; if ( d->m_generator ) { QMetaObject::invokeMethod( d->m_generator, "printError", Qt::DirectConnection, Q_RETURN_ARG(Okular::Generator::PrintError, err) ); } Q_ASSERT( err != Generator::NoPrintError ); switch ( err ) { case Generator::TemporaryFileOpenPrintError: return i18n( "Could not open a temporary file" ); case Generator::FileConversionPrintError: return i18n( "Print conversion failed" ); case Generator::PrintingProcessCrashPrintError: return i18n( "Printing process crashed" ); case Generator::PrintingProcessStartPrintError: return i18n( "Printing process could not start" ); case Generator::PrintToFilePrintError: return i18n( "Printing to file failed" ); case Generator::InvalidPrinterStatePrintError: return i18n( "Printer was in invalid state" ); case Generator::UnableToFindFilePrintError: return i18n( "Unable to find file to print" ); case Generator::NoFileToPrintError: return i18n( "There was no file to print" ); case Generator::NoBinaryToPrintError: return i18n( "Could not find a suitable binary for printing. Make sure CUPS lpr binary is available" ); case Generator::InvalidPageSizePrintError: return i18n( "The page print size is invalid" ); case Generator::NoPrintError: return QString(); case Generator::UnknownPrintError: return QString(); } return QString(); } QWidget* Document::printConfigurationWidget() const { if ( d->m_generator ) { PrintInterface * iface = qobject_cast< Okular::PrintInterface * >( d->m_generator ); return iface ? iface->printConfigurationWidget() : nullptr; } else return nullptr; } void Document::fillConfigDialog( KConfigDialog * dialog ) { if ( !dialog ) return; // We know it's a BackendConfigDialog, but check anyway BackendConfigDialog *bcd = dynamic_cast( dialog ); if ( !bcd ) return; // ensure that we have all the generators with settings loaded QVector offers = DocumentPrivate::configurableGenerators(); d->loadServiceList( offers ); // We want the generators to be sorted by name so let's fill in a QMap // this sorts by internal id which is not awesome, but at least the sorting // is stable between runs that before it wasn't QMap sortedGenerators; QHash< QString, GeneratorInfo >::iterator it = d->m_loadedGenerators.begin(); QHash< QString, GeneratorInfo >::iterator itEnd = d->m_loadedGenerators.end(); for ( ; it != itEnd; ++it ) { sortedGenerators.insert(it.key(), it.value()); } bool pagesAdded = false; QMap< QString, GeneratorInfo >::iterator sit = sortedGenerators.begin(); QMap< QString, GeneratorInfo >::iterator sitEnd = sortedGenerators.end(); for ( ; sit != sitEnd; ++sit ) { Okular::ConfigInterface * iface = d->generatorConfig( sit.value() ); if ( iface ) { iface->addPages( dialog ); pagesAdded = true; if ( sit.value().generator == d->m_generator ) { const int rowCount = bcd->thePageWidget()->model()->rowCount(); KPageView *view = bcd->thePageWidget(); view->setCurrentPage( view->model()->index( rowCount - 1, 0 ) ); } } } if ( pagesAdded ) { connect( dialog, &KConfigDialog::settingsChanged, this, [this] { d->slotGeneratorConfigChanged(); } ); } } QVector DocumentPrivate::configurableGenerators() { const QVector available = availableGenerators(); QVector result; for (const KPluginMetaData& md : available) { if (md.rawData()[QStringLiteral("X-KDE-okularHasInternalSettings")].toBool()) { result << md; } } return result; } KPluginMetaData Document::generatorInfo() const { if (!d->m_generator) return KPluginMetaData(); auto genIt = d->m_loadedGenerators.constFind(d->m_generatorName); Q_ASSERT(genIt != d->m_loadedGenerators.constEnd()); return genIt.value().metadata; } int Document::configurableGenerators() const { return DocumentPrivate::configurableGenerators().size(); } QStringList Document::supportedMimeTypes() const { // TODO: make it a static member of DocumentPrivate? QStringList result = d->m_supportedMimeTypes; if (result.isEmpty()) { const QVector available = DocumentPrivate::availableGenerators(); for (const KPluginMetaData& md : available) { result << md.mimeTypes(); } // Remove duplicate mimetypes represented by different names QMimeDatabase mimeDatabase; QSet uniqueMimetypes; for (const QString &mimeName : qAsConst(result)) { uniqueMimetypes.insert(mimeDatabase.mimeTypeForName(mimeName)); } result.clear(); for (const QMimeType &mimeType : uniqueMimetypes) { result.append(mimeType.name()); } // Add the Okular archive mimetype result << QStringLiteral("application/vnd.kde.okular-archive"); // Sorting by mimetype name doesn't make a ton of sense, // but ensures that the list is ordered the same way every time std::sort(result.begin(), result.end()); d->m_supportedMimeTypes = result; } return result; } bool Document::canSwapBackingFile() const { if ( !d->m_generator ) return false; return d->m_generator->hasFeature( Generator::SwapBackingFile ); } bool Document::swapBackingFile( const QString &newFileName, const QUrl &url ) { if ( !d->m_generator ) return false; if ( !d->m_generator->hasFeature( Generator::SwapBackingFile ) ) return false; // Save metadata about the file we're about to close d->saveDocumentInfo(); d->clearAndWaitForRequests(); qCDebug(OkularCoreDebug) << "Swapping backing file to" << newFileName; QVector< Page * > newPagesVector; Generator::SwapBackingFileResult result = d->m_generator->swapBackingFile( newFileName, newPagesVector ); if (result != Generator::SwapBackingFileError) { QLinkedList< ObjectRect* > rectsToDelete; QLinkedList< Annotation* > annotationsToDelete; QSet< PagePrivate* > pagePrivatesToDelete; if (result == Generator::SwapBackingFileReloadInternalData) { // Here we need to replace everything that the old generator // had created with what the new one has without making it look like // we have actually closed and opened the file again // Simple sanity check if (newPagesVector.count() != d->m_pagesVector.count()) return false; // Update the undo stack contents for (int i = 0; i < d->m_undoStack->count(); ++i) { // Trust me on the const_cast ^_^ QUndoCommand *uc = const_cast( d->m_undoStack->command( i ) ); if (OkularUndoCommand *ouc = dynamic_cast( uc )) { const bool success = ouc->refreshInternalPageReferences( newPagesVector ); if ( !success ) { qWarning() << "Document::swapBackingFile: refreshInternalPageReferences failed" << ouc; return false; } } else { qWarning() << "Document::swapBackingFile: Unhandled undo command" << uc; return false; } } for (int i = 0; i < d->m_pagesVector.count(); ++i) { // switch the PagePrivate* from newPage to oldPage // this way everyone still holding Page* doesn't get // disturbed by it Page *oldPage = d->m_pagesVector[i]; Page *newPage = newPagesVector[i]; newPage->d->adoptGeneratedContents(oldPage->d); pagePrivatesToDelete << oldPage->d; oldPage->d = newPage->d; oldPage->d->m_page = oldPage; oldPage->d->m_doc = d; newPage->d = nullptr; annotationsToDelete << oldPage->m_annotations; rectsToDelete << oldPage->m_rects; oldPage->m_annotations = newPage->m_annotations; oldPage->m_rects = newPage->m_rects; } qDeleteAll( newPagesVector ); } d->m_url = url; d->m_docFileName = newFileName; d->updateMetadataXmlNameAndDocSize(); d->m_bookmarkManager->setUrl( d->m_url ); d->m_documentInfo = DocumentInfo(); d->m_documentInfoAskedKeys.clear(); if ( d->m_synctex_scanner ) { synctex_scanner_free( d->m_synctex_scanner ); d->m_synctex_scanner = synctex_scanner_new_with_output_file( QFile::encodeName( newFileName ).constData(), nullptr, 1); if ( !d->m_synctex_scanner && QFile::exists(newFileName + QLatin1String( "sync" ) ) ) { d->loadSyncFile(newFileName); } } foreachObserver( notifySetup( d->m_pagesVector, DocumentObserver::UrlChanged ) ); qDeleteAll( annotationsToDelete ); qDeleteAll( rectsToDelete ); qDeleteAll( pagePrivatesToDelete ); return true; } else { return false; } } bool Document::swapBackingFileArchive( const QString &newFileName, const QUrl &url ) { qCDebug(OkularCoreDebug) << "Swapping backing archive to" << newFileName; ArchiveData *newArchive = DocumentPrivate::unpackDocumentArchive( newFileName ); if ( !newArchive ) return false; const QString tempFileName = newArchive->document.fileName(); const bool success = swapBackingFile( tempFileName, url ); if ( success ) { delete d->m_archiveData; d->m_archiveData = newArchive; } return success; } void Document::setHistoryClean( bool clean ) { if ( clean ) d->m_undoStack->setClean(); else d->m_undoStack->resetClean(); } bool Document::canSaveChanges() const { if ( !d->m_generator ) return false; Q_ASSERT( !d->m_generatorName.isEmpty() ); QHash< QString, GeneratorInfo >::iterator genIt = d->m_loadedGenerators.find( d->m_generatorName ); Q_ASSERT( genIt != d->m_loadedGenerators.end() ); SaveInterface* saveIface = d->generatorSave( genIt.value() ); if ( !saveIface ) return false; return saveIface->supportsOption( SaveInterface::SaveChanges ); } bool Document::canSaveChanges( SaveCapability cap ) const { switch ( cap ) { case SaveFormsCapability: /* Assume that if the generator supports saving, forms can be saved. * We have no means to actually query the generator at the moment * TODO: Add some method to query the generator in SaveInterface */ return canSaveChanges(); case SaveAnnotationsCapability: return d->canAddAnnotationsNatively(); } return false; } bool Document::saveChanges( const QString &fileName ) { QString errorText; return saveChanges( fileName, &errorText ); } bool Document::saveChanges( const QString &fileName, QString *errorText ) { if ( !d->m_generator || fileName.isEmpty() ) return false; Q_ASSERT( !d->m_generatorName.isEmpty() ); QHash< QString, GeneratorInfo >::iterator genIt = d->m_loadedGenerators.find( d->m_generatorName ); Q_ASSERT( genIt != d->m_loadedGenerators.end() ); SaveInterface* saveIface = d->generatorSave( genIt.value() ); if ( !saveIface || !saveIface->supportsOption( SaveInterface::SaveChanges ) ) return false; return saveIface->save( fileName, SaveInterface::SaveChanges, errorText ); } void Document::registerView( View *view ) { if ( !view ) return; Document *viewDoc = view->viewDocument(); if ( viewDoc ) { // check if already registered for this document if ( viewDoc == this ) return; viewDoc->unregisterView( view ); } d->m_views.insert( view ); view->d_func()->document = d; } void Document::unregisterView( View *view ) { if ( !view ) return; Document *viewDoc = view->viewDocument(); if ( !viewDoc || viewDoc != this ) return; view->d_func()->document = nullptr; d->m_views.remove( view ); } QByteArray Document::fontData(const FontInfo &font) const { QByteArray result; if (d->m_generator) { QMetaObject::invokeMethod(d->m_generator, "requestFontData", Qt::DirectConnection, Q_ARG(Okular::FontInfo, font), Q_ARG(QByteArray*, &result)); } return result; } ArchiveData *DocumentPrivate::unpackDocumentArchive( const QString &archivePath ) { QMimeDatabase db; const QMimeType mime = db.mimeTypeForFile( archivePath, QMimeDatabase::MatchExtension ); if ( !mime.inherits( QStringLiteral("application/vnd.kde.okular-archive") ) ) return nullptr; KZip okularArchive( archivePath ); if ( !okularArchive.open( QIODevice::ReadOnly ) ) return nullptr; const KArchiveDirectory * mainDir = okularArchive.directory(); // Check the archive doesn't have folders, we don't create them when saving the archive // and folders mean paths and paths mean path traversal issues const QStringList mainDirEntries = mainDir->entries(); for ( const QString &entry : mainDirEntries ) { if ( mainDir->entry( entry )->isDirectory() ) { qWarning() << "Warning: Found a directory inside" << archivePath << " - Okular does not create files like that so it is most probably forged."; return nullptr; } } const KArchiveEntry * mainEntry = mainDir->entry( QStringLiteral("content.xml") ); if ( !mainEntry || !mainEntry->isFile() ) return nullptr; std::unique_ptr< QIODevice > mainEntryDevice( static_cast< const KZipFileEntry * >( mainEntry )->createDevice() ); QDomDocument doc; if ( !doc.setContent( mainEntryDevice.get() ) ) return nullptr; mainEntryDevice.reset(); QDomElement root = doc.documentElement(); if ( root.tagName() != QLatin1String("OkularArchive") ) return nullptr; QString documentFileName; QString metadataFileName; QDomElement el = root.firstChild().toElement(); for ( ; !el.isNull(); el = el.nextSibling().toElement() ) { if ( el.tagName() == QLatin1String("Files") ) { QDomElement fileEl = el.firstChild().toElement(); for ( ; !fileEl.isNull(); fileEl = fileEl.nextSibling().toElement() ) { if ( fileEl.tagName() == QLatin1String("DocumentFileName") ) documentFileName = fileEl.text(); else if ( fileEl.tagName() == QLatin1String("MetadataFileName") ) metadataFileName = fileEl.text(); } } } if ( documentFileName.isEmpty() ) return nullptr; const KArchiveEntry * docEntry = mainDir->entry( documentFileName ); if ( !docEntry || !docEntry->isFile() ) return nullptr; std::unique_ptr< ArchiveData > archiveData( new ArchiveData() ); const int dotPos = documentFileName.indexOf( QLatin1Char('.') ); if ( dotPos != -1 ) archiveData->document.setFileTemplate(QDir::tempPath() + QLatin1String("/okular_XXXXXX") + documentFileName.mid(dotPos)); if ( !archiveData->document.open() ) return nullptr; archiveData->originalFileName = documentFileName; { std::unique_ptr< QIODevice > docEntryDevice( static_cast< const KZipFileEntry * >( docEntry )->createDevice() ); copyQIODevice( docEntryDevice.get(), &archiveData->document ); archiveData->document.close(); } const KArchiveEntry * metadataEntry = mainDir->entry( metadataFileName ); if ( metadataEntry && metadataEntry->isFile() ) { std::unique_ptr< QIODevice > metadataEntryDevice( static_cast< const KZipFileEntry * >( metadataEntry )->createDevice() ); archiveData->metadataFile.setFileTemplate(QDir::tempPath() + QLatin1String("/okular_XXXXXX.xml")); if ( archiveData->metadataFile.open() ) { copyQIODevice( metadataEntryDevice.get(), &archiveData->metadataFile ); archiveData->metadataFile.close(); } } return archiveData.release(); } Document::OpenResult Document::openDocumentArchive( const QString & docFile, const QUrl & url, const QString & password ) { d->m_archiveData = DocumentPrivate::unpackDocumentArchive( docFile ); if ( !d->m_archiveData ) return OpenError; const QString tempFileName = d->m_archiveData->document.fileName(); QMimeDatabase db; const QMimeType docMime = db.mimeTypeForFile( tempFileName, QMimeDatabase::MatchExtension ); const OpenResult ret = openDocument( tempFileName, url, docMime, password ); if ( ret != OpenSuccess ) { delete d->m_archiveData; d->m_archiveData = nullptr; } return ret; } bool Document::saveDocumentArchive( const QString &fileName ) { if ( !d->m_generator ) return false; /* If we opened an archive, use the name of original file (eg foo.pdf) * instead of the archive's one (eg foo.okular) */ QString docFileName = d->m_archiveData ? d->m_archiveData->originalFileName : d->m_url.fileName(); if ( docFileName == QLatin1String( "-" ) ) return false; QString docPath = d->m_docFileName; const QFileInfo fi( docPath ); if ( fi.isSymLink() ) docPath = fi.symLinkTarget(); KZip okularArchive( fileName ); if ( !okularArchive.open( QIODevice::WriteOnly ) ) return false; const KUser user; #ifndef Q_OS_WIN const KUserGroup userGroup( user.groupId() ); #else const KUserGroup userGroup( QString( "" ) ); #endif QDomDocument contentDoc( QStringLiteral("OkularArchive") ); QDomProcessingInstruction xmlPi = contentDoc.createProcessingInstruction( QStringLiteral( "xml" ), QStringLiteral( "version=\"1.0\" encoding=\"utf-8\"" ) ); contentDoc.appendChild( xmlPi ); QDomElement root = contentDoc.createElement( QStringLiteral("OkularArchive") ); contentDoc.appendChild( root ); QDomElement filesNode = contentDoc.createElement( QStringLiteral("Files") ); root.appendChild( filesNode ); QDomElement fileNameNode = contentDoc.createElement( QStringLiteral("DocumentFileName") ); filesNode.appendChild( fileNameNode ); fileNameNode.appendChild( contentDoc.createTextNode( docFileName ) ); QDomElement metadataFileNameNode = contentDoc.createElement( QStringLiteral("MetadataFileName") ); filesNode.appendChild( metadataFileNameNode ); metadataFileNameNode.appendChild( contentDoc.createTextNode( QStringLiteral("metadata.xml") ) ); // If the generator can save annotations natively, do it QTemporaryFile modifiedFile; bool annotationsSavedNatively = false; bool formsSavedNatively = false; if ( d->canAddAnnotationsNatively() || canSaveChanges( SaveFormsCapability ) ) { if ( !modifiedFile.open() ) return false; const QString modifiedFileName = modifiedFile.fileName(); modifiedFile.close(); // We're only interested in the file name QString errorText; if ( saveChanges( modifiedFileName, &errorText ) ) { docPath = modifiedFileName; // Save this instead of the original file annotationsSavedNatively = d->canAddAnnotationsNatively(); formsSavedNatively = canSaveChanges( SaveFormsCapability ); } else { qCWarning(OkularCoreDebug) << "saveChanges failed: " << errorText; qCDebug(OkularCoreDebug) << "Falling back to saving a copy of the original file"; } } PageItems saveWhat = None; if ( !annotationsSavedNatively ) saveWhat |= AnnotationPageItems; if ( !formsSavedNatively ) saveWhat |= FormFieldPageItems; QTemporaryFile metadataFile; if ( !d->savePageDocumentInfo( &metadataFile, saveWhat ) ) return false; const QByteArray contentDocXml = contentDoc.toByteArray(); const mode_t perm = 0100644; okularArchive.writeFile( QStringLiteral("content.xml"), contentDocXml, perm, user.loginName(), userGroup.name() ); okularArchive.addLocalFile( docPath, docFileName ); okularArchive.addLocalFile( metadataFile.fileName(), QStringLiteral("metadata.xml") ); if ( !okularArchive.close() ) return false; return true; } bool Document::extractArchivedFile( const QString &destFileName ) { if ( !d->m_archiveData ) return false; // Remove existing file, if present (QFile::copy doesn't overwrite by itself) QFile::remove( destFileName ); return d->m_archiveData->document.copy( destFileName ); } QPrinter::Orientation Document::orientation() const { double width, height; int landscape, portrait; const Okular::Page *currentPage; // if some pages are landscape and others are not, the most common wins, as // QPrinter does not accept a per-page setting landscape = 0; portrait = 0; for (uint i = 0; i < pages(); i++) { currentPage = page(i); width = currentPage->width(); height = currentPage->height(); if (currentPage->orientation() == Okular::Rotation90 || currentPage->orientation() == Okular::Rotation270) qSwap(width, height); if (width > height) landscape++; else portrait++; } return (landscape > portrait) ? QPrinter::Landscape : QPrinter::Portrait; } void Document::setAnnotationEditingEnabled( bool enable ) { d->m_annotationEditingEnabled = enable; foreachObserver( notifySetup( d->m_pagesVector, 0 ) ); } void Document::walletDataForFile( const QString &fileName, QString *walletName, QString *walletFolder, QString *walletKey ) const { if (d->m_generator) { d->m_generator->walletDataForFile( fileName, walletName, walletFolder, walletKey ); } else if (d->m_walletGenerator) { d->m_walletGenerator->walletDataForFile( fileName, walletName, walletFolder, walletKey ); } } bool Document::isDocdataMigrationNeeded() const { return d->m_docdataMigrationNeeded; } void Document::docdataMigrationDone() { if (d->m_docdataMigrationNeeded) { d->m_docdataMigrationNeeded = false; foreachObserver( notifySetup( d->m_pagesVector, 0 ) ); } } QAbstractItemModel * Document::layersModel() const { return d->m_generator ? d->m_generator->layersModel() : nullptr; } QString Document::openError() const { return d->m_openError; } QByteArray Document::requestSignedRevisionData( const Okular::SignatureInfo &info ) { QFile f( d->m_docFileName ); if ( !f.open( QIODevice::ReadOnly ) ) { KMessageBox::error( nullptr, i18n("Could not open '%1'. File does not exist", d->m_docFileName ) ); return {}; } const QList byteRange = info.signedRangeBounds(); f.seek( byteRange.first() ); QByteArray data; QDataStream stream( &data, QIODevice::WriteOnly ); stream << f.read( byteRange.last() - byteRange.first() ); f.close(); return data; } void Document::refreshPixmaps( int pageNumber ) { d->refreshPixmaps( pageNumber ); } void DocumentPrivate::executeScript( const QString &function ) { if( !m_scripter ) m_scripter = new Scripter( this ); m_scripter->execute( JavaScript, function ); } void DocumentPrivate::requestDone( PixmapRequest * req ) { if ( !req ) return; if ( !m_generator || m_closingLoop ) { m_pixmapRequestsMutex.lock(); m_executingPixmapRequests.removeAll( req ); m_pixmapRequestsMutex.unlock(); delete req; if ( m_closingLoop ) m_closingLoop->exit(); return; } #ifndef NDEBUG if ( !m_generator->canGeneratePixmap() ) qCDebug(OkularCoreDebug) << "requestDone with generator not in READY state."; #endif if ( !req->shouldAbortRender() ) { // [MEM] 1.1 find and remove a previous entry for the same page and id QLinkedList< AllocatedPixmap * >::iterator aIt = m_allocatedPixmaps.begin(); QLinkedList< AllocatedPixmap * >::iterator aEnd = m_allocatedPixmaps.end(); for ( ; aIt != aEnd; ++aIt ) if ( (*aIt)->page == req->pageNumber() && (*aIt)->observer == req->observer() ) { AllocatedPixmap * p = *aIt; m_allocatedPixmaps.erase( aIt ); m_allocatedPixmapsTotalMemory -= p->memory; delete p; break; } DocumentObserver *observer = req->observer(); if ( m_observers.contains(observer) ) { // [MEM] 1.2 append memory allocation descriptor to the FIFO qulonglong memoryBytes = 0; const TilesManager *tm = req->d->tilesManager(); if ( tm ) memoryBytes = tm->totalMemory(); else memoryBytes = 4 * req->width() * req->height(); AllocatedPixmap * memoryPage = new AllocatedPixmap( req->observer(), req->pageNumber(), memoryBytes ); m_allocatedPixmaps.append( memoryPage ); m_allocatedPixmapsTotalMemory += memoryBytes; // 2. notify an observer that its pixmap changed observer->notifyPageChanged( req->pageNumber(), DocumentObserver::Pixmap ); } #ifndef NDEBUG else qCWarning(OkularCoreDebug) << "Receiving a done request for the defunct observer" << observer; #endif } // 3. delete request m_pixmapRequestsMutex.lock(); m_executingPixmapRequests.removeAll( req ); m_pixmapRequestsMutex.unlock(); delete req; // 4. start a new generation if some is pending m_pixmapRequestsMutex.lock(); bool hasPixmaps = !m_pixmapRequestsStack.isEmpty(); m_pixmapRequestsMutex.unlock(); if ( hasPixmaps ) sendGeneratorPixmapRequest(); } void DocumentPrivate::setPageBoundingBox( int page, const NormalizedRect& boundingBox ) { Page * kp = m_pagesVector[ page ]; if ( !m_generator || !kp ) return; if ( kp->boundingBox() == boundingBox ) return; kp->setBoundingBox( boundingBox ); // notify observers about the change foreachObserverD( notifyPageChanged( page, DocumentObserver::BoundingBox ) ); // TODO: For generators that generate the bbox by pixmap scanning, if the first generated pixmap is very small, the bounding box will forever be inaccurate. // TODO: Crop computation should also consider annotations, actions, etc. to make sure they're not cropped away. // TODO: Help compute bounding box for generators that create a QPixmap without a QImage, like text and plucker. // TODO: Don't compute the bounding box if no one needs it (e.g., Trim Borders is off). } void DocumentPrivate::calculateMaxTextPages() { int multipliers = qMax(1, qRound(getTotalMemory() / 536870912.0)); // 512 MB switch (SettingsCore::memoryLevel()) { case SettingsCore::EnumMemoryLevel::Low: m_maxAllocatedTextPages = multipliers * 2; break; case SettingsCore::EnumMemoryLevel::Normal: m_maxAllocatedTextPages = multipliers * 50; break; case SettingsCore::EnumMemoryLevel::Aggressive: m_maxAllocatedTextPages = multipliers * 250; break; case SettingsCore::EnumMemoryLevel::Greedy: m_maxAllocatedTextPages = multipliers * 1250; break; } } void DocumentPrivate::textGenerationDone( Page *page ) { if ( !m_pageController ) return; // 1. If we reached the cache limit, delete the first text page from the fifo if (m_allocatedTextPagesFifo.size() == m_maxAllocatedTextPages) { int pageToKick = m_allocatedTextPagesFifo.takeFirst(); if (pageToKick != page->number()) // this should never happen but better be safe than sorry { m_pagesVector.at(pageToKick)->setTextPage( nullptr ); // deletes the textpage } } // 2. Add the page to the fifo of generated text pages m_allocatedTextPagesFifo.append( page->number() ); } void Document::setRotation( int r ) { d->setRotationInternal( r, true ); } void DocumentPrivate::setRotationInternal( int r, bool notify ) { Rotation rotation = (Rotation)r; if ( !m_generator || ( m_rotation == rotation ) ) return; // tell the pages to rotate QVector< Okular::Page * >::const_iterator pIt = m_pagesVector.constBegin(); QVector< Okular::Page * >::const_iterator pEnd = m_pagesVector.constEnd(); for ( ; pIt != pEnd; ++pIt ) (*pIt)->d->rotateAt( rotation ); if ( notify ) { // notify the generator that the current rotation has changed m_generator->rotationChanged( rotation, m_rotation ); } // set the new rotation m_rotation = rotation; if ( notify ) { foreachObserverD( notifySetup( m_pagesVector, DocumentObserver::NewLayoutForPages ) ); foreachObserverD( notifyContentsCleared( DocumentObserver::Pixmap | DocumentObserver::Highlights | DocumentObserver::Annotations ) ); } qCDebug(OkularCoreDebug) << "Rotated:" << r; } void Document::setPageSize( const PageSize &size ) { if ( !d->m_generator || !d->m_generator->hasFeature( Generator::PageSizes ) ) return; if ( d->m_pageSizes.isEmpty() ) d->m_pageSizes = d->m_generator->pageSizes(); int sizeid = d->m_pageSizes.indexOf( size ); if ( sizeid == -1 ) return; // tell the pages to change size QVector< Okular::Page * >::const_iterator pIt = d->m_pagesVector.constBegin(); QVector< Okular::Page * >::const_iterator pEnd = d->m_pagesVector.constEnd(); for ( ; pIt != pEnd; ++pIt ) (*pIt)->d->changeSize( size ); // clear 'memory allocation' descriptors qDeleteAll( d->m_allocatedPixmaps ); d->m_allocatedPixmaps.clear(); d->m_allocatedPixmapsTotalMemory = 0; // notify the generator that the current page size has changed d->m_generator->pageSizeChanged( size, d->m_pageSize ); // set the new page size d->m_pageSize = size; foreachObserver( notifySetup( d->m_pagesVector, DocumentObserver::NewLayoutForPages ) ); foreachObserver( notifyContentsCleared( DocumentObserver::Pixmap | DocumentObserver::Highlights ) ); qCDebug(OkularCoreDebug) << "New PageSize id:" << sizeid; } /** DocumentViewport **/ DocumentViewport::DocumentViewport( int n ) : pageNumber( n ) { // default settings rePos.enabled = false; rePos.normalizedX = 0.5; rePos.normalizedY = 0.0; rePos.pos = Center; autoFit.enabled = false; autoFit.width = false; autoFit.height = false; } DocumentViewport::DocumentViewport( const QString & xmlDesc ) : pageNumber( -1 ) { // default settings (maybe overridden below) rePos.enabled = false; rePos.normalizedX = 0.5; rePos.normalizedY = 0.0; rePos.pos = Center; autoFit.enabled = false; autoFit.width = false; autoFit.height = false; // check for string presence if ( xmlDesc.isEmpty() ) return; // decode the string bool ok; int field = 0; QString token = xmlDesc.section( QLatin1Char(';'), field, field ); while ( !token.isEmpty() ) { // decode the current token if ( field == 0 ) { pageNumber = token.toInt( &ok ); if ( !ok ) return; } else if ( token.startsWith( QLatin1String("C1") ) ) { rePos.enabled = true; rePos.normalizedX = token.section( QLatin1Char(':'), 1, 1 ).toDouble(); rePos.normalizedY = token.section( QLatin1Char(':'), 2, 2 ).toDouble(); rePos.pos = Center; } else if ( token.startsWith( QLatin1String("C2") ) ) { rePos.enabled = true; rePos.normalizedX = token.section( QLatin1Char(':'), 1, 1 ).toDouble(); rePos.normalizedY = token.section( QLatin1Char(':'), 2, 2 ).toDouble(); if (token.section( QLatin1Char(':'), 3, 3 ).toInt() == 1) rePos.pos = Center; else rePos.pos = TopLeft; } else if ( token.startsWith( QLatin1String("AF1") ) ) { autoFit.enabled = true; autoFit.width = token.section( QLatin1Char(':'), 1, 1 ) == QLatin1String("T"); autoFit.height = token.section( QLatin1Char(':'), 2, 2 ) == QLatin1String("T"); } // proceed tokenizing string field++; token = xmlDesc.section( QLatin1Char(';'), field, field ); } } QString DocumentViewport::toString() const { // start string with page number QString s = QString::number( pageNumber ); // if has center coordinates, save them on string if ( rePos.enabled ) s += QStringLiteral( ";C2:" ) + QString::number( rePos.normalizedX ) + QLatin1Char(':') + QString::number( rePos.normalizedY ) + QLatin1Char(':') + QString::number( rePos.pos ); // if has autofit enabled, save its state on string if ( autoFit.enabled ) s += QStringLiteral( ";AF1:" ) + (autoFit.width ? QLatin1Char('T') : QLatin1Char('F')) + QLatin1Char(':') + (autoFit.height ? QLatin1Char('T') : QLatin1Char('F')); return s; } bool DocumentViewport::isValid() const { return pageNumber >= 0; } bool DocumentViewport::operator==( const DocumentViewport & other ) const { bool equal = ( pageNumber == other.pageNumber ) && ( rePos.enabled == other.rePos.enabled ) && ( autoFit.enabled == other.autoFit.enabled ); if ( !equal ) return false; if ( rePos.enabled && (( rePos.normalizedX != other.rePos.normalizedX) || ( rePos.normalizedY != other.rePos.normalizedY ) || rePos.pos != other.rePos.pos) ) return false; if ( autoFit.enabled && (( autoFit.width != other.autoFit.width ) || ( autoFit.height != other.autoFit.height )) ) return false; return true; } bool DocumentViewport::operator<( const DocumentViewport & other ) const { // TODO: Check autoFit and Position if ( pageNumber != other.pageNumber ) return pageNumber < other.pageNumber; if ( !rePos.enabled && other.rePos.enabled ) return true; if ( !other.rePos.enabled ) return false; if ( rePos.normalizedY != other.rePos.normalizedY ) return rePos.normalizedY < other.rePos.normalizedY; return rePos.normalizedX < other.rePos.normalizedX; } /** DocumentInfo **/ DocumentInfo::DocumentInfo() : d(new DocumentInfoPrivate()) { } DocumentInfo::DocumentInfo(const DocumentInfo &info) : d(new DocumentInfoPrivate()) { *this = info; } DocumentInfo& DocumentInfo::operator=(const DocumentInfo &info) { if (this != &info) { d->values = info.d->values; d->titles = info.d->titles; } return *this; } DocumentInfo::~DocumentInfo() { delete d; } void DocumentInfo::set( const QString &key, const QString &value, const QString &title ) { d->values[ key ] = value; d->titles[ key ] = title; } void DocumentInfo::set( Key key, const QString &value ) { d->values[ getKeyString( key ) ] = value; } QStringList DocumentInfo::keys() const { return d->values.keys(); } QString DocumentInfo::get( Key key ) const { return get( getKeyString( key ) ); } QString DocumentInfo::get( const QString &key ) const { return d->values[ key ]; } QString DocumentInfo::getKeyString( Key key ) //const { switch ( key ) { case Title: return QStringLiteral("title"); break; case Subject: return QStringLiteral("subject"); break; case Description: return QStringLiteral("description"); break; case Author: return QStringLiteral("author"); break; case Creator: return QStringLiteral("creator"); break; case Producer: return QStringLiteral("producer"); break; case Copyright: return QStringLiteral("copyright"); break; case Pages: return QStringLiteral("pages"); break; case CreationDate: return QStringLiteral("creationDate"); break; case ModificationDate: return QStringLiteral("modificationDate"); break; case MimeType: return QStringLiteral("mimeType"); break; case Category: return QStringLiteral("category"); break; case Keywords: return QStringLiteral("keywords"); break; case FilePath: return QStringLiteral("filePath"); break; case DocumentSize: return QStringLiteral("documentSize"); break; case PagesSize: return QStringLiteral("pageSize"); break; default: qCWarning(OkularCoreDebug) << "Unknown" << key; return QString(); break; } } DocumentInfo::Key DocumentInfo::getKeyFromString( const QString &key ) //const { if (key == QLatin1String("title")) return Title; else if (key == QLatin1String("subject")) return Subject; else if (key == QLatin1String("description")) return Description; else if (key == QLatin1String("author")) return Author; else if (key == QLatin1String("creator")) return Creator; else if (key == QLatin1String("producer")) return Producer; else if (key == QLatin1String("copyright")) return Copyright; else if (key == QLatin1String("pages")) return Pages; else if (key == QLatin1String("creationDate")) return CreationDate; else if (key == QLatin1String("modificationDate")) return ModificationDate; else if (key == QLatin1String("mimeType")) return MimeType; else if (key == QLatin1String("category")) return Category; else if (key == QLatin1String("keywords")) return Keywords; else if (key == QLatin1String("filePath")) return FilePath; else if (key == QLatin1String("documentSize")) return DocumentSize; else if (key == QLatin1String("pageSize")) return PagesSize; else return Invalid; } QString DocumentInfo::getKeyTitle( Key key ) //const { switch ( key ) { case Title: return i18n( "Title" ); break; case Subject: return i18n( "Subject" ); break; case Description: return i18n( "Description" ); break; case Author: return i18n( "Author" ); break; case Creator: return i18n( "Creator" ); break; case Producer: return i18n( "Producer" ); break; case Copyright: return i18n( "Copyright" ); break; case Pages: return i18n( "Pages" ); break; case CreationDate: return i18n( "Created" ); break; case ModificationDate: return i18n( "Modified" ); break; case MimeType: return i18n( "Mime Type" ); break; case Category: return i18n( "Category" ); break; case Keywords: return i18n( "Keywords" ); break; case FilePath: return i18n( "File Path" ); break; case DocumentSize: return i18n( "File Size" ); break; case PagesSize: return i18n("Page Size"); break; default: return QString(); break; } } QString DocumentInfo::getKeyTitle( const QString &key ) const { QString title = getKeyTitle ( getKeyFromString( key ) ); if ( title.isEmpty() ) title = d->titles[ key ]; return title; } /** DocumentSynopsis **/ DocumentSynopsis::DocumentSynopsis() : QDomDocument( QStringLiteral("DocumentSynopsis") ) { // void implementation, only subclassed for naming } DocumentSynopsis::DocumentSynopsis( const QDomDocument &document ) : QDomDocument( document ) { } /** EmbeddedFile **/ EmbeddedFile::EmbeddedFile() { } EmbeddedFile::~EmbeddedFile() { } VisiblePageRect::VisiblePageRect( int page, const NormalizedRect &rectangle ) : pageNumber( page ), rect( rectangle ) { } #undef foreachObserver #undef foreachObserverD /* kate: replace-tabs on; indent-width 4; */ diff --git a/core/document_p.h b/core/document_p.h index b80d4635e..2413844f0 100644 --- a/core/document_p.h +++ b/core/document_p.h @@ -1,361 +1,361 @@ /*************************************************************************** * Copyright (C) 2004-2005 by Enrico Ros * * Copyright (C) 2004-2007 by Albert Astals Cid * * * * 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. * ***************************************************************************/ #ifndef _OKULAR_DOCUMENT_P_H_ #define _OKULAR_DOCUMENT_P_H_ #include "document.h" #include "script/event_p.h" #include "synctex/synctex_parser.h" #include // qt/kde/system includes #include #include #include #include #include #include #include #include // local includes #include "fontinfo.h" #include "generator.h" class QUndoStack; class QEventLoop; class QFile; class QTimer; class QTemporaryFile; class KPluginMetaData; struct AllocatedPixmap; struct ArchiveData; struct RunningSearch; namespace Okular { class ScriptAction; class ConfigInterface; class PageController; class SaveInterface; class Scripter; class View; } struct GeneratorInfo { explicit GeneratorInfo( Okular::Generator *g, const KPluginMetaData &data) : generator( g ), metadata( data ), config( nullptr ), save( nullptr ), configChecked( false ), saveChecked( false ) {} Okular::Generator * generator; KPluginMetaData metadata; Okular::ConfigInterface * config; Okular::SaveInterface * save; bool configChecked : 1; bool saveChecked : 1; }; namespace Okular { class BackendConfigDialog : public KConfigDialog { public: BackendConfigDialog(QWidget *parent, const QString &name, KCoreConfigSkeleton *config) : KConfigDialog(parent, name, config) { } KPageWidget *thePageWidget() { return pageWidget(); } }; class FontExtractionThread; struct DoContinueDirectionMatchSearchStruct { QSet< int > *pagesToNotify; RegularAreaRect *match; int currentPage; int searchID; }; enum LoadDocumentInfoFlag { LoadNone = 0, LoadPageInfo = 1, // Load annotations and forms LoadGeneralInfo = 2, // History, rotation, ... LoadAllInfo = 0xff }; Q_DECLARE_FLAGS(LoadDocumentInfoFlags, LoadDocumentInfoFlag) class DocumentPrivate { public: explicit DocumentPrivate( Document *parent ) : m_parent( parent ), m_tempFile( nullptr ), m_docSize( -1 ), m_allocatedPixmapsTotalMemory( 0 ), m_maxAllocatedTextPages( 0 ), m_warnedOutOfMemory( false ), m_rotation( Rotation0 ), m_exportCached( false ), m_bookmarkManager( nullptr ), m_memCheckTimer( nullptr ), m_saveBookmarksTimer( nullptr ), m_generator( nullptr ), m_walletGenerator( nullptr ), m_generatorsLoaded( false ), m_pageController( nullptr ), m_closingLoop( nullptr ), m_scripter( nullptr ), m_archiveData( nullptr ), m_fontsCached( false ), m_annotationEditingEnabled ( true ), m_annotationBeingModified( false ), m_docdataMigrationNeeded( false ), m_synctex_scanner( nullptr ) { calculateMaxTextPages(); } // private methods bool updateMetadataXmlNameAndDocSize(); QString pagesSizeString() const; QString namePaperSize(double inchesWidth, double inchesHeight) const; - QString localizedSize(const QSizeF &size) const; + QString localizedSize(const QSizeF size) const; qulonglong calculateMemoryToFree(); void cleanupPixmapMemory(); void cleanupPixmapMemory( qulonglong memoryToFree ); AllocatedPixmap * searchLowestPriorityPixmap( bool unloadableOnly = false, bool thenRemoveIt = false, DocumentObserver *observer = nullptr /* any */ ); void calculateMaxTextPages(); qulonglong getTotalMemory(); qulonglong getFreeMemory( qulonglong *freeSwap = nullptr ); bool loadDocumentInfo( LoadDocumentInfoFlags loadWhat ); bool loadDocumentInfo( QFile &infoFile, LoadDocumentInfoFlags loadWhat ); void loadViewsInfo( View *view, const QDomElement &e ); void saveViewsInfo( View *view, QDomElement &e ) const; QUrl giveAbsoluteUrl( const QString & fileName ) const; bool openRelativeFile( const QString & fileName ); Generator * loadGeneratorLibrary( const KPluginMetaData& service ); void loadAllGeneratorLibraries(); void loadServiceList( const QVector& offers ); void unloadGenerator( const GeneratorInfo& info ); void cacheExportFormats(); void setRotationInternal( int r, bool notify ); ConfigInterface* generatorConfig( GeneratorInfo& info ); SaveInterface* generatorSave( GeneratorInfo& info ); Document::OpenResult openDocumentInternal( const KPluginMetaData& offer, bool isstdin, const QString& docFile, const QByteArray& filedata, const QString& password ); static ArchiveData *unpackDocumentArchive( const QString &archivePath ); bool savePageDocumentInfo( QTemporaryFile *infoFile, int what ) const; DocumentViewport nextDocumentViewport() const; void notifyAnnotationChanges( int page ); void notifyFormChanges( int page ); bool canAddAnnotationsNatively() const; bool canModifyExternalAnnotations() const; bool canRemoveExternalAnnotations() const; OKULARCORE_EXPORT static QString docDataFileName(const QUrl &url, qint64 document_size); bool cancelRenderingBecauseOf( PixmapRequest *executingRequest, PixmapRequest *newRequest ); // Methods that implement functionality needed by undo commands void performAddPageAnnotation( int page, Annotation *annotation ); void performRemovePageAnnotation( int page, Annotation * annotation ); void performModifyPageAnnotation( int page, Annotation * annotation, bool appearanceChanged ); void performSetAnnotationContents( const QString & newContents, Annotation *annot, int pageNumber ); void recalculateForms(); // private slots void saveDocumentInfo() const; void slotTimedMemoryCheck(); void sendGeneratorPixmapRequest(); void rotationFinished( int page, Okular::Page *okularPage ); void slotFontReadingProgress( int page ); void fontReadingGotFont( const Okular::FontInfo& font ); void slotGeneratorConfigChanged(); void refreshPixmaps( int ); void _o_configChanged(); void doContinueDirectionMatchSearch(void *doContinueDirectionMatchSearchStruct); void doContinueAllDocumentSearch(void *pagesToNotifySet, void *pageMatchesMap, int currentPage, int searchID); void doContinueGooglesDocumentSearch(void *pagesToNotifySet, void *pageMatchesMap, int currentPage, int searchID, const QStringList & words); void doProcessSearchMatch( RegularAreaRect *match, RunningSearch *search, QSet< int > *pagesToNotify, int currentPage, int searchID, bool moveViewport, const QColor & color ); /** * Executes a JavaScript script from the setInterval function. * * @since 1.9 */ void executeScript( const QString &function ); // generators stuff /** * This method is used by the generators to signal the finish of * the pixmap generation @p request. */ void requestDone( PixmapRequest * request ); void textGenerationDone( Page *page ); /** * Sets the bounding box of the given @p page (in terms of upright orientation, i.e., Rotation0). */ void setPageBoundingBox( int page, const NormalizedRect& boundingBox ); /** * Request a particular metadata of the Document itself (ie, not something * depending on the document type/backend). */ QVariant documentMetaData( const Generator::DocumentMetaDataKey key, const QVariant &option ) const; /** * Return whether the normalized rectangle @p rectOfInterest on page number @p rectPage * is fully visible. */ bool isNormalizedRectangleFullyVisible( const Okular::NormalizedRect & rectOfInterest, int rectPage ); // For sync files void loadSyncFile( const QString & filePath ); void clearAndWaitForRequests(); /* * Executes a ScriptAction with the event passed as parameter. */ void executeScriptEvent( const std::shared_ptr< Event > &event, const Okular::ScriptAction * linkscript ); /* * Find the corresponding page number for the form field passed as parameter. */ int findFieldPageNumber( Okular::FormField *field ); // member variables Document *m_parent; QPointer m_widget; // find descriptors, mapped by ID (we handle multiple searches) QMap< int, RunningSearch * > m_searches; bool m_searchCancelled; // needed because for remote documents docFileName is a local file and // we want the remote url when the document refers to relativeNames QUrl m_url; // cached stuff QString m_docFileName; QString m_xmlFileName; QTemporaryFile *m_tempFile; qint64 m_docSize; // viewport stuff QLinkedList< DocumentViewport > m_viewportHistory; QLinkedList< DocumentViewport >::iterator m_viewportIterator; DocumentViewport m_nextDocumentViewport; // see Link::Goto for an explanation QString m_nextDocumentDestination; // observers / requests / allocator stuff QSet< DocumentObserver * > m_observers; QLinkedList< PixmapRequest * > m_pixmapRequestsStack; QLinkedList< PixmapRequest * > m_executingPixmapRequests; QMutex m_pixmapRequestsMutex; QLinkedList< AllocatedPixmap * > m_allocatedPixmaps; qulonglong m_allocatedPixmapsTotalMemory; QList< int > m_allocatedTextPagesFifo; int m_maxAllocatedTextPages; bool m_warnedOutOfMemory; // the rotation applied to the document Rotation m_rotation; // the current size of the pages (if available), and the cache of the // available page sizes PageSize m_pageSize; PageSize::List m_pageSizes; // cache of the export formats bool m_exportCached; ExportFormat::List m_exportFormats; ExportFormat m_exportToText; // our bookmark manager BookmarkManager *m_bookmarkManager; // timers (memory checking / info saver) QTimer *m_memCheckTimer; QTimer *m_saveBookmarksTimer; QHash m_loadedGenerators; Generator * m_generator; QString m_generatorName; Generator * m_walletGenerator; bool m_generatorsLoaded; QVector< Page * > m_pagesVector; QVector< VisiblePageRect * > m_pageRects; // cache of the mimetype we support QStringList m_supportedMimeTypes; PageController *m_pageController; QEventLoop *m_closingLoop; Scripter *m_scripter; ArchiveData *m_archiveData; QString m_archivedFileName; QPointer< FontExtractionThread > m_fontThread; bool m_fontsCached; QSet m_documentInfoAskedKeys; DocumentInfo m_documentInfo; FontInfo::List m_fontsCache; QSet< View * > m_views; bool m_annotationEditingEnabled; bool m_annotationBeingModified; // is an annotation currently being moved or resized? bool m_metadataLoadingCompleted; QUndoStack *m_undoStack; QDomNode m_prevPropsOfAnnotBeingModified; // Since 0.21, we no longer support saving annotations and form data in // the docdata/ directory and we ask the user to migrate them to an // external file as soon as possible, otherwise the document will be // shown in read-only mode. This flag is set if the docdata/ XML file // for the current document contains any annotation or form. bool m_docdataMigrationNeeded; synctex_scanner_p m_synctex_scanner; QString m_openError; // generator selection static QVector availableGenerators(); static QVector configurableGenerators(); static KPluginMetaData generatorForMimeType(const QMimeType& type, QWidget* widget, const QVector &triedOffers = QVector()); }; class DocumentInfoPrivate { public: QMap values; // key -> value QMap titles; // key -> title For the custom keys }; } #endif /* kate: replace-tabs on; indent-width 4; */ diff --git a/core/fileprinter.cpp b/core/fileprinter.cpp index e66288cbe..6df768093 100644 --- a/core/fileprinter.cpp +++ b/core/fileprinter.cpp @@ -1,685 +1,685 @@ /*************************************************************************** * Copyright (C) 2007,2010 by John Layt * * * * FilePrinterPreview based on KPrintPreview (originally LGPL) * * Copyright (c) 2007 Alex Merry * * * * 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. * ***************************************************************************/ #include "fileprinter.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "debug_p.h" using namespace Okular; -int FilePrinter::printFile(QPrinter &printer, const QString file, // NOLINT(performance-unnecessary-value-param) TODO when BIC changes are allowed +int FilePrinter::printFile(QPrinter &printer, const QString file, // NOLINT(performance-unnecessary-value-param) clazy:exclude=function-args-by-ref TODO when BIC changes are allowed QPrinter::Orientation documentOrientation, FileDeletePolicy fileDeletePolicy, PageSelectPolicy pageSelectPolicy, const QString &pageRange ) { return printFile( printer, file, documentOrientation, fileDeletePolicy, pageSelectPolicy, pageRange, ScaleMode::FitToPrintArea ); } -int FilePrinter::printFile(QPrinter &printer, const QString file, // NOLINT(performance-unnecessary-value-param) TODO when BIC changes are allowed +int FilePrinter::printFile(QPrinter &printer, const QString file, // NOLINT(performance-unnecessary-value-param) clazy:exclude=function-args-by-ref TODO when BIC changes are allowed QPrinter::Orientation documentOrientation, FileDeletePolicy fileDeletePolicy, PageSelectPolicy pageSelectPolicy, const QString &pageRange, ScaleMode scaleMode ) { FilePrinter fp; return fp.doPrintFiles( printer, QStringList( file ), fileDeletePolicy, pageSelectPolicy, pageRange, documentOrientation, scaleMode ); } -int FilePrinter::doPrintFiles(QPrinter &printer, const QStringList fileList, FileDeletePolicy fileDeletePolicy, // NOLINT(performance-unnecessary-value-param) TODO when BIC changes are allowed +int FilePrinter::doPrintFiles(QPrinter &printer, const QStringList fileList, FileDeletePolicy fileDeletePolicy, // NOLINT(performance-unnecessary-value-param) clazy:exclude=function-args-by-ref TODO when BIC changes are allowed PageSelectPolicy pageSelectPolicy, const QString &pageRange, QPrinter::Orientation documentOrientation ) { return doPrintFiles( printer, fileList, fileDeletePolicy, pageSelectPolicy, pageRange, documentOrientation, ScaleMode::FitToPrintArea ); } int FilePrinter::doPrintFiles( QPrinter &printer, QStringList fileList, FileDeletePolicy fileDeletePolicy, PageSelectPolicy pageSelectPolicy, const QString &pageRange, QPrinter::Orientation documentOrientation, ScaleMode scaleMode ) { if ( fileList.size() < 1 ) { return -8; } for (QStringList::ConstIterator it = fileList.constBegin(); it != fileList.constEnd(); ++it) { if (!QFile::exists(*it)) { return -7; } } if ( printer.printerState() == QPrinter::Aborted || printer.printerState() == QPrinter::Error ) { return -6; } QString exe; QStringList argList; int ret; // Print to File if a filename set, assumes there must be only 1 file if ( !printer.outputFileName().isEmpty() ) { if ( QFile::exists( printer.outputFileName() ) ) { QFile::remove( printer.outputFileName() ); } QFileInfo inputFileInfo = QFileInfo( fileList[0] ); QFileInfo outputFileInfo = QFileInfo( printer.outputFileName() ); bool doDeleteFile = (fileDeletePolicy == FilePrinter::SystemDeletesFiles); if ( inputFileInfo.suffix() == outputFileInfo.suffix() ) { if ( doDeleteFile ) { bool res = QFile::rename( fileList[0], printer.outputFileName() ); if ( res ) { doDeleteFile = false; ret = 0; } else { ret = -5; } } else { bool res = QFile::copy( fileList[0], printer.outputFileName() ); if ( res ) { ret = 0; } else { ret = -5; } } } else if ( inputFileInfo.suffix() == QLatin1String("ps") && printer.outputFormat() == QPrinter::PdfFormat && ps2pdfAvailable() ) { exe = QStringLiteral("ps2pdf"); argList << fileList[0] << printer.outputFileName(); qCDebug(OkularCoreDebug) << "Executing" << exe << "with arguments" << argList; ret = KProcess::execute( exe, argList ); } else if ( inputFileInfo.suffix() == QLatin1String("pdf") && printer.outputFormat() == QPrinter::NativeFormat && pdf2psAvailable() ) { exe = QStringLiteral("pdf2ps"); argList << fileList[0] << printer.outputFileName(); qCDebug(OkularCoreDebug) << "Executing" << exe << "with arguments" << argList; ret = KProcess::execute( exe, argList ); } else { ret = -5; } if ( doDeleteFile ) { QFile::remove( fileList[0] ); } } else { // Print to a printer via lpr command //Decide what executable to use to print with, need the CUPS version of lpr if available //Some distros name the CUPS version of lpr as lpr-cups or lpr.cups so try those first //before default to lpr, or failing that to lp if ( !QStandardPaths::findExecutable(QStringLiteral("lpr-cups")).isEmpty() ) { exe = QStringLiteral("lpr-cups"); } else if ( !QStandardPaths::findExecutable(QStringLiteral("lpr.cups")).isEmpty() ) { exe = QStringLiteral("lpr.cups"); } else if ( !QStandardPaths::findExecutable(QStringLiteral("lpr")).isEmpty() ) { exe = QStringLiteral("lpr"); } else if ( !QStandardPaths::findExecutable(QStringLiteral("lp")).isEmpty() ) { exe = QStringLiteral("lp"); } else { return -9; } bool useCupsOptions = cupsAvailable(); argList = printArguments( printer, fileDeletePolicy, pageSelectPolicy, useCupsOptions, pageRange, exe, documentOrientation, scaleMode ) << fileList; qCDebug(OkularCoreDebug) << "Executing" << exe << "with arguments" << argList; ret = KProcess::execute( exe, argList ); } return ret; } QList FilePrinter::pageList( QPrinter &printer, int lastPage, const QList &selectedPageList ) { return pageList( printer, lastPage, 0, selectedPageList ); } QList FilePrinter::pageList( QPrinter &printer, int lastPage, int currentPage, const QList &selectedPageList ) { if ( printer.printRange() == QPrinter::Selection) { return selectedPageList; } int startPage, endPage; QList list; if ( printer.printRange() == QPrinter::PageRange ) { startPage = printer.fromPage(); endPage = printer.toPage(); } else if ( printer.printRange() == QPrinter::CurrentPage) { startPage = currentPage; endPage = currentPage; } else { //AllPages startPage = 1; endPage = lastPage; } for (int i = startPage; i <= endPage; i++ ) { list << i; } return list; } QString FilePrinter::pageRange( QPrinter &printer, int lastPage, const QList &selectedPageList ) { if ( printer.printRange() == QPrinter::Selection) { return pageListToPageRange( selectedPageList ); } if ( printer.printRange() == QPrinter::PageRange ) { return QStringLiteral("%1-%2").arg(printer.fromPage()).arg(printer.toPage()); } return QStringLiteral("1-%2").arg( lastPage ); } QString FilePrinter::pageListToPageRange( const QList &pageList ) { QString pageRange; int count = pageList.count(); int i = 0; int seqStart = i; int seqEnd; while ( i != count ) { if ( i + 1 == count || pageList[i] + 1 != pageList[i+1] ) { seqEnd = i; if ( !pageRange.isEmpty() ) { pageRange.append(QLatin1Char(',')); } if ( seqStart == seqEnd ) { pageRange.append(pageList[i]); } else { pageRange.append(QStringLiteral("%1-%2").arg(seqStart).arg(seqEnd)); } seqStart = i + 1; } i++; } return pageRange; } bool FilePrinter::ps2pdfAvailable() { return ( !QStandardPaths::findExecutable(QStringLiteral("ps2pdf")).isEmpty() ); } bool FilePrinter::pdf2psAvailable() { return ( !QStandardPaths::findExecutable(QStringLiteral("pdf2ps")).isEmpty() ); } bool FilePrinter::cupsAvailable() { #if defined(Q_OS_UNIX) && !defined(Q_OS_OSX) // Ideally we would have access to the private Qt method // QCUPSSupport::cupsAvailable() to do this as it is very complex routine. // However, if CUPS is available then QPrinter::numCopies() will always return 1 // whereas if CUPS is not available it will return the real number of copies. // This behaviour is guaranteed never to change, so we can use it as a reliable substitute. QPrinter testPrinter; testPrinter.setNumCopies( 2 ); return ( testPrinter.numCopies() == 1 ); #else return false; #endif } bool FilePrinter::detectCupsService() { QTcpSocket qsock; qsock.connectToHost(QStringLiteral("localhost"), 631); bool rtn = qsock.waitForConnected() && qsock.isValid(); qsock.abort(); return rtn; } bool FilePrinter::detectCupsConfig() { if ( QFile::exists(QStringLiteral("/etc/cups/cupsd.conf")) ) return true; if ( QFile::exists(QStringLiteral("/usr/etc/cups/cupsd.conf")) ) return true; if ( QFile::exists(QStringLiteral("/usr/local/etc/cups/cupsd.conf")) ) return true; if ( QFile::exists(QStringLiteral("/opt/etc/cups/cupsd.conf")) ) return true; if ( QFile::exists(QStringLiteral("/opt/local/etc/cups/cupsd.conf")) ) return true; return false; } QSize FilePrinter::psPaperSize( QPrinter &printer ) { QSize size = printer.pageLayout().pageSize().sizePoints(); if ( printer.pageSize() == QPrinter::Custom ) { return QSize( (int) printer.widthMM() * ( 25.4 / 72 ), (int) printer.heightMM() * ( 25.4 / 72 ) ); } if ( printer.orientation() == QPrinter::Landscape ) { size.transpose(); } return size; } Generator::PrintError FilePrinter::printError( int c ) { Generator::PrintError pe; if ( c >= 0 ) { pe = Generator::NoPrintError; } else { switch ( c ) { case -1: pe = Generator::PrintingProcessCrashPrintError; break; case -2: pe = Generator::PrintingProcessStartPrintError; break; case -5: pe = Generator::PrintToFilePrintError; break; case -6: pe = Generator::InvalidPrinterStatePrintError; break; case -7: pe = Generator::UnableToFindFilePrintError; break; case -8: pe = Generator::NoFileToPrintError; break; case -9: pe = Generator::NoBinaryToPrintError; break; default: pe = Generator::UnknownPrintError; } } return pe; } QStringList FilePrinter::printArguments( QPrinter &printer, FileDeletePolicy fileDeletePolicy, PageSelectPolicy pageSelectPolicy, bool useCupsOptions, const QString &pageRange, const QString &version, QPrinter::Orientation documentOrientation ) { return printArguments( printer, fileDeletePolicy, pageSelectPolicy, useCupsOptions, pageRange, version, documentOrientation, ScaleMode::FitToPrintArea); } QStringList FilePrinter::printArguments( QPrinter &printer, FileDeletePolicy fileDeletePolicy, PageSelectPolicy pageSelectPolicy, bool useCupsOptions, const QString &pageRange, const QString &version, QPrinter::Orientation documentOrientation, ScaleMode scaleMode ) { QStringList argList; if ( ! destination( printer, version ).isEmpty() ) { argList << destination( printer, version ); } if ( ! copies( printer, version ).isEmpty() ) { argList << copies( printer, version ); } if ( ! jobname( printer, version ).isEmpty() ) { argList << jobname( printer, version ); } if ( ! pages( printer, pageSelectPolicy, pageRange, useCupsOptions, version ).isEmpty() ) { argList << pages( printer, pageSelectPolicy, pageRange, useCupsOptions, version ); } if ( useCupsOptions && ! cupsOptions( printer, documentOrientation, scaleMode ).isEmpty() ) { argList << cupsOptions( printer, documentOrientation, scaleMode); } if ( ! deleteFile( printer, fileDeletePolicy, version ).isEmpty() ) { argList << deleteFile( printer, fileDeletePolicy, version ); } if ( version == QLatin1String("lp") ) { argList << QStringLiteral("--"); } return argList; } QStringList FilePrinter::destination( QPrinter &printer, const QString &version ) { if ( version == QLatin1String("lp") ) { return QStringList(QStringLiteral("-d")) << printer.printerName(); } if ( version.startsWith( QLatin1String("lpr") ) ) { return QStringList(QStringLiteral("-P")) << printer.printerName(); } return QStringList(); } QStringList FilePrinter::copies( QPrinter &printer, const QString &version ) { int cp = printer.actualNumCopies(); if ( version == QLatin1String("lp") ) { return QStringList(QStringLiteral("-n")) << QStringLiteral("%1").arg( cp ); } if ( version.startsWith( QLatin1String("lpr") ) ) { return QStringList() << QStringLiteral("-#%1").arg( cp ); } return QStringList(); } QStringList FilePrinter::jobname( QPrinter &printer, const QString &version ) { if ( ! printer.docName().isEmpty() ) { if ( version == QLatin1String("lp") ) { return QStringList(QStringLiteral("-t")) << printer.docName(); } if ( version.startsWith( QLatin1String("lpr") ) ) { const QString shortenedDocName = QString::fromUtf8(printer.docName().toUtf8().left(255)); return QStringList(QStringLiteral("-J")) << shortenedDocName; } } return QStringList(); } QStringList FilePrinter::deleteFile( QPrinter &, FileDeletePolicy fileDeletePolicy, const QString &version ) { if ( fileDeletePolicy == FilePrinter::SystemDeletesFiles && version.startsWith( QLatin1String("lpr") ) ) { return QStringList(QStringLiteral("-r")); } return QStringList(); } QStringList FilePrinter::pages( QPrinter &printer, PageSelectPolicy pageSelectPolicy, const QString &pageRange, bool useCupsOptions, const QString &version ) { if ( pageSelectPolicy == FilePrinter::SystemSelectsPages ) { if ( printer.printRange() == QPrinter::Selection && ! pageRange.isEmpty() ) { if ( version == QLatin1String("lp") ) { return QStringList(QStringLiteral("-P")) << pageRange ; } if ( version.startsWith( QLatin1String("lpr") ) && useCupsOptions ) { return QStringList(QStringLiteral("-o")) << QStringLiteral("page-ranges=%1").arg( pageRange ); } } if ( printer.printRange() == QPrinter::PageRange ) { if ( version == QLatin1String("lp") ) { return QStringList(QStringLiteral("-P")) << QStringLiteral("%1-%2").arg( printer.fromPage() ) .arg( printer.toPage() ); } if ( version.startsWith( QLatin1String("lpr") ) && useCupsOptions ) { return QStringList(QStringLiteral("-o")) << QStringLiteral("page-ranges=%1-%2").arg( printer.fromPage() ) .arg( printer.toPage() ); } } } return QStringList(); // AllPages } QStringList FilePrinter::cupsOptions( QPrinter &printer, QPrinter::Orientation documentOrientation ) { return cupsOptions( printer, documentOrientation, ScaleMode::FitToPrintArea ); } QStringList FilePrinter::cupsOptions( QPrinter &printer, QPrinter::Orientation documentOrientation, ScaleMode scaleMode ) { QStringList optionList; if ( ! optionMedia( printer ).isEmpty() ) { optionList << optionMedia( printer ); } if ( ! optionOrientation( printer, documentOrientation ).isEmpty() ) { optionList << optionOrientation( printer, documentOrientation ); } if ( ! optionDoubleSidedPrinting( printer ).isEmpty() ) { optionList << optionDoubleSidedPrinting( printer ); } if ( ! optionPageOrder( printer ).isEmpty() ) { optionList << optionPageOrder( printer ); } if ( ! optionCollateCopies( printer ).isEmpty() ) { optionList << optionCollateCopies( printer ); } if ( ! optionPageMargins( printer, scaleMode ).isEmpty() ) { optionList << optionPageMargins( printer, scaleMode ); } optionList << optionCupsProperties( printer ); return optionList; } QStringList FilePrinter::optionMedia( QPrinter &printer ) { if ( ! mediaPageSize( printer ).isEmpty() && ! mediaPaperSource( printer ).isEmpty() ) { return QStringList(QStringLiteral("-o")) << QStringLiteral("media=%1,%2").arg( mediaPageSize( printer ), mediaPaperSource( printer ) ); } if ( ! mediaPageSize( printer ).isEmpty() ) { return QStringList(QStringLiteral("-o")) << QStringLiteral("media=%1").arg( mediaPageSize( printer ) ); } if ( ! mediaPaperSource( printer ).isEmpty() ) { return QStringList(QStringLiteral("-o")) << QStringLiteral("media=%1").arg( mediaPaperSource( printer ) ); } return QStringList(); } QString FilePrinter::mediaPageSize( QPrinter &printer ) { switch ( printer.pageSize() ) { case QPrinter::A0: return QStringLiteral("A0"); case QPrinter::A1: return QStringLiteral("A1"); case QPrinter::A2: return QStringLiteral("A2"); case QPrinter::A3: return QStringLiteral("A3"); case QPrinter::A4: return QStringLiteral("A4"); case QPrinter::A5: return QStringLiteral("A5"); case QPrinter::A6: return QStringLiteral("A6"); case QPrinter::A7: return QStringLiteral("A7"); case QPrinter::A8: return QStringLiteral("A8"); case QPrinter::A9: return QStringLiteral("A9"); case QPrinter::B0: return QStringLiteral("B0"); case QPrinter::B1: return QStringLiteral("B1"); case QPrinter::B10: return QStringLiteral("B10"); case QPrinter::B2: return QStringLiteral("B2"); case QPrinter::B3: return QStringLiteral("B3"); case QPrinter::B4: return QStringLiteral("B4"); case QPrinter::B5: return QStringLiteral("B5"); case QPrinter::B6: return QStringLiteral("B6"); case QPrinter::B7: return QStringLiteral("B7"); case QPrinter::B8: return QStringLiteral("B8"); case QPrinter::B9: return QStringLiteral("B9"); case QPrinter::C5E: return QStringLiteral("C5"); //Correct Translation? case QPrinter::Comm10E: return QStringLiteral("Comm10"); //Correct Translation? case QPrinter::DLE: return QStringLiteral("DL"); //Correct Translation? case QPrinter::Executive: return QStringLiteral("Executive"); case QPrinter::Folio: return QStringLiteral("Folio"); case QPrinter::Ledger: return QStringLiteral("Ledger"); case QPrinter::Legal: return QStringLiteral("Legal"); case QPrinter::Letter: return QStringLiteral("Letter"); case QPrinter::Tabloid: return QStringLiteral("Tabloid"); case QPrinter::Custom: return QStringLiteral("Custom.%1x%2mm") .arg( printer.widthMM() ) .arg( printer.heightMM() ); default: return QString(); } } // What about Upper and MultiPurpose? And others in PPD??? QString FilePrinter::mediaPaperSource( QPrinter &printer ) { switch ( printer.paperSource() ) { case QPrinter::Auto: return QString(); case QPrinter::Cassette: return QStringLiteral("Cassette"); case QPrinter::Envelope: return QStringLiteral("Envelope"); case QPrinter::EnvelopeManual: return QStringLiteral("EnvelopeManual"); case QPrinter::FormSource: return QStringLiteral("FormSource"); case QPrinter::LargeCapacity: return QStringLiteral("LargeCapacity"); case QPrinter::LargeFormat: return QStringLiteral("LargeFormat"); case QPrinter::Lower: return QStringLiteral("Lower"); case QPrinter::MaxPageSource: return QStringLiteral("MaxPageSource"); case QPrinter::Middle: return QStringLiteral("Middle"); case QPrinter::Manual: return QStringLiteral("Manual"); case QPrinter::OnlyOne: return QStringLiteral("OnlyOne"); case QPrinter::Tractor: return QStringLiteral("Tractor"); case QPrinter::SmallFormat: return QStringLiteral("SmallFormat"); default: return QString(); } } QStringList FilePrinter::optionOrientation( QPrinter &printer, QPrinter::Orientation documentOrientation ) { // portrait and landscape options rotate the document according to the document orientation // If we want to print a landscape document as one would expect it, we have to pass the // portrait option so that the document is not rotated additionally if ( printer.orientation() == documentOrientation ) { // the user wants the document printed as is return QStringList(QStringLiteral("-o")) << QStringLiteral("portrait"); } else { // the user expects the document being rotated by 90 degrees return QStringList(QStringLiteral("-o")) << QStringLiteral("landscape"); } } QStringList FilePrinter::optionDoubleSidedPrinting( QPrinter &printer ) { switch ( printer.duplex() ) { case QPrinter::DuplexNone: return QStringList(QStringLiteral("-o")) << QStringLiteral("sides=one-sided"); case QPrinter::DuplexAuto: if ( printer.orientation() == QPrinter::Landscape ) { return QStringList(QStringLiteral("-o")) << QStringLiteral("sides=two-sided-short-edge"); } else { return QStringList(QStringLiteral("-o")) << QStringLiteral("sides=two-sided-long-edge"); } case QPrinter::DuplexLongSide: return QStringList(QStringLiteral("-o")) << QStringLiteral("sides=two-sided-long-edge"); case QPrinter::DuplexShortSide: return QStringList(QStringLiteral("-o")) << QStringLiteral("sides=two-sided-short-edge"); default: return QStringList(); //Use printer default } } QStringList FilePrinter::optionPageOrder( QPrinter &printer ) { if ( printer.pageOrder() == QPrinter::LastPageFirst ) { return QStringList(QStringLiteral("-o")) << QStringLiteral("outputorder=reverse"); } return QStringList(QStringLiteral("-o")) << QStringLiteral("outputorder=normal"); } QStringList FilePrinter::optionCollateCopies( QPrinter &printer ) { if ( printer.collateCopies() ) { return QStringList(QStringLiteral("-o")) << QStringLiteral("Collate=True"); } return QStringList(QStringLiteral("-o")) << QStringLiteral("Collate=False"); } QStringList FilePrinter::optionPageMargins( QPrinter &printer ) { return optionPageMargins( printer, ScaleMode::FitToPrintArea ); } QStringList FilePrinter::optionPageMargins( QPrinter &printer, ScaleMode scaleMode ) { if (printer.printEngine()->property(QPrintEngine::PPK_PageMargins).isNull()) { return QStringList(); } else { qreal l(0), t(0), r(0), b(0); if (!printer.fullPage()) { printer.getPageMargins( &l, &t, &r, &b, QPrinter::Point ); } QStringList marginOptions; marginOptions << (QStringLiteral("-o")) << QStringLiteral("page-left=%1").arg(l) << QStringLiteral("-o") << QStringLiteral("page-top=%1").arg(t) << QStringLiteral("-o") << QStringLiteral("page-right=%1").arg(r) << QStringLiteral("-o") << QStringLiteral("page-bottom=%1").arg(b); if (scaleMode == ScaleMode::FitToPrintArea) { marginOptions << QStringLiteral("-o") << QStringLiteral("fit-to-page"); } return marginOptions; } } QStringList FilePrinter::optionCupsProperties( QPrinter &printer ) { QStringList dialogOptions = printer.printEngine()->property(QPrintEngine::PrintEnginePropertyKey(0xfe00)).toStringList(); QStringList cupsOptions; for ( int i = 0; i < dialogOptions.count(); i = i + 2 ) { if ( dialogOptions[i+1].isEmpty() ) { cupsOptions << QStringLiteral("-o") << dialogOptions[i]; } else { cupsOptions << QStringLiteral("-o") << dialogOptions[i] + QLatin1Char('=') + dialogOptions[i+1]; } } return cupsOptions; } /* kate: replace-tabs on; indent-width 4; */ diff --git a/core/generator.cpp b/core/generator.cpp index 6265e9342..879819831 100644 --- a/core/generator.cpp +++ b/core/generator.cpp @@ -1,810 +1,810 @@ /*************************************************************************** * Copyright (C) 2005 by Piotr Szymanski * * Copyright (C) 2008 by Albert Astals Cid * * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * * company, info@kdab.com. Work sponsored by the * * LiMux project of the city of Munich * * * * 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. * ***************************************************************************/ #include "generator.h" #include "generator_p.h" #include "observer.h" #include #include #include #include #include #include #include #include #ifdef WITH_KWALLET #include #endif #include "document.h" #include "document_p.h" #include "page.h" #include "page_p.h" #include "textpage.h" #include "utils.h" using namespace Okular; GeneratorPrivate::GeneratorPrivate() : m_document( nullptr ), mPixmapGenerationThread( nullptr ), mTextPageGenerationThread( nullptr ), mPixmapReady( true ), mTextPageReady( true ), m_closing( false ), m_closingLoop( nullptr ), m_dpi(72.0, 72.0) { qRegisterMetaType(); } GeneratorPrivate::~GeneratorPrivate() { if ( mPixmapGenerationThread ) mPixmapGenerationThread->wait(); delete mPixmapGenerationThread; if ( mTextPageGenerationThread ) mTextPageGenerationThread->wait(); delete mTextPageGenerationThread; } PixmapGenerationThread* GeneratorPrivate::pixmapGenerationThread() { if ( mPixmapGenerationThread ) return mPixmapGenerationThread; Q_Q( Generator ); mPixmapGenerationThread = new PixmapGenerationThread( q ); QObject::connect( mPixmapGenerationThread, &PixmapGenerationThread::finished, q, [this] { pixmapGenerationFinished(); }, Qt::QueuedConnection ); return mPixmapGenerationThread; } TextPageGenerationThread* GeneratorPrivate::textPageGenerationThread() { if ( mTextPageGenerationThread ) return mTextPageGenerationThread; Q_Q( Generator ); mTextPageGenerationThread = new TextPageGenerationThread( q ); QObject::connect( mTextPageGenerationThread, &TextPageGenerationThread::finished, q, [this] { textpageGenerationFinished(); }, Qt::QueuedConnection ); return mTextPageGenerationThread; } void GeneratorPrivate::pixmapGenerationFinished() { Q_Q( Generator ); PixmapRequest *request = mPixmapGenerationThread->request(); const QImage& img = mPixmapGenerationThread->image(); mPixmapGenerationThread->endGeneration(); QMutexLocker locker( threadsLock() ); if ( m_closing ) { mPixmapReady = true; delete request; if ( mTextPageReady ) { locker.unlock(); m_closingLoop->quit(); } return; } if ( !request->shouldAbortRender() ) { request->page()->setPixmap( request->observer(), new QPixmap( QPixmap::fromImage( img ) ), request->normalizedRect() ); const int pageNumber = request->page()->number(); if ( mPixmapGenerationThread->calcBoundingBox() ) q->updatePageBoundingBox( pageNumber, mPixmapGenerationThread->boundingBox() ); } else { // Cancel the text page generation too if it's still running if ( mTextPageGenerationThread && mTextPageGenerationThread->isRunning() ) { mTextPageGenerationThread->abortExtraction(); mTextPageGenerationThread->wait(); } } mPixmapReady = true; q->signalPixmapRequestDone( request ); } void GeneratorPrivate::textpageGenerationFinished() { Q_Q( Generator ); Page *page = mTextPageGenerationThread->page(); mTextPageGenerationThread->endGeneration(); QMutexLocker locker( threadsLock() ); mTextPageReady = true; if ( m_closing ) { delete mTextPageGenerationThread->textPage(); if ( mPixmapReady ) { locker.unlock(); m_closingLoop->quit(); } return; } if ( mTextPageGenerationThread->textPage() ) { TextPage *tp = mTextPageGenerationThread->textPage(); page->setTextPage( tp ); q->signalTextGenerationDone( page, tp ); } } QMutex* GeneratorPrivate::threadsLock() { return &m_threadsMutex; } QVariant GeneratorPrivate::metaData( const QString &, const QVariant & ) const { return QVariant(); } QImage GeneratorPrivate::image( PixmapRequest * ) { return QImage(); } Generator::Generator(QObject* parent, const QVariantList &args) : Generator( *new GeneratorPrivate(), parent, args ) { // the delegated constructor does it all } Generator::Generator(GeneratorPrivate &dd, QObject *parent, const QVariantList &args) : QObject(parent), d_ptr(&dd) { d_ptr->q_ptr = this; Q_UNUSED(args) } Generator::~Generator() { delete d_ptr; } bool Generator::loadDocument( const QString & fileName, QVector< Page * > & pagesVector ) { Q_UNUSED(fileName); Q_UNUSED(pagesVector); return false; } bool Generator::loadDocumentFromData( const QByteArray &, QVector< Page * > & ) { return false; } Document::OpenResult Generator::loadDocumentWithPassword( const QString & fileName, QVector< Page * > & pagesVector, const QString & ) { return loadDocument( fileName, pagesVector ) ? Document::OpenSuccess : Document::OpenError; } Document::OpenResult Generator::loadDocumentFromDataWithPassword( const QByteArray & fileData, QVector< Page * > & pagesVector, const QString & ) { return loadDocumentFromData( fileData, pagesVector ) ? Document::OpenSuccess : Document::OpenError; } Generator::SwapBackingFileResult Generator::swapBackingFile( QString const &/*newFileName */, QVector & /*newPagesVector*/ ) { return SwapBackingFileError; } bool Generator::closeDocument() { Q_D( Generator ); d->m_closing = true; d->threadsLock()->lock(); if ( !( d->mPixmapReady && d->mTextPageReady ) ) { QEventLoop loop; d->m_closingLoop = &loop; d->threadsLock()->unlock(); loop.exec(); d->m_closingLoop = nullptr; } else { d->threadsLock()->unlock(); } bool ret = doCloseDocument(); d->m_closing = false; return ret; } bool Generator::canGeneratePixmap() const { Q_D( const Generator ); return d->mPixmapReady; } void Generator::generatePixmap( PixmapRequest *request ) { Q_D( Generator ); d->mPixmapReady = false; const bool calcBoundingBox = !request->isTile() && !request->page()->isBoundingBoxKnown(); if ( request->asynchronous() && hasFeature( Threaded ) ) { if ( d->textPageGenerationThread()->isFinished() && !canGenerateTextPage() ) { // It can happen that the text generation has already finished but // mTextPageReady is still false because textpageGenerationFinished // didn't have time to run, if so queue ourselves QTimer::singleShot(0, this, [this, request] { generatePixmap(request); }); return; } /** * We create the text page for every page that is visible to the * user, so he can use the text extraction tools without a delay. */ if ( hasFeature( TextExtraction ) && !request->page()->hasTextPage() && canGenerateTextPage() && !d->m_closing ) { d->mTextPageReady = false; d->textPageGenerationThread()->setPage( request->page() ); // dummy is used as a way to make sure the lambda gets disconnected each time it is executed // since not all the times the pixmap generation thread starts we want the text generation thread to also start QObject *dummy = new QObject(); connect(d_ptr->pixmapGenerationThread(), &QThread::started, dummy, [this, dummy] { delete dummy; d_ptr->textPageGenerationThread()->startGeneration(); }); } // pixmap generation thread must be started *after* connect(), else we may miss the start signal and get lock-ups (see bug 396137) d->pixmapGenerationThread()->startGeneration( request, calcBoundingBox ); return; } const QImage& img = image( request ); request->page()->setPixmap( request->observer(), new QPixmap( QPixmap::fromImage( img ) ), request->normalizedRect() ); const int pageNumber = request->page()->number(); d->mPixmapReady = true; signalPixmapRequestDone( request ); if ( calcBoundingBox ) updatePageBoundingBox( pageNumber, Utils::imageBoundingBox( &img ) ); } bool Generator::canGenerateTextPage() const { Q_D( const Generator ); return d->mTextPageReady; } void Generator::generateTextPage( Page *page ) { TextRequest treq( page ); TextPage *tp = textPage( &treq ); page->setTextPage( tp ); signalTextGenerationDone( page, tp ); } QImage Generator::image( PixmapRequest *request ) { Q_D( Generator ); return d->image( request ); } TextPage* Generator::textPage( TextRequest * ) { return nullptr; } DocumentInfo Generator::generateDocumentInfo(const QSet &keys) const { Q_UNUSED(keys); return DocumentInfo(); } const DocumentSynopsis * Generator::generateDocumentSynopsis() { return nullptr; } FontInfo::List Generator::fontsForPage( int ) { return FontInfo::List(); } const QList * Generator::embeddedFiles() const { return nullptr; } Generator::PageSizeMetric Generator::pagesSizeMetric() const { return None; } bool Generator::isAllowed( Permission ) const { return true; } void Generator::rotationChanged( Rotation, Rotation ) { } PageSize::List Generator::pageSizes() const { return PageSize::List(); } void Generator::pageSizeChanged( const PageSize &, const PageSize & ) { } bool Generator::print( QPrinter& ) { return false; } Generator::PrintError Generator::printError() const { return UnknownPrintError; } void Generator::opaqueAction( const BackendOpaqueAction * /*action*/ ) { } QVariant Generator::metaData( const QString &key, const QVariant &option ) const { Q_D( const Generator ); return d->metaData( key, option ); } ExportFormat::List Generator::exportFormats() const { return ExportFormat::List(); } bool Generator::exportTo( const QString&, const ExportFormat& ) { return false; } void Generator::walletDataForFile( const QString &fileName, QString *walletName, QString *walletFolder, QString *walletKey ) const { #ifdef WITH_KWALLET *walletKey = fileName.section( QLatin1Char('/'), -1, -1); *walletName = KWallet::Wallet::NetworkWallet(); *walletFolder = QStringLiteral("KPdf"); #endif } bool Generator::hasFeature( GeneratorFeature feature ) const { Q_D( const Generator ); return d->m_features.contains( feature ); } void Generator::signalPixmapRequestDone( PixmapRequest * request ) { Q_D( Generator ); if ( d->m_document ) d->m_document->requestDone( request ); else { delete request; } } void Generator::signalTextGenerationDone( Page *page, TextPage *textPage ) { Q_D( Generator ); if ( d->m_document ) d->m_document->textGenerationDone( page ); else delete textPage; } void Generator::signalPartialPixmapRequest( PixmapRequest *request, const QImage &image ) { if ( request->shouldAbortRender() ) return; PagePrivate *pagePrivate = PagePrivate::get( request->page() ); pagePrivate->setPixmap( request->observer(), new QPixmap( QPixmap::fromImage( image ) ), request->normalizedRect(), true /* isPartialPixmap */ ); const int pageNumber = request->page()->number(); request->observer()->notifyPageChanged( pageNumber, Okular::DocumentObserver::Pixmap ); } const Document * Generator::document() const { Q_D( const Generator ); if ( d->m_document ) { return d->m_document->m_parent; } return nullptr; } void Generator::setFeature( GeneratorFeature feature, bool on ) { Q_D( Generator ); if ( on ) d->m_features.insert( feature ); else d->m_features.remove( feature ); } QVariant Generator::documentMetaData( const QString &key, const QVariant &option ) const { Q_D( const Generator ); if ( !d->m_document ) return QVariant(); if (key == QLatin1String("PaperColor")) return documentMetaData(PaperColorMetaData, option); if (key == QLatin1String("GraphicsAntialias")) return documentMetaData(GraphicsAntialiasMetaData, option); if (key == QLatin1String("TextAntialias")) return documentMetaData(TextAntialiasMetaData, option); if (key == QLatin1String("TextHinting")) return documentMetaData(TextHintingMetaData, option); return QVariant(); } QVariant Generator::documentMetaData( const DocumentMetaDataKey key, const QVariant &option ) const { Q_D( const Generator ); if ( !d->m_document ) return QVariant(); return d->m_document->documentMetaData( key, option ); } QMutex* Generator::userMutex() const { Q_D( const Generator ); return &d->m_mutex; } void Generator::updatePageBoundingBox( int page, const NormalizedRect & boundingBox ) { Q_D( Generator ); if ( d->m_document ) // still connected to document? d->m_document->setPageBoundingBox( page, boundingBox ); } void Generator::requestFontData(const Okular::FontInfo & /*font*/, QByteArray * /*data*/) { } -void Generator::setDPI(const QSizeF & dpi) +void Generator::setDPI(const QSizeF & dpi) // clazy:exclude=function-args-by-value TODO remove the & when we do a BIC change elsewhere { Q_D( Generator ); d->m_dpi = dpi; } QSizeF Generator::dpi() const { Q_D( const Generator ); return d->m_dpi; } QAbstractItemModel * Generator::layersModel() const { return nullptr; } TextRequest::TextRequest() : d( new TextRequestPrivate ) { d->mPage = nullptr; d->mShouldAbortExtraction = 0; } TextRequest::TextRequest( Page *page ) : d( new TextRequestPrivate ) { d->mPage = page; d->mShouldAbortExtraction = 0; } TextRequest::~TextRequest() { delete d; } Page *TextRequest::page() const { return d->mPage; } bool TextRequest::shouldAbortExtraction() const { return d->mShouldAbortExtraction != 0; } TextRequestPrivate *TextRequestPrivate::get(const TextRequest *req) { return req->d; } PixmapRequest::PixmapRequest( DocumentObserver *observer, int pageNumber, int width, int height, int priority, PixmapRequestFeatures features ) : d( new PixmapRequestPrivate ) { d->mObserver = observer; d->mPageNumber = pageNumber; d->mWidth = ceil(width * qApp->devicePixelRatio()); d->mHeight = ceil(height * qApp->devicePixelRatio()); d->mPriority = priority; d->mFeatures = features; d->mForce = false; d->mTile = false; d->mNormalizedRect = NormalizedRect(); d->mPartialUpdatesWanted = false; d->mShouldAbortRender = 0; } PixmapRequest::~PixmapRequest() { delete d; } DocumentObserver *PixmapRequest::observer() const { return d->mObserver; } int PixmapRequest::pageNumber() const { return d->mPageNumber; } int PixmapRequest::width() const { return d->mWidth; } int PixmapRequest::height() const { return d->mHeight; } int PixmapRequest::priority() const { return d->mPriority; } bool PixmapRequest::asynchronous() const { return d->mFeatures & Asynchronous; } bool PixmapRequest::preload() const { return d->mFeatures & Preload; } Page* PixmapRequest::page() const { return d->mPage; } void PixmapRequest::setTile( bool tile ) { d->mTile = tile; } bool PixmapRequest::isTile() const { return d->mTile; } void PixmapRequest::setNormalizedRect( const NormalizedRect &rect ) { if ( d->mNormalizedRect == rect ) return; d->mNormalizedRect = rect; } const NormalizedRect& PixmapRequest::normalizedRect() const { return d->mNormalizedRect; } void PixmapRequest::setPartialUpdatesWanted(bool partialUpdatesWanted) { d->mPartialUpdatesWanted = partialUpdatesWanted; } bool PixmapRequest::partialUpdatesWanted() const { return d->mPartialUpdatesWanted; } bool PixmapRequest::shouldAbortRender() const { return d->mShouldAbortRender != 0; } Okular::TilesManager* PixmapRequestPrivate::tilesManager() const { return mPage->d->tilesManager(mObserver); } PixmapRequestPrivate *PixmapRequestPrivate::get(const PixmapRequest *req) { return req->d; } void PixmapRequestPrivate::swap() { qSwap( mWidth, mHeight ); } class Okular::ExportFormatPrivate : public QSharedData { public: ExportFormatPrivate( const QString &description, const QMimeType &mimeType, const QIcon &icon = QIcon() ) : QSharedData(), mDescription( description ), mMimeType( mimeType ), mIcon( icon ) { } ~ExportFormatPrivate() { } QString mDescription; QMimeType mMimeType; QIcon mIcon; }; ExportFormat::ExportFormat() : d( new ExportFormatPrivate( QString(), QMimeType() ) ) { } ExportFormat::ExportFormat( const QString &description, const QMimeType &mimeType ) : d( new ExportFormatPrivate( description, mimeType ) ) { } ExportFormat::ExportFormat( const QIcon &icon, const QString &description, const QMimeType &mimeType ) : d( new ExportFormatPrivate( description, mimeType, icon ) ) { } ExportFormat::~ExportFormat() { } ExportFormat::ExportFormat( const ExportFormat &other ) : d( other.d ) { } ExportFormat& ExportFormat::operator=( const ExportFormat &other ) { if ( this == &other ) return *this; d = other.d; return *this; } QString ExportFormat::description() const { return d->mDescription; } QMimeType ExportFormat::mimeType() const { return d->mMimeType; } QIcon ExportFormat::icon() const { return d->mIcon; } bool ExportFormat::isNull() const { return !d->mMimeType.isValid() || d->mDescription.isNull(); } ExportFormat ExportFormat::standardFormat( StandardExportFormat type ) { QMimeDatabase db; switch ( type ) { case PlainText: return ExportFormat( QIcon::fromTheme( QStringLiteral("text-x-generic") ), i18n( "Plain &Text..." ), db.mimeTypeForName( QStringLiteral("text/plain") ) ); break; case PDF: return ExportFormat( QIcon::fromTheme( QStringLiteral("application-pdf") ), i18n( "PDF" ), db.mimeTypeForName( QStringLiteral("application/pdf") ) ); break; case OpenDocumentText: return ExportFormat( QIcon::fromTheme( QStringLiteral("application-vnd.oasis.opendocument.text") ), i18nc( "This is the document format", "OpenDocument Text" ), db.mimeTypeForName( QStringLiteral("application/vnd.oasis.opendocument.text") ) ); break; case HTML: return ExportFormat( QIcon::fromTheme( QStringLiteral("text-html") ), i18nc( "This is the document format", "HTML" ), db.mimeTypeForName( QStringLiteral("text/html") ) ); break; } return ExportFormat(); } bool ExportFormat::operator==( const ExportFormat &other ) const { return d == other.d; } bool ExportFormat::operator!=( const ExportFormat &other ) const { return d != other.d; } QDebug operator<<( QDebug str, const Okular::PixmapRequest &req ) { PixmapRequestPrivate *reqPriv = PixmapRequestPrivate::get(&req); str << "PixmapRequest:" << &req; str << "- observer:" << (qulonglong)req.observer(); str << "- page:" << req.pageNumber(); str << "- width:" << req.width(); str << "- height:" << req.height(); str << "- priority:" << req.priority(); str << "- async:" << ( req.asynchronous() ? "true" : "false" ); str << "- tile:" << ( req.isTile() ? "true" : "false" ); str << "- rect:" << req.normalizedRect(); str << "- preload:" << ( req.preload() ? "true" : "false" ); str << "- partialUpdates:" << ( req.partialUpdatesWanted() ? "true" : "false" ); str << "- shouldAbort:" << ( req.shouldAbortRender() ? "true" : "false" ); str << "- force:" << ( reqPriv->mForce ? "true" : "false" ); return str; } /* kate: replace-tabs on; indent-width 4; */ diff --git a/core/generator.h b/core/generator.h index 94bfa8473..1738c753a 100644 --- a/core/generator.h +++ b/core/generator.h @@ -1,820 +1,820 @@ /*************************************************************************** * Copyright (C) 2004-5 by Enrico Ros * * Copyright (C) 2005 by Piotr Szymanski * * Copyright (C) 2008 by Albert Astals Cid * * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * * company, info@kdab.com. Work sponsored by the * * LiMux project of the city of Munich * * * * 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. * ***************************************************************************/ #ifndef _OKULAR_GENERATOR_H_ #define _OKULAR_GENERATOR_H_ #include "okularcore_export.h" #include "document.h" #include "fontinfo.h" #include "global.h" #include "pagesize.h" #include #include #include #include #include #include #include #include #include #define OKULAR_EXPORT_PLUGIN(classname, json ) \ static_assert(json[0] != '\0', "arg2 must be a string literal"); \ K_PLUGIN_CLASS_WITH_JSON(classname, json) class QByteArray; class QMutex; class QPrinter; class QIcon; namespace Okular { class BackendOpaqueAction; class DocumentFonts; class DocumentInfo; class DocumentObserver; class DocumentSynopsis; class EmbeddedFile; class ExportFormatPrivate; class FontInfo; class GeneratorPrivate; class Page; class PixmapRequest; class PixmapRequestPrivate; class TextPage; class TextRequest; class TextRequestPrivate; class NormalizedRect; /* Note: on contents generation and asynchronous queries. * Many observers may want to request data synchronously or asynchronously. * - Sync requests. These should be done in-place. * - Async request must be done in real background. That usually means a * thread, such as QThread derived classes. * Once contents are available, they must be immediately stored in the * Page they refer to, and a signal is emitted as soon as storing * (even for sync or async queries) has been done. */ /** * @short Defines an entry for the export menu * * This class encapsulates information about an export format. * Every Generator can support 0 or more export formats which can be * queried with @ref Generator::exportFormats(). */ class OKULARCORE_EXPORT ExportFormat { public: typedef QList List; /** * Creates an empty export format. * * @see isNull() */ ExportFormat(); /** * Creates a new export format. * * @param description The i18n'ed description of the format. * @param mimeType The supported mime type of the format. */ ExportFormat( const QString &description, const QMimeType &mimeType ); /** * Creates a new export format. * * @param icon The icon used in the GUI for this format. * @param description The i18n'ed description of the format. * @param mimeType The supported mime type of the format. */ ExportFormat( const QIcon &icon, const QString &description, const QMimeType &mimeType ); /** * Destroys the export format. */ ~ExportFormat(); /** * @internal */ ExportFormat( const ExportFormat &other ); /** * @internal */ ExportFormat& operator=( const ExportFormat &other ); /** * Returns the description of the format. */ QString description() const; /** * Returns the mime type of the format. */ QMimeType mimeType() const; /** * Returns the icon for GUI representations of the format. */ QIcon icon() const; /** * Returns whether the export format is null/valid. * * An ExportFormat is null if the mimetype is not valid or the * description is empty, or both. */ bool isNull() const; /** * Type of standard export format. */ enum StandardExportFormat { PlainText, ///< Plain text PDF, ///< PDF, aka Portable Document Format OpenDocumentText, ///< OpenDocument Text format @since 0.8 (KDE 4.2) HTML ///< OpenDocument Text format @since 0.8 (KDE 4.2) }; /** * Builds a standard format for the specified @p type . */ static ExportFormat standardFormat( StandardExportFormat type ); bool operator==( const ExportFormat &other ) const; bool operator!=( const ExportFormat &other ) const; private: /// @cond PRIVATE friend class ExportFormatPrivate; /// @endcond QSharedDataPointer d; }; /** * @short [Abstract Class] The information generator. * * Most of class members are virtuals and some of them pure virtual. The pure * virtuals provide the minimal functionalities for a Generator, that is being * able to generate QPixmap for the Page 's of the Document. * * Implementing the other functions will make the Generator able to provide * more contents and/or functionalities (like text extraction). * * Generation/query is requested by the Document class only, and that * class stores the resulting data into Page s. The data will then be * displayed by the GUI components (PageView, ThumbnailList, etc..). * * @see PrintInterface, ConfigInterface, GuiInterface */ class OKULARCORE_EXPORT Generator : public QObject { /// @cond PRIVATE friend class PixmapGenerationThread; friend class TextPageGenerationThread; /// @endcond Q_OBJECT public: /** * Describe the possible optional features that a Generator can * provide. */ enum GeneratorFeature { Threaded, ///< Whether the Generator supports asynchronous generation of pictures or text pages TextExtraction, ///< Whether the Generator can extract text from the document in the form of TextPage's ReadRawData, ///< Whether the Generator can read a document directly from its raw data. FontInfo, ///< Whether the Generator can provide information about the fonts used in the document PageSizes, ///< Whether the Generator can change the size of the document pages. PrintNative, ///< Whether the Generator supports native cross-platform printing (QPainter-based). PrintPostscript, ///< Whether the Generator supports postscript-based file printing. PrintToFile, ///< Whether the Generator supports export to PDF & PS through the Print Dialog TiledRendering, ///< Whether the Generator can render tiles @since 0.16 (KDE 4.10) SwapBackingFile, ///< Whether the Generator can hot-swap the file it's reading from @since 1.3 SupportsCancelling ///< Whether the Generator can cancel requests @since 1.4 }; /** * Creates a new generator. */ explicit Generator(QObject* parent = nullptr, const QVariantList& args = QVariantList()); /** * Destroys the generator. */ ~Generator() override; /** * Loads the document with the given @p fileName and fills the * @p pagesVector with the parsed pages. * * @note If you implement the WithPassword variants you don't need to implement this one * * @returns true on success, false otherwise. */ virtual bool loadDocument( const QString & fileName, QVector< Page * > & pagesVector ); /** * Loads the document from the raw data @p fileData and fills the * @p pagesVector with the parsed pages. * * @note If you implement the WithPassword variants you don't need to implement this one * * @note the Generator has to have the feature @ref ReadRawData enabled * * @returns true on success, false otherwise. */ virtual bool loadDocumentFromData( const QByteArray & fileData, QVector< Page * > & pagesVector ); /** * Loads the document with the given @p fileName and @p password and fills the * @p pagesVector with the parsed pages. * * @note Do not implement this if your format doesn't support passwords, it'll cleanly call loadDocument() * * @since 0.20 (KDE 4.14) * * @returns a LoadResult defining the result of the operation */ virtual Document::OpenResult loadDocumentWithPassword( const QString & fileName, QVector< Page * > & pagesVector, const QString &password ); /** * Loads the document from the raw data @p fileData and @p password and fills the * @p pagesVector with the parsed pages. * * @note Do not implement this if your format doesn't support passwords, it'll cleanly call loadDocumentFromData() * * @note the Generator has to have the feature @ref ReadRawData enabled * * @since 0.20 (KDE 4.14) * * @returns a LoadResult defining the result of the operation */ virtual Document::OpenResult loadDocumentFromDataWithPassword( const QByteArray & fileData, QVector< Page * > & pagesVector, const QString &password ); /** * Describes the result of an swap file operation. * * @since 1.3 */ enum SwapBackingFileResult { SwapBackingFileError, //< The document could not be swapped SwapBackingFileNoOp, //< The document was swapped and nothing needs to be done SwapBackingFileReloadInternalData //< The document was swapped and internal data (forms, annotations, etc) needs to be reloaded }; /** * Changes the path of the file we are reading from. The new path must * point to a copy of the same document. * * @note the Generator has to have the feature @ref SwapBackingFile enabled * * @since 1.3 */ virtual SwapBackingFileResult swapBackingFile( const QString & newFileName, QVector & newPagesVector ); /** * This method is called when the document is closed and not used * any longer. * * @returns true on success, false otherwise. */ bool closeDocument(); /** * This method returns whether the generator is ready to * handle a new pixmap request. */ virtual bool canGeneratePixmap() const; /** * This method can be called to trigger the generation of * a new pixmap as described by @p request. */ virtual void generatePixmap( PixmapRequest * request ); /** * This method returns whether the generator is ready to * handle a new text page request. */ virtual bool canGenerateTextPage() const; /** * This method can be called to trigger the generation of * a text page for the given @p page. * * The generation is done in the calling thread. * * @see TextPage */ void generateTextPage( Page * page ); /** * Returns the general information object of the document. * * Changed signature in okular version 0.21 */ virtual DocumentInfo generateDocumentInfo( const QSet &keys ) const; /** * Returns the 'table of content' object of the document or 0 if * no table of content is available. */ virtual const DocumentSynopsis * generateDocumentSynopsis(); /** * Returns the 'list of embedded fonts' object of the specified \p page * of the document. * * \param page a page of the document, starting from 0 - -1 indicates all * the other fonts */ virtual FontInfo::List fontsForPage( int page ); /** * Returns the 'list of embedded files' object of the document or 0 if * no list of embedded files is available. */ virtual const QList * embeddedFiles() const; /** * This enum identifies the metric of the page size. */ enum PageSizeMetric { None, ///< The page size is not defined in a physical metric. Points, ///< The page size is given in 1/72 inches. Pixels ///< The page size is given in screen pixels @since 0.19 (KDE 4.13) }; /** * This method returns the metric of the page size. Default is @ref None. */ virtual PageSizeMetric pagesSizeMetric() const; /** * Returns whether the given @p action is allowed in the document. * @see @ref Okular::Permission */ virtual bool isAllowed( Permission action ) const; /** * This method is called when the orientation has been changed by the user. */ virtual void rotationChanged( Rotation orientation, Rotation oldOrientation ); /** * Returns the list of supported page sizes. */ virtual PageSize::List pageSizes() const; /** * This method is called when the page size has been changed by the user. */ virtual void pageSizeChanged( const PageSize &pageSize, const PageSize &oldPageSize ); /** * This method is called to print the document to the given @p printer. */ virtual bool print( QPrinter &printer ); /** * Possible print errors * @since 0.11 (KDE 4.5) */ enum PrintError { NoPrintError, ///< There was no print error UnknownPrintError, TemporaryFileOpenPrintError, FileConversionPrintError, PrintingProcessCrashPrintError, PrintingProcessStartPrintError, PrintToFilePrintError, InvalidPrinterStatePrintError, UnableToFindFilePrintError, NoFileToPrintError, NoBinaryToPrintError, InvalidPageSizePrintError ///< @since 0.18.2 (KDE 4.12.2) }; /** * This method returns the meta data of the given @p key with the given @p option * of the document. */ virtual QVariant metaData( const QString &key, const QVariant &option ) const; /** * Returns the list of additional supported export formats. */ virtual ExportFormat::List exportFormats() const; /** * This method is called to export the document in the given @p format and save it * under the given @p fileName. The format must be one of the supported export formats. */ virtual bool exportTo( const QString &fileName, const ExportFormat &format ); /** * This method is called to know which wallet data should be used for the given file name. * Unless you have very special requirements to where wallet data should be stored you * don't need to reimplement this method. */ virtual void walletDataForFile( const QString &fileName, QString *walletName, QString *walletFolder, QString *walletKey ) const; /** * Query for the specified @p feature. */ bool hasFeature( GeneratorFeature feature ) const; /** * Update DPI of the generator * * @since 0.19 (KDE 4.13) */ - void setDPI(const QSizeF &dpi); + void setDPI(const QSizeF &dpi); // TODO remove the & when we do a BIC change elsewhere /** * Returns the 'layers model' object of the document or NULL if * layers model is not available. * * @since 0.24 */ virtual QAbstractItemModel * layersModel() const; /** * Calls the backend to execute an BackendOpaqueAction */ virtual void opaqueAction( const BackendOpaqueAction *action ); Q_SIGNALS: /** * This signal should be emitted whenever an error occurred in the generator. * * @param message The message which should be shown to the user. * @param duration The time that the message should be shown to the user. */ void error( const QString &message, int duration ); /** * This signal should be emitted whenever the user should be warned. * * @param message The message which should be shown to the user. * @param duration The time that the message should be shown to the user. */ void warning( const QString &message, int duration ); /** * This signal should be emitted whenever the user should be noticed. * * @param message The message which should be shown to the user. * @param duration The time that the message should be shown to the user. */ void notice( const QString &message, int duration ); protected: /** * This method must be called when the pixmap request triggered by generatePixmap() * has been finished. */ void signalPixmapRequestDone( PixmapRequest * request ); /** * This method must be called when a text generation has been finished. */ void signalTextGenerationDone( Page *page, TextPage *textPage ); /** * This method is called when the document is closed and not used * any longer. * * @returns true on success, false otherwise. */ virtual bool doCloseDocument() = 0; /** * Returns the image of the page as specified in * the passed pixmap @p request. * * Must return a null image if the request was cancelled and the generator supports cancelling * * @warning this method may be executed in its own separated thread if the * @ref Threaded is enabled! */ virtual QImage image( PixmapRequest *request ); /** * Returns the text page for the given @p request. * * Must return a null pointer if the request was cancelled and the generator supports cancelling * * @warning this method may be executed in its own separated thread if the * @ref Threaded is enabled! * * @since 1.4 */ virtual TextPage* textPage( TextRequest *request ); /** * Returns a pointer to the document. */ const Document * document() const; /** * Toggle the @p feature . */ void setFeature( GeneratorFeature feature, bool on = true ); /** * Internal document setting */ enum DocumentMetaDataKey { PaperColorMetaData, ///< Returns (QColor) the paper color if set in Settings or the default color (white) if option is true (otherwise returns a non initialized QColor) TextAntialiasMetaData, ///< Returns (bool) text antialias from Settings (option is not used) GraphicsAntialiasMetaData, ///< Returns (bool)graphic antialias from Settings (option is not used) TextHintingMetaData ///< Returns (bool)text hinting from Settings (option is not used) }; /** * Request a meta data of the Document, if available, like an internal * setting. * * @since 1.1 */ QVariant documentMetaData( const DocumentMetaDataKey key, const QVariant &option = QVariant() ) const; /** * Request a meta data of the Document, if available, like an internal * setting. */ OKULARCORE_DEPRECATED QVariant documentMetaData( const QString &key, const QVariant &option = QVariant() ) const; /** * Return the pointer to a mutex the generator can use freely. */ QMutex* userMutex() const; /** * Set the bounding box of a page after the page has already been handed * to the Document. Call this instead of Page::setBoundingBox() to ensure * that all observers are notified. * * @since 0.7 (KDE 4.1) */ void updatePageBoundingBox( int page, const NormalizedRect & boundingBox ); /** * Returns DPI, previously set via setDPI() * @since 0.19 (KDE 4.13) */ QSizeF dpi() const; protected Q_SLOTS: /** * Gets the font data for the given font * * @since 0.8 (KDE 4.1) */ void requestFontData(const Okular::FontInfo &font, QByteArray *data); /** * This method can be called to trigger a partial pixmap update for the given request * Make sure you call it in a way it's executed in the main thread. * @since 1.3 */ void signalPartialPixmapRequest( Okular::PixmapRequest *request, const QImage &image ); protected: /** * Returns the last print error in case print() failed * @since 0.11 (KDE 4.5) */ // TODO Make print() return a PrintError instead of bool and remove this function when a BIC change happens somewhere else Q_INVOKABLE Okular::Generator::PrintError printError() const; /// @cond PRIVATE Generator(GeneratorPrivate &dd, QObject *parent, const QVariantList &args); Q_DECLARE_PRIVATE( Generator ) GeneratorPrivate *d_ptr; friend class Document; friend class DocumentPrivate; /// @endcond PRIVATE private: Q_DISABLE_COPY( Generator ) }; /** * @short Describes a pixmap type request. */ class OKULARCORE_EXPORT PixmapRequest { friend class Document; friend class DocumentPrivate; public: enum PixmapRequestFeature { NoFeature = 0, Asynchronous = 1, Preload = 2 }; Q_DECLARE_FLAGS( PixmapRequestFeatures, PixmapRequestFeature ) /** * Creates a new pixmap request. * * @param observer The observer. * @param pageNumber The page number. * @param width The width of the page. * @param height The height of the page. * @param priority The priority of the request. * @param features The features of generation. */ PixmapRequest( DocumentObserver *observer, int pageNumber, int width, int height, int priority, PixmapRequestFeatures features ); /** * Destroys the pixmap request. */ ~PixmapRequest(); /** * Returns the observer of the request. */ DocumentObserver *observer() const; /** * Returns the page number of the request. */ int pageNumber() const; /** * Returns the page width of the requested pixmap. */ int width() const; /** * Returns the page height of the requested pixmap. */ int height() const; /** * Returns the priority (less it better, 0 is maximum) of the * request. */ int priority() const; /** * Returns whether the generation should be done synchronous or * asynchronous. * * If asynchronous, the pixmap is created in a thread and the observer * is notified when the job is done. */ bool asynchronous() const; /** * Returns whether the generation request is for a page that is not important * i.e. it's just for speeding up future rendering */ bool preload() const; /** * Returns a pointer to the page where the pixmap shall be generated for. */ Page *page() const; /** * Sets whether the generator should render only the given normalized * rect or the entire page * * @since 0.16 (KDE 4.10) */ void setTile( bool tile ); /** * Returns whether the generator should render just the region given by * normalizedRect() or the entire page. * * @since 0.16 (KDE 4.10) */ bool isTile() const; /** * Sets the region of the page to request. * * @since 0.16 (KDE 4.10) */ void setNormalizedRect( const NormalizedRect &rect ); /** * Returns the normalized region of the page to request. * * @since 0.16 (KDE 4.10) */ const NormalizedRect& normalizedRect() const; /** * Sets whether the request should report back updates if possible * * @since 1.3 */ void setPartialUpdatesWanted(bool partialUpdatesWanted); /** * Should the request report back updates if possible? * * @since 1.3 */ bool partialUpdatesWanted() const; /** * Should the request be aborted if possible? * * @since 1.4 */ bool shouldAbortRender() const; private: Q_DISABLE_COPY( PixmapRequest ) friend class PixmapRequestPrivate; PixmapRequestPrivate* const d; }; /** * @short Describes a text request. * * @since 1.4 */ class OKULARCORE_EXPORT TextRequest { public: /** * Creates a new text request. */ explicit TextRequest( Page *page ); TextRequest(); /** * Destroys the pixmap request. */ ~TextRequest(); /** * Returns a pointer to the page where the pixmap shall be generated for. */ Page *page() const; /** * Should the request be aborted if possible? */ bool shouldAbortExtraction() const; private: Q_DISABLE_COPY( TextRequest ) friend TextRequestPrivate; TextRequestPrivate* const d; }; } Q_DECLARE_METATYPE(Okular::Generator::PrintError) Q_DECLARE_METATYPE(Okular::PixmapRequest*) #define OkularGeneratorInterface_iid "org.kde.okular.Generator" Q_DECLARE_INTERFACE(Okular::Generator, OkularGeneratorInterface_iid) #ifndef QT_NO_DEBUG_STREAM OKULARCORE_EXPORT QDebug operator<<( QDebug str, const Okular::PixmapRequest &req ); #endif #endif /* kate: replace-tabs on; indent-width 4; */ diff --git a/core/movie.cpp b/core/movie.cpp index bf792b15f..98028a898 100644 --- a/core/movie.cpp +++ b/core/movie.cpp @@ -1,167 +1,167 @@ /*************************************************************************** * Copyright (C) 2008 by Pino Toscano * * Copyright (C) 2012 by Guillermo A. Amaral B. * * * * 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. * ***************************************************************************/ #include "movie.h" // qt/kde includes #include #include #include #include #include #include "debug_p.h" using namespace Okular; class Movie::Private { public: Private( const QString &url ) : m_url( url ), m_rotation( Rotation0 ), m_playMode( PlayLimited ), m_playRepetitions( 1.0 ), m_tmp( nullptr ), m_showControls( false ), m_autoPlay( false ), m_showPosterImage( false ) { } QString m_url; QSize m_aspect; Rotation m_rotation; PlayMode m_playMode; double m_playRepetitions; QTemporaryFile *m_tmp; QImage m_posterImage; bool m_showControls : 1; bool m_autoPlay : 1; bool m_showPosterImage : 1; }; Movie::Movie( const QString& fileName ) : d( new Private( fileName ) ) { } Movie::Movie( const QString& fileName, const QByteArray &data ) : d( new Private( fileName ) ) { /* Store movie data as temporary file. * * Originally loaded movie data directly using a QBuffer, but sadly phonon * fails to play on a few of my test systems (I think it's the Phonon * GStreamer backend). Storing the data in a temporary file works fine * though, not to mention, it releases much needed memory. (gamaral) */ d->m_tmp = new QTemporaryFile( QStringLiteral( "%1/okrXXXXXX" ).arg( QDir::tempPath() ) ); if ( d->m_tmp->open() ) { d->m_tmp->write( data ); d->m_tmp->flush(); } else qCDebug(OkularCoreDebug) << "Failed to create temporary file for video data."; } Movie::~Movie() { delete d->m_tmp; delete d; } QString Movie::url() const { if (d->m_tmp) return d->m_tmp->fileName(); else return d->m_url; } -void Movie::setSize( const QSize &aspect ) +void Movie::setSize( const QSize &aspect ) // clazy:exclude=function-args-by-value TODO remove the & when we do a BIC change elsewhere { d->m_aspect = aspect; } QSize Movie::size() const { return d->m_aspect; } void Movie::setRotation( Rotation rotation ) { d->m_rotation = rotation; } Rotation Movie::rotation() const { return d->m_rotation; } void Movie::setShowControls( bool show ) { d->m_showControls = show; } bool Movie::showControls() const { return d->m_showControls; } void Movie::setPlayMode( Movie::PlayMode mode ) { d->m_playMode = mode; } Movie::PlayMode Movie::playMode() const { return d->m_playMode; } void Movie::setPlayRepetitions( double repetitions ) { d->m_playRepetitions = repetitions; } double Movie::playRepetitions() const { return d->m_playRepetitions; } void Movie::setAutoPlay( bool autoPlay ) { d->m_autoPlay = autoPlay; } bool Movie::autoPlay() const { return d->m_autoPlay; } void Movie::setShowPosterImage( bool show ) { d->m_showPosterImage = show; } bool Movie::showPosterImage() const { return d->m_showPosterImage; } void Movie::setPosterImage( const QImage &image ) { d->m_posterImage = image; } QImage Movie::posterImage() const { return d->m_posterImage; } diff --git a/core/movie.h b/core/movie.h index 1779347ec..84bf014f7 100644 --- a/core/movie.h +++ b/core/movie.h @@ -1,161 +1,161 @@ /*************************************************************************** * Copyright (C) 2008 by Pino Toscano * * Copyright (C) 2012 by Guillermo A. Amaral B. * * * * 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. * ***************************************************************************/ #ifndef _OKULAR_MOVIE_H_ #define _OKULAR_MOVIE_H_ #include "global.h" #include "okularcore_export.h" #include class QImage; namespace Okular { /** * @short Contains information about a movie object. * * @since 0.8 (KDE 4.2) */ class OKULARCORE_EXPORT Movie { public: /** * The play mode for playing the movie */ enum PlayMode { PlayLimited, ///< Play a fixed amount of times, closing the movie controls at the end @since 0.24 PlayOpen, ///< Like PlayLimited, but leaving the controls open PlayRepeat, ///< Play continuously until stopped PlayPalindrome ///< Play forward, then backward, then again forward and so on until stopped }; /** * Creates a new movie object with the given external @p fileName. */ explicit Movie( const QString& fileName ); /** * Creates a new movie object with the given movie data. */ explicit Movie( const QString& fileName, const QByteArray &data ); /** * Destroys the movie object. */ ~Movie(); /** * Returns the url of the movie. */ QString url() const; /** * Sets the size for the movie. */ - void setSize( const QSize &aspect ); + void setSize( const QSize &aspect ); // TODO remove the & when we do a BIC change elsewhere /** * Returns the size of the movie. */ QSize size() const; /** * Sets the @p rotation of the movie. */ void setRotation( Rotation rotation ); /** * Returns the rotation of the movie. */ Rotation rotation() const; /** * Sets whether show a bar with movie controls */ void setShowControls( bool show ); /** * Whether show a bar with movie controls */ bool showControls() const; /** * Sets the way the movie should be played */ void setPlayMode( PlayMode mode ); /** * How to play the movie */ PlayMode playMode() const; /** * Sets how many times the movie should be played * @since 0.24 */ void setPlayRepetitions( double repetitions ); /** * How many times to play the movie * @since 0.24 */ double playRepetitions() const; /** * Sets whether to play the movie automatically */ void setAutoPlay( bool autoPlay ); /** * Whether to play the movie automatically */ bool autoPlay() const; /** * Sets whether to show a poster image. * * @since 4.10 */ void setShowPosterImage( bool show ); /** * Whether to show a poster image. * * @since 4.10 */ bool showPosterImage() const; /** * Sets the poster image. * * @since 4.10 */ void setPosterImage( const QImage &image ); /** * Returns the poster image. * * @since 4.10 */ QImage posterImage() const; private: class Private; Private* const d; Q_DISABLE_COPY( Movie ) }; } #endif diff --git a/core/script/kjs_ocg.cpp b/core/script/kjs_ocg.cpp index 9c1c1fee6..7e762f20e 100644 --- a/core/script/kjs_ocg.cpp +++ b/core/script/kjs_ocg.cpp @@ -1,86 +1,86 @@ /*************************************************************************** * Copyright (C) 2019 by João Netto * * * * 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. * ***************************************************************************/ #include "kjs_ocg_p.h" #include #include #include #include #include #include #include using namespace Okular; static KJSPrototype *g_OCGProto; typedef QHash< QPair< int, int > *, QAbstractItemModel* > OCGCache; Q_GLOBAL_STATIC( OCGCache, g_OCGCache ) // OCG.state (getter) static KJSObject OCGGetState( KJSContext *, void *object ) { QPair< int, int > *pair = reinterpret_cast< QPair< int, int >* > ( object ); QAbstractItemModel *model = g_OCGCache->value( pair ); const QModelIndex index = model->index( pair->first, pair->second ); const bool state = model->data( index, Qt::CheckStateRole ).toBool(); return KJSBoolean( state ); } // OCG.state (setter) static void OCGSetState( KJSContext* ctx, void* object, KJSObject value ) { QPair< int, int > *pair = reinterpret_cast< QPair< int, int >* > ( object ); QAbstractItemModel *model = g_OCGCache->value( pair ); const QModelIndex index = model->index( pair->first, pair->second ); const bool state = value.toBoolean( ctx ); model->setData( index, QVariant( state ? Qt::Checked : Qt::Unchecked ), Qt::CheckStateRole ); } void JSOCG::initType( KJSContext *ctx ) { static bool initialized = false; if ( initialized ) return; initialized = true; g_OCGProto = new KJSPrototype(); g_OCGProto->defineProperty( ctx, QStringLiteral("state"), OCGGetState, OCGSetState ); } KJSObject JSOCG::object( KJSContext *ctx ) { return g_OCGProto->constructObject( ctx, nullptr ); } -KJSObject JSOCG::wrapOCGObject( KJSContext *ctx, QAbstractItemModel *model, const int &i, const int &j ) +KJSObject JSOCG::wrapOCGObject( KJSContext *ctx, QAbstractItemModel *model, const int i, const int j ) { QPair< int, int > *pair = new QPair< int ,int >( i, j ); g_OCGCache->insert( pair, model ); return g_OCGProto->constructObject( ctx, pair ); } void JSOCG::clearCachedFields() { if ( g_OCGCache.exists() ) { g_OCGCache->clear(); } } diff --git a/core/script/kjs_ocg_p.h b/core/script/kjs_ocg_p.h index 894757df7..a633e5f1e 100644 --- a/core/script/kjs_ocg_p.h +++ b/core/script/kjs_ocg_p.h @@ -1,30 +1,30 @@ /*************************************************************************** * Copyright (C) 2019 by João Netto * * * * 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. * ***************************************************************************/ #ifndef OKULAR_SCRIPT_KJS_OCG_P_H #define OKULAR_SCRIPT_KJS_OCG_P_H class KJSContext; class KJSObject; class QAbstractItemModel; namespace Okular { class JSOCG { public: static void initType( KJSContext *ctx ); static KJSObject object( KJSContext *ctx ); - static KJSObject wrapOCGObject( KJSContext *ctx, QAbstractItemModel *model, const int &i, const int &j ); + static KJSObject wrapOCGObject( KJSContext *ctx, QAbstractItemModel *model, const int i, const int j ); static void clearCachedFields(); }; } #endif diff --git a/core/textpage.cpp b/core/textpage.cpp index bb98ea3a5..d9caf7e20 100644 --- a/core/textpage.cpp +++ b/core/textpage.cpp @@ -1,2035 +1,2035 @@ /*************************************************************************** * Copyright (C) 2005 by Piotr Szymanski * * * * 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. * ***************************************************************************/ #include "textpage.h" #include "textpage_p.h" #include #include "area.h" #include "debug_p.h" #include "misc.h" #include "page.h" #include "page_p.h" #include #include #include using namespace Okular; class SearchPoint { public: SearchPoint() : offset_begin( -1 ), offset_end( -1 ) { } /** The TinyTextEntity containing the first character of the match. */ TextList::ConstIterator it_begin; /** The TinyTextEntity containing the last character of the match. */ TextList::ConstIterator it_end; /** The index of the first character of the match in (*it_begin)->text(). * Satisfies 0 <= offset_begin < (*it_begin)->text().length(). */ int offset_begin; /** One plus the index of the last character of the match in (*it_end)->text(). * Satisfies 0 < offset_end <= (*it_end)->text().length(). */ int offset_end; }; /* text comparison functions */ static bool CaseInsensitiveCmpFn( const QStringRef & from, const QStringRef & to ) { return from.compare( to, Qt::CaseInsensitive ) == 0; } static bool CaseSensitiveCmpFn( const QStringRef & from, const QStringRef & to ) { return from.compare( to, Qt::CaseSensitive ) == 0; } /** * Returns true iff segments [@p left1, @p right1] and [@p left2, @p right2] on the real line * overlap within @p threshold percent, i. e. iff the ratio of the length of the * intersection of the segments to the length of the shortest of the two input segments * is not smaller than the threshold. */ static bool segmentsOverlap(double left1, double right1, double left2, double right2, int threshold) { // check if one consumes another fully (speed optimization) if (left1 <= left2 && right1 >= right2) return true; if (left1 >= left2 && right1 <= right2) return true; // check if there is overlap above threshold if (right2 >= left1 && right1 >= left2) { double overlap = (right2 >= right1) ? right1 - left2 : right2 - left1; double length1 = right1 - left1, length2 = right2 - left2; return overlap * 100 >= threshold * qMin(length1, length2); } return false; } -static bool doesConsumeY(const QRect& first, const QRect& second, int threshold) +static bool doesConsumeY(const QRect first, const QRect second, int threshold) { return segmentsOverlap(first.top(), first.bottom(), second.top(), second.bottom(), threshold); } static bool doesConsumeY(const NormalizedRect& first, const NormalizedRect& second, int threshold) { return segmentsOverlap(first.top, first.bottom, second.top, second.bottom, threshold); } /* Rationale behind TinyTextEntity: instead of storing directly a QString for the text of an entity, we store the UTF-16 data and their length. This way, we save about 4 int's wrt a QString, and we can create a new string from that raw data (that's the only penalty of that). Even better, if the string we need to store has at most MaxStaticChars characters, then we store those in place of the QChar* that would be used (with new[] + free[]) for the data. */ class TinyTextEntity { static const int MaxStaticChars = sizeof( void* ) / sizeof( QChar ); public: TinyTextEntity( const QString &text, const NormalizedRect &rect ) : area( rect ) { Q_ASSERT_X( !text.isEmpty(), "TinyTextEntity", "empty string" ); Q_ASSERT_X( sizeof( d ) == sizeof( void * ), "TinyTextEntity", "internal storage is wider than QChar*, fix it!" ); length = text.length(); switch ( length ) { #if QT_POINTER_SIZE >= 8 case 4: d.qc[3] = text.at( 3 ).unicode(); // fall through case 3: d.qc[2] = text.at( 2 ).unicode(); #endif // fall through case 2: d.qc[1] = text.at( 1 ).unicode(); // fall through case 1: d.qc[0] = text.at( 0 ).unicode(); break; default: d.data = new QChar[ length ]; std::memcpy( d.data, text.constData(), length * sizeof( QChar ) ); } } ~TinyTextEntity() { if ( length > MaxStaticChars ) { delete [] d.data; } } inline QString text() const { return length <= MaxStaticChars ? QString::fromRawData( ( const QChar * )&d.qc[0], length ) : QString::fromRawData( d.data, length ); } inline NormalizedRect transformedArea( const QTransform &matrix ) const { NormalizedRect transformed_area = area; transformed_area.transform( matrix ); return transformed_area; } NormalizedRect area; private: Q_DISABLE_COPY( TinyTextEntity ) union { QChar *data; ushort qc[MaxStaticChars]; } d; int length; }; TextEntity::TextEntity( const QString &text, NormalizedRect *area ) : m_text( text ), m_area( area ), d( nullptr ) { } TextEntity::~TextEntity() { delete m_area; } QString TextEntity::text() const { return m_text; } NormalizedRect* TextEntity::area() const { return m_area; } NormalizedRect TextEntity::transformedArea(const QTransform &matrix) const { NormalizedRect transformed_area = *m_area; transformed_area.transform( matrix ); return transformed_area; } TextPagePrivate::TextPagePrivate() : m_page( nullptr ) { } TextPagePrivate::~TextPagePrivate() { qDeleteAll( m_searchPoints ); qDeleteAll( m_words ); } TextPage::TextPage() : d( new TextPagePrivate() ) { } TextPage::TextPage( const TextEntity::List &words ) : d( new TextPagePrivate() ) { TextEntity::List::ConstIterator it = words.constBegin(), itEnd = words.constEnd(); for ( ; it != itEnd; ++it ) { TextEntity *e = *it; if ( !e->text().isEmpty() ) d->m_words.append( new TinyTextEntity( e->text(), *e->area() ) ); delete e; } } TextPage::~TextPage() { delete d; } void TextPage::append( const QString &text, NormalizedRect *area ) { if ( !text.isEmpty() ) d->m_words.append( new TinyTextEntity( text.normalized(QString::NormalizationForm_KC), *area ) ); delete area; } struct WordWithCharacters { WordWithCharacters(TinyTextEntity *w, const TextList &c) : word(w), characters(c) { } inline QString text() const { return word->text(); } inline const NormalizedRect &area() const { return word->area; } TinyTextEntity *word; TextList characters; }; typedef QList WordsWithCharacters; /** * We will divide the whole page in some regions depending on the horizontal and * vertical spacing among different regions. Each region will have an area and an * associated WordsWithCharacters in sorted order. */ class RegionText { public: RegionText() { }; - RegionText(const WordsWithCharacters &wordsWithCharacters, const QRect &area) + RegionText(const WordsWithCharacters &wordsWithCharacters, const QRect area) : m_region_wordWithCharacters(wordsWithCharacters), m_area(area) { } inline QString string() const { QString res; for (const WordWithCharacters &word : m_region_wordWithCharacters) { res += word.text(); } return res; } inline WordsWithCharacters text() const { return m_region_wordWithCharacters; } inline QRect area() const { return m_area; } - inline void setArea(const QRect &area) + inline void setArea(const QRect area) { m_area = area; } inline void setText(const WordsWithCharacters &wordsWithCharacters) { m_region_wordWithCharacters = wordsWithCharacters; } private: WordsWithCharacters m_region_wordWithCharacters; QRect m_area; }; RegularAreaRect * TextPage::textArea ( TextSelection * sel) const { if ( d->m_words.isEmpty() ) return new RegularAreaRect(); /** It works like this: There are two cursors, we need to select all the text between them. The coordinates are normalised, leftTop is (0,0) rightBottom is (1,1), so for cursors start (sx,sy) and end (ex,ey) we start with finding text rectangles under those points, if not we search for the first that is to the right to it in the same baseline, if none found, then we search for the first rectangle with a baseline under the cursor, having two points that are the best rectangles to both of the cursors: (rx,ry)x(tx,ty) for start and (ux,uy)x(vx,vy) for end, we do a 1. (rx,ry)x(1,ty) 2. (0,ty)x(1,uy) 3. (0,uy)x(vx,vy) To find the closest rectangle to cursor (cx,cy) we search for a rectangle that either contains the cursor or that has a left border >= cx and bottom border >= cy. */ RegularAreaRect * ret= new RegularAreaRect; PagePrivate *pagePrivate = PagePrivate::get(d->m_page); const QTransform matrix = pagePrivate ? pagePrivate->rotationMatrix() : QTransform(); #if 0 int it = -1; int itB = -1; int itE = -1; // ending cursor is higher than start cursor, we need to find positions in reverse NormalizedRect tmp; NormalizedRect start; NormalizedRect end; NormalizedPoint startC = sel->start(); double startCx = startC.x; double startCy = startC.y; NormalizedPoint endC = sel->end(); double endCx = endC.x; double endCy = endC.y; if ( sel->direction() == 1 || ( sel->itB() == -1 && sel->direction() == 0 ) ) { #ifdef DEBUG_TEXTPAGE qCWarning(OkularCoreDebug) << "running first loop"; #endif const int count = d->m_words.count(); for ( it = 0; it < count; it++ ) { tmp = *d->m_words[ it ]->area(); if ( tmp.contains( startCx, startCy ) || ( tmp.top <= startCy && tmp.bottom >= startCy && tmp.left >= startCx ) || ( tmp.top >= startCy)) { /// we have found the (rx,ry)x(tx,ty) itB = it; #ifdef DEBUG_TEXTPAGE qCWarning(OkularCoreDebug) << "start is" << itB << "count is" << d->m_words.count(); #endif break; } } sel->itB( itB ); } itB = sel->itB(); #ifdef DEBUG_TEXTPAGE qCWarning(OkularCoreDebug) << "direction is" << sel->direction(); qCWarning(OkularCoreDebug) << "reloaded start is" << itB << "against" << sel->itB(); #endif if ( sel->direction() == 0 || ( sel->itE() == -1 && sel->direction() == 1 ) ) { #ifdef DEBUG_TEXTPAGE qCWarning(OkularCoreDebug) << "running second loop"; #endif for ( it = d->m_words.count() - 1; it >= itB; it-- ) { tmp = *d->m_words[ it ]->area(); if ( tmp.contains( endCx, endCy ) || ( tmp.top <= endCy && tmp.bottom >= endCy && tmp.right <= endCx ) || ( tmp.bottom <= endCy ) ) { /// we have found the (ux,uy)x(vx,vy) itE = it; #ifdef DEBUG_TEXTPAGE qCWarning(OkularCoreDebug) << "ending is" << itE << "count is" << d->m_words.count(); qCWarning(OkularCoreDebug) << "conditions" << tmp.contains( endCx, endCy ) << " " << ( tmp.top <= endCy && tmp.bottom >= endCy && tmp.right <= endCx ) << " " << ( tmp.top >= endCy); #endif break; } } sel->itE( itE ); } #ifdef DEBUG_TEXTPAGE qCWarning(OkularCoreDebug) << "reloaded ending is" << itE << "against" << sel->itE(); #endif if ( sel->itB() != -1 && sel->itE() != -1 ) { start = *d->m_words[ sel->itB() ]->area(); end = *d->m_words[ sel->itE() ]->area(); NormalizedRect first, second, third; /// finding out if there is more than one baseline between them is a hard and discussable task /// we will create a rectangle (rx,0)x(tx,1) and will check how many times does it intersect the /// areas, if more than one -> we have a three or over line selection first = start; second.top = start.bottom; first.right = second.right = 1; third = end; third.left = second.left = 0; second.bottom = end.top; int selMax = qMax( sel->itB(), sel->itE() ); for ( it = qMin( sel->itB(), sel->itE() ); it <= selMax; ++it ) { tmp = *d->m_words[ it ]->area(); if ( tmp.intersects( &first ) || tmp.intersects( &second ) || tmp.intersects( &third ) ) ret->appendShape( d->m_words.at( it )->transformedArea( matrix ) ); } } #else const double scaleX = d->m_page->width(); const double scaleY = d->m_page->height(); NormalizedPoint startC = sel->start(); NormalizedPoint endC = sel->end(); NormalizedPoint temp; // if startPoint is right to endPoint swap them if(startC.x > endC.x) { temp = startC; startC = endC; endC = temp; } // minX,maxX,minY,maxY gives the bounding rectangle coordinates of the document const NormalizedRect boundingRect = d->m_page->boundingBox(); const QRect content = boundingRect.geometry(scaleX,scaleY); const double minX = content.left(); const double maxX = content.right(); const double minY = content.top(); const double maxY = content.bottom(); /** * We will now find out the TinyTextEntity for the startRectangle and TinyTextEntity for * the endRectangle. We have four cases: * * Case 1(a): both startpoint and endpoint are out of the bounding Rectangle and at one side, so the rectangle made of start * and endPoint are outof the bounding rect (do not intersect) * * Case 1(b): both startpoint and endpoint are out of bounding rect, but they are in different side, so is their rectangle * * Case 2(a): find the rectangle which contains start and endpoint and having some * TextEntity * * Case 2(b): if 2(a) fails (if startPoint and endPoint both are unchanged), then we check whether there is any * TextEntity within the rect made by startPoint and endPoint * * Case 3: Now, we may have two type of selection. * 1. startpoint is left-top of start_end and endpoint is right-bottom * 2. startpoint is left-bottom of start_end and endpoint is top-right * * Also, as 2(b) is passed, we might have it,itEnd or both unchanged, but the fact is that we have * text within them. so, we need to search for the best suitable textposition for start and end. * * Case 3(a): We search the nearest rectangle consisting of some * TinyTextEntity right to or bottom of the startPoint for selection 01. * And, for selection 02, we have to search for right and top * * Case 3(b): For endpoint, we have to find the point top of or left to * endpoint if we have selection 01. * Otherwise, the search will be left and bottom */ // we know that startC.x > endC.x, we need to decide which is top and which is bottom const NormalizedRect start_end = (startC.y < endC.y) ? NormalizedRect(startC.x, startC.y, endC.x, endC.y) : NormalizedRect(startC.x, endC.y, endC.x, startC.y); // Case 1(a) if(!boundingRect.intersects(start_end)) return ret; // case 1(b) /** note that, after swapping of start and end, we know that, start is always left to end. but, we cannot say start is positioned upper than end. **/ else { // if start is left to content rect take it to content rect boundary if(startC.x * scaleX < minX) startC.x = minX/scaleX; if(endC.x * scaleX > maxX) endC.x = maxX/scaleX; // if start is top to end (selection type 01) if(startC.y * scaleY < minY) startC.y = minY/scaleY; if(endC.y * scaleY > maxY) endC.y = maxY/scaleY; // if start is bottom to end (selection type 02) if(startC.y * scaleY > maxY) startC.y = maxY/scaleY; if(endC.y * scaleY < minY) endC.y = minY/scaleY; } TextList::ConstIterator it = d->m_words.constBegin(), itEnd = d->m_words.constEnd(); TextList::ConstIterator start = it, end = itEnd, tmpIt = it; //, tmpItEnd = itEnd; const MergeSide side = d->m_page ? (MergeSide)d->m_page->totalOrientation() : MergeRight; NormalizedRect tmp; //case 2(a) for ( ; it != itEnd; ++it ) { tmp = (*it)->area; if(tmp.contains(startC.x,startC.y)){ start = it; } if(tmp.contains(endC.x,endC.y)){ end = it; } } //case 2(b) it = tmpIt; if(start == it && end == itEnd) { for ( ; it != itEnd; ++it ) { // is there any text rectangle within the start_end rect tmp = (*it)->area; if(start_end.intersects(tmp)) break; } // we have searched every text entities, but none is within the rectangle created by start and end // so, no selection should be done if(it == itEnd) { return ret; } } it = tmpIt; bool selection_two_start = false; //case 3.a if(start == it) { bool flagV = false; NormalizedRect rect; // selection type 01 if(startC.y <= endC.y) { for ( ; it != itEnd; ++it ) { rect= (*it)->area; rect.isBottom(startC) ? flagV = false: flagV = true; if(flagV && rect.isRight(startC)) { start = it; break; } } } //selection type 02 else { selection_two_start = true; int distance = scaleX + scaleY + 100; int count = 0; for ( ; it != itEnd; ++it ) { rect= (*it)->area; if(rect.isBottomOrLevel(startC) && rect.isRight(startC)) { count++; QRect entRect = rect.geometry(scaleX,scaleY); int xdist, ydist; xdist = entRect.center().x() - startC.x * scaleX; ydist = entRect.center().y() - startC.y * scaleY; //make them positive if(xdist < 0) xdist = -xdist; if(ydist < 0) ydist = -ydist; if( (xdist + ydist) < distance) { distance = xdist+ ydist; start = it; } } } } } //case 3.b if(end == itEnd) { it = tmpIt; itEnd = itEnd-1; bool flagV = false; NormalizedRect rect; if(startC.y <= endC.y) { for ( ; itEnd >= it; itEnd-- ) { rect= (*itEnd)->area; rect.isTop(endC) ? flagV = false: flagV = true; if(flagV && rect.isLeft(endC)) { end = itEnd; break; } } } else { int distance = scaleX + scaleY + 100; for ( ; itEnd >= it; itEnd-- ) { rect= (*itEnd)->area; if(rect.isTopOrLevel(endC) && rect.isLeft(endC)) { QRect entRect = rect.geometry(scaleX,scaleY); int xdist, ydist; xdist = entRect.center().x() - endC.x * scaleX; ydist = entRect.center().y() - endC.y * scaleY; //make them positive if(xdist < 0) xdist = -xdist; if(ydist < 0) ydist = -ydist; if( (xdist + ydist) < distance) { distance = xdist+ ydist; end = itEnd; } } } } } /* if start and end in selection 02 are in the same column, and we start at an empty space we have to remove the selection of last character */ if(selection_two_start) { if(start > end) { start = start - 1; } } // if start is less than end swap them if(start > end) { it = start; start = end; end = it; } // removes the possibility of crash, in case none of 1 to 3 is true if(end == d->m_words.constEnd()) end--; for( ;start <= end ; start++) { ret->appendShape( (*start)->transformedArea( matrix ), side ); } #endif return ret; } RegularAreaRect* TextPage::findText( int searchID, const QString &query, SearchDirection direct, Qt::CaseSensitivity caseSensitivity, const RegularAreaRect *area ) { SearchDirection dir=direct; // invalid search request if ( d->m_words.isEmpty() || query.isEmpty() || ( area && area->isNull() ) ) return nullptr; TextList::ConstIterator start; int start_offset = 0; TextList::ConstIterator end; const QMap< int, SearchPoint* >::const_iterator sIt = d->m_searchPoints.constFind( searchID ); if ( sIt == d->m_searchPoints.constEnd() ) { // if no previous run of this search is found, then set it to start // from the beginning (respecting the search direction) if ( dir == NextResult ) dir = FromTop; else if ( dir == PreviousResult ) dir = FromBottom; } bool forward = true; switch ( dir ) { case FromTop: start = d->m_words.constBegin(); start_offset = 0; end = d->m_words.constEnd(); break; case FromBottom: start = d->m_words.constEnd(); start_offset = 0; end = d->m_words.constBegin(); forward = false; break; case NextResult: start = (*sIt)->it_end; start_offset = (*sIt)->offset_end; end = d->m_words.constEnd(); break; case PreviousResult: start = (*sIt)->it_begin; start_offset = (*sIt)->offset_begin; end = d->m_words.constBegin(); forward = false; break; }; RegularAreaRect* ret = nullptr; const TextComparisonFunction cmpFn = caseSensitivity == Qt::CaseSensitive ? CaseSensitiveCmpFn : CaseInsensitiveCmpFn; if ( forward ) { ret = d->findTextInternalForward( searchID, query, cmpFn, start, start_offset, end ); } else { ret = d->findTextInternalBackward( searchID, query, cmpFn, start, start_offset, end ); } return ret; } // hyphenated '-' must be at the end of a word, so hyphenation means // we have a '-' just followed by a '\n' character // check if the string contains a '-' character // if the '-' is the last entry static int stringLengthAdaptedWithHyphen(const QString &str, const TextList::ConstIterator &it, const TextList::ConstIterator &textListEnd) { int len = str.length(); // hyphenated '-' must be at the end of a word, so hyphenation means // we have a '-' just followed by a '\n' character // check if the string contains a '-' character // if the '-' is the last entry if ( str.endsWith( QLatin1Char('-') ) ) { // validity chek of it + 1 if ( ( it + 1 ) != textListEnd ) { // 1. if the next character is '\n' const QString &lookahedStr = (*(it+1))->text(); if (lookahedStr.startsWith(QLatin1Char('\n'))) { len -= 1; } else { // 2. if the next word is in a different line or not const NormalizedRect& hyphenArea = (*it)->area; const NormalizedRect& lookaheadArea = (*(it + 1))->area; // lookahead to check whether both the '-' rect and next character rect overlap if( !doesConsumeY( hyphenArea, lookaheadArea, 70 ) ) { len -= 1; } } } } // else if it is the second last entry - for example in pdf format else if (str.endsWith(QLatin1String("-\n"))) { len -= 2; } return len; } RegularAreaRect* TextPagePrivate::searchPointToArea(const SearchPoint* sp) { PagePrivate *pagePrivate = PagePrivate::get(m_page); const QTransform matrix = pagePrivate ? pagePrivate->rotationMatrix() : QTransform(); RegularAreaRect* ret=new RegularAreaRect; for (TextList::ConstIterator it = sp->it_begin; ; it++) { const TinyTextEntity* curEntity = *it; ret->append( curEntity->transformedArea( matrix ) ); if (it == sp->it_end) { break; } } ret->simplify(); return ret; } RegularAreaRect* TextPagePrivate::findTextInternalForward( int searchID, const QString &_query, TextComparisonFunction comparer, const TextList::ConstIterator &start, int start_offset, const TextList::ConstIterator &end) { // normalize query search all unicode (including glyphs) const QString query = _query.normalized(QString::NormalizationForm_KC); // j is the current position in our query // len is the length of the string in TextEntity // queryLeft is the length of the query we have left int j=0, queryLeft=query.length(); TextList::ConstIterator it = start; int offset = start_offset; TextList::ConstIterator it_begin = TextList::ConstIterator(); int offset_begin = 0; //dummy initial value to suppress compiler warnings while ( it != end ) { const TinyTextEntity* curEntity = *it; const QString& str = curEntity->text(); int len = stringLengthAdaptedWithHyphen(str, it, m_words.constEnd()); if (offset >= len) { it++; offset = 0; continue; } if ( it_begin == TextList::ConstIterator() ) { it_begin = it; offset_begin = offset; } int min=qMin(queryLeft,len-offset); { #ifdef DEBUG_TEXTPAGE qCDebug(OkularCoreDebug) << str.midRef(offset, min) << ":" << _query.midRef(j, min); #endif // we have equal (or less than) area of the query left as the length of the current // entity if ( !comparer( str.midRef( offset, min ), query.midRef( j, min ) ) ) { // we have not matched // this means we do not have a complete match // we need to get back to query start // and continue the search from this place #ifdef DEBUG_TEXTPAGE qCDebug(OkularCoreDebug) << "\tnot matched"; #endif j = 0; queryLeft=query.length(); it = it_begin; offset = offset_begin+1; it_begin = TextList::ConstIterator(); } else { // we have a match // move the current position in the query // to the position after the length of this string // we matched // subtract the length of the current entity from // the left length of the query #ifdef DEBUG_TEXTPAGE qCDebug(OkularCoreDebug) << "\tmatched"; #endif j += min; queryLeft -= min; if (queryLeft==0) { // save or update the search point for the current searchID QMap< int, SearchPoint* >::iterator sIt = m_searchPoints.find( searchID ); if ( sIt == m_searchPoints.end() ) { sIt = m_searchPoints.insert( searchID, new SearchPoint ); } SearchPoint* sp = *sIt; sp->it_begin = it_begin; sp->it_end = it; sp->offset_begin = offset_begin; sp->offset_end = offset + min; return searchPointToArea(sp); } it++; offset = 0; } } } // end of loop - it means that we've ended the textentities const QMap< int, SearchPoint* >::iterator sIt = m_searchPoints.find( searchID ); if ( sIt != m_searchPoints.end() ) { SearchPoint* sp = *sIt; m_searchPoints.erase( sIt ); delete sp; } return nullptr; } RegularAreaRect* TextPagePrivate::findTextInternalBackward( int searchID, const QString &_query, TextComparisonFunction comparer, const TextList::ConstIterator &start, int start_offset, const TextList::ConstIterator &end) { // normalize query to search all unicode (including glyphs) const QString query = _query.normalized(QString::NormalizationForm_KC); // j is the current position in our query // len is the length of the string in TextEntity // queryLeft is the length of the query we have left int j=query.length(), queryLeft=query.length(); TextList::ConstIterator it = start; int offset = start_offset; TextList::ConstIterator it_begin = TextList::ConstIterator(); int offset_begin = 0; //dummy initial value to suppress compiler warnings while ( true ) { if (offset <= 0) { if ( it == end ) { break; } it--; } const TinyTextEntity* curEntity = *it; const QString& str = curEntity->text(); int len = stringLengthAdaptedWithHyphen(str, it, m_words.constEnd()); if (offset <= 0) { offset = len; } if ( it_begin == TextList::ConstIterator() ) { it_begin = it; offset_begin = offset; } int min=qMin(queryLeft,offset); { #ifdef DEBUG_TEXTPAGE qCDebug(OkularCoreDebug) << str.midRef(offset-min, min) << " : " << _query.midRef(j-min, min); #endif // we have equal (or less than) area of the query left as the length of the current // entity // Note len is not str.length() so we can't use rightRef here if ( !comparer( str.midRef(offset-min, min ), query.midRef( j - min, min ) ) ) { // we have not matched // this means we do not have a complete match // we need to get back to query start // and continue the search from this place #ifdef DEBUG_TEXTPAGE qCDebug(OkularCoreDebug) << "\tnot matched"; #endif j = query.length(); queryLeft = query.length(); it = it_begin; offset = offset_begin-1; it_begin = TextList::ConstIterator(); } else { // we have a match // move the current position in the query // to the position after the length of this string // we matched // subtract the length of the current entity from // the left length of the query #ifdef DEBUG_TEXTPAGE qCDebug(OkularCoreDebug) << "\tmatched"; #endif j -= min; queryLeft -= min; if ( queryLeft == 0 ) { // save or update the search point for the current searchID QMap< int, SearchPoint* >::iterator sIt = m_searchPoints.find( searchID ); if ( sIt == m_searchPoints.end() ) { sIt = m_searchPoints.insert( searchID, new SearchPoint ); } SearchPoint* sp = *sIt; sp->it_begin = it; sp->it_end = it_begin; sp->offset_begin = offset - min; sp->offset_end = offset_begin; return searchPointToArea(sp); } offset = 0; } } } // end of loop - it means that we've ended the textentities const QMap< int, SearchPoint* >::iterator sIt = m_searchPoints.find( searchID ); if ( sIt != m_searchPoints.end() ) { SearchPoint* sp = *sIt; m_searchPoints.erase( sIt ); delete sp; } return nullptr; } QString TextPage::text(const RegularAreaRect *area) const { return text(area, AnyPixelTextAreaInclusionBehaviour); } QString TextPage::text(const RegularAreaRect *area, TextAreaInclusionBehaviour b) const { if ( area && area->isNull() ) return QString(); TextList::ConstIterator it = d->m_words.constBegin(), itEnd = d->m_words.constEnd(); QString ret; if ( area ) { for ( ; it != itEnd; ++it ) { if (b == AnyPixelTextAreaInclusionBehaviour) { if ( area->intersects( (*it)->area ) ) { ret += (*it)->text(); } } else { NormalizedPoint center = (*it)->area.center(); if ( area->contains( center.x, center.y ) ) { ret += (*it)->text(); } } } } else { for ( ; it != itEnd; ++it ) ret += (*it)->text(); } return ret; } static bool compareTinyTextEntityX(const WordWithCharacters &first, const WordWithCharacters &second) { QRect firstArea = first.area().roundedGeometry(1000,1000); QRect secondArea = second.area().roundedGeometry(1000,1000); return firstArea.left() < secondArea.left(); } static bool compareTinyTextEntityY(const WordWithCharacters &first, const WordWithCharacters &second) { const QRect firstArea = first.area().roundedGeometry(1000,1000); const QRect secondArea = second.area().roundedGeometry(1000,1000); return firstArea.top() < secondArea.top(); } /** * Sets a new world list. Deleting the contents of the old one */ void TextPagePrivate::setWordList(const TextList &list) { qDeleteAll(m_words); m_words = list; } /** * Remove all the spaces in between texts. It will make all the generators * same, whether they save spaces(like pdf) or not(like djvu). */ static void removeSpace(TextList *words) { TextList::Iterator it = words->begin(); const QString str(QLatin1Char(' ')); while ( it != words->end() ) { if((*it)->text() == str) { it = words->erase(it); } else { ++it; } } } /** * We will read the TinyTextEntity from characters and try to create words from there. * Note: characters might be already characters for some generators, but we will keep * the nomenclature characters for the generator produced data. The resulting * WordsWithCharacters memory has to be managed by the caller, both the * WordWithCharacters::word and WordWithCharacters::characters contents */ static WordsWithCharacters makeWordFromCharacters(const TextList &characters, int pageWidth, int pageHeight) { /** * We will traverse characters and try to create words from the TinyTextEntities in it. * We will search TinyTextEntity blocks and merge them until we get a * space between two consecutive TinyTextEntities. When we get a space * we can take it as a end of word. Then we store the word as a TinyTextEntity * and keep it in newList. * We create a RegionText named regionWord that contains the word and the characters associated with it and * a rectangle area of the element in newList. */ WordsWithCharacters wordsWithCharacters; TextList::ConstIterator it = characters.begin(), itEnd = characters.end(), tmpIt; int newLeft,newRight,newTop,newBottom; int index = 0; for( ; it != itEnd ; it++) { QString textString = (*it)->text(); QString newString; QRect lineArea = (*it)->area.roundedGeometry(pageWidth,pageHeight),elementArea; TextList wordCharacters; tmpIt = it; int space = 0; while (!space) { if (textString.length()) { newString.append(textString); // when textString is the start of the word if (tmpIt == it) { NormalizedRect newRect(lineArea,pageWidth,pageHeight); wordCharacters.append(new TinyTextEntity(textString.normalized (QString::NormalizationForm_KC), newRect)); } else { NormalizedRect newRect(elementArea,pageWidth,pageHeight); wordCharacters.append(new TinyTextEntity(textString.normalized (QString::NormalizationForm_KC), newRect)); } } ++it; /* we must have to put this line before the if condition of it==itEnd otherwise the last character can be missed */ if (it == itEnd) break; elementArea = (*it)->area.roundedGeometry(pageWidth,pageHeight); if (!doesConsumeY(elementArea, lineArea, 60)) { --it; break; } const int text_y1 = elementArea.top() , text_x1 = elementArea.left(), text_y2 = elementArea.y() + elementArea.height(), text_x2 = elementArea.x() + elementArea.width(); const int line_y1 = lineArea.top() ,line_x1 = lineArea.left(), line_y2 = lineArea.y() + lineArea.height(), line_x2 = lineArea.x() + lineArea.width(); space = elementArea.left() - lineArea.right(); if (space != 0) { it--; break; } newLeft = text_x1 < line_x1 ? text_x1 : line_x1; newRight = line_x2 > text_x2 ? line_x2 : text_x2; newTop = text_y1 > line_y1 ? line_y1 : text_y1; newBottom = text_y2 > line_y2 ? text_y2 : line_y2; lineArea.setLeft (newLeft); lineArea.setTop (newTop); lineArea.setWidth( newRight - newLeft ); lineArea.setHeight( newBottom - newTop ); textString = (*it)->text(); } // if newString is not empty, save it if (!newString.isEmpty()) { const NormalizedRect newRect(lineArea, pageWidth, pageHeight); TinyTextEntity *word = new TinyTextEntity(newString.normalized(QString::NormalizationForm_KC), newRect); wordsWithCharacters.append(WordWithCharacters(word, wordCharacters)); index++; } if(it == itEnd) break; } return wordsWithCharacters; } /** * Create Lines from the words and sort them */ QList< QPair > makeAndSortLines(const WordsWithCharacters &wordsTmp, int pageWidth, int pageHeight) { /** * We cannot assume that the generator will give us texts in the right order. * We can only assume that we will get texts in the page and their bounding * rectangle. The texts can be character, word, half-word anything. * So, we need to: ** * 1. Sort rectangles/boxes containing texts by y0(top) * 2. Create textline where there is y overlap between TinyTextEntity 's * 3. Within each line sort the TinyTextEntity 's by x0(left) */ QList< QPair > lines; /* Make a new copy of the TextList in the words, so that the wordsTmp and lines do not contain same pointers for all the TinyTextEntity. */ QList words = wordsTmp; // Step 1 std::sort(words.begin(),words.end(), compareTinyTextEntityY); // Step 2 QList::Iterator it = words.begin(), itEnd = words.end(); //for every non-space texts(characters/words) in the textList for( ; it != itEnd ; it++) { const QRect elementArea = (*it).area().roundedGeometry(pageWidth,pageHeight); bool found = false; for( QPair &linesI : lines) { /* the line area which will be expanded line_rects is only necessary to preserve the topmin and bottommax of all the texts in the line, left and right is not necessary at all */ QRect &lineArea = linesI.second; const int text_y1 = elementArea.top() , text_y2 = elementArea.top() + elementArea.height() , text_x1 = elementArea.left(), text_x2 = elementArea.left() + elementArea.width(); const int line_y1 = lineArea.top() , line_y2 = lineArea.top() + lineArea.height(), line_x1 = lineArea.left(), line_x2 = lineArea.left() + lineArea.width(); /* if the new text and the line has y overlapping parts of more than 70%, the text will be added to this line */ if(doesConsumeY(elementArea,lineArea,70)) { WordsWithCharacters &line = linesI.first; line.append(*it); const int newLeft = line_x1 < text_x1 ? line_x1 : text_x1; const int newRight = line_x2 > text_x2 ? line_x2 : text_x2; const int newTop = line_y1 < text_y1 ? line_y1 : text_y1; const int newBottom = text_y2 > line_y2 ? text_y2 : line_y2; lineArea = QRect( newLeft,newTop, newRight - newLeft, newBottom - newTop ); found = true; } if(found) break; } /* when we have found a new line create a new TextList containing only one element and append it to the lines */ if(!found) { WordsWithCharacters tmp; tmp.append((*it)); lines.append(QPair(tmp, elementArea)); } } // Step 3 for(QPair &line : lines) { WordsWithCharacters &list = line.first; std::sort(list.begin(), list.end(), compareTinyTextEntityX); } return lines; } /** * Calculate Statistical information from the lines we made previously */ static void calculateStatisticalInformation(const QList &words, int pageWidth, int pageHeight, int *word_spacing, int *line_spacing, int *col_spacing) { /** * For the region, defined by line_rects and lines * 1. Make line statistical analysis to find the line spacing * 2. Make character statistical analysis to differentiate between * word spacing and column spacing. */ /** * Step 0 */ const QList< QPair > sortedLines = makeAndSortLines(words, pageWidth, pageHeight); /** * Step 1 */ QMap line_space_stat; for(int i = 0 ; i < sortedLines.length(); i++) { const QRect rectUpper = sortedLines.at(i).second; if(i+1 == sortedLines.length()) break; const QRect rectLower = sortedLines.at(i+1).second; int linespace = rectLower.top() - (rectUpper.top() + rectUpper.height()); if(linespace < 0) linespace =-linespace; if(line_space_stat.contains(linespace)) line_space_stat[linespace]++; else line_space_stat[linespace] = 1; } *line_spacing = 0; int weighted_count = 0; QMapIterator iterate_linespace(line_space_stat); while(iterate_linespace.hasNext()) { iterate_linespace.next(); *line_spacing += iterate_linespace.value() * iterate_linespace.key(); weighted_count += iterate_linespace.value(); } if (*line_spacing != 0) *line_spacing = (int) ( (double)*line_spacing / (double) weighted_count + 0.5); /** * Step 2 */ // We would like to use QMap instead of QHash as it will keep the keys sorted QMap hor_space_stat; QMap col_space_stat; QList< QList > space_rects; QVector max_hor_space_rects; // Space in every line for(const QPair &sortedLine : sortedLines) { const WordsWithCharacters list = sortedLine.first; QList line_space_rects; int maxSpace = 0, minSpace = pageWidth; // for every TinyTextEntity element in the line WordsWithCharacters::ConstIterator it = list.begin(), itEnd = list.end(); QRect max_area1,max_area2; QString before_max, after_max; // for every line for( ; it != itEnd ; it++ ) { const QRect area1 = (*it).area().roundedGeometry(pageWidth,pageHeight); if( it+1 == itEnd ) break; const QRect area2 = (*(it+1)).area().roundedGeometry(pageWidth,pageHeight); int space = area2.left() - area1.right(); if(space > maxSpace) { max_area1 = area1; max_area2 = area2; maxSpace = space; before_max = (*it).text(); after_max = (*(it+1)).text(); } if(space < minSpace && space != 0) minSpace = space; //if we found a real space, whose length is not zero and also less than the pageWidth if(space != 0 && space != pageWidth) { // increase the count of the space amount if(hor_space_stat.contains(space)) hor_space_stat[space]++; else hor_space_stat[space] = 1; int left,right,top,bottom; left = area1.right(); right = area2.left(); top = area2.top() < area1.top() ? area2.top() : area1.top(); bottom = area2.bottom() > area1.bottom() ? area2.bottom() : area1.bottom(); QRect rect(left,top,right-left,bottom-top); line_space_rects.append(rect); } } space_rects.append(line_space_rects); if(hor_space_stat.contains(maxSpace)) { if(hor_space_stat[maxSpace] != 1) hor_space_stat[maxSpace]--; else hor_space_stat.remove(maxSpace); } if(maxSpace != 0) { if (col_space_stat.contains(maxSpace)) col_space_stat[maxSpace]++; else col_space_stat[maxSpace] = 1; //store the max rect of each line const int left = max_area1.right(); const int right = max_area2.left(); const int top = (max_area1.top() > max_area2.top()) ? max_area2.top() : max_area1.top(); const int bottom = (max_area1.bottom() < max_area2.bottom()) ? max_area2.bottom() : max_area1.bottom(); const QRect rect(left,top,right-left,bottom-top); max_hor_space_rects.append(rect); } else max_hor_space_rects.append(QRect(0,0,0,0)); } // All the between word space counts are in hor_space_stat *word_spacing = 0; weighted_count = 0; QMapIterator iterate(hor_space_stat); while (iterate.hasNext()) { iterate.next(); if(iterate.key() > 0) { *word_spacing += iterate.value() * iterate.key(); weighted_count += iterate.value(); } } if(weighted_count) *word_spacing = (int) ((double)*word_spacing / (double)weighted_count + 0.5); *col_spacing = 0; QMapIterator iterate_col(col_space_stat); while (iterate_col.hasNext()) { iterate_col.next(); if(iterate_col.value() > *col_spacing) *col_spacing = iterate_col.value(); } *col_spacing = col_space_stat.key(*col_spacing); // if there is just one line in a region, there is no point in dividing it if(sortedLines.length() == 1) *word_spacing = *col_spacing; } /** * Implements the XY Cut algorithm for textpage segmentation * The resulting RegionTextList will contain RegionText whose WordsWithCharacters::word and * WordsWithCharacters::characters are reused from wordsWithCharacters (i.e. no new nor delete happens in this function) */ static RegionTextList XYCutForBoundingBoxes(const QList &wordsWithCharacters, const NormalizedRect &boundingBox, int pageWidth, int pageHeight) { RegionTextList tree; QRect contentRect(boundingBox.geometry(pageWidth,pageHeight)); const RegionText root(wordsWithCharacters, contentRect); // start the tree with the root, it is our only region at the start tree.push_back(root); int i = 0; // while traversing the tree has not been ended while(i < tree.length()) { const RegionText node = tree.at(i); QRect regionRect = node.area(); /** * 1. calculation of projection profiles */ // allocate the size of proj profiles and initialize with 0 int size_proj_y = node.area().height(); int size_proj_x = node.area().width(); //dynamic memory allocation QVarLengthArray proj_on_xaxis(size_proj_x); QVarLengthArray proj_on_yaxis(size_proj_y); for( int j = 0 ; j < size_proj_y ; ++j ) proj_on_yaxis[j] = 0; for( int j = 0 ; j < size_proj_x ; ++j ) proj_on_xaxis[j] = 0; const QList list = node.text(); // Calculate tcx and tcy locally for each new region int word_spacing, line_spacing, column_spacing; calculateStatisticalInformation(list, pageWidth, pageHeight, &word_spacing, &line_spacing, &column_spacing); const int tcx = word_spacing * 2; const int tcy = line_spacing * 2; int maxX = 0 , maxY = 0; int avgX = 0; int count; // for every text in the region for( const WordWithCharacters &wwc : list ) { TinyTextEntity *ent = wwc.word; const QRect entRect = ent->area.geometry(pageWidth, pageHeight); // calculate vertical projection profile proj_on_xaxis1 for(int k = entRect.left() ; k <= entRect.left() + entRect.width() ; ++k) { if( ( k-regionRect.left() ) < size_proj_x && ( k-regionRect.left() ) >= 0 ) proj_on_xaxis[k - regionRect.left()] += entRect.height(); } // calculate horizontal projection profile in the same way for(int k = entRect.top() ; k <= entRect.top() + entRect.height() ; ++k) { if( ( k-regionRect.top() ) < size_proj_y && ( k-regionRect.top() ) >= 0 ) proj_on_yaxis[k - regionRect.top()] += entRect.width(); } } for( int j = 0 ; j < size_proj_y ; ++j ) { if (proj_on_yaxis[j] > maxY) maxY = proj_on_yaxis[j]; } avgX = count = 0; for( int j = 0 ; j < size_proj_x ; ++j ) { if(proj_on_xaxis[j] > maxX) maxX = proj_on_xaxis[j]; if(proj_on_xaxis[j]) { count++; avgX+= proj_on_xaxis[j]; } } if(count) avgX /= count; /** * 2. Cleanup Boundary White Spaces and removal of noise */ int xbegin = 0, xend = size_proj_x - 1; int ybegin = 0, yend = size_proj_y - 1; while(xbegin < size_proj_x && proj_on_xaxis[xbegin] <= 0) xbegin++; while(xend >= 0 && proj_on_xaxis[xend] <= 0) xend--; while(ybegin < size_proj_y && proj_on_yaxis[ybegin] <= 0) ybegin++; while(yend >= 0 && proj_on_yaxis[yend] <= 0) yend--; //update the regionRect int old_left = regionRect.left(), old_top = regionRect.top(); regionRect.setLeft(old_left + xbegin); regionRect.setRight(old_left + xend); regionRect.setTop(old_top + ybegin); regionRect.setBottom(old_top + yend); int tnx = (int)((double)avgX * 10.0 / 100.0 + 0.5), tny = 0; for( int j = 0 ; j < size_proj_x ; ++j ) proj_on_xaxis[j] -= tnx; for( int j = 0 ; j < size_proj_y ; ++j ) proj_on_yaxis[j] -= tny; /** * 3. Find the Widest gap */ int gap_hor = -1, pos_hor = -1; int begin = -1, end = -1; // find all hor_gaps and find the maximum between them for(int j = 1 ; j < size_proj_y ; ++j) { //transition from white to black if(begin >= 0 && proj_on_yaxis[j-1] <= 0 && proj_on_yaxis[j] > 0) end = j; //transition from black to white if(proj_on_yaxis[j-1] > 0 && proj_on_yaxis[j] <= 0) begin = j; if(begin > 0 && end > 0 && end-begin > gap_hor) { gap_hor = end - begin; pos_hor = (end + begin) / 2; begin = -1; end = -1; } } begin = -1, end = -1; int gap_ver = -1, pos_ver = -1; //find all the ver_gaps and find the maximum between them for(int j = 1 ; j < size_proj_x ; ++j) { //transition from white to black if(begin >= 0 && proj_on_xaxis[j-1] <= 0 && proj_on_xaxis[j] > 0){ end = j; } //transition from black to white if(proj_on_xaxis[j-1] > 0 && proj_on_xaxis[j] <= 0) begin = j; if(begin > 0 && end > 0 && end-begin > gap_ver) { gap_ver = end - begin; pos_ver = (end + begin) / 2; begin = -1; end = -1; } } int cut_pos_x = pos_ver, cut_pos_y = pos_hor; int gap_x = gap_ver, gap_y = gap_hor; /** * 4. Cut the region and make nodes (left,right) or (up,down) */ bool cut_hor = false, cut_ver = false; // For horizontal cut const int topHeight = cut_pos_y - (regionRect.top() - old_top); const QRect topRect(regionRect.left(), regionRect.top(), regionRect.width(), topHeight); const QRect bottomRect(regionRect.left(), regionRect.top() + topHeight, regionRect.width(), regionRect.height() - topHeight ); // For vertical Cut const int leftWidth = cut_pos_x - (regionRect.left() - old_left); const QRect leftRect(regionRect.left(), regionRect.top(), leftWidth, regionRect.height()); const QRect rightRect(regionRect.left() + leftWidth, regionRect.top(), regionRect.width() - leftWidth, regionRect.height()); if(gap_y >= gap_x && gap_y >= tcy) cut_hor = true; else if(gap_y >= gap_x && gap_y <= tcy && gap_x >= tcx) cut_ver = true; else if(gap_x >= gap_y && gap_x >= tcx) cut_ver = true; else if(gap_x >= gap_y && gap_x <= tcx && gap_y >= tcy) cut_hor = true; // no cut possible else { // we can now update the node rectangle with the shrinked rectangle RegionText tmpNode = tree.at(i); tmpNode.setArea(regionRect); tree.replace(i,tmpNode); i++; continue; } WordsWithCharacters list1,list2; // horizontal cut, topRect and bottomRect if(cut_hor) { for( const WordWithCharacters &word : list ) { const QRect wordRect = word.area().geometry(pageWidth,pageHeight); if(topRect.intersects(wordRect)) list1.append(word); else list2.append(word); } RegionText node1(list1,topRect); RegionText node2(list2,bottomRect); tree.replace(i,node1); tree.insert(i+1,node2); } //vertical cut, leftRect and rightRect else if(cut_ver) { for( const WordWithCharacters &word : list ) { const QRect wordRect = word.area().geometry(pageWidth,pageHeight); if(leftRect.intersects(wordRect)) list1.append(word); else list2.append(word); } RegionText node1(list1,leftRect); RegionText node2(list2,rightRect); tree.replace(i,node1); tree.insert(i+1,node2); } } return tree; } /** * Add spaces in between words in a line. It reuses the pointers passed in tree and might add new ones. You will need to take care of deleting them if needed */ WordsWithCharacters addNecessarySpace(RegionTextList tree, int pageWidth, int pageHeight) { /** * 1. Call makeAndSortLines before adding spaces in between words in a line * 2. Now add spaces between every two words in a line * 3. Finally, extract all the space separated texts from each region and return it */ // Only change the texts under RegionTexts, not the area for(RegionText &tmpRegion : tree) { // Step 01 QList< QPair > sortedLines = makeAndSortLines(tmpRegion.text(), pageWidth, pageHeight); // Step 02 for(QPair &sortedLine : sortedLines) { WordsWithCharacters &list = sortedLine.first; for(int k = 0 ; k < list.length() ; k++ ) { const QRect area1 = list.at(k).area().roundedGeometry(pageWidth,pageHeight); if( k+1 >= list.length() ) break; const QRect area2 = list.at(k+1).area().roundedGeometry(pageWidth,pageHeight); const int space = area2.left() - area1.right(); if(space != 0) { // Make a TinyTextEntity of string space and push it between it and it+1 const int left = area1.right(); const int right = area2.left(); const int top = area2.top() < area1.top() ? area2.top() : area1.top(); const int bottom = area2.bottom() > area1.bottom() ? area2.bottom() : area1.bottom(); const QString spaceStr(QStringLiteral(" ")); const QRect rect(QPoint(left,top),QPoint(right,bottom)); const NormalizedRect entRect(rect,pageWidth,pageHeight); TinyTextEntity *ent1 = new TinyTextEntity(spaceStr, entRect); TinyTextEntity *ent2 = new TinyTextEntity(spaceStr, entRect); WordWithCharacters word(ent1, QList() << ent2); list.insert(k+1, word); // Skip the space k++; } } } WordsWithCharacters tmpList; for(const QPair &sortedLine : qAsConst(sortedLines)) { tmpList += sortedLine.first; } tmpRegion.setText(tmpList); } // Step 03 WordsWithCharacters tmp; for(const RegionText &tmpRegion : qAsConst(tree)) { tmp += tmpRegion.text(); } return tmp; } /** * Correct the textOrder, all layout recognition works here */ void TextPagePrivate::correctTextOrder() { // m_page->width() and m_page->height() are in pixels at // 100% zoom level, and thus depend on display DPI. // To avoid Okular failing on lowDPI displays, // we scale pageWidth and pageHeight so their sum equals 2000. const double scalingFactor = 2000.0 / (m_page->width() + m_page->height()); const int pageWidth = (int) (scalingFactor * m_page->width() ); const int pageHeight = (int) (scalingFactor * m_page->height()); TextList characters = m_words; /** * Remove spaces from the text */ removeSpace(&characters); /** * Construct words from characters */ const QList wordsWithCharacters = makeWordFromCharacters(characters, pageWidth, pageHeight); /** * Make a XY Cut tree for segmentation of the texts */ const RegionTextList tree = XYCutForBoundingBoxes(wordsWithCharacters, m_page->boundingBox(), pageWidth, pageHeight); /** * Add spaces to the word */ const WordsWithCharacters listWithWordsAndSpaces = addNecessarySpace(tree, pageWidth, pageHeight); /** * Break the words into characters */ TextList listOfCharacters; for (const WordWithCharacters &word : listWithWordsAndSpaces) { delete word.word; listOfCharacters.append(word.characters); } setWordList(listOfCharacters); } TextEntity::List TextPage::words(const RegularAreaRect *area, TextAreaInclusionBehaviour b) const { if ( area && area->isNull() ) return TextEntity::List(); TextEntity::List ret; if ( area ) { for (const TinyTextEntity *te : qAsConst(d->m_words)) { if (b == AnyPixelTextAreaInclusionBehaviour) { if ( area->intersects( te->area ) ) { ret.append( new TextEntity( te->text(), new Okular::NormalizedRect( te->area) ) ); } } else { const NormalizedPoint center = te->area.center(); if ( area->contains( center.x, center.y ) ) { ret.append( new TextEntity( te->text(), new Okular::NormalizedRect( te->area) ) ); } } } } else { for (const TinyTextEntity *te : qAsConst(d->m_words)) { ret.append( new TextEntity( te->text(), new Okular::NormalizedRect( te->area) ) ); } } return ret; } RegularAreaRect * TextPage::wordAt( const NormalizedPoint &p, QString *word ) const { TextList::ConstIterator itBegin = d->m_words.constBegin(), itEnd = d->m_words.constEnd(); TextList::ConstIterator it = itBegin; TextList::ConstIterator posIt = itEnd; for ( ; it != itEnd; ++it ) { if ( (*it)->area.contains( p.x, p.y ) ) { posIt = it; break; } } QString text; if ( posIt != itEnd ) { if ( (*posIt)->text().simplified().isEmpty() ) { return nullptr; } // Find the first TinyTextEntity of the word while ( posIt != itBegin ) { --posIt; const QString itText = (*posIt)->text(); if ( itText.right(1).at(0).isSpace() ) { if (itText.endsWith(QLatin1String("-\n"))) { // Is an hyphenated word // continue searching the start of the word back continue; } if (itText == QLatin1String("\n") && posIt != itBegin ) { --posIt; if ((*posIt)->text().endsWith(QLatin1String("-"))) { // Is an hyphenated word // continue searching the start of the word back continue; } ++posIt; } ++posIt; break; } } RegularAreaRect *ret = new RegularAreaRect(); for ( ; posIt != itEnd; ++posIt ) { const QString itText = (*posIt)->text(); if ( itText.simplified().isEmpty() ) { break; } ret->appendShape( (*posIt)->area ); text += (*posIt)->text(); if (itText.right(1).at(0).isSpace()) { if (!text.endsWith(QLatin1String("-\n"))) { break; } } } if (word) { *word = text; } return ret; } else { return nullptr; } } diff --git a/core/utils.cpp b/core/utils.cpp index 8733982d0..0c2c073c7 100644 --- a/core/utils.cpp +++ b/core/utils.cpp @@ -1,172 +1,172 @@ /*************************************************************************** * Copyright (C) 2006 by Luigi Toscano * * Copyright (C) 2008 by Pino Toscano * * * * 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. * ***************************************************************************/ #include "utils.h" #include "utils_p.h" #include "debug_p.h" #include "settings_core.h" #include #include #include #include #include #include #include using namespace Okular; -QRect Utils::rotateRect( const QRect & source, int width, int height, int orientation ) +QRect Utils::rotateRect( const QRect & source, int width, int height, int orientation ) // clazy:exclude=function-args-by-value TODO remove the & when we do a BIC change elsewhere { QRect ret; // adapt the coordinates of the boxes to the rotation switch ( orientation ) { case 1: ret = QRect( width - source.y() - source.height(), source.x(), source.height(), source.width() ); break; case 2: ret = QRect( width - source.x() - source.width(), height - source.y() - source.height(), source.width(), source.height() ); break; case 3: ret = QRect( source.y(), height - source.x() - source.width(), source.height(), source.width() ); break; case 0: // no modifications default: // other cases ret = source; } return ret; } QSizeF Utils::realDpi(QWidget* widgetOnScreen) { const QScreen* screen = widgetOnScreen && widgetOnScreen->window() && widgetOnScreen->window()->windowHandle() ? widgetOnScreen->window()->windowHandle()->screen() : qGuiApp->primaryScreen(); if (screen) { const QSizeF res(screen->physicalDotsPerInchX(), screen->physicalDotsPerInchY()); if (res.width() > 0 && res.height() > 0) { if (qAbs(res.width() - res.height()) / qMin(res.height(), res.width()) < 0.05) { return res; } else { qCDebug(OkularCoreDebug) << "QScreen calculation returned a non square dpi." << res << ". Falling back"; } } } return QSizeF(72, 72); } inline static bool isPaperColor( QRgb argb, QRgb paperColor ) { return ( argb & 0xFFFFFF ) == ( paperColor & 0xFFFFFF); // ignore alpha } NormalizedRect Utils::imageBoundingBox( const QImage * image ) { if ( !image ) return NormalizedRect(); const int width = image->width(); const int height = image->height(); const QRgb paperColor = SettingsCore::paperColor().rgb(); int left, top, bottom, right, x, y; #ifdef BBOX_DEBUG QTime time; time.start(); #endif // Scan pixels for top non-white for ( top = 0; top < height; ++top ) for ( x = 0; x < width; ++x ) if ( !isPaperColor( image->pixel( x, top ), paperColor ) ) goto got_top; return NormalizedRect( 0, 0, 0, 0 ); // the image is blank got_top: left = right = x; // Scan pixels for bottom non-white for ( bottom = height-1; bottom >= top; --bottom ) for ( x = width-1; x >= 0; --x ) if ( !isPaperColor( image->pixel( x, bottom ), paperColor ) ) goto got_bottom; Q_ASSERT( 0 ); // image changed?! got_bottom: if ( x < left ) left = x; if ( x > right ) right = x; // Scan for leftmost and rightmost (we already found some bounds on these): for ( y = top; y <= bottom && ( left > 0 || right < width-1 ); ++y ) { for ( x = 0; x < left; ++x ) if ( !isPaperColor( image->pixel( x, y ), paperColor ) ) left = x; for ( x = width-1; x > right+1; --x ) if ( !isPaperColor( image->pixel( x, y ), paperColor ) ) right = x; } NormalizedRect bbox( QRect( left, top, ( right - left + 1), ( bottom - top + 1 ) ), image->width(), image->height() ); #ifdef BBOX_DEBUG qCDebug(OkularCoreDebug) << "Computed bounding box" << bbox << "in" << time.elapsed() << "ms"; #endif return bbox; } void Okular::copyQIODevice( QIODevice *from, QIODevice *to ) { QByteArray buffer( 65536, '\0' ); qint64 read = 0; qint64 written = 0; while ( ( read = from->read( buffer.data(), buffer.size() ) ) > 0 ) { written = to->write( buffer.constData(), read ); if ( read != written ) break; } } QTransform Okular::buildRotationMatrix(Rotation rotation) { QTransform matrix; matrix.rotate( (int)rotation * 90 ); switch ( rotation ) { case Rotation90: matrix.translate( 0, -1 ); break; case Rotation180: matrix.translate( -1, -1 ); break; case Rotation270: matrix.translate( -1, 0 ); break; default: ; } return matrix; } /* kate: replace-tabs on; indent-width 4; */ diff --git a/core/utils.h b/core/utils.h index 9de3f99d3..595ca5d1f 100644 --- a/core/utils.h +++ b/core/utils.h @@ -1,60 +1,60 @@ /*************************************************************************** * Copyright (C) 2006 by Luigi Toscano * * * * 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. * ***************************************************************************/ #ifndef _OKULAR_UTILS_H_ #define _OKULAR_UTILS_H_ #include "okularcore_export.h" #include "area.h" class QRect; class QImage; class QWidget; namespace Okular { /** * @short General utility functions. * * This class contains some static functions of general utility. */ class OKULARCORE_EXPORT Utils { public: /** * Rotate the rect \p source in the area \p width x \p height with the * specified \p orientation . */ - static QRect rotateRect( const QRect & source, int width, int height, int orientation ); + static QRect rotateRect( const QRect & source, int width, int height, int orientation ); // TODO remove the & when we do a BIC change elsewhere /** * Return the real DPI of the display containing given widget * * On X11, it can indicate the real horizontal DPI value without any Xrdb * setting. Otherwise, returns the same as realDpiX/Y(), * * @since 0.19 (KDE 4.13) */ static QSizeF realDpi(QWidget* widgetOnScreen); /** * Compute the smallest rectangle that contains all non-white pixels in image), * in normalized [0,1] coordinates. * * @since 0.7 (KDE 4.1) */ static NormalizedRect imageBoundingBox( const QImage* image ); }; } #endif /* kate: replace-tabs on; indent-width 4; */ diff --git a/generators/chm/lib/helper_search_index.cpp b/generators/chm/lib/helper_search_index.cpp index 92d50f34b..ecdc51b46 100644 --- a/generators/chm/lib/helper_search_index.cpp +++ b/generators/chm/lib/helper_search_index.cpp @@ -1,492 +1,492 @@ /* * Kchmviewer - a CHM and EPUB file viewer with broad language support * Copyright (C) 2004-2014 George Yunaev, gyunaev@ulduzsoft.com * * 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 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include "ebook.h" #include "ebook_search.h" #include "helper_search_index.h" static const int DICT_VERSION = 4; namespace QtAs { // Those characters are splitters (i.e. split the word), but added themselves into dictionary too. // This makes the dictionary MUCH larger, but ensure that for the piece of "window->print" both // search for "print" and "->print" will find it. static const char SPLIT_CHARACTERS[] = "!()*&^%#@[]{}':;,.?/|/?<>\\-+=~`"; // Those characters are parts of word - for example, '_' is here, and search for _debug will find only _debug. static const char WORD_CHARACTERS[] = "$_"; struct Term { Term() : frequency(-1) {} Term( const QString &t, int f, const QVector &l ) : term( t ), frequency( f ), documents( l ) {} QString term; int frequency; QVectordocuments; bool operator<( const Term &i2 ) const { return frequency < i2.frequency; } }; QDataStream &operator>>( QDataStream &s, Document &l ) { s >> l.docNumber; s >> l.frequency; return s; } -QDataStream &operator<<( QDataStream &s, const Document &l ) +QDataStream &operator<<( QDataStream &s, const Document l ) { s << (short)l.docNumber; s << (short)l.frequency; return s; } Index::Index() : QObject( nullptr ) { lastWindowClosed = false; connect( qApp, &QGuiApplication::lastWindowClosed, this, &Index::setLastWinClosed ); } void Index::setLastWinClosed() { lastWindowClosed = true; } bool Index::makeIndex(const QList< QUrl >& docs, EBook *chmFile ) { if ( docs.isEmpty() ) return false; docList = docs; if ( chmFile->hasFeature( EBook::FEATURE_ENCODING ) ) entityDecoder.changeEncoding( QTextCodec::codecForName( chmFile->currentEncoding().toUtf8() ) ); QList< QUrl >::ConstIterator it = docList.constBegin(); int steps = docList.count() / 100; if ( !steps ) steps++; int prog = 0; for ( int i = 0; it != docList.constEnd(); ++it, ++i ) { if ( lastWindowClosed ) return false; QUrl filename = *it; QStringList terms; if ( parseDocumentToStringlist( chmFile, filename, terms ) ) { for ( QStringList::ConstIterator tit = terms.constBegin(); tit != terms.constEnd(); ++tit ) insertInDict( *tit, i ); } if ( i%steps == 0 ) { prog++; prog = qMin( prog, 99 ); emit indexingProgress( prog, tr("Processing document %1") .arg( (*it).path() ) ); } } emit indexingProgress( 100, tr("Processing completed") ); return true; } void Index::insertInDict( const QString &str, int docNum ) { Entry *e = nullptr; if ( dict.count() ) e = dict[ str ]; if ( e ) { if ( e->documents.last().docNumber != docNum ) e->documents.append( Document(docNum, 1 ) ); else e->documents.last().frequency++; } else { dict.insert( str, new Entry( docNum ) ); } } bool Index::parseDocumentToStringlist(EBook *chmFile, const QUrl& filename, QStringList& tokenlist ) { QString parsedbuf, parseentity, text; if ( !chmFile->getFileContentAsString( text, filename ) || text.isEmpty() ) { qWarning( "Search index generator: could not retrieve the document content for %s", qPrintable( filename.toString() ) ); return false; } m_charssplit = SPLIT_CHARACTERS; m_charsword = WORD_CHARACTERS; tokenlist.clear(); // State machine states enum state_t { STATE_OUTSIDE_TAGS, // outside HTML tags; parse text STATE_IN_HTML_TAG, // inside HTML tags; wait for end tag STATE_IN_QUOTES, // inside HTML tags and inside quotes; wait for end quote (in var QuoteChar) STATE_IN_HTML_ENTITY // inside HTML entity; parse the entity }; state_t state = STATE_OUTSIDE_TAGS; QChar QuoteChar; // used in STATE_IN_QUOTES for ( int j = 0; j < text.length(); j++ ) { QChar ch = text[j]; if ( (j % 20000) == 0 ) qApp->processEvents( QEventLoop::ExcludeUserInputEvents ); if ( state == STATE_IN_HTML_TAG ) { // We are inside HTML tag. // Ignore everything until we see '>' (end of HTML tag) or quote char (quote start) if ( ch == '"' || ch == '\'' ) { state = STATE_IN_QUOTES; QuoteChar = ch; } else if ( ch == '>' ) state = STATE_OUTSIDE_TAGS; continue; } else if ( state == STATE_IN_QUOTES ) { // We are inside quoted text inside HTML tag. // Ignore everything until we see the quote character again if ( ch == QuoteChar ) state = STATE_IN_HTML_TAG; continue; } else if ( state == STATE_IN_HTML_ENTITY ) { // We are inside encoded HTML entity (like  ). // Collect to parsedbuf everything until we see ; if ( ch.isLetterOrNumber() ) { // get next character of this entity parseentity.append( ch ); continue; } // The entity ended state = STATE_OUTSIDE_TAGS; // Some shitty HTML does not terminate entities correctly. Screw it. if ( ch != ';' && ch != '<' ) { if ( parseentity.isEmpty() ) { // straight '&' symbol. Add and continue. parsedbuf += "&"; } else qWarning( "Index::parseDocument: incorrectly terminated HTML entity '&%s%c', ignoring", qPrintable( parseentity ), ch.toLatin1() ); j--; // parse this character again, but in different state continue; } // Don't we have a space? if ( parseentity.toLower() != "nbsp" ) { QString entity = entityDecoder.decode( parseentity ); if ( entity.isNull() ) { // decodeEntity() already printed error message //qWarning( "Index::parseDocument: failed to decode entity &%s;", parsedbuf.ascii() ); continue; } parsedbuf += entity; continue; } else ch = ' '; // We got a space, so treat it like it, and not add it to parsebuf } // // Now process STATE_OUTSIDE_TAGS // // Check for start of HTML tag, and switch to STATE_IN_HTML_TAG if it is if ( ch == '<' ) { state = STATE_IN_HTML_TAG; goto tokenize_buf; } // Check for start of HTML entity if ( ch == '&' ) { state = STATE_IN_HTML_ENTITY; parseentity = QString(); continue; } // Replace quote by ' - quotes are used in search window to set the phrase if ( ch == '"' ) ch = '\''; // Ok, we have a valid character outside HTML tags, and probably some in buffer already. // If it is char or letter, add it and continue if ( ch.isLetterOrNumber() || m_charsword.indexOf( ch ) != -1 ) { parsedbuf.append( ch ); continue; } // If it is a split char, add the word to the dictionary, and then add the char itself. if ( m_charssplit.indexOf( ch ) != -1 ) { if ( !parsedbuf.isEmpty() ) tokenlist.push_back( parsedbuf.toLower() ); tokenlist.push_back( ch.toLower() ); parsedbuf = QString(); continue; } tokenize_buf: // Just add the word; it is most likely a space or terminated by tokenizer. if ( !parsedbuf.isEmpty() ) { tokenlist.push_back( parsedbuf.toLower() ); parsedbuf = QString(); } } // Add the last word if still here - for broken htmls. if ( !parsedbuf.isEmpty() ) tokenlist.push_back( parsedbuf.toLower() ); return true; } void Index::writeDict( QDataStream& stream ) { stream << DICT_VERSION; stream << m_charssplit; stream << m_charsword; // Document list stream << docList; // Dictionary for( QHash::ConstIterator it = dict.constBegin(); it != dict.constEnd(); ++it ) { stream << it.key(); stream << (int) it.value()->documents.count(); stream << it.value()->documents; } } bool Index::readDict( QDataStream& stream ) { dict.clear(); docList.clear(); QString key; int version, numOfDocs; stream >> version; if ( version < 2 ) return false; stream >> m_charssplit; stream >> m_charsword; // Read the document list stream >> docList; while ( !stream.atEnd() ) { stream >> key; stream >> numOfDocs; QVector docs( numOfDocs ); stream >> docs; dict.insert( key, new Entry( docs ) ); } return dict.size() > 0; } QList< QUrl > Index::query(const QStringList &terms, const QStringList &termSeq, const QStringList &seqWords, EBook *chmFile ) { QList termList; QStringList::ConstIterator it = terms.begin(); for ( it = terms.begin(); it != terms.end(); ++it ) { Entry *e = nullptr; if ( dict[ *it ] ) { e = dict[ *it ]; termList.append( Term( *it, e->documents.count(), e->documents ) ); } else { return QList< QUrl >(); } } if ( !termList.count() ) return QList< QUrl >(); std::sort(termList.begin(), termList.end()); QVector minDocs = termList.takeFirst().documents; for(const Term &t : qAsConst(termList)) { const QVector docs = t.documents; for(QVector::Iterator minDoc_it = minDocs.begin(); minDoc_it != minDocs.end(); ) { bool found = false; for (QVector::ConstIterator doc_it = docs.constBegin(); doc_it != docs.constEnd(); ++doc_it ) { if ( (*minDoc_it).docNumber == (*doc_it).docNumber ) { (*minDoc_it).frequency += (*doc_it).frequency; found = true; break; } } if ( !found ) minDoc_it = minDocs.erase( minDoc_it ); else ++minDoc_it; } } QList< QUrl > results; std::sort(minDocs.begin(), minDocs.end()); if ( termSeq.isEmpty() ) { for(const Document &doc : qAsConst(minDocs)) results << docList.at((int)doc.docNumber); return results; } QUrl fileName; for(const Document &doc : qAsConst(minDocs)) { fileName = docList[ (int)doc.docNumber ]; if ( searchForPhrases( termSeq, seqWords, fileName, chmFile ) ) results << fileName; } return results; } bool Index::searchForPhrases( const QStringList &phrases, const QStringList &words, const QUrl &filename, EBook * chmFile ) { QStringList parsed_document; if ( !parseDocumentToStringlist( chmFile, filename, parsed_document ) ) return false; miniDict.clear(); // Initialize the dictionary with the words in phrase(s) for ( const QString &word : words ) miniDict.insert( word, new PosEntry( 0 ) ); // Fill the dictionary with the words from the document unsigned int word_offset = 3; for ( QStringList::ConstIterator it = parsed_document.constBegin(); it != parsed_document.constEnd(); it++, word_offset++ ) { PosEntry * entry = miniDict[ *it ]; if ( entry ) entry->positions.append( word_offset ); } // Dump it /* QDictIterator it( miniDict ); for( ; it.current(); ++it ) { QString text( it.currentKey() ); QValueList pos = miniDict[text]->positions; for ( unsigned int i = 1; i < pos.size(); i++ ) text += " " + QString::number( pos[i] ); qDebug( "%s", text.ascii()); } */ QList first_word_positions; for ( QStringList::ConstIterator phrase_it = phrases.constBegin(); phrase_it != phrases.constEnd(); phrase_it++ ) { QStringList phrasewords = phrase_it->split( ' ' ); first_word_positions = miniDict[ phrasewords[0] ]->positions; for ( int j = 1; j < phrasewords.count(); ++j ) { QList next_word_it = miniDict[ phrasewords[j] ]->positions; QList::iterator dict_it = first_word_positions.begin(); while ( dict_it != first_word_positions.end() ) { if ( next_word_it.indexOf( *dict_it + 1 ) != -1 ) { (*dict_it)++; ++dict_it; } else dict_it = first_word_positions.erase( dict_it ); } } } if ( first_word_positions.count() ) return true; return false; } }; diff --git a/generators/chm/lib/helper_search_index.h b/generators/chm/lib/helper_search_index.h index 932ef1f69..c61ebefbf 100644 --- a/generators/chm/lib/helper_search_index.h +++ b/generators/chm/lib/helper_search_index.h @@ -1,127 +1,129 @@ /* * Kchmviewer - a CHM and EPUB file viewer with broad language support * Copyright (C) 2004-2014 George Yunaev, gyunaev@ulduzsoft.com * * 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 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef EBOOK_SEARCH_INDEX_H #define EBOOK_SEARCH_INDEX_H #include #include #include #include #include #include "helper_entitydecoder.h" class EBook; // This code is based on some pretty old version of Qt Assistant namespace QtAs { struct Document { Document( int d, int f ) : docNumber( d ), frequency( f ) {} Document() : docNumber( -1 ), frequency( 0 ) {} - bool operator==( const Document &doc ) const + bool operator==( const Document doc ) const { return docNumber == doc.docNumber; } - bool operator<( const Document &doc ) const + bool operator<( const Document doc ) const { return frequency > doc.frequency; } - bool operator<=( const Document &doc ) const + bool operator<=( const Document doc ) const { return frequency >= doc.frequency; } - bool operator>( const Document &doc ) const + bool operator>( const Document doc ) const { return frequency < doc.frequency; } qint16 docNumber; qint16 frequency; }; QDataStream &operator>>( QDataStream &s, Document &l ); -QDataStream &operator<<( QDataStream &s, const Document &l ); +QDataStream &operator<<( QDataStream &s, const Document l ); class Index : public QObject { Q_OBJECT public: Index(); void writeDict( QDataStream& stream ); bool readDict( QDataStream& stream ); bool makeIndex(const QList &docs, EBook * chmFile ); QList query( const QStringList&, const QStringList&, const QStringList&, EBook * chmFile ); QString getCharsSplit() const { return m_charssplit; } QString getCharsPartOfWord() const { return m_charsword; } signals: void indexingProgress( int, const QString& ); public slots: void setLastWinClosed(); private: struct Entry { Entry( int d ) { documents.append( Document( d, 1 ) ); } Entry( const QVector &l ) : documents( l ) {} QVector documents; }; struct PosEntry { PosEntry( int p ) { positions.append( p ); } QList positions; }; bool parseDocumentToStringlist( EBook * chmFile, const QUrl& filename, QStringList& tokenlist ); void insertInDict( const QString&, int ); QStringList getWildcardTerms( const QString& ); QStringList split( const QString& ); QList setupDummyTerm( const QStringList& ); bool searchForPhrases(const QStringList &phrases, const QStringList &words, const QUrl &filename, EBook * chmFile ); QList< QUrl > docList; QHash dict; QHashminiDict; bool lastWindowClosed; HelperEntityDecoder entityDecoder; // Those characters are splitters (i.e. split the word), but added themselves into dictionary too. // This makes the dictionary MUCH larger, but ensure that for the piece of "window->print" both // search for "print" and "->print" will find it. QString m_charssplit; // Those characters are parts of word - for example, '_' is here, and search for _debug will find only _debug. QString m_charsword; }; }; +Q_DECLARE_TYPEINFO(QtAs::Document, Q_MOVABLE_TYPE); + #endif // EBOOK_SEARCH_INDEX_H diff --git a/generators/djvu/kdjvu.cpp b/generators/djvu/kdjvu.cpp index 77d079cc4..7f2d50c66 100644 --- a/generators/djvu/kdjvu.cpp +++ b/generators/djvu/kdjvu.cpp @@ -1,1162 +1,1162 @@ /*************************************************************************** * Copyright (C) 2006 by Pino Toscano * * * * 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. * ***************************************************************************/ #include "kdjvu.h" #include #include #include #include #include #include #include #include #include #include #include #include #include -QDebug &operator<<( QDebug & s, const ddjvu_rect_t &r ) +QDebug &operator<<( QDebug & s, const ddjvu_rect_t r ) { s.nospace() << "[" << r.x << "," << r.y << " - " << r.w << "x" << r.h << "]"; return s.space(); } static void which_ddjvu_message( const ddjvu_message_t *msg ) { #ifdef KDJVU_DEBUG qDebug() << "which_djvu_message(...):" << msg->m_any.tag; switch( msg->m_any.tag ) { case DDJVU_ERROR: qDebug().nospace() << "ERROR: file " << msg->m_error.filename << ", line " << msg->m_error.lineno; qDebug().nospace() << "ERROR: function '" << msg->m_error.function << "'"; qDebug().nospace() << "ERROR: '" << msg->m_error.message << "'"; break; case DDJVU_INFO: qDebug().nospace() << "INFO: '" << msg->m_info.message << "'"; break; case DDJVU_CHUNK: qDebug().nospace() << "CHUNK: '" << QByteArray( msg->m_chunk.chunkid ) << "'"; break; case DDJVU_PROGRESS: qDebug().nospace() << "PROGRESS: '" << msg->m_progress.percent << "'"; break; default: ; } #else Q_UNUSED( msg ); #endif } /** * Explore the message queue until there are message left in it. */ static void handle_ddjvu_messages( ddjvu_context_t *ctx, int wait ) { const ddjvu_message_t *msg; if ( wait ) ddjvu_message_wait( ctx ); while ( ( msg = ddjvu_message_peek( ctx ) ) ) { which_ddjvu_message( msg ); ddjvu_message_pop( ctx ); } } /** * Explore the message queue until the message \p mid is found. */ static void wait_for_ddjvu_message( ddjvu_context_t *ctx, ddjvu_message_tag_t mid ) { ddjvu_message_wait( ctx ); const ddjvu_message_t *msg; while ( ( msg = ddjvu_message_peek( ctx ) ) && msg && ( msg->m_any.tag != mid ) ) { which_ddjvu_message( msg ); ddjvu_message_pop( ctx ); } } /** * Convert a clockwise coefficient \p r for a rotation to a counter-clockwise * and vice versa. */ static int flipRotation( int r ) { return ( 4 - r ) % 4; } static miniexp_t find_second_in_pair( miniexp_t theexp, const char* which ) { miniexp_t exp = theexp; while ( exp ) { miniexp_t cur = miniexp_car( exp ); if ( !miniexp_consp( cur ) || !miniexp_symbolp( miniexp_car( cur ) ) ) { exp = miniexp_cdr( exp ); continue; } const QString id = QString::fromUtf8( miniexp_to_name( miniexp_car( cur ) ) ); if ( id == QLatin1String( which ) ) return miniexp_cadr( cur ); exp = miniexp_cdr( exp ); } return miniexp_nil; } static bool find_replace_or_add_second_in_pair( miniexp_t theexp, const char* which, miniexp_t replacement ) { miniexp_t exp = miniexp_cdddr( theexp ); while ( exp ) { miniexp_t cur = miniexp_car( exp ); if ( !miniexp_consp( cur ) || !miniexp_symbolp( miniexp_car( cur ) ) ) { exp = miniexp_cdr( exp ); continue; } const QString id = QString::fromUtf8( miniexp_to_name( miniexp_car( cur ) ) ); if ( id == QLatin1String( which ) ) { miniexp_t reversed = miniexp_reverse( cur ); miniexp_rplaca( reversed, replacement ); cur = miniexp_reverse( reversed ); return true; } exp = miniexp_cdr( exp ); } // TODO add the new replacement ad the end of the list return false; } // ImageCacheItem class ImageCacheItem { public: ImageCacheItem( int p, int w, int h, const QImage& i ) : page( p ), width( w ), height( h ), img( i ) { } int page; int width; int height; QImage img; }; // KdjVu::Page KDjVu::Page::Page() { } KDjVu::Page::~Page() { } int KDjVu::Page::width() const { return m_width; } int KDjVu::Page::height() const { return m_height; } int KDjVu::Page::dpi() const { return m_dpi; } int KDjVu::Page::orientation() const { return m_orientation; } // KDjVu::Link KDjVu::Link::~Link() { } KDjVu::Link::LinkArea KDjVu::Link::areaType() const { return m_area; } QPoint KDjVu::Link::point() const { return m_point; } QSize KDjVu::Link::size() const { return m_size; } QPolygon KDjVu::Link::polygon() const { return m_poly; } // KDjVu::PageLink KDjVu::PageLink::PageLink() { } int KDjVu::PageLink::type() const { return KDjVu::Link::PageLink; } QString KDjVu::PageLink::page() const { return m_page; } // KDjVu::UrlLink KDjVu::UrlLink::UrlLink() { } int KDjVu::UrlLink::type() const { return KDjVu::Link::UrlLink; } QString KDjVu::UrlLink::url() const { return m_url; } // KDjVu::Annotation KDjVu::Annotation::Annotation( miniexp_t anno ) : m_anno( anno ) { } KDjVu::Annotation::~Annotation() { } QPoint KDjVu::Annotation::point() const { miniexp_t area = miniexp_nth( 3, m_anno ); int a = miniexp_to_int( miniexp_nth( 1, area ) ); int b = miniexp_to_int( miniexp_nth( 2, area ) ); return QPoint( a, b ); } QString KDjVu::Annotation::comment() const { return QString::fromUtf8( miniexp_to_str( miniexp_nth( 2, m_anno ) ) ); } void KDjVu::Annotation::setComment( const QString &comment ) { miniexp_t exp = m_anno; exp = miniexp_cdr( exp ); exp = miniexp_cdr( exp ); miniexp_rplaca( exp, miniexp_string( comment.toUtf8().constData() ) ); } QColor KDjVu::Annotation::color() const { return QColor(); } void KDjVu::Annotation::setColor( const QColor & ) { } // KDjVu::TextAnnotation KDjVu::TextAnnotation::TextAnnotation( miniexp_t anno ) : Annotation( anno ), m_inlineText( true ) { const int num = miniexp_length( m_anno ); for ( int j = 4; j < num; ++j ) { miniexp_t curelem = miniexp_nth( j, m_anno ); if ( !miniexp_listp( curelem ) ) continue; QString id = QString::fromUtf8( miniexp_to_name( miniexp_nth( 0, curelem ) ) ); if ( id == QLatin1String( "pushpin" ) ) m_inlineText = false; } } QSize KDjVu::TextAnnotation::size() const { miniexp_t area = miniexp_nth( 3, m_anno ); int c = miniexp_to_int( miniexp_nth( 3, area ) ); int d = miniexp_to_int( miniexp_nth( 4, area ) ); return QSize( c, d ); } int KDjVu::TextAnnotation::type() const { return KDjVu::Annotation::TextAnnotation; } QColor KDjVu::TextAnnotation::color() const { miniexp_t col = find_second_in_pair( m_anno, "backclr" ); if ( !miniexp_symbolp( col ) ) return Qt::transparent; return QColor( QString::fromUtf8( miniexp_to_name( col ) ) ); } void KDjVu::TextAnnotation::setColor( const QColor &color ) { const QByteArray col = color.name().toLatin1(); find_replace_or_add_second_in_pair( m_anno, "backclr", miniexp_symbol( col.constData() ) ); } bool KDjVu::TextAnnotation::inlineText() const { return m_inlineText; } // KDjVu::LineAnnotation KDjVu::LineAnnotation::LineAnnotation( miniexp_t anno ) : Annotation( anno ), m_isArrow( false ), m_width( miniexp_nil ) { const int num = miniexp_length( m_anno ); for ( int j = 4; j < num; ++j ) { miniexp_t curelem = miniexp_nth( j, m_anno ); if ( !miniexp_listp( curelem ) ) continue; QString id = QString::fromUtf8( miniexp_to_name( miniexp_nth( 0, curelem ) ) ); if ( id == QLatin1String( "arrow" ) ) m_isArrow = true; else if ( id == QLatin1String( "width" ) ) m_width = curelem; } } int KDjVu::LineAnnotation::type() const { return KDjVu::Annotation::LineAnnotation; } QColor KDjVu::LineAnnotation::color() const { miniexp_t col = find_second_in_pair( m_anno, "lineclr" ); if ( !miniexp_symbolp( col ) ) return Qt::black; return QColor( QString::fromUtf8( miniexp_to_name( col ) ) ); } void KDjVu::LineAnnotation::setColor( const QColor &color ) { const QByteArray col = color.name().toLatin1(); find_replace_or_add_second_in_pair( m_anno, "lineclr", miniexp_symbol( col.constData() ) ); } QPoint KDjVu::LineAnnotation::point2() const { miniexp_t area = miniexp_nth( 3, m_anno ); int c = miniexp_to_int( miniexp_nth( 3, area ) ); int d = miniexp_to_int( miniexp_nth( 4, area ) ); return QPoint( c, d ); } bool KDjVu::LineAnnotation::isArrow() const { return m_isArrow; } int KDjVu::LineAnnotation::width() const { if ( m_width == miniexp_nil ) return 1; return miniexp_to_int( miniexp_cadr( m_width ) ); } void KDjVu::LineAnnotation::setWidth( int width ) { find_replace_or_add_second_in_pair( m_anno, "width", miniexp_number( width ) ); } // KDjVu::TextEntity KDjVu::TextEntity::TextEntity() { } KDjVu::TextEntity::~TextEntity() { } QString KDjVu::TextEntity::text() const { return m_text; } QRect KDjVu::TextEntity::rect() const { return m_rect; } class KDjVu::Private { public: Private() : m_djvu_cxt( nullptr ), m_djvu_document( nullptr ), m_format( nullptr ), m_docBookmarks( nullptr ), m_cacheEnabled( true ) { } QImage generateImageTile( ddjvu_page_t *djvupage, int& res, int width, int row, int xdelta, int height, int col, int ydelta ); void readBookmarks(); void fillBookmarksRecurse( QDomDocument& maindoc, QDomNode& curnode, miniexp_t exp, int offset = -1 ); void readMetaData( int page ); int pageWithName( const QString & name ); ddjvu_context_t *m_djvu_cxt; ddjvu_document_t *m_djvu_document; ddjvu_format_t *m_format; QVector m_pages; QVector m_pages_cache; QList mImgCache; QHash m_metaData; QDomDocument * m_docBookmarks; QHash m_pageNamesCache; bool m_cacheEnabled; static unsigned int s_formatmask[4]; }; unsigned int KDjVu::Private::s_formatmask[4] = { 0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000 }; QImage KDjVu::Private::generateImageTile( ddjvu_page_t *djvupage, int& res, int width, int row, int xdelta, int height, int col, int ydelta ) { ddjvu_rect_t renderrect; renderrect.x = row * xdelta; renderrect.y = col * ydelta; int realwidth = qMin( width - renderrect.x, xdelta ); int realheight = qMin( height - renderrect.y, ydelta ); renderrect.w = realwidth; renderrect.h = realheight; #ifdef KDJVU_DEBUG qDebug() << "renderrect:" << renderrect; #endif ddjvu_rect_t pagerect; pagerect.x = 0; pagerect.y = 0; pagerect.w = width; pagerect.h = height; #ifdef KDJVU_DEBUG qDebug() << "pagerect:" << pagerect; #endif handle_ddjvu_messages( m_djvu_cxt, false ); QImage res_img( realwidth, realheight, QImage::Format_RGB32 ); // the following line workarounds a rare crash in djvulibre; // it should be fixed with >= 3.5.21 ddjvu_page_get_width( djvupage ); res = ddjvu_page_render( djvupage, DDJVU_RENDER_COLOR, &pagerect, &renderrect, m_format, res_img.bytesPerLine(), (char *)res_img.bits() ); if (!res) { res_img.fill(Qt::white); } #ifdef KDJVU_DEBUG qDebug() << "rendering result:" << res; #endif handle_ddjvu_messages( m_djvu_cxt, false ); return res_img; } void KDjVu::Private::readBookmarks() { if ( !m_djvu_document ) return; miniexp_t outline; while ( ( outline = ddjvu_document_get_outline( m_djvu_document ) ) == miniexp_dummy ) handle_ddjvu_messages( m_djvu_cxt, true ); if ( miniexp_listp( outline ) && ( miniexp_length( outline ) > 0 ) && miniexp_symbolp( miniexp_nth( 0, outline ) ) && ( QString::fromUtf8( miniexp_to_name( miniexp_nth( 0, outline ) ) ) == QLatin1String( "bookmarks" ) ) ) { m_docBookmarks = new QDomDocument( QStringLiteral("KDjVuBookmarks") ); fillBookmarksRecurse( *m_docBookmarks, *m_docBookmarks, outline, 1 ); ddjvu_miniexp_release( m_djvu_document, outline ); } } void KDjVu::Private::fillBookmarksRecurse( QDomDocument& maindoc, QDomNode& curnode, miniexp_t exp, int offset ) { if ( !miniexp_listp( exp ) ) return; int l = miniexp_length( exp ); for ( int i = qMax( offset, 0 ); i < l; ++i ) { miniexp_t cur = miniexp_nth( i, exp ); if ( miniexp_consp( cur ) && ( miniexp_length( cur ) > 0 ) && miniexp_stringp( miniexp_nth( 0, cur ) ) && miniexp_stringp( miniexp_nth( 1, cur ) ) ) { QString title = QString::fromUtf8( miniexp_to_str( miniexp_nth( 0, cur ) ) ); QString dest = QString::fromUtf8( miniexp_to_str( miniexp_nth( 1, cur ) ) ); QDomElement el = maindoc.createElement( QStringLiteral("item") ); el.setAttribute( QStringLiteral("title"), title ); if ( !dest.isEmpty() ) { if ( dest.at( 0 ) == QLatin1Char( '#' ) ) { dest.remove( 0, 1 ); bool isNumber = false; dest.toInt( &isNumber ); if ( isNumber ) { // it might be an actual page number, but could also be a page label // so resolve the number, and get the real page number int pageNo = pageWithName( dest ); if ( pageNo != -1 ) { el.setAttribute( QStringLiteral("PageNumber"), QString::number( pageNo + 1 ) ); } else { el.setAttribute( QStringLiteral("PageNumber"), dest ); } } else { el.setAttribute( QStringLiteral("PageName"), dest ); } } else { el.setAttribute( QStringLiteral("URL"), dest ); } } curnode.appendChild( el ); if ( !el.isNull() && ( miniexp_length( cur ) > 2 ) ) { fillBookmarksRecurse( maindoc, el, cur, 2 ); } } } } void KDjVu::Private::readMetaData( int page ) { if ( !m_djvu_document ) return; miniexp_t annots; while ( ( annots = ddjvu_document_get_pageanno( m_djvu_document, page ) ) == miniexp_dummy ) handle_ddjvu_messages( m_djvu_cxt, true ); if ( !miniexp_listp( annots ) || miniexp_length( annots ) == 0 ) return; miniexp_t exp = miniexp_nth( 0, annots ); int size = miniexp_length( exp ); if ( size <= 1 || qstrncmp( miniexp_to_name( miniexp_nth( 0, exp ) ), "metadata", 8 ) ) return; for ( int i = 1; i < size; ++i ) { miniexp_t cur = miniexp_nth( i, exp ); if ( miniexp_length( cur ) != 2 ) continue; QString id = QString::fromUtf8( miniexp_to_name( miniexp_nth( 0, cur ) ) ); QString value = QString::fromUtf8( miniexp_to_str( miniexp_nth( 1, cur ) ) ); m_metaData[ id.toLower() ] = value; } } int KDjVu::Private::pageWithName( const QString & name ) { const int pageNo = m_pageNamesCache.value( name, -1 ); if ( pageNo != -1 ) return pageNo; const QByteArray utfName = name.toUtf8(); const int fileNum = ddjvu_document_get_filenum( m_djvu_document ); ddjvu_fileinfo_t info; for ( int i = 0; i < fileNum; ++i ) { if ( DDJVU_JOB_OK != ddjvu_document_get_fileinfo( m_djvu_document, i, &info ) ) continue; if ( info.type != 'P' ) continue; if ( ( utfName == info.id ) || ( utfName == info.name ) || ( utfName == info.title ) ) { m_pageNamesCache.insert( name, info.pageno ); return info.pageno; } } return -1; } KDjVu::KDjVu() : d( new Private ) { // creating the djvu context d->m_djvu_cxt = ddjvu_context_create( "KDjVu" ); // creating the rendering format #if DDJVUAPI_VERSION >= 18 d->m_format = ddjvu_format_create( DDJVU_FORMAT_RGBMASK32, 4, Private::s_formatmask ); #else d->m_format = ddjvu_format_create( DDJVU_FORMAT_RGBMASK32, 3, Private::s_formatmask ); #endif ddjvu_format_set_row_order( d->m_format, 1 ); ddjvu_format_set_y_direction( d->m_format, 1 ); } KDjVu::~KDjVu() { closeFile(); ddjvu_format_release( d->m_format ); ddjvu_context_release( d->m_djvu_cxt ); delete d; } bool KDjVu::openFile( const QString & fileName ) { // first, close the old file if ( d->m_djvu_document ) closeFile(); // load the document... d->m_djvu_document = ddjvu_document_create_by_filename( d->m_djvu_cxt, QFile::encodeName( fileName ).constData(), true ); if ( !d->m_djvu_document ) return false; // ...and wait for its loading wait_for_ddjvu_message( d->m_djvu_cxt, DDJVU_DOCINFO ); if ( ddjvu_document_decoding_error( d->m_djvu_document ) ) { ddjvu_document_release( d->m_djvu_document ); d->m_djvu_document = nullptr; return false; } qDebug() << "# of pages:" << ddjvu_document_get_pagenum( d->m_djvu_document ); int numofpages = ddjvu_document_get_pagenum( d->m_djvu_document ); d->m_pages.clear(); d->m_pages.resize( numofpages ); d->m_pages_cache.clear(); d->m_pages_cache.resize( numofpages ); // get the document type QString doctype; switch ( ddjvu_document_get_type( d->m_djvu_document ) ) { case DDJVU_DOCTYPE_UNKNOWN: doctype = i18nc( "Type of DjVu document", "Unknown" ); break; case DDJVU_DOCTYPE_SINGLEPAGE: doctype = i18nc( "Type of DjVu document", "Single Page" ); break; case DDJVU_DOCTYPE_BUNDLED: doctype = i18nc( "Type of DjVu document", "Bundled" ); break; case DDJVU_DOCTYPE_INDIRECT: doctype = i18nc( "Type of DjVu document", "Indirect" ); break; case DDJVU_DOCTYPE_OLD_BUNDLED: doctype = i18nc( "Type of DjVu document", "Bundled (old)" ); break; case DDJVU_DOCTYPE_OLD_INDEXED: doctype = i18nc( "Type of DjVu document", "Indexed (old)" ); break; } if ( !doctype.isEmpty() ) d->m_metaData[ QStringLiteral("documentType") ] = doctype; // get the number of components d->m_metaData[ QStringLiteral("componentFile") ] = ddjvu_document_get_filenum( d->m_djvu_document ); // read the pages for ( int i = 0; i < numofpages; ++i ) { ddjvu_status_t sts; ddjvu_pageinfo_t info; while ( ( sts = ddjvu_document_get_pageinfo( d->m_djvu_document, i, &info ) ) < DDJVU_JOB_OK ) handle_ddjvu_messages( d->m_djvu_cxt, true ); if ( sts >= DDJVU_JOB_FAILED ) { qDebug().nospace() << "\t>>> page " << i << " failed: " << sts; return false; } KDjVu::Page *p = new KDjVu::Page(); p->m_width = info.width; p->m_height = info.height; p->m_dpi = info.dpi; #if DDJVUAPI_VERSION >= 18 p->m_orientation = flipRotation( info.rotation ); #else p->m_orientation = 0; #endif d->m_pages[i] = p; } // reading the metadata from the first page only should be enough if ( numofpages > 0 ) d->readMetaData( 0 ); return true; } void KDjVu::closeFile() { // deleting the old TOC delete d->m_docBookmarks; d->m_docBookmarks = nullptr; // deleting the pages qDeleteAll( d->m_pages ); d->m_pages.clear(); // releasing the djvu pages QVector::Iterator it = d->m_pages_cache.begin(), itEnd = d->m_pages_cache.end(); for ( ; it != itEnd; ++it ) ddjvu_page_release( *it ); d->m_pages_cache.clear(); // clearing the image cache qDeleteAll( d->mImgCache ); d->mImgCache.clear(); // clearing the old metadata d->m_metaData.clear(); // cleaning the page names mapping d->m_pageNamesCache.clear(); // releasing the old document if ( d->m_djvu_document ) ddjvu_document_release( d->m_djvu_document ); d->m_djvu_document = nullptr; } QVariant KDjVu::metaData( const QString & key ) const { QHash::ConstIterator it = d->m_metaData.constFind( key ); return it != d->m_metaData.constEnd() ? it.value() : QVariant(); } const QDomDocument * KDjVu::documentBookmarks() const { if ( !d->m_docBookmarks ) d->readBookmarks(); return d->m_docBookmarks; } void KDjVu::linksAndAnnotationsForPage( int pageNum, QList *links, QList *annotations ) const { if ( ( pageNum < 0 ) || ( pageNum >= d->m_pages.count() ) || ( !links && !annotations ) ) return; miniexp_t annots; while ( ( annots = ddjvu_document_get_pageanno( d->m_djvu_document, pageNum ) ) == miniexp_dummy ) handle_ddjvu_messages( d->m_djvu_cxt, true ); if ( !miniexp_listp( annots ) ) return; if ( links ) links->clear(); if ( annotations ) annotations->clear(); int l = miniexp_length( annots ); for ( int i = 0; i < l; ++i ) { miniexp_t cur = miniexp_nth( i, annots ); int num = miniexp_length( cur ); if ( ( num < 4 ) || !miniexp_symbolp( miniexp_nth( 0, cur ) ) || ( qstrncmp( miniexp_to_name( miniexp_nth( 0, cur ) ), "maparea", 7 ) != 0 ) ) continue; QString target; QString type; if ( miniexp_symbolp( miniexp_nth( 0, miniexp_nth( 3, cur ) ) ) ) type = QString::fromUtf8( miniexp_to_name( miniexp_nth( 0, miniexp_nth( 3, cur ) ) ) ); KDjVu::Link* link = nullptr; KDjVu::Annotation* ann = nullptr; miniexp_t urlexp = miniexp_nth( 1, cur ); if ( links && ( type == QLatin1String( "rect" ) || type == QLatin1String( "oval" ) || type == QLatin1String( "poly" ) ) ) { if ( miniexp_stringp( urlexp ) ) { target = QString::fromUtf8( miniexp_to_str( miniexp_nth( 1, cur ) ) ); } else if ( miniexp_listp( urlexp ) && ( miniexp_length( urlexp ) == 3 ) && miniexp_symbolp( miniexp_nth( 0, urlexp ) ) && ( qstrncmp( miniexp_to_name( miniexp_nth( 0, urlexp ) ), "url", 3 ) == 0 ) ) { target = QString::fromUtf8( miniexp_to_str( miniexp_nth( 1, urlexp ) ) ); } if ( target.isEmpty() || ( ( target.length() > 0 ) && target.at(0) == QLatin1Char( '#' ) ) ) { KDjVu::PageLink* plink = new KDjVu::PageLink(); plink->m_page = target; link = plink; } else { KDjVu::UrlLink* ulink = new KDjVu::UrlLink(); ulink->m_url = target; link = ulink; } } else if ( annotations && ( type == QLatin1String( "text" ) || type == QLatin1String( "line" ) ) ) { if ( type == QLatin1String( "text" ) ) { KDjVu::TextAnnotation * textann = new KDjVu::TextAnnotation( cur ); ann = textann; } else if ( type == QLatin1String( "line" ) ) { KDjVu::LineAnnotation * lineann = new KDjVu::LineAnnotation( cur ); ann = lineann; } } if ( link /* safety check */ && links ) { link->m_area = KDjVu::Link::UnknownArea; miniexp_t area = miniexp_nth( 3, cur ); int arealength = miniexp_length( area ); if ( ( arealength == 5 ) && ( type == QLatin1String( "rect" ) || type == QLatin1String( "oval" ) ) ) { link->m_point = QPoint( miniexp_to_int( miniexp_nth( 1, area ) ), miniexp_to_int( miniexp_nth( 2, area ) ) ); link->m_size = QSize( miniexp_to_int( miniexp_nth( 3, area ) ), miniexp_to_int( miniexp_nth( 4, area ) ) ); if ( type == QLatin1String( "rect" ) ) { link->m_area = KDjVu::Link::RectArea; } else { link->m_area = KDjVu::Link::EllipseArea; } } else if ( ( arealength > 0 ) && ( arealength % 2 == 1 ) && type == QLatin1String( "poly" ) ) { link->m_area = KDjVu::Link::PolygonArea; QPolygon poly; for ( int j = 1; j < arealength; j += 2 ) { poly << QPoint( miniexp_to_int( miniexp_nth( j, area ) ), miniexp_to_int( miniexp_nth( j + 1, area ) ) ); } link->m_poly = poly; } if ( link->m_area != KDjVu::Link::UnknownArea ) links->append( link ); } else if ( ann /* safety check */ && annotations ) { annotations->append( ann ); } } } const QVector &KDjVu::pages() const { return d->m_pages; } QImage KDjVu::image( int page, int width, int height, int rotation ) { if ( d->m_cacheEnabled ) { bool found = false; QList::Iterator it = d->mImgCache.begin(), itEnd = d->mImgCache.end(); for ( ; ( it != itEnd ) && !found; ++it ) { ImageCacheItem* cur = *it; if ( ( cur->page == page ) && ( rotation % 2 == 0 ? cur->width == width && cur->height == height : cur->width == height && cur->height == width ) ) found = true; } if ( found ) { // taking the element and pushing to the top of the list --it; ImageCacheItem* cur2 = *it; d->mImgCache.erase( it ); d->mImgCache.push_front( cur2 ); return cur2->img; } } if ( !d->m_pages_cache.at( page ) ) { ddjvu_page_t *newpage = ddjvu_page_create_by_pageno( d->m_djvu_document, page ); // wait for the new page to be loaded ddjvu_status_t sts; while ( ( sts = ddjvu_page_decoding_status( newpage ) ) < DDJVU_JOB_OK ) handle_ddjvu_messages( d->m_djvu_cxt, true ); d->m_pages_cache[page] = newpage; } ddjvu_page_t *djvupage = d->m_pages_cache[page]; /* if ( ddjvu_page_get_rotation( djvupage ) != flipRotation( rotation ) ) { // TODO: test documents with initial rotation != 0 // ddjvu_page_set_rotation( djvupage, m_pages.at( page )->orientation() ); ddjvu_page_set_rotation( djvupage, (ddjvu_page_rotation_t)flipRotation( rotation ) ); } */ static const int xdelta = 1500; static const int ydelta = 1500; int xparts = width / xdelta + 1; int yparts = height / ydelta + 1; QImage newimg; int res = 10000; if ( ( xparts == 1 ) && ( yparts == 1 ) ) { // only one part -- render at once with no need to auxiliary image newimg = d->generateImageTile( djvupage, res, width, 0, xdelta, height, 0, ydelta ); } else { // more than one part -- need to render piece-by-piece and to compose // the results newimg = QImage( width, height, QImage::Format_RGB32 ); QPainter p; p.begin( &newimg ); int parts = xparts * yparts; for ( int i = 0; i < parts; ++i ) { int row = i % xparts; int col = i / xparts; int tmpres = 0; QImage tempp = d->generateImageTile( djvupage, tmpres, width, row, xdelta, height, col, ydelta ); if ( tmpres ) { p.drawImage( row * xdelta, col * ydelta, tempp ); } res = qMin( tmpres, res ); } p.end(); } if ( res && d->m_cacheEnabled ) { // delete all the cached pixmaps for the current page with a size that // differs no more than 35% of the new pixmap size int imgsize = newimg.width() * newimg.height(); if ( imgsize > 0 ) { for( int i = 0; i < d->mImgCache.count(); ) { ImageCacheItem* cur = d->mImgCache.at(i); if ( ( cur->page == page ) && ( abs( cur->img.width() * cur->img.height() - imgsize ) < imgsize * 0.35 ) ) { d->mImgCache.removeAt( i ); delete cur; } else ++i; } } // the image cache has too many elements, remove the last if ( d->mImgCache.size() >= 10 ) { delete d->mImgCache.last(); d->mImgCache.removeLast(); } ImageCacheItem* ich = new ImageCacheItem( page, width, height, newimg ); d->mImgCache.push_front( ich ); } return newimg; } bool KDjVu::exportAsPostScript( const QString & fileName, const QList& pageList ) const { if ( !d->m_djvu_document || fileName.trimmed().isEmpty() || pageList.isEmpty() ) return false; QFile f( fileName ); f.open( QIODevice::ReadWrite ); bool ret = exportAsPostScript( &f, pageList ); if ( ret ) { f.close(); } return ret; } bool KDjVu::exportAsPostScript( QFile* file, const QList& pageList ) const { if ( !d->m_djvu_document || !file || pageList.isEmpty() ) return false; FILE* f = fdopen( file->handle(), "w+" ); if ( !f ) { qDebug() << "error while getting the FILE*"; return false; } QString pl; for ( const int p : pageList ) { if ( !pl.isEmpty() ) pl += QLatin1String( "," ); pl += QString::number( p ); } pl.prepend( QStringLiteral ( "-page=" ) ); // setting the options static const int optc = 1; const char ** optv = (const char**)malloc( 1 * sizeof( char* ) ); QByteArray plb = pl.toLatin1(); optv[0] = plb.constData(); ddjvu_job_t *printjob = ddjvu_document_print( d->m_djvu_document, f, optc, optv ); while ( !ddjvu_job_done( printjob ) ) handle_ddjvu_messages( d->m_djvu_cxt, true ); free( optv ); return fclose( f ) == 0; } QList KDjVu::textEntities( int page, const QString & granularity ) const { if ( ( page < 0 ) || ( page >= d->m_pages.count() ) ) return QList(); miniexp_t r; while ( ( r = ddjvu_document_get_pagetext( d->m_djvu_document, page, nullptr ) ) == miniexp_dummy ) handle_ddjvu_messages( d->m_djvu_cxt, true ); if ( r == miniexp_nil ) return QList(); QList ret; int height = d->m_pages.at( page )->height(); QQueue queue; queue.enqueue( r ); while ( !queue.isEmpty() ) { miniexp_t cur = queue.dequeue(); if ( miniexp_listp( cur ) && ( miniexp_length( cur ) > 0 ) && miniexp_symbolp( miniexp_nth( 0, cur ) ) ) { int size = miniexp_length( cur ); QString sym = QString::fromUtf8( miniexp_to_name( miniexp_nth( 0, cur ) ) ); if ( sym == granularity ) { if ( size >= 6 ) { int xmin = miniexp_to_int( miniexp_nth( 1, cur ) ); int ymin = miniexp_to_int( miniexp_nth( 2, cur ) ); int xmax = miniexp_to_int( miniexp_nth( 3, cur ) ); int ymax = miniexp_to_int( miniexp_nth( 4, cur ) ); QRect rect( xmin, height - ymax, xmax - xmin, ymax - ymin ); KDjVu::TextEntity entity; entity.m_rect = rect; entity.m_text = QString::fromUtf8( miniexp_to_str( miniexp_nth( 5, cur ) ) ); ret.append( entity ); } } else { for ( int i = 5; i < size; ++i ) queue.enqueue( miniexp_nth( i, cur ) ); } } } return ret; } void KDjVu::setCacheEnabled( bool enable ) { if ( enable == d->m_cacheEnabled ) return; d->m_cacheEnabled = enable; if ( !d->m_cacheEnabled ) { qDeleteAll( d->mImgCache ); d->mImgCache.clear(); } } bool KDjVu::isCacheEnabled() const { return d->m_cacheEnabled; } int KDjVu::pageNumber( const QString & name ) const { if ( !d->m_djvu_document ) return -1; return d->pageWithName( name ); } diff --git a/generators/dvi/anchor.h b/generators/dvi/anchor.h index 2ffe8e7c0..5e5a4d536 100644 --- a/generators/dvi/anchor.h +++ b/generators/dvi/anchor.h @@ -1,61 +1,61 @@ // -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; c-brace-offset: 0; -*- // // anchor.h // // Part of KVIEWSHELL - A framework for multipage text/gfx viewers // // (C) 2004-2005 Stefan Kebekus // Distributed under the GPL #ifndef ANCHOR_H #define ANCHOR_H #include "length.h" #include "pageNumber.h" /** \brief Page number and vertical position in physical coordinates This very simple class contains a page number and a vertical position in physical coordinates. The vertical position is given by the distance from the top of the page. Anchors are completely independent of documents, there is no need for a document to exists that contains the given page, nor does the page number need to be valid. @author Stefan Kebekus @version 1.0 0 */ class Anchor { public: /** \brief Constructs an anchor that points to an invalid page */ Anchor() {page = 0;} /** \brief Constructs an anchor that points to a given position on a given page The class contains no code to make sure in any way that the page number pg exists, and that page pg, if it exists, is taller than distance_from_top @param pg number of the page @param _distance_from_top distance from the top of the page */ - Anchor(const PageNumber& pg, const Length& _distance_from_top): page(pg), distance_from_top(_distance_from_top) {} + Anchor(const PageNumber pg, const Length _distance_from_top): page(pg), distance_from_top(_distance_from_top) {} /** \brief quick validity check for anchors @returns true if the page number is valid, and 0mm <= distance_from_top <= 2m */ bool isValid() const {return page.isValid() && (0.0 <= distance_from_top.getLength_in_mm()) && (distance_from_top.getLength_in_mm() <= 2000.0);} /** \brief Page number that this anchor point to */ PageNumber page; /** \brief Distance from the top of the page in inch */ Length distance_from_top; }; #endif diff --git a/generators/dvi/dviRenderer.cpp b/generators/dvi/dviRenderer.cpp index 65a459e30..d50c191bc 100644 --- a/generators/dvi/dviRenderer.cpp +++ b/generators/dvi/dviRenderer.cpp @@ -1,771 +1,771 @@ // -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; c-brace-offset: 0; -*- // // Class: dviRenderer // // Class for rendering TeX DVI files. // Part of KDVI- A previewer for TeX DVI files. // // (C) 2001-2005 Stefan Kebekus // Distributed under the GPL // #include #include "debug_dvi.h" #include "dviRenderer.h" #include "dviFile.h" #include "dvisourcesplitter.h" #include "hyperlink.h" #include "debug_dvi.h" #include "prebookmark.h" #include "psgs.h" //#include "renderedDviPagePixmap.h" #include "dviPageInfo.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include //------ now comes the dviRenderer class implementation ---------- dviRenderer::dviRenderer(bool useFontHinting) : dviFile(nullptr), font_pool(useFontHinting), resolutionInDPI(0), embedPS_progress(nullptr), embedPS_numOfProgressedFiles(0), shrinkfactor(3), source_href(nullptr), HTML_href(nullptr), editorCommand(QLatin1String("")), PostScriptOutPutString(nullptr), PS_interface(new ghostscript_interface), _postscript(true), line_boundary_encountered(false), word_boundary_encountered(false), current_page(0), penWidth_in_mInch(0), number_of_elements_in_path(0), currentlyDrawnPage(nullptr), m_eventLoop(nullptr), foreGroundPainter(nullptr), fontpoolLocateFontsDone(false) { #ifdef DEBUG_DVIRENDERER //qCDebug(OkularDviDebug) << "dviRenderer( parent=" << par << " )"; #endif connect(&font_pool, &fontPool::error, this, &dviRenderer::error); connect(&font_pool, &fontPool::warning, this, &dviRenderer::warning); connect(PS_interface, &ghostscript_interface::error, this, &dviRenderer::error); } dviRenderer::~dviRenderer() { #ifdef DEBUG_DVIRENDERER qCDebug(OkularDviDebug) << "~dviRenderer"; #endif QMutexLocker locker(&mutex); delete PS_interface; delete dviFile; } #if 0 void dviRenderer::setPrefs(bool flag_showPS, const QString &str_editorCommand, bool useFontHints ) { //QMutexLocker locker(&mutex); _postscript = flag_showPS; editorCommand = str_editorCommand; font_pool.setParameters( useFontHints ); } #endif //------ this function calls the dvi interpreter ---------- void dviRenderer::drawPage(RenderedDocumentPagePixmap* page) { #ifdef DEBUG_DVIRENDERER //qCDebug(OkularDviDebug) << "dviRenderer::drawPage(documentPage *) called, page number " << page->pageNumber; #endif // Paranoid safety checks if (page == nullptr) { qCCritical(OkularDviDebug) << "dviRenderer::drawPage(documentPage *) called with argument == 0"; return; } // Paranoid safety checks if (page->pageNumber == 0) { qCCritical(OkularDviDebug) << "dviRenderer::drawPage(documentPage *) called for a documentPage with page number 0"; return; } QMutexLocker locker(&mutex); if ( dviFile == nullptr ) { qCCritical(OkularDviDebug) << "dviRenderer::drawPage(documentPage *) called, but no dviFile class allocated."; page->clear(); return; } if (page->pageNumber > dviFile->total_pages) { qCCritical(OkularDviDebug) << "dviRenderer::drawPage(documentPage *) called for a documentPage with page number " << page->pageNumber << " but the current dviFile has only " << dviFile->total_pages << " pages."; return; } if ( dviFile->dvi_Data() == nullptr ) { qCCritical(OkularDviDebug) << "dviRenderer::drawPage(documentPage *) called, but no dviFile is loaded yet."; page->clear(); return; } /* locateFonts() is here just once (if it has not been executed not been executed yet), so that it is possible to easily intercept the cancel signal (because for example the user tries to open another document); it would not have been possible (or more complicated) in case it was still in the document loading section. */ if ( !fontpoolLocateFontsDone ) { font_pool.locateFonts(); fontpoolLocateFontsDone = true; } double resolution = page->resolution; if (resolution != resolutionInDPI) setResolution(resolution); currentlyDrawnPage = page; shrinkfactor = 1200/resolutionInDPI; current_page = page->pageNumber-1; // Reset colors colorStack.clear(); globalColor = Qt::black; int pageWidth = page->width; int pageHeight = page->height; QImage img(pageWidth, pageHeight, QImage::Format_RGB32); foreGroundPainter = new QPainter(&img); if (foreGroundPainter != nullptr) { errorMsg.clear(); draw_page(); delete foreGroundPainter; foreGroundPainter = nullptr; } else { qCDebug(OkularDviDebug) << "painter creation failed."; } page->img = img; //page->setImage(img); // Postprocess hyperlinks // Without that, based on the way TeX draws certain characters like german "Umlaute", // some hyperlinks would be broken into two overlapping parts, in the middle of a word. QVector::iterator i = page->hyperLinkList.begin(); QVector::iterator j; while (i != page->hyperLinkList.end()) { // Iterator j always points to the element after i. j = i; j++; if (j == page->hyperLinkList.end()) break; Hyperlink& hi = *i; Hyperlink& hj = *j; bool merged = false; // Merge all hyperlinks that point to the same target, and have the same baseline. while (hi.linkText == hj.linkText && hi.baseline == hj.baseline) { merged = true; hi.box = hi.box.united(hj.box); j++; if (j == page->hyperLinkList.end()) break; hj = *j; } if (merged) { i = page->hyperLinkList.erase(++i, j); } else { i++; } } if (errorMsg.isEmpty() != true) { emit error(i18n("File corruption. %1", errorMsg), -1); errorMsg.clear(); currentlyDrawnPage = nullptr; return; } #if 0 // Tell the user (once) if the DVI file contains source specials // ... we don't want our great feature to go unnoticed. RenderedDviPagePixmap* currentDVIPage = dynamic_cast(currentlyDrawnPage); if (currentDVIPage) { if ((dviFile->sourceSpecialMarker == true) && (currentDVIPage->sourceHyperLinkList.size() > 0)) { dviFile->sourceSpecialMarker = false; // Show the dialog as soon as event processing is finished, and // the program is idle //FIXME //QTimer::singleShot( 0, this, SLOT(showThatSourceInformationIsPresent()) ); } } #endif currentlyDrawnPage = nullptr; } void dviRenderer::getText(RenderedDocumentPagePixmap* page) { bool postscriptBackup = _postscript; // Disable postscript-specials temporarily to speed up text extraction. _postscript = false; drawPage(page); _postscript = postscriptBackup; } /* void dviRenderer::showThatSourceInformationIsPresent() { // In principle, we should use a KMessagebox here, but we want to // add a button "Explain in more detail..." which opens the // Helpcenter. Thus, we practically re-implement the KMessagebox // here. Most of the code is stolen from there. // Check if the 'Don't show again' feature was used KConfig *config = KSharedConfig::openConfig(); KConfigGroup saver(config, "Notification Messages"); bool showMsg = config->readEntry( "KDVI-info_on_source_specials", true); if (showMsg) { KDialogBase dialog(i18n("KDVI: Information"), KDialogBase::Yes, KDialogBase::Yes, KDialogBase::Yes, parentWidget, "information", true, true, KStandardGuiItem::ok()); QWidget *topcontents = new QWidget (&dialog); QVBoxLayout *topcontentsVBoxLayout = new QVBoxLayout(topcontents); topcontentsVBoxLayout->setContentsMargins(0, 0, 0, 0); topcontentsVBoxLayout->setSpacing(KDialog::spacingHint()*2); topcontentsVBoxLayout->setContentsMargins(KDialog::marginHint()*2, KDialog::marginHint()*2, KDialog::marginHint()*2, KDialog::marginHint()*2); QWidget *contents = new QWidget(topcontents); topcontentsVBoxLayout->addWidget(contents); QHBoxLayout * lay = new QHBoxLayout(contents); lay->setSpacing(KDialog::spacingHint()*2); lay->addStretch(1); QLabel *label1 = new QLabel( contents); label1->setPixmap(QMessageBox::standardIcon(QMessageBox::Information)); lay->addWidget(label1); QLabel *label2 = new QLabel( i18n("This DVI file contains source file information. You may click into the text with the " "middle mouse button, and an editor will open the TeX-source file immediately."), contents); label2->setMinimumSize(label2->sizeHint()); lay->addWidget(label2); lay->addStretch(1); QSize extraSize = QSize(50,30); QCheckBox *checkbox = new QCheckBox(i18n("Do not show this message again"), topcontents); topcontentsVBoxLayout->addWidget(checkbox); extraSize = QSize(50,0); dialog.setHelpLinkText(i18n("Explain in more detail...")); dialog.setHelp("inverse-search", "kdvi"); dialog.enableLinkedHelp(true); dialog.setMainWidget(topcontents); dialog.enableButtonSeparator(false); dialog.incInitialSize( extraSize ); dialog.exec(); showMsg = !checkbox->isChecked(); if (!showMsg) { KConfigGroup saver(config, "Notification Messages"); config->writeEntry( "KDVI-info_on_source_specials", showMsg); } config->sync(); } } */ void dviRenderer::embedPostScript() { #ifdef DEBUG_DVIRENDERER qCDebug(OkularDviDebug) << "dviRenderer::embedPostScript()"; #endif if (!dviFile) return; /* embedPS_progress = new QProgressDialog(parentWidget); embedPS_progress->setWindowTitle(i18n("Embedding PostScript Files")); embedPS_progress->setLabelText(QString()); */ if (!embedPS_progress) return; embedPS_progress->setCancelButton(nullptr); embedPS_progress->setCancelButton(nullptr); embedPS_progress->setMinimumDuration(400); embedPS_progress->setMaximum(dviFile->numberOfExternalPSFiles); embedPS_progress->setValue(0); embedPS_numOfProgressedFiles = 0; quint16 currPageSav = current_page; errorMsg.clear(); for(current_page=0; current_page < dviFile->total_pages; current_page++) { if (current_page < dviFile->total_pages) { command_pointer = dviFile->dvi_Data() + dviFile->page_offset[int(current_page)]; end_pointer = dviFile->dvi_Data() + dviFile->page_offset[int(current_page+1)]; } else command_pointer = end_pointer = nullptr; memset((char *) &currinf.data, 0, sizeof(currinf.data)); currinf.fonttable = &(dviFile->tn_table); currinf._virtual = nullptr; prescan(&dviRenderer::prescan_embedPS); } delete embedPS_progress; embedPS_progress = nullptr; if (!errorMsg.isEmpty()) { emit warning(i18n("Not all PostScript files could be embedded into your document. %1", errorMsg), -1); errorMsg.clear(); } else { emit notice(i18n("All external PostScript files were embedded into your document."), -1); } // Prescan phase starts here #ifdef PERFORMANCE_MEASUREMENT //qCDebug(OkularDviDebug) << "Time elapsed till prescan phase starts " << performanceTimer.elapsed() << "ms"; //QTime preScanTimer; //preScanTimer.start(); #endif dviFile->numberOfExternalPSFiles = 0; prebookmarks.clear(); for(current_page=0; current_page < dviFile->total_pages; current_page++) { PostScriptOutPutString = new QString(); if (current_page < dviFile->total_pages) { command_pointer = dviFile->dvi_Data() + dviFile->page_offset[int(current_page)]; end_pointer = dviFile->dvi_Data() + dviFile->page_offset[int(current_page+1)]; } else command_pointer = end_pointer = nullptr; memset((char *) &currinf.data, 0, sizeof(currinf.data)); currinf.fonttable = &(dviFile->tn_table); currinf._virtual = nullptr; prescan(&dviRenderer::prescan_parseSpecials); if (!PostScriptOutPutString->isEmpty()) PS_interface->setPostScript(current_page, *PostScriptOutPutString); delete PostScriptOutPutString; } PostScriptOutPutString = nullptr; #ifdef PERFORMANCE_MEASUREMENT //qCDebug(OkularDviDebug) << "Time required for prescan phase: " << preScanTimer.restart() << "ms"; #endif current_page = currPageSav; _isModified = true; } bool dviRenderer::isValidFile(const QString& filename) const { QFile f(filename); if (!f.open(QIODevice::ReadOnly)) return false; unsigned char test[4]; if ( f.read( (char *)test,2)<2 || test[0] != 247 || test[1] != 2 ) return false; int n = f.size(); if ( n < 134 ) // Too short for a dvi file return false; f.seek( n-4 ); unsigned char trailer[4] = { 0xdf,0xdf,0xdf,0xdf }; if ( f.read( (char *)test, 4 )<4 || strncmp( (char *)test, (char *) trailer, 4 ) != 0 ) return false; // We suppose now that the dvi file is complete and OK return true; } bool dviRenderer::setFile(const QString &fname, const QUrl &base) { #ifdef DEBUG_DVIRENDERER qCDebug(OkularDviDebug) << "dviRenderer::setFile( fname='" << fname << "' )"; //, ref='" << ref << "', sourceMarker=" << sourceMarker << " )"; #endif //QMutexLocker lock(&mutex); QFileInfo fi(fname); QString filename = fi.absoluteFilePath(); // If fname is the empty string, then this means: "close". Delete // the dvifile and the pixmap. if (fname.isEmpty()) { // Delete DVI file delete dviFile; dviFile = nullptr; return true; } // Make sure the file actually exists. if (!fi.exists() || fi.isDir()) { emit error(i18n("The specified file '%1' does not exist.", filename), -1); return false; } QApplication::setOverrideCursor( Qt::WaitCursor ); dvifile *dviFile_new = new dvifile(filename, &font_pool); if ((dviFile == nullptr) || (dviFile->filename != filename)) dviFile_new->sourceSpecialMarker = true; else dviFile_new->sourceSpecialMarker = false; if ((dviFile_new->dvi_Data() == nullptr)||(dviFile_new->errorMsg.isEmpty() != true)) { QApplication::restoreOverrideCursor(); if (dviFile_new->errorMsg.isEmpty() != true) emit error(i18n("File corruption. %1", dviFile_new->errorMsg), -1); delete dviFile_new; return false; } delete dviFile; dviFile = dviFile_new; numPages = dviFile->total_pages; _isModified = false; baseURL = base; font_pool.setExtraSearchPath( fi.absolutePath() ); font_pool.setCMperDVIunit( dviFile->getCmPerDVIunit() ); // Extract PostScript from the DVI file, and store the PostScript // specials in PostScriptDirectory, and the headers in the // PostScriptHeaderString. PS_interface->clear(); // If the DVI file comes from a remote URL (e.g. downloaded from a // web server), we limit the PostScript files that can be accessed // by this file to the download directory, in order to limit the // possibilities of a denial of service attack. QString includePath; if (!baseURL.isLocalFile()) { includePath = filename; includePath.truncate(includePath.lastIndexOf(QLatin1Char('/'))); } PS_interface->setIncludePath(includePath); // We will also generate a list of hyperlink-anchors and source-file // anchors in the document. So declare the existing lists empty. //anchorList.clear(); sourceHyperLinkAnchors.clear(); //bookmarks.clear(); prebookmarks.clear(); if (dviFile->page_offset.isEmpty() == true) return false; // We should pre-scan the document now (to extract embedded, // PostScript, Hyperlinks, ets). // PRESCAN STARTS HERE #ifdef PERFORMANCE_MEASUREMENT //qCDebug(OkularDviDebug) << "Time elapsed till prescan phase starts " << performanceTimer.elapsed() << "ms"; //QTime preScanTimer; //preScanTimer.start(); #endif dviFile->numberOfExternalPSFiles = 0; quint16 currPageSav = current_page; prebookmarks.clear(); for(current_page=0; current_page < dviFile->total_pages; current_page++) { PostScriptOutPutString = new QString(); if (current_page < dviFile->total_pages) { command_pointer = dviFile->dvi_Data() + dviFile->page_offset[int(current_page)]; end_pointer = dviFile->dvi_Data() + dviFile->page_offset[int(current_page+1)]; } else command_pointer = end_pointer = nullptr; memset((char *) &currinf.data, 0, sizeof(currinf.data)); currinf.fonttable = &(dviFile->tn_table); currinf._virtual = nullptr; prescan(&dviRenderer::prescan_parseSpecials); if (!PostScriptOutPutString->isEmpty()) PS_interface->setPostScript(current_page, *PostScriptOutPutString); delete PostScriptOutPutString; } PostScriptOutPutString = nullptr; #if 0 // Generate the list of bookmarks bookmarks.clear(); Q3PtrStack stack; stack.setAutoDelete (false); QVector::iterator it; for( it = prebookmarks.begin(); it != prebookmarks.end(); ++it ) { Bookmark *bmk = new Bookmark((*it).title, findAnchor((*it).anchorName)); if (stack.isEmpty()) bookmarks.append(bmk); else { stack.top()->subordinateBookmarks.append(bmk); stack.remove(); } for(int i=0; i<(*it).noOfChildren; i++) stack.push(bmk); } prebookmarks.clear(); #endif #ifdef PERFORMANCE_MEASUREMENT //qCDebug(OkularDviDebug) << "Time required for prescan phase: " << preScanTimer.restart() << "ms"; #endif current_page = currPageSav; // PRESCAN ENDS HERE pageSizes.resize(0); if (dviFile->suggestedPageSize != nullptr) { // Fill the vector pageSizes with total_pages identical entries pageSizes.fill(*(dviFile->suggestedPageSize), dviFile->total_pages); } QApplication::restoreOverrideCursor(); return true; } Anchor dviRenderer::parseReference(const QString &reference) { QMutexLocker locker(&mutex); #ifdef DEBUG_DVIRENDERER qCCritical(OkularDviDebug) << "dviRenderer::parseReference( " << reference << " ) called" << endl; #endif if (dviFile == nullptr) return Anchor(); // case 1: The reference is a number, which we'll interpret as a // page number. bool ok; int page = reference.toInt ( &ok ); if (ok == true) { if (page < 0) page = 0; if (page > dviFile->total_pages) page = dviFile->total_pages; return Anchor(page, Length() ); } // case 2: The reference is of form "src:1111Filename", where "1111" // points to line number 1111 in the file "Filename". KDVI then // looks for source specials of the form "src:xxxxFilename", and // tries to find the special with the biggest xxxx if (reference.indexOf(QStringLiteral("src:"), 0, Qt::CaseInsensitive) == 0) { // Extract the file name and the numeral part from the reference string DVI_SourceFileSplitter splitter(reference, dviFile->filename); quint32 refLineNumber = splitter.line(); QString refFileName = splitter.filePath(); if (sourceHyperLinkAnchors.isEmpty()) { emit warning(i18n("You have asked Okular to locate the place in the DVI file which corresponds to " "line %1 in the TeX-file %2. It seems, however, that the DVI file " "does not contain the necessary source file information. ", refLineNumber, refFileName), -1); return Anchor(); } // Go through the list of source file anchors, and find the anchor // whose line number is the biggest among those that are smaller // than the refLineNumber. That way, the position in the DVI file // which is highlighted is always a little further up than the // position in the editor, e.g. if the DVI file contains // positional information at the beginning of every paragraph, // KDVI jumps to the beginning of the paragraph that the cursor is // in, and never to the next paragraph. If source file anchors for // the refFileName can be found, but none of their line numbers is // smaller than the refLineNumber, the reason is most likely, that // the cursor in the editor stands somewhere in the preamble of // the LaTeX file. In that case, we jump to the beginning of the // document. bool anchorForRefFileFound = false; // Flag that is set if source file anchors for the refFileName could be found at all QVector::iterator bestMatch = sourceHyperLinkAnchors.end(); QVector::iterator it; for( it = sourceHyperLinkAnchors.begin(); it != sourceHyperLinkAnchors.end(); ++it ) if (refFileName.trimmed() == it->fileName.trimmed() || refFileName.trimmed() == it->fileName.trimmed() + QStringLiteral(".tex") ) { anchorForRefFileFound = true; if ( (it->line <= refLineNumber) && ( (bestMatch == sourceHyperLinkAnchors.end()) || (it->line > bestMatch->line) ) ) bestMatch = it; } if (bestMatch != sourceHyperLinkAnchors.end()) return Anchor(bestMatch->page, bestMatch->distance_from_top); else if (anchorForRefFileFound == false) { emit warning(i18n("Okular was not able to locate the place in the DVI file which corresponds to " "line %1 in the TeX-file %2.", refLineNumber, refFileName), -1); } else return Anchor(); return Anchor(); } return Anchor(); } void dviRenderer::setResolution(double resolution_in_DPI) { // Ignore minute changes. The difference to the current value would // hardly be visible anyway. That saves a lot of re-painting, // e.g. when the user resizes the window, and a flickery mouse // changes the window size by 1 pixel all the time. if (fabs(resolutionInDPI-resolution_in_DPI) < 1) return; resolutionInDPI = resolution_in_DPI; // Pass the information on to the font pool. font_pool.setDisplayResolution( resolutionInDPI ); shrinkfactor = 1200/resolutionInDPI; return; } -void dviRenderer::handleSRCLink(const QString &linkText, const QPoint& point, DocumentWidget *widget) +void dviRenderer::handleSRCLink(const QString &linkText, const QPoint point, DocumentWidget *widget) { Q_UNUSED( linkText ); Q_UNUSED( point ); Q_UNUSED( widget ); #if 0 QExplicitlySharedDataPointer editor(new DVISourceEditor(*this, parentWidget, linkText, point, win)); if (editor->started()) editor_ = editor; #endif } QString dviRenderer::PDFencodingToQString(const QString& _pdfstring) { // This method locates special PDF characters in a string and // replaces them by UTF8. See Section 3.2.3 of the PDF reference // guide for information. QString pdfstring = _pdfstring; pdfstring = pdfstring.replace(QLatin1String("\\n"), QLatin1String("\n")); pdfstring = pdfstring.replace(QLatin1String("\\r"), QLatin1String("\n")); pdfstring = pdfstring.replace(QLatin1String("\\t"), QLatin1String("\t")); pdfstring = pdfstring.replace(QLatin1String("\\f"), QLatin1String("\f")); pdfstring = pdfstring.replace(QLatin1String("\\("), QLatin1String("(")); pdfstring = pdfstring.replace(QLatin1String("\\)"), QLatin1String(")")); pdfstring = pdfstring.replace(QLatin1String("\\\\"), QLatin1String("\\")); // Now replace octal character codes with the characters they encode QRegularExpression regex( QStringLiteral("(\\\\)(\\d\\d\\d)") ); // matches "\xyz" where x,y,z are numbers QRegularExpressionMatch match; while ((match = regex.match(pdfstring)).hasMatch()) { pdfstring = pdfstring.replace(match.capturedStart(0), 4, QChar(match.captured(2).toInt(nullptr, 8))); } regex.setPattern( QStringLiteral("(\\\\)(\\d\\d)") ); // matches "\xy" where x,y are numbers while ((match = regex.match(pdfstring)).hasMatch()) { pdfstring = pdfstring.replace(match.capturedStart(0), 3, QChar(match.captured(2).toInt(nullptr,8))); } regex.setPattern( QStringLiteral("(\\\\)(\\d)") ); // matches "\x" where x is a number while ((match = regex.match(pdfstring)).hasMatch()) { pdfstring = pdfstring.replace(match.capturedStart(0), 2, QChar(match.captured(2).toInt(nullptr,8))); } return pdfstring; } void dviRenderer::exportPDF() { /* QExplicitlySharedDataPointer exporter(new DVIExportToPDF(*this, parentWidget)); if (exporter->started()) all_exports_[exporter.data()] = exporter; */ } void dviRenderer::exportPS(const QString& fname, const QStringList& options, QPrinter* printer, QPrinter::Orientation orientation) { QExplicitlySharedDataPointer exporter(new DVIExportToPS(*this, fname, options, printer, font_pool.getUseFontHints(), orientation)); if (exporter->started()) all_exports_[exporter.data()] = exporter; } /* void dviRenderer::editor_finished(const DVISourceEditor*) { editor_.attach(0); } */ void dviRenderer::export_finished(const DVIExport* key) { typedef QMap > ExportMap; ExportMap::iterator it = all_exports_.find(key); if (it != all_exports_.end()) all_exports_.remove(key); } void dviRenderer::setEventLoop(QEventLoop *el) { if (el == nullptr) { delete m_eventLoop; m_eventLoop = nullptr; } else m_eventLoop = el; } diff --git a/generators/dvi/dviRenderer.h b/generators/dvi/dviRenderer.h index 59b80c9f7..195b95f9f 100644 --- a/generators/dvi/dviRenderer.h +++ b/generators/dvi/dviRenderer.h @@ -1,331 +1,331 @@ // -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; c-brace-offset: 0; -*- // // Class: dviRenderer // // Class for rendering TeX DVI files. // Part of KDVI- A previewer for TeX DVI files. // // (C) 2001-2006 Stefan Kebekus. Distributed under the GPL. #ifndef _dvirenderer_h_ #define _dvirenderer_h_ #include "bigEndianByteReader.h" //#include "documentRenderer.h" #include "dviexport.h" //#include "dvisourceeditor.h" #include "fontpool.h" #include "dviPageInfo.h" #include "pageSize.h" #include "anchor.h" #include "prebookmark.h" #include #include #include #include #include #include #include #include #include #include class Anchor; class DocumentWidget; class dvifile; class dviRenderer; class ghostscript_interface; class QEventLoop; class QProgressDialog; class PreBookmark; class TeXFontDefinition; extern const int MFResolutions[]; class DVI_SourceFileAnchor { public: DVI_SourceFileAnchor() {} - DVI_SourceFileAnchor(const QString& name, quint32 ln, quint32 pg, const Length& _distance_from_top) + DVI_SourceFileAnchor(const QString& name, quint32 ln, quint32 pg, const Length _distance_from_top) : fileName(name), line(ln), page(pg), distance_from_top(_distance_from_top) {} QString fileName; quint32 line; quint32 page; Length distance_from_top; }; /** Compound of registers, as defined in section 2.6.2 of the DVI driver standard, Level 0, published by the TUG DVI driver standards committee. */ struct framedata { long dvi_h; long dvi_v; long w; long x; long y; long z; int pxl_v; }; /* this information is saved when using virtual fonts */ typedef void (dviRenderer::*set_char_proc)(unsigned int, unsigned int); typedef void (dviRenderer::*parseSpecials)(char *, quint8 *); struct drawinf { struct framedata data; TeXFontDefinition* fontp; set_char_proc set_char_p; QHash* fonttable; TeXFontDefinition* _virtual; }; class dviRenderer : public QObject /*: public DocumentRenderer*/, bigEndianByteReader { Q_OBJECT public: dviRenderer(bool useFontHinting); ~dviRenderer() override; virtual bool setFile(const QString &fname, const QUrl &base); dvifile* dviFile; #if 0 bool isModified() const {return _isModified;}; void setPrefs(bool flag_showPS, const QString &editorCommand, bool useFontHints ); #endif virtual bool supportsTextSearch() const {return true;} bool showPS() { return _postscript; } int curr_page() { return current_page+1; } virtual bool isValidFile(const QString& fileName) const; /** This method will try to parse the reference part of the DVI file's URL, (either a number, which is supposed to be a page number, or src:\\) and see if a corresponding section of the DVI file can be found. If so, it returns an anchor to that section. If not, it returns an invalid anchor. */ virtual Anchor parseReference(const QString &reference); Anchor findAnchor(const QString &); virtual PageNumber totalPages() const; void setEventLoop(QEventLoop *el); // These should not be public... only for the moment void read_postamble(); void draw_part(double current_dimconv, bool is_vfmacro); void set_vf_char(unsigned int cmd, unsigned int ch); void set_char(unsigned int cmd, unsigned int ch); void set_empty_char(unsigned int cmd, unsigned int ch); void set_no_char(unsigned int cmd, unsigned int ch); void applicationDoSpecial(char * cp); void special(long nbytes); void printErrorMsgForSpecials(const QString& msg); void color_special(const QString& msg); void html_href_special(const QString& msg); void html_anchor_end(); void draw_page(); void export_finished(const DVIExport*); //void editor_finished(const DVISourceEditor*); QVector getPrebookmarks() const { return prebookmarks; } Q_SIGNALS: /** * The following three signals are modeleed on the corresponding signals * of the Document class. */ void error( const QString &message, int duration ); void warning( const QString &message, int duration ); void notice( const QString &message, int duration ); public Q_SLOTS: void exportPS(const QString& fname = QString(), const QStringList& options = QStringList(), QPrinter* printer = nullptr, QPrinter::Orientation orientation = QPrinter::Portrait); void exportPDF(); - void handleSRCLink(const QString &linkText, const QPoint& point, DocumentWidget *widget); + void handleSRCLink(const QString &linkText, const QPoint point, DocumentWidget *widget); void embedPostScript(); virtual void drawPage(RenderedDocumentPagePixmap* page); virtual void getText(RenderedDocumentPagePixmap* page); - SimplePageSize sizeOfPage(const PageNumber& page); + SimplePageSize sizeOfPage(const PageNumber page); const QVector& sourceAnchors() { return sourceHyperLinkAnchors; } private Q_SLOTS: /** This method shows a dialog that tells the user that source information is present, and gives the opportunity to open the manual and learn more about forward and inverse search */ // void showThatSourceInformationIsPresent(); private: friend class DVIExportToPS; friend class DVIExport; // friend class DVISourceEditor; /** URL to the DVI file This field is initialized by the setFile() method. See the explanation there. */ QUrl baseURL; /** This method parses a color specification of type "gray 0.5", "rgb 0.5 0.7 1.0", "hsb ...", "cmyk .." or "PineGreen". See the source code for details. */ QColor parseColorSpecification(const QString& colorSpec); /** This map contains the colors which are known by name. This field is initialized in the method parseColorSpecification() as soon as it is needed. */ QMap namedColors; /** This method locates special PDF characters in a string and replaces them by UTF8. See Section 3.2.3 of the PDF reference guide for information */ QString PDFencodingToQString(const QString& pdfstring); void setResolution(double resolution_in_DPI); fontPool font_pool; double resolutionInDPI; // @@@ explanation void prescan(parseSpecials specialParser); void prescan_embedPS(char *cp, quint8 *); void prescan_removePageSizeInfo(char *cp, quint8 *); void prescan_parseSpecials(char *cp, quint8 *); void prescan_ParsePapersizeSpecial(const QString& cp); void prescan_ParseBackgroundSpecial(const QString& cp); void prescan_ParseHTMLAnchorSpecial(const QString& cp); void prescan_ParsePSHeaderSpecial(const QString& cp); void prescan_ParsePSBangSpecial(const QString& cp); void prescan_ParsePSQuoteSpecial(const QString& cp); void prescan_ParsePSSpecial(const QString& cp); void prescan_ParsePSFileSpecial(const QString& cp); void prescan_ParseSourceSpecial(const QString& cp); void prescan_setChar(unsigned int ch); /* */ QVector prebookmarks; /** Utility fields used by the embedPostScript method*/ QProgressDialog *embedPS_progress; quint16 embedPS_numOfProgressedFiles; /** Shrink factor. Units are not quite clear */ double shrinkfactor; QString errorMsg; /** Methods which handle certain special commands. */ void epsf_special(const QString& cp); void source_special(const QString& cp); /** TPIC specials */ void TPIC_setPen_special(const QString& cp); void TPIC_addPath_special(const QString& cp); void TPIC_flushPath_special(); // List of source-hyperlinks on all pages. This vector is generated // when the DVI-file is first loaded, i.e. when draw_part is called // with PostScriptOutPutString != NULL QVector sourceHyperLinkAnchors; // If not NULL, the text currently drawn represents a source // hyperlink to the (relative) URL given in the string; QString *source_href; // If not NULL, the text currently drawn represents a hyperlink to // the (relative) URL given in the string; QString *HTML_href; QString editorCommand; /** Stack for register compounds, used for the DVI-commands PUSH/POP as explained in section 2.5 and 2.6.2 of the DVI driver standard, Level 0, published by the TUG DVI driver standards committee. */ QStack stack; /** A stack where color are stored, according to the documentation of DVIPS */ QStack colorStack; /** The global color is to be used when the color stack is empty */ QColor globalColor; /** If PostScriptOutPutFile is non-zero, then no rendering takes place. Instead, the PostScript code which is generated by the \special-commands is written to the PostScriptString */ QString *PostScriptOutPutString; ghostscript_interface *PS_interface; /** true, if gs should be used, otherwise, only bounding boxes are drawn. */ bool _postscript; /** This flag is used when rendering a dvi-page. It is set to "true" when any dvi-command other than "set" or "put" series of commands is encountered. This is considered to mark the end of a word. */ bool line_boundary_encountered; bool word_boundary_encountered; unsigned int current_page; /** Data required for handling TPIC specials */ float penWidth_in_mInch; QPolygon TPIC_path; quint16 number_of_elements_in_path; drawinf currinf; RenderedDocumentPagePixmap* currentlyDrawnPage; QMap > all_exports_; //QExplicitlySharedDataPointer editor_; /** Flag if document is modified This flag indicates if the document was modified after it was loaded. It is set to 'false' in the constructor, in the clear() and setFile() method. It can be set to 'true' be methods that modify the document (e.g. the deletePages() method of the djvu implementation of this class). */ bool _isModified; QMutex mutex; quint16 numPages; //TODO: merge into dviPageInfo QVector pageSizes; QMap anchorList; QEventLoop* m_eventLoop; QPainter* foreGroundPainter; // was the locateFonts method of font pool executed? bool fontpoolLocateFontsDone; }; #endif diff --git a/generators/dvi/dviRenderer_dr.cpp b/generators/dvi/dviRenderer_dr.cpp index 55fc27397..212b73237 100644 --- a/generators/dvi/dviRenderer_dr.cpp +++ b/generators/dvi/dviRenderer_dr.cpp @@ -1,49 +1,49 @@ // -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; c-brace-offset: 0; -*- // // Extracted from: // Class: documentRenderer // // Abstract Widget for displaying document types // Needs to be implemented from the actual parts // using kviewshell // Part of KViewshell - A generic interface for document viewers. // // (C) 2004-2005 Wilfried Huss // (C) 2004-2006 Stefan Kebekus. // Distributed under the GPL. #include "dviRenderer.h" -SimplePageSize dviRenderer::sizeOfPage(const PageNumber& page) +SimplePageSize dviRenderer::sizeOfPage(const PageNumber page) { #if !defined(QT_NO_THREAD) // Wait for all access to this DocumentRenderer to finish //QMutexLocker locker(&mutex); #endif if (!page.isValid()) return SimplePageSize(); if (page > totalPages()) return SimplePageSize(); if (page > pageSizes.size()) return SimplePageSize(); return pageSizes[page-1]; } Anchor dviRenderer::findAnchor(const QString &locallink) { QMap::Iterator it = anchorList.find(locallink); if (it != anchorList.end()) return *it; else return Anchor(); } PageNumber dviRenderer::totalPages() const { PageNumber temp = numPages; return temp; } diff --git a/generators/dvi/generator_dvi.cpp b/generators/dvi/generator_dvi.cpp index 85b96272a..e85e64a21 100644 --- a/generators/dvi/generator_dvi.cpp +++ b/generators/dvi/generator_dvi.cpp @@ -1,576 +1,576 @@ /*************************************************************************** * Copyright (C) 2006-2009 by Luigi Toscano * * * * 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. * ***************************************************************************/ #include #include #include #include #include #include #include "generator_dvi.h" #include "debug_dvi.h" #include "dviFile.h" #include "dviPageInfo.h" #include "dviRenderer.h" #include "pageSize.h" #include "dviexport.h" #include "TeXFont.h" #include #include #include #include #include #include #include #include #include #include #include #ifdef DVI_OPEN_BUSYLOOP #include #endif OKULAR_EXPORT_PLUGIN(DviGenerator, "libokularGenerator_dvi.json") DviGenerator::DviGenerator( QObject *parent, const QVariantList &args ) : Okular::Generator( parent, args ), m_fontExtracted( false ), m_docSynopsis( nullptr ), m_dviRenderer( nullptr ) { setFeature( Threaded ); setFeature( TextExtraction ); setFeature( FontInfo ); setFeature( PrintPostscript ); if ( Okular::FilePrinter::ps2pdfAvailable() ) setFeature( PrintToFile ); } bool DviGenerator::loadDocument( const QString & fileName, QVector< Okular::Page * > &pagesVector ) { //qCDebug(OkularDviDebug) << "file:" << fileName; QUrl base( QUrl::fromLocalFile(fileName) ); (void)userMutex(); m_dviRenderer = new dviRenderer(documentMetaData(TextHintingMetaData, QVariant()).toBool()); connect(m_dviRenderer, &dviRenderer::error, this, &DviGenerator::error); connect(m_dviRenderer, &dviRenderer::warning, this, &DviGenerator::warning); connect(m_dviRenderer, &dviRenderer::notice, this, &DviGenerator::notice); #ifdef DVI_OPEN_BUSYLOOP static const ushort s_waitTime = 800; // milliseconds static const int s_maxIterations = 10; int iter = 0; for ( ; !m_dviRenderer->isValidFile( fileName ) && iter < s_maxIterations; ++iter ) { qCDebug(OkularDviDebug).nospace() << "file not valid after iteration #" << iter << "/" << s_maxIterations << ", waiting for " << s_waitTime; QThread::msleep(s_waitTime); } if ( iter >= s_maxIterations && !m_dviRenderer->isValidFile( fileName ) ) { qCDebug(OkularDviDebug) << "file still not valid after" << s_maxIterations; delete m_dviRenderer; m_dviRenderer = 0; return false; } #else if ( !m_dviRenderer->isValidFile( fileName ) ) { delete m_dviRenderer; m_dviRenderer = nullptr; return false; } #endif if ( ! m_dviRenderer->setFile( fileName, base ) ) { delete m_dviRenderer; m_dviRenderer = nullptr; return false; } qCDebug(OkularDviDebug) << "# of pages:" << m_dviRenderer->dviFile->total_pages; m_resolution = dpi().height(); loadPages( pagesVector ); return true; } bool DviGenerator::doCloseDocument() { delete m_docSynopsis; m_docSynopsis = nullptr; delete m_dviRenderer; m_dviRenderer = nullptr; m_linkGenerated.clear(); m_fontExtracted = false; return true; } void DviGenerator::fillViewportFromAnchor( Okular::DocumentViewport &vp, - const Anchor &anch, const Okular::Page *page ) const + const Anchor anch, const Okular::Page *page ) const { fillViewportFromAnchor( vp, anch, page->width(), page->height() ); } void DviGenerator::fillViewportFromAnchor( Okular::DocumentViewport &vp, - const Anchor &anch, int pW, int pH ) const + const Anchor anch, int pW, int pH ) const { vp.pageNumber = anch.page - 1; SimplePageSize ps = m_dviRenderer->sizeOfPage( vp.pageNumber ); double resolution = 0; if (ps.isValid()) resolution = (double)(pW)/ps.width().getLength_in_inch(); else resolution = m_resolution; double py = (double)anch.distance_from_top.getLength_in_inch()*resolution + 0.5; vp.rePos.normalizedX = 0.5; vp.rePos.normalizedY = py/(double)pH; vp.rePos.enabled = true; vp.rePos.pos = Okular::DocumentViewport::Center; } QLinkedList DviGenerator::generateDviLinks( const dviPageInfo *pageInfo ) { QLinkedList dviLinks; int pageWidth = pageInfo->width, pageHeight = pageInfo->height; for ( const Hyperlink &dviLink : qAsConst(pageInfo->hyperLinkList) ) { QRect boxArea = dviLink.box; double nl = (double)boxArea.left() / pageWidth, nt = (double)boxArea.top() / pageHeight, nr = (double)boxArea.right() / pageWidth, nb = (double)boxArea.bottom() / pageHeight; QString linkText = dviLink.linkText; if ( linkText.startsWith(QLatin1String("#")) ) linkText = linkText.mid( 1 ); Anchor anch = m_dviRenderer->findAnchor( linkText ); Okular::Action *okuLink = nullptr; /* distinguish between local (-> anchor) and remote links */ if (anch.isValid()) { /* internal link */ Okular::DocumentViewport vp; fillViewportFromAnchor( vp, anch, pageWidth, pageHeight ); okuLink = new Okular::GotoAction( QLatin1String(""), vp ); } else { okuLink = new Okular::BrowseAction( QUrl::fromUserInput( dviLink.linkText ) ); } if ( okuLink ) { Okular::ObjectRect *orlink = new Okular::ObjectRect( nl, nt, nr, nb, false, Okular::ObjectRect::Action, okuLink ); dviLinks.push_front( orlink ); } } return dviLinks; } QImage DviGenerator::image( Okular::PixmapRequest *request ) { dviPageInfo *pageInfo = new dviPageInfo(); pageSize ps; QImage ret; pageInfo->width = request->width(); pageInfo->height = request->height(); pageInfo->pageNumber = request->pageNumber() + 1; // pageInfo->resolution = m_resolution; QMutexLocker lock( userMutex() ); if ( m_dviRenderer ) { SimplePageSize s = m_dviRenderer->sizeOfPage( pageInfo->pageNumber ); /* if ( s.width() != pageInfo->width) */ // if (!useDocumentSpecifiedSize) // s = userPreferredSize; if (s.isValid()) { ps = s; /* it should be the user specified size, if any, instead */ } pageInfo->resolution = (double)(pageInfo->width)/ps.width().getLength_in_inch(); #if 0 qCDebug(OkularDviDebug) << *request << ", res:" << pageInfo->resolution << " - (" << pageInfo->width << "," << ps.width().getLength_in_inch() << ")," << ps.width().getLength_in_mm() << endl; #endif m_dviRenderer->drawPage( pageInfo ); if ( ! pageInfo->img.isNull() ) { qCDebug(OkularDviDebug) << "Image OK"; ret = pageInfo->img; if ( !m_linkGenerated[ request->pageNumber() ] ) { request->page()->setObjectRects( generateDviLinks( pageInfo ) ); m_linkGenerated[ request->pageNumber() ] = true; } } } lock.unlock(); delete pageInfo; return ret; } Okular::TextPage* DviGenerator::textPage( Okular::TextRequest *request ) { const Okular::Page *page = request->page(); qCDebug(OkularDviDebug); dviPageInfo *pageInfo = new dviPageInfo(); pageSize ps; pageInfo->width=page->width(); pageInfo->height=page->height(); pageInfo->pageNumber = page->number() + 1; pageInfo->resolution = m_resolution; QMutexLocker lock( userMutex() ); // get page text from m_dviRenderer Okular::TextPage *ktp = nullptr; if ( m_dviRenderer ) { SimplePageSize s = m_dviRenderer->sizeOfPage( pageInfo->pageNumber ); pageInfo->resolution = (double)(pageInfo->width)/ps.width().getLength_in_inch(); m_dviRenderer->getText( pageInfo ); lock.unlock(); ktp = extractTextFromPage( pageInfo ); } delete pageInfo; return ktp; } Okular::TextPage *DviGenerator::extractTextFromPage( dviPageInfo *pageInfo ) { QList textOfThePage; QVector::ConstIterator it = pageInfo->textBoxList.constBegin(); QVector::ConstIterator itEnd = pageInfo->textBoxList.constEnd(); int pageWidth = pageInfo->width, pageHeight = pageInfo->height; for ( ; it != itEnd ; ++it ) { TextBox curTB = *it; #if 0 qCDebug(OkularDviDebug) << "orientation: " << orientation << ", curTB.box: " << curTB.box << ", ( " << pageWidth << "," << pageHeight << " )" < &keys ) const { Okular::DocumentInfo docInfo; if ( keys.contains( Okular::DocumentInfo::MimeType ) ) docInfo.set( Okular::DocumentInfo::MimeType, QStringLiteral("application/x-dvi") ); QMutexLocker lock( userMutex() ); if ( m_dviRenderer && m_dviRenderer->dviFile ) { dvifile *dvif = m_dviRenderer->dviFile; // read properties from dvif //docInfo.set( "filename", dvif->filename, i18n("Filename") ); if ( keys.contains( Okular::DocumentInfo::CustomKeys ) ) docInfo.set( QStringLiteral("generatorDate"), dvif->generatorString, i18n("Generator/Date") ); if ( keys.contains( Okular::DocumentInfo::Pages ) ) docInfo.set( Okular::DocumentInfo::Pages, QString::number( dvif->total_pages ) ); } return docInfo; } const Okular::DocumentSynopsis *DviGenerator::generateDocumentSynopsis() { if ( m_docSynopsis ) return m_docSynopsis; m_docSynopsis = new Okular::DocumentSynopsis(); userMutex()->lock(); QVector prebookmarks = m_dviRenderer->getPrebookmarks(); userMutex()->unlock(); if ( prebookmarks.isEmpty() ) return m_docSynopsis; QStack stack; QVector::ConstIterator it = prebookmarks.constBegin(); QVector::ConstIterator itEnd = prebookmarks.constEnd(); for( ; it != itEnd; ++it ) { QDomElement domel = m_docSynopsis->createElement( (*it).title ); Anchor a = m_dviRenderer->findAnchor((*it).anchorName); if ( a.isValid() ) { Okular::DocumentViewport vp; const Okular::Page *p = document()->page( a.page - 1 ); fillViewportFromAnchor( vp, a, (int)p->width(), (int)p->height() ); domel.setAttribute( QStringLiteral("Viewport"), vp.toString() ); } if ( stack.isEmpty() ) m_docSynopsis->appendChild( domel ); else { stack.top().appendChild( domel ); stack.pop(); } for ( int i = 0; i < (*it).noOfChildren; ++i ) stack.push( domel ); } return m_docSynopsis; } Okular::FontInfo::List DviGenerator::fontsForPage( int page ) { Q_UNUSED( page ); Okular::FontInfo::List list; // the list of the fonts is extracted once if ( m_fontExtracted ) return list; if ( m_dviRenderer && m_dviRenderer->dviFile && m_dviRenderer->dviFile->font_pool ) { const QList fonts = m_dviRenderer->dviFile->font_pool->fontList; for (const TeXFontDefinition *font : fonts) { Okular::FontInfo of; QString name; int zoom = (int)(font->enlargement*100 + 0.5); #ifdef HAVE_FREETYPE if ( font->getFullFontName().isEmpty() ) { name = QStringLiteral( "%1, %2%" ) .arg( font->fontname ) .arg( zoom ); } else { name = QStringLiteral( "%1 (%2), %3%" ) .arg( font->fontname, font->getFullFontName(), QString::number(zoom) ); } #else name = QString( "%1, %2%" ) .arg( font->fontname ) .arg( zoom ); #endif of.setName( name ); QString fontFileName; if (!(font->flags & TeXFontDefinition::FONT_VIRTUAL)) { if ( font->font != nullptr ) fontFileName = font->font->errorMessage; else fontFileName = i18n("Font file not found"); if ( fontFileName.isEmpty() ) fontFileName = font->filename; } of.setFile( fontFileName ); Okular::FontInfo::FontType ft; switch ( font->getFontType() ) { case TeXFontDefinition::TEX_PK: ft = Okular::FontInfo::TeXPK; break; case TeXFontDefinition::TEX_VIRTUAL: ft = Okular::FontInfo::TeXVirtual; break; case TeXFontDefinition::TEX_FONTMETRIC: ft = Okular::FontInfo::TeXFontMetric; break; case TeXFontDefinition::FREETYPE: ft = Okular::FontInfo::TeXFreeTypeHandled; break; } of.setType( ft ); // DVI has not the concept of "font embedding" of.setEmbedType( Okular::FontInfo::NotEmbedded ); of.setCanBeExtracted( false ); list.append( of ); } m_fontExtracted = true; } return list; } void DviGenerator::loadPages( QVector< Okular::Page * > &pagesVector ) { QSize pageRequiredSize; int numofpages = m_dviRenderer->dviFile->total_pages; pagesVector.resize( numofpages ); m_linkGenerated.fill( false, numofpages ); //qCDebug(OkularDviDebug) << "resolution:" << m_resolution << ", dviFile->preferred?"; /* get the suggested size */ if ( m_dviRenderer->dviFile->suggestedPageSize ) { pageRequiredSize = m_dviRenderer->dviFile->suggestedPageSize->sizeInPixel( m_resolution ); } else { pageSize ps; pageRequiredSize = ps.sizeInPixel( m_resolution ); } for ( int i = 0; i < numofpages; ++i ) { //qCDebug(OkularDviDebug) << "getting status of page" << i << ":"; if ( pagesVector[i] ) { delete pagesVector[i]; } Okular::Page * page = new Okular::Page( i, pageRequiredSize.width(), pageRequiredSize.height(), Okular::Rotation0 ); pagesVector[i] = page; } qCDebug(OkularDviDebug) << "pagesVector successfully inizialized!"; // filling the pages with the source references rects const QVector& sourceAnchors = m_dviRenderer->sourceAnchors(); QVector< QLinkedList< Okular::SourceRefObjectRect * > > refRects( numofpages ); for ( const DVI_SourceFileAnchor &sfa : sourceAnchors ) { if ( sfa.page < 1 || (int)sfa.page > numofpages ) continue; Okular::NormalizedPoint p( -1.0, (double)sfa.distance_from_top.getLength_in_pixel( dpi().height() ) / (double)pageRequiredSize.height() ); Okular::SourceReference * sourceRef = new Okular::SourceReference( sfa.fileName, sfa.line ); refRects[ sfa.page - 1 ].append( new Okular::SourceRefObjectRect( p, sourceRef ) ); } for ( int i = 0; i < refRects.size(); ++i ) if ( !refRects.at(i).isEmpty() ) pagesVector[i]->setSourceReferences( refRects.at(i) ); } bool DviGenerator::print( QPrinter& printer ) { // Create tempfile to write to QTemporaryFile tf(QDir::tempPath() + QLatin1String("/okular_XXXXXX.ps" )); if ( !tf.open() ) return false; const QList pageList = Okular::FilePrinter::pageList( printer, m_dviRenderer->totalPages(), document()->currentPage() + 1, document()->bookmarkedPageList() ); QString pages; QStringList printOptions; // List of pages to print. for ( const int p : pageList ) { pages += QStringLiteral(",%1").arg(p); } if ( !pages.isEmpty() ) printOptions << QStringLiteral("-pp") << pages.mid(1); QEventLoop el; m_dviRenderer->setEventLoop( &el ); m_dviRenderer->exportPS( tf.fileName(), printOptions, &printer, document()->orientation() ); tf.close(); // Error messages are handled by the generator - ugly, but it works. return true; } QVariant DviGenerator::metaData( const QString & key, const QVariant & option ) const { if ( key == QLatin1String("NamedViewport") && !option.toString().isEmpty() ) { const Anchor anchor = m_dviRenderer->parseReference( option.toString() ); if ( anchor.isValid() ) { const Okular::Page *page = document()->page( anchor.page - 1 ); Q_ASSERT_X( page, "DviGenerator::metaData()", "NULL page as result of valid Anchor" ); Okular::DocumentViewport viewport; fillViewportFromAnchor( viewport, anchor, page ); if ( viewport.isValid() ) { return viewport.toString(); } } } return QVariant(); } Q_LOGGING_CATEGORY(OkularDviDebug, "org.kde.okular.generators.dvi.core", QtWarningMsg) Q_LOGGING_CATEGORY(OkularDviShellDebug, "org.kde.okular.generators.dvi.shell", QtWarningMsg) #include "generator_dvi.moc" diff --git a/generators/dvi/generator_dvi.h b/generators/dvi/generator_dvi.h index 3f7b4f8e7..a57dc25bb 100644 --- a/generators/dvi/generator_dvi.h +++ b/generators/dvi/generator_dvi.h @@ -1,70 +1,70 @@ /*************************************************************************** * Copyright (C) 2006 by Luigi Toscano * * * * 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. * ***************************************************************************/ #ifndef _DVI_GENERATOR_H_ #define _DVI_GENERATOR_H_ #include #include class dviRenderer; class dviPageInfo; class Anchor; namespace Okular { class DocumentViewport; class ObjectRect; } class DviGenerator : public Okular::Generator { Q_OBJECT Q_INTERFACES( Okular::Generator ) public: DviGenerator( QObject *parent, const QVariantList &args ); bool loadDocument( const QString & fileName, QVector< Okular::Page * > & pagesVector ) override; // document information Okular::DocumentInfo generateDocumentInfo( const QSet &keys ) const override; // table of contents const Okular::DocumentSynopsis *generateDocumentSynopsis() override; // list of fonts Okular::FontInfo::List fontsForPage( int page ) override; bool print( QPrinter &printer ) override; QVariant metaData( const QString & key, const QVariant & option ) const override; protected: bool doCloseDocument() override; QImage image( Okular::PixmapRequest * request ) override; Okular::TextPage* textPage( Okular::TextRequest *request ) override; private: double m_resolution; bool m_fontExtracted; Okular::DocumentSynopsis *m_docSynopsis; dviRenderer *m_dviRenderer; QBitArray m_linkGenerated; void loadPages( QVector< Okular::Page * > & pagesVector ); Okular::TextPage *extractTextFromPage( dviPageInfo *pageInfo ); - void fillViewportFromAnchor( Okular::DocumentViewport &vp, const Anchor &anch, + void fillViewportFromAnchor( Okular::DocumentViewport &vp, const Anchor anch, int pW, int pH ) const; - void fillViewportFromAnchor( Okular::DocumentViewport &vp, const Anchor &anch, + void fillViewportFromAnchor( Okular::DocumentViewport &vp, const Anchor anch, const Okular::Page *page ) const; QLinkedList generateDviLinks( const dviPageInfo *pageInfo ); }; #endif diff --git a/generators/dvi/hyperlink.h b/generators/dvi/hyperlink.h index 70ce775be..5f4940505 100644 --- a/generators/dvi/hyperlink.h +++ b/generators/dvi/hyperlink.h @@ -1,78 +1,78 @@ // -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; c-brace-offset: 0; -*- // // Class: hyperlink // // Part of KDVI- A previewer for TeX DVI files. // // (C) 2004-2005 Stefan Kebekus. Distributed under the GPL. #ifndef _hyperlink_h_ #define _hyperlink_h_ #include #include /** Represents a named, rectangular region in a rendered documentPage This trivial class is used in the documentPage class to represent a hyperlink in a rendered documentPage. @author Stefan Kebekus @version 1.0.0 */ class Hyperlink { public: /** \brief Default Constructor The default constructor leaves all fields uninitialized. */ Hyperlink() {} /** \brief Constructor Trivial constructor leaves that initialized all members. @param bl value for the baseline field @param re value for the box @param lT valus for the text field */ - Hyperlink(quint32 bl, const QRect& re, const QString& lT): baseline(bl), box(re), linkText(lT) {} + Hyperlink(quint32 bl, const QRect re, const QString& lT): baseline(bl), box(re), linkText(lT) {} /** \brief Base line of a hyperlink This field specifies the Y-coordinate of the base line of the bounding box in the same coordinates that were used when the associated documentPage was rendered by the documentRenderer.drawPage() method. It is used to underline hyperlinks in blue. Note that this field does generally differ from the Y-coordinate of the bottom of the bounding box, e.g. if the text in the box contains characters with underlengths, such as 'y', 'j' or 'g'. */ quint32 baseline; /** \brief Bounding box of the text or hyperlink This rectangle specifies where on the page the hyperlink is found. It uses the same coordinates that were used when the associated documentPage was rendered by the documentRenderer.drawPage() method. The box is used to determine if the mouse pointer hovers over the link. */ QRect box; /** \brief Name of the region This field contains the name of the target, e.g. "http://www.kde.org". If the Hyperlink class is used to represent text, then the text is stored here. */ QString linkText; }; #endif diff --git a/generators/dvi/length.h b/generators/dvi/length.h index 119e277d6..be0b72305 100644 --- a/generators/dvi/length.h +++ b/generators/dvi/length.h @@ -1,200 +1,200 @@ // -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; c-brace-offset: 0; -*- // // Class: length // // Part of KVIEWSHELL // // (C) 2005 Stefan Kebekus // (C) 2006 Wilfried Huss // // Distributed under the GPL. #ifndef _length_h_ #define _length_h_ #include #include class QString; #define mm_per_cm 10.0 #define mm_per_m 1000.0 #define mm_per_inch 25.4 #define mm_per_TeXPoint (2540.0/7227.0) #define mm_per_bigPoint (25.4/72.0) #define mm_per_pica (25.4/6.0) #define mm_per_didot (25.4*0.0148) #define mm_per_cicero (25.4*0.178) #define mm_per_scaledPoint (25.4/(72.27 * 65536.0)) /** @short Represents a phyical length This class is used to represent a physical length. Its main purpose it to help in the conversion of units, and to avoid confusion about units. To avoid misunderstandings, there is no default constructor so that this class needs to be explicitly initialized with one of the functions below. @warning Lengths are stored internally in mm. If you convert to or from any other unit, expect floating point round-off errors. @author Stefan Kebekus @version 1.0.0 */ class Length { public: /** constructs a 'length = 0mm' object */ Length() {length_in_mm = 0;} /** sets the length in millimeters */ void setLength_in_mm(double l) {length_in_mm = l;} /** sets the length in centimeters */ void setLength_in_cm(double l) {length_in_mm = l*mm_per_cm;} /** sets the length in meters */ void setLength_in_m(double l) {length_in_mm = l*mm_per_m;} /** sets the length in inches */ void setLength_in_inch(double l) {length_in_mm = l*mm_per_inch;} /** sets the length in TeX points */ void setLength_in_TeXPoints(double l) {length_in_mm = l*mm_per_TeXPoint;} /** sets the length in big points (1/72 of an inch) */ void setLength_in_bigPoints(double l) {length_in_mm = l*mm_per_bigPoint;} /** sets the length in picas (1/6 of an inch) */ void setLength_in_pica(double l) {length_in_mm = l*mm_per_pica;} /** sets the length in didots (0.0148 inches) */ void setLength_in_didot(double l) {length_in_mm = l*mm_per_didot;} /** sets the length in ciceros (0.178 inches) */ void setLength_in_cicero(double l) {length_in_mm = l*mm_per_cicero;} /** sets the length in scaled points (1 scaled point = 65536 TeX points) */ void setLength_in_scaledPoints(double l) {length_in_mm = l*mm_per_scaledPoint;} /** sets the length (@param l ) in pixels. The parameter @param res is the resolution of the used device in DPI. */ void setLength_in_pixel(int l, double res) { setLength_in_inch(l / res); } /** @returns the length in millimeters */ double getLength_in_mm() const {return length_in_mm;} /** @returns the length in centimeters */ double getLength_in_cm() const {return length_in_mm/mm_per_cm;} /** @returns the length in meters */ double getLength_in_m() const {return length_in_mm/mm_per_m;} /** @returns the length in inches */ double getLength_in_inch() const {return length_in_mm/mm_per_inch;} /** @returns the length in TeX points */ double getLength_in_TeXPoints() const {return length_in_mm/mm_per_TeXPoint;} /** @returns the length in big points (1/72 of an inch) */ double getLength_in_bigPoints() const {return length_in_mm/mm_per_bigPoint;} /** @returns the length in picas (1/6 of an inch) */ double getLength_in_pica() const {return length_in_mm/mm_per_pica;} /** @returns the length in didots (0.0148 inches) */ double getLength_in_didot() const {return length_in_mm/mm_per_didot;} /** @returns the length in ciceros (0.178 inches) */ double getLength_in_cicero() const {return length_in_mm/mm_per_cicero;} /** @returns the length in scaled points (1 scaled point = 65536 TeX points) */ double getLength_in_scaledPoints() const {return length_in_mm/mm_per_scaledPoint;} /** @returns the length in pixel. The parameter @param res is the resolution of the used device in DPI. */ int getLength_in_pixel(double res) const { return int(getLength_in_inch() * res); } /** @returns true is lengths differ by no more than 2mm */ - bool isNearlyEqual(const Length &o) const {return fabs(length_in_mm-o.getLength_in_mm()) <= 2.0;} + bool isNearlyEqual(const Length o) const {return fabs(length_in_mm-o.getLength_in_mm()) <= 2.0;} /** Comparison of two lengthes */ - bool operator > (const Length &o) const {return (length_in_mm > o.getLength_in_mm());} - bool operator < (const Length &o) const {return (length_in_mm < o.getLength_in_mm());} + bool operator > (const Length o) const {return (length_in_mm > o.getLength_in_mm());} + bool operator < (const Length o) const {return (length_in_mm < o.getLength_in_mm());} /** Comparison of two lengthes */ - bool operator >= (const Length &o) const {return (length_in_mm >= o.getLength_in_mm());} - bool operator <= (const Length &o) const {return (length_in_mm <= o.getLength_in_mm());} + bool operator >= (const Length o) const {return (length_in_mm >= o.getLength_in_mm());} + bool operator <= (const Length o) const {return (length_in_mm <= o.getLength_in_mm());} /** Ratio of two lengthes @warning There is no safeguared to prevent you from division by zero. If the length in the denominator is near 0.0, a floating point exception may occur. @returns the ratio of the two lengthes as a double */ - double operator / (const Length &o) const {return (length_in_mm/o.getLength_in_mm());} + double operator / (const Length o) const {return (length_in_mm/o.getLength_in_mm());} /** Sum of two lengthes @returns the sum of the lengthes as a Length */ - Length operator + (const Length &o) const {Length r; r.length_in_mm = length_in_mm + o.length_in_mm; return r; } + Length operator + (const Length o) const {Length r; r.length_in_mm = length_in_mm + o.length_in_mm; return r; } /** Difference of two lengthes @returns the difference of the lengthes as a Length */ - Length operator - (const Length &o) const {Length r; r.length_in_mm = length_in_mm - o.length_in_mm; return r; } + Length operator - (const Length o) const {Length r; r.length_in_mm = length_in_mm - o.length_in_mm; return r; } /** Division of a length @warning There is no safeguared to prevent you from division by zero. If the number in the denominator is near 0.0, a floating point exception may occur. @returns a fraction of the original length as a Length */ Length operator / (const double l) const {Length r; r.length_in_mm = length_in_mm/l; return r; } /** Multiplication of a length @returns a multiplied length as a Length */ Length operator * (const double l) const {Length r; r.length_in_mm = length_in_mm*l; return r; } /** This method converts a string that gives a distance in one of the commonly used units, such as "12.3mm", "12 inch" or "15 didot" to millimeters. For a complete list of supported units, see the static lists that are hardcoded in "units.cpp". If the conversion is not possible *ok is set to "false" and an undefined value is returned. If the unit could not be recognized, an error message is printed via kdError(). Otherwise, *ok is set to true. It is possible in rare circumstances that ok is set to true although the string is malformed. It is fine to set ok to 0. */ static float convertToMM(const QString &distance, bool *ok=nullptr); private: /** Length in millimeters */ double length_in_mm; }; #undef mm_per_cm #undef mm_per_m #undef mm_per_inch #undef mm_per_TeXPoint #undef mm_per_bigPoint #undef mm_per_pica #undef mm_per_didot #undef mm_per_cicero #undef mm_per_scaledPoint #endif diff --git a/generators/dvi/psgs.cpp b/generators/dvi/psgs.cpp index 418b15bc7..36dd7ca9a 100644 --- a/generators/dvi/psgs.cpp +++ b/generators/dvi/psgs.cpp @@ -1,353 +1,353 @@ // -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; c-brace-offset: 0; -*- // // ghostscript_interface // // Part of KDVI - A framework for multipage text/gfx viewers // // (C) 2004 Stefan Kebekus // Distributed under the GPL #include #include "psgs.h" #include "psheader.cpp" #include "dviFile.h" #include "debug_dvi.h" #include "pageNumber.h" #include "debug_dvi.h" #include #include #include #include #include #include #include #include #include #include //#define DEBUG_PSGS //extern char psheader[]; pageInfo::pageInfo(const QString& _PostScriptString) { PostScriptString = new QString(_PostScriptString); background = Qt::white; permanentBackground = Qt::white; } pageInfo::~pageInfo() { if (PostScriptString != nullptr) delete PostScriptString; } // ====================================================== ghostscript_interface::ghostscript_interface() { PostScriptHeaderString = new QString(); knownDevices.append(QStringLiteral("png16m")); knownDevices.append(QStringLiteral("jpeg")); knownDevices.append(QStringLiteral("pnn")); knownDevices.append(QStringLiteral("pnnraw")); gsDevice = knownDevices.begin(); } ghostscript_interface::~ghostscript_interface() { if (PostScriptHeaderString != nullptr) delete PostScriptHeaderString; qDeleteAll(pageList); } -void ghostscript_interface::setPostScript(const PageNumber& page, const QString& PostScript) { +void ghostscript_interface::setPostScript(const PageNumber page, const QString& PostScript) { #ifdef DEBUG_PSGS qCDebug(OkularDviDebug) << "ghostscript_interface::setPostScript( " << page << ", ... )"; #endif if (pageList.value(page) == 0) { pageInfo *info = new pageInfo(PostScript); // Check if dict is big enough if (pageList.count() > pageList.capacity() -2) pageList.reserve(pageList.capacity()*2); pageList.insert(page, info); } else *(pageList.value(page)->PostScriptString) = PostScript; } void ghostscript_interface::setIncludePath(const QString &_includePath) { if (_includePath.isEmpty()) includePath = QLatin1Char('*'); // Allow all files else includePath = _includePath + QStringLiteral("/*"); } -void ghostscript_interface::setBackgroundColor(const PageNumber& page, const QColor& background_color, bool permanent) { +void ghostscript_interface::setBackgroundColor(const PageNumber page, const QColor& background_color, bool permanent) { #ifdef DEBUG_PSGS qCDebug(OkularDviDebug) << "ghostscript_interface::setBackgroundColor( " << page << ", " << background_color << " )"; #endif if (pageList.value(page) == 0) { pageInfo *info = new pageInfo(QString()); info->background = background_color; if (permanent) info->permanentBackground = background_color; // Check if dict is big enough if (pageList.count() > pageList.capacity() -2) pageList.reserve(pageList.capacity()*2); pageList.insert(page, info); } else { pageList.value(page)->background = background_color; if (permanent) pageList.value(page)->permanentBackground = background_color; } } -void ghostscript_interface::restoreBackgroundColor(const PageNumber& page) +void ghostscript_interface::restoreBackgroundColor(const PageNumber page) { #ifdef DEBUG_PSGS qCDebug(OkularDviDebug) << "ghostscript_interface::restoreBackgroundColor( " << page << " )"; #endif if (pageList.value(page) == 0) return; pageInfo *info = pageList.value(page); info->background = info->permanentBackground; } // Returns the background color for a certain page. This color is // always guaranteed to be valid -QColor ghostscript_interface::getBackgroundColor(const PageNumber& page) const { +QColor ghostscript_interface::getBackgroundColor(const PageNumber page) const { #ifdef DEBUG_PSGS qCDebug(OkularDviDebug) << "ghostscript_interface::getBackgroundColor( " << page << " )"; #endif if (pageList.value(page) == 0) return Qt::white; else return pageList.value(page)->background; } void ghostscript_interface::clear() { PostScriptHeaderString->truncate(0); // Deletes all items, removes temporary files, etc. qDeleteAll(pageList); pageList.clear(); } -void ghostscript_interface::gs_generate_graphics_file(const PageNumber& page, const QString& filename, long magnification) { +void ghostscript_interface::gs_generate_graphics_file(const PageNumber page, const QString& filename, long magnification) { #ifdef DEBUG_PSGS qCDebug(OkularDviDebug) << "ghostscript_interface::gs_generate_graphics_file( " << page << ", " << filename << " )"; #endif if (knownDevices.isEmpty()) { qCCritical(OkularDviDebug) << "No known devices found" << endl; return; } pageInfo *info = pageList.value(page); // Generate a PNG-file // Step 1: Write the PostScriptString to a File QTemporaryFile PSfile(QDir::tempPath() + QLatin1String("/okular_XXXXXX.ps")); PSfile.setAutoRemove(false); PSfile.open(); const QString PSfileName = PSfile.fileName(); QTextStream os(&PSfile); os << "%!PS-Adobe-2.0\n" << "%%Creator: kdvi\n" << "%%Title: KDVI temporary PostScript\n" << "%%Pages: 1\n" << "%%PageOrder: Ascend\n" // HSize and VSize in 1/72 inch << "%%BoundingBox: 0 0 " << (qint32)(72*(pixel_page_w/resolution)) << ' ' << (qint32)(72*(pixel_page_h/resolution)) << '\n' << "%%EndComments\n" << "%!\n" << psheader << "TeXDict begin " // HSize in (1/(65781.76*72))inch << (qint32)(72*65781*(pixel_page_w/resolution)) << ' ' // VSize in (1/(65781.76*72))inch << (qint32)(72*65781*(pixel_page_h/resolution)) << ' ' // Magnification << (qint32)(magnification) // dpi and vdpi << " 300 300" // Name << " (test.dvi)" << " @start end\n" << "TeXDict begin\n" // Start page << "1 0 bop 0 0 a \n"; if (!PostScriptHeaderString->toLatin1().isNull()) os << PostScriptHeaderString->toLatin1(); if (info->background != Qt::white) { QString colorCommand = QStringLiteral("gsave %1 %2 %3 setrgbcolor clippath fill grestore\n"). arg(info->background.red()/255.0). arg(info->background.green()/255.0). arg(info->background.blue()/255.0); os << colorCommand.toLatin1(); } if (!info->PostScriptString->isNull()) os << *(info->PostScriptString); os << "end\n" << "showpage \n"; PSfile.close(); // Step 2: Call GS with the File QFile::remove(filename); KProcess proc; proc.setOutputChannelMode(KProcess::SeparateChannels); QStringList argus; argus << QStringLiteral("gs"); argus << QStringLiteral("-dSAFER") << QStringLiteral("-dPARANOIDSAFER") << QStringLiteral("-dDELAYSAFER") << QStringLiteral("-dNOPAUSE") << QStringLiteral("-dBATCH"); argus << QStringLiteral("-sDEVICE=%1").arg(*gsDevice); argus << QStringLiteral("-sOutputFile=%1").arg(filename); argus << QStringLiteral("-sExtraIncludePath=%1").arg(includePath); argus << QStringLiteral("-g%1x%2").arg(pixel_page_w).arg(pixel_page_h); // page size in pixels argus << QStringLiteral("-r%1").arg(resolution); // resolution in dpi argus << QStringLiteral("-dTextAlphaBits=4 -dGraphicsAlphaBits=2"); // Antialiasing argus << QStringLiteral("-c") << QStringLiteral("<< /PermitFileReading [ ExtraIncludePath ] /PermitFileWriting [] /PermitFileControl [] >> setuserparams .locksafe"); argus << QStringLiteral("-f") << PSfileName; #ifdef DEBUG_PSGS qCDebug(OkularDviDebug) << argus.join(" "); #endif proc << argus; int res = proc.execute(); if ( res ) { // Starting ghostscript did not work. // TODO: Issue error message, switch PS support off. qCCritical(OkularDviDebug) << "ghostview could not be started" << endl; } PSfile.remove(); // Check if gs has indeed produced a file. if (QFile::exists(filename) == false) { qCCritical(OkularDviDebug) << "GS did not produce output." << endl; // No. Check is the reason is that the device is not compiled into // ghostscript. If so, try again with another device. QString GSoutput; proc.setReadChannel(QProcess::StandardOutput); while(proc.canReadLine()) { GSoutput = QString::fromLocal8Bit(proc.readLine()); if (GSoutput.contains(QStringLiteral("Unknown device"))) { qCDebug(OkularDviDebug) << QString::fromLatin1("The version of ghostview installed on this computer does not support " "the '%1' ghostview device driver.").arg(*gsDevice) << endl; knownDevices.erase(gsDevice); gsDevice = knownDevices.begin(); if (knownDevices.isEmpty()) // TODO: show a requestor of some sort. emit error(i18n("The version of Ghostview that is installed on this computer does not contain " "any of the Ghostview device drivers that are known to Okular. PostScript " "support has therefore been turned off in Okular."), -1); #if 0 i18n("

The Ghostview program, which Okular uses internally to display the " "PostScript graphics that is included in this DVI file, is generally able to " "write its output in a variety of formats. The sub-programs that Ghostview uses " "for these tasks are called 'device drivers'; there is one device driver for " "each format that Ghostview is able to write. Different versions of Ghostview " "often have different sets of device drivers available. It seems that the " "version of Ghostview that is installed on this computer does not contain " "any of the device drivers that are known to Okular.

" "

It seems unlikely that a regular installation of Ghostview would not contain " "these drivers. This error may therefore point to a serious misconfiguration of " "the Ghostview installation on your computer.

" "

If you want to fix the problems with Ghostview, you can use the command " "gs --help to display the list of device drivers contained in " "Ghostview. Among others, Okular can use the 'png256', 'jpeg' and 'pnm' " "drivers. Note that Okular needs to be restarted to re-enable PostScript support." "

")); #endif else { qCDebug(OkularDviDebug) << QStringLiteral("Okular will now try to use the '%1' device driver.").arg(*gsDevice); gs_generate_graphics_file(page, filename, magnification); } return; } } } } -void ghostscript_interface::graphics(const PageNumber& page, double dpi, long magnification, QPainter* paint) { +void ghostscript_interface::graphics(const PageNumber page, double dpi, long magnification, QPainter* paint) { #ifdef DEBUG_PSGS qCDebug(OkularDviDebug) << "ghostscript_interface::graphics( " << page << ", " << dpi << ", ... ) called."; #endif if (paint == nullptr) { qCCritical(OkularDviDebug) << "ghostscript_interface::graphics(PageNumber page, double dpi, long magnification, QPainter *paint) called with paint == 0" << endl; return; } resolution = dpi; pixel_page_w = paint->viewport().width(); pixel_page_h = paint->viewport().height(); pageInfo *info = pageList.value(page); // No PostScript? Then return immediately. if ((info == nullptr) || (info->PostScriptString->isEmpty())) { #ifdef DEBUG_PSGS qCDebug(OkularDviDebug) << "No PostScript found. Not drawing anything."; #endif return; } QTemporaryFile gfxFile; gfxFile.open(); const QString gfxFileName = gfxFile.fileName(); // We are want the filename, not the file. gfxFile.close(); gs_generate_graphics_file(page, gfxFileName, magnification); QImage MemoryCopy(gfxFileName); paint->drawImage(0, 0, MemoryCopy); return; } QString ghostscript_interface::locateEPSfile(const QString &filename, const QUrl &base) { // If the base URL indicates that the DVI file is local, try to find // the graphics file in the directory where the DVI file resides if (base.isLocalFile()) { QString path = base.path(); // -> "/bar/foo.dvi" QFileInfo fi1(path); QFileInfo fi2(fi1.dir(),filename); if (fi2.exists()) return fi2.absoluteFilePath(); } // Otherwise, use kpsewhich to find the eps file. KProcess proc; proc << QStringLiteral("kpsewhich") << filename; proc.execute(); return QString::fromLocal8Bit(proc.readLine().trimmed()); } diff --git a/generators/dvi/psgs.h b/generators/dvi/psgs.h index 7dcec202a..5fdf47d9b 100644 --- a/generators/dvi/psgs.h +++ b/generators/dvi/psgs.h @@ -1,112 +1,112 @@ // -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; c-brace-offset: 0; -*- // // ghostscript_interface // // Part of KDVI - A framework for multipage text/gfx viewers // // (C) 2004 Stefan Kebekus // Distributed under the GPL #ifndef _PSGS_H_ #define _PSGS_H_ #include #include #include #include #include class QUrl; class PageNumber; class QPainter; class pageInfo { public: pageInfo(const QString& _PostScriptString); ~pageInfo(); pageInfo(const pageInfo &) = delete; pageInfo &operator=(const pageInfo &) = delete; QColor background; QColor permanentBackground; QString *PostScriptString; }; class ghostscript_interface : public QObject { Q_OBJECT public: ghostscript_interface(); ~ghostscript_interface() override; void clear(); // sets the PostScript which is used on a certain page - void setPostScript(const PageNumber& page, const QString& PostScript); + void setPostScript(const PageNumber page, const QString& PostScript); // sets path from additional postscript files may be read void setIncludePath(const QString &_includePath); // Sets the background color for a certain page. If permanent is false then the original // background color can be restored by calling restoreBackground(page). // The Option permanent = false is used when we want to display a different paper // color as the one specified in the dvi file. - void setBackgroundColor(const PageNumber& page, const QColor& background_color, bool permanent = true); + void setBackgroundColor(const PageNumber page, const QColor& background_color, bool permanent = true); // Restore the background to the color which was specified by the last call to setBackgroundColor() // With option permanent = true. - void restoreBackgroundColor(const PageNumber& page); + void restoreBackgroundColor(const PageNumber page); // Draws the graphics of the page into the painter, if possible. If // the page does not contain any graphics, nothing happens - void graphics(const PageNumber& page, double dpi, long magnification, QPainter* paint); + void graphics(const PageNumber page, double dpi, long magnification, QPainter* paint); // Returns the background color for a certain page. If no color was // set, Qt::white is returned. - QColor getBackgroundColor(const PageNumber& page) const; + QColor getBackgroundColor(const PageNumber page) const; QString *PostScriptHeaderString; /** This method tries to find the PostScript file 'filename' in the DVI file's directory (if the base-URL indicates that the DVI file is local), and, if that fails, uses kpsewhich to find the file. If the file is found, the full path (including file name) is returned. Otherwise, the method returns the first argument. TODO: use the DVI file's baseURL, once this is implemented. */ static QString locateEPSfile(const QString &filename, const QUrl &base); private: - void gs_generate_graphics_file(const PageNumber& page, const QString& filename, long magnification); + void gs_generate_graphics_file(const PageNumber page, const QString& filename, long magnification); QHash pageList; double resolution; // in dots per inch int pixel_page_w; // in pixels int pixel_page_h; // in pixels QString includePath; // Output device that ghostscript is supposed tp use. Default is // "png256". If that does not work, gs_generate_graphics_file will // automatically try other known device drivers. If no known output // device can be found, something is badly wrong. In that case, // "gsDevice" is set to an empty string, and // gs_generate_graphics_file will return immediately. QList::iterator gsDevice; // A list of known devices, set by the constructor. This includes // "png256", "pnm". If a device is found to not work, its name is // removed from the list, and another device name is tried. QStringList knownDevices; Q_SIGNALS: /** Passed through to the top-level kpart. */ void error( const QString &message, int duration ); }; #endif diff --git a/generators/dvi/simplePageSize.h b/generators/dvi/simplePageSize.h index 3ebeef10d..6e14eb176 100644 --- a/generators/dvi/simplePageSize.h +++ b/generators/dvi/simplePageSize.h @@ -1,165 +1,165 @@ // -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; c-brace-offset: 0; -*- // // simplePageSize.h // // Part of KVIEWSHELL - A framework for multipage text/gfx viewers // // (C) 2002-2004 Stefan Kebekus // Distributed under the GPL #ifndef SIMPLEPAGESIZE_H #define SIMPLEPAGESIZE_H #include "length.h" #include class QPaintDevice; /** \brief This class represents physical page sizes. This class represents page sizes. It contains nothing but two numbers, the page width, and page height, and a few utility functions that convert page sizes to pixel sizes and to compute the aspect ratio. A page with width<=1mm or height<=1mm is considered invalid. pageSize is a more elaborate class that is derived from SimplePageSize and knows about standard paper sizes. @author Stefan Kebekus @version 1.0 0 */ class SimplePageSize { public: /** Constructs an invalid SimplePageSize, with size 0x0mm */ SimplePageSize() { pageWidth.setLength_in_mm(0.0); pageHeight.setLength_in_mm(0.0); } /** Constructs a SimplePagesize with given page width and height in mm. Recall that if width or height is less or equal than 1mm, the page size is considered 'invalid' by the isValid() method. @param width @param height */ - SimplePageSize(const Length& width, const Length& height) { pageWidth = width; pageHeight = height; } + SimplePageSize(const Length width, const Length height) { pageWidth = width; pageHeight = height; } virtual ~SimplePageSize() {} /** \brief Sets the page width and height If width or height is less or equal than 1mm, the page size is considered 'invalid' by the isValid() method. @param width @param height */ - virtual void setPageSize(const Length& width, const Length& height) { pageWidth = width; pageHeight = height; } + virtual void setPageSize(const Length width, const Length height) { pageWidth = width; pageHeight = height; } /** \brief Returns the page width. */ Length width() const { return pageWidth; } /** \brief Returns the page height. */ Length height() const { return pageHeight; } /** \brief Aspect ratio @returns if the paper size is valid, this method returns the ratio width/height. Returns 1.0 otherwise. */ double aspectRatio() const { return isValid() ? (pageWidth/pageHeight) : 1.0; } /** \brief Converts the physical page size to a pixel size @param resolution in dots per inch @returns the pixel size, represented by a QSize. If the page size is invalid, the result is undefined. */ QSize sizeInPixel(double resolution) const {return QSize( (int)(resolution*pageWidth.getLength_in_inch() + 0.5), (int)(resolution*pageHeight.getLength_in_inch() + 0.5)); } /** \brief Zoom value required to scale to a certain height If the pageSize is valid, this method returns the zoom value required to scale the page size down to 'height' pixels on the currently used display. If the pageSize is invalid, an error message is printed, and an undefined value is returned. @param height target height in pixels @param pd the widget to be printed on. @returns the zoom value required to scale the page size down to 'height' pixels. If the pageSize is invalid, an undefined value is returned. */ double zoomForHeight(quint32 height, const QPaintDevice& pd) const; /** \brief Zoom value required to scale to a certain height If the pageSize is valid, this method returns the zoom value required to scale the page size down to 'width' pixels on the currently used display. If the pageSize is invalid, an error message is printed, and an undefined value is returned. @param width target width in pixels @param pd the widget to be printed on. @returns the zoom value required to scale the page size down to 'width' pixels. If the pageSize is invalid, an undefined value is returned. */ double zoomForWidth(quint32 width, const QPaintDevice& pd) const; /** \brief Returns a zoom to fit into a certain page size This method computes the larget zoom value such that *this, zoomed by the computed values fits into the page size 'target'. If *this or if target are invalid, or is this->isSmall() is true, an undefined value is returned. If height or width of this is nearly 0.0, a floating point exception may occur. */ double zoomToFitInto(const SimplePageSize &target) const; /** \brief Validity check @returns 'True' if the page width and height are both larger than 1mm */ bool isValid() const { return ( (pageWidth.getLength_in_mm() > 1.0) && (pageHeight.getLength_in_mm() > 1.0) ); } /** \brief Validity check: @returns 'True' if the page ares is less than 1.0 square mm */ bool isSmall() const { return (pageWidth.getLength_in_mm()*pageHeight.getLength_in_mm() < 1.0); } /** \brief Approximate equality @param size pageSize object to compare this object with @returns 'True' if height and width of the two objects differ by at most 2mm, 'false' otherwise */ bool isNearlyEqual(const SimplePageSize &size) const {return (pageWidth.isNearlyEqual(size.pageWidth) && pageHeight.isNearlyEqual(size.pageHeight)); } /** Test if paper size is higher than wide @returns 'True' if the paper size is higher than wide */ bool isPortrait() const { return (pageHeight >= pageWidth);} /** Rotates by 90 degrees @returns a SimplePageSize with height and width swapped. The original instance is unchanged. */ SimplePageSize rotate90() const { return SimplePageSize(pageHeight, pageWidth);} protected: Length pageWidth; Length pageHeight; }; #endif diff --git a/generators/dvi/textBox.h b/generators/dvi/textBox.h index e0ecb33ba..75994755e 100644 --- a/generators/dvi/textBox.h +++ b/generators/dvi/textBox.h @@ -1,66 +1,66 @@ // -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; c-brace-offset: 0; -*- // // Class: textBox // // Part of KDVI- A previewer for TeX DVI files. // // (C) 2004-2005 Stefan Kebekus. Distributed under the GPL. #ifndef _textbox_h_ #define _textbox_h_ #include #include /** Represents a rectangular region in a RenderedDocumentPage that contains text This trivial class is used in the RenderedDocumentPage class to give a non-graphical representation of text in a rendered document page. This is used, e.g. by text search and the text selection functions that need to know the contents and the position of text on a page @author Stefan Kebekus @version 1.0.0 */ class TextBox { public: /** \brief Default Constructor The default constructor leaves all fields uninitialized. */ TextBox() {} /** \brief Constructor Trivial constructor leaves that initialized all members. @param re value for the box @param lT valus for the text field */ - TextBox(const QRect& re, const QString& lT): box(re), text(lT) {} + TextBox(const QRect re, const QString& lT): box(re), text(lT) {} /** \brief Bounding box of the text or hyperlink This rectangle specifies where on the page the text or hyperlink is found. It uses the same coordinates that were used when the associated documentPage was rendered by the documentRenderer.drawPage() method. The contents of the box is graphically inverted to indicate marked text. */ QRect box; /** \brief Name of the region The text associated with the box is stored here. */ QString text; }; #endif diff --git a/generators/epub/converter.cpp b/generators/epub/converter.cpp index 87aaa2350..c35a3d985 100644 --- a/generators/epub/converter.cpp +++ b/generators/epub/converter.cpp @@ -1,458 +1,458 @@ /*************************************************************************** * Copyright (C) 2008 by Ely Levy * * * * 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. * ***************************************************************************/ #include "converter.h" #include #include #include #include #include #include // Because of the HACK #include #include #include #include #include #include #include using namespace Epub; Converter::Converter() : mTextDocument(nullptr) { } Converter::~Converter() { } // join the char * array into one QString QString _strPack(char **str, int size) { QString res; res = QString::fromUtf8(str[0]); for (int i=1;igetEpub(), type, &size); if (data) { emit addMetaData(key, _strPack((char **)data, size)); for (int i=0;iend(); bit = bit.next()) { for (QTextBlock::iterator fit = bit.begin(); !(fit.atEnd()); ++fit) { QTextFragment frag = fit.fragment(); if (frag.isValid() && frag.charFormat().isAnchor()) { QString hrefString = frag.charFormat().anchorHref(); // remove ./ or ../ // making it easier to compare, with links while(!hrefString.isNull() && ( hrefString.at(0) == QLatin1Char('.') || hrefString.at(0) == QLatin1Char('/')) ){ hrefString.remove(0,1); } QUrl href(hrefString); if (href.isValid() && !href.isEmpty()) { if (href.isRelative()) { // Inside document link if(!hrefString.indexOf(QLatin1Char('#'))) hrefString = name + hrefString; else if(QFileInfo(hrefString).path() == QLatin1String(".") && curDir != QLatin1String(".")) hrefString = curDir + QLatin1Char('/') + hrefString; // QTextCharFormat sometimes splits a link in two // if there's no white space between words & the first one is an anchor // consider whole word to be an anchor ++fit; int fragLen = frag.length(); if(!fit.atEnd() && ((fit.fragment().position() - frag.position()) == 1)) fragLen += fit.fragment().length(); --fit; _insert_local_links(hrefString, QPair(frag.position(), frag.position()+fragLen)); } else { // Outside document link Okular::BrowseAction *action = new Okular::BrowseAction(QUrl(href.toString())); emit addAction(action, frag.position(), frag.position() + frag.length()); } } const QStringList &names = frag.charFormat().anchorNames(); if (!names.empty()) { for (QStringList::const_iterator lit = names.constBegin(); lit != names.constEnd(); ++lit) { mSectionMap.insert(name + QLatin1Char('#') + *lit, bit); } } } // end anchor case } } } -void Converter::_insert_local_links(const QString &key, const QPair &value) +void Converter::_insert_local_links(const QString &key, const QPair value) { if(mLocalLinks.contains(key)){ mLocalLinks[key].append(value); } else { QVector< QPair > vec; vec.append(value); mLocalLinks.insert(key,vec); } } static QPoint calculateXYPosition( QTextDocument *document, int startPosition ) { const QTextBlock startBlock = document->findBlock( startPosition ); const QRectF startBoundingRect = document->documentLayout()->blockBoundingRect( startBlock ); QTextLayout *startLayout = startBlock.layout(); if (!startLayout) { qWarning() << "Start layout not found" << startLayout; return QPoint(); } int startPos = startPosition - startBlock.position(); const QTextLine startLine = startLayout->lineForTextPosition( startPos ); double x = startBoundingRect.x() ; double y = startBoundingRect.y() + startLine.y(); y = (int)y % 800; return QPoint(x,y); } QTextDocument* Converter::convert( const QString &fileName ) { EpubDocument *newDocument = new EpubDocument(fileName); if (!newDocument->isValid()) { emit error(i18n("Error while opening the EPub document."), -1); delete newDocument; return nullptr; } mTextDocument = newDocument; QTextCursor *_cursor = new QTextCursor( mTextDocument ); mLocalLinks.clear(); mSectionMap.clear(); // Emit the document meta data _emitData(Okular::DocumentInfo::Title, EPUB_TITLE); _emitData(Okular::DocumentInfo::Author, EPUB_CREATOR); _emitData(Okular::DocumentInfo::Subject, EPUB_SUBJECT); _emitData(Okular::DocumentInfo::Creator, EPUB_PUBLISHER); _emitData(Okular::DocumentInfo::Description, EPUB_DESCRIPTION); _emitData(Okular::DocumentInfo::CreationDate, EPUB_DATE); _emitData(Okular::DocumentInfo::Category, EPUB_TYPE); _emitData(Okular::DocumentInfo::Copyright, EPUB_RIGHTS); emit addMetaData( Okular::DocumentInfo::MimeType, QStringLiteral("application/epub+zip")); struct eiterator *it; // iterate over the book it = epub_get_iterator(mTextDocument->getEpub(), EITERATOR_SPINE, 0); // if the background color of the document is non-white it will be handled by QTextDocument::setHtml() bool firstPage = true; QVector movieAnnots; QVector soundActions; // HACK BEGIN Get the links without CSS to be blue // Remove if Qt ever gets fixed and the code in textdocumentgenerator.cpp works const QPalette orig = qApp->palette(); QPalette p = orig; p.setColor(QPalette::Link, Qt::blue); // HACK END const QSize videoSize(320, 240); do{ if(!epub_it_get_curr(it)) { continue; } movieAnnots.clear(); soundActions.clear(); const QString link = QString::fromUtf8(epub_it_get_curr_url(it)); mTextDocument->setCurrentSubDocument(link); QString htmlContent = QString::fromUtf8(epub_it_get_curr(it)); // as QTextCharFormat::anchorNames() ignores sections, replace it with

htmlContent.replace(QRegExp(QStringLiteral("< *section")),QStringLiteral("maxContentHeight(); const int maxWidth = mTextDocument->maxContentWidth(); QDomDocument dom; if(dom.setContent(htmlContent)) { QDomNodeList svgs = dom.elementsByTagName(QStringLiteral("svg")); if(!svgs.isEmpty()) { QList< QDomNode > imgNodes; for (int i = 0; i < svgs.length(); ++i) { QDomNodeList images = svgs.at(i).toElement().elementsByTagName(QStringLiteral("image")); for (int j = 0; j < images.length(); ++j) { QString lnk = images.at(i).toElement().attribute(QStringLiteral("xlink:href")); int ht = images.at(i).toElement().attribute(QStringLiteral("height")).toInt(); int wd = images.at(i).toElement().attribute(QStringLiteral("width")).toInt(); QImage img = mTextDocument->loadResource(QTextDocument::ImageResource,QUrl(lnk)).value(); if(ht == 0) ht = img.height(); if(wd == 0) wd = img.width(); if(ht > maxHeight) ht = maxHeight; if(wd > maxWidth) wd = maxWidth; mTextDocument->addResource(QTextDocument::ImageResource,QUrl(lnk),img); QDomDocument newDoc; newDoc.setContent(QStringLiteral("").arg(lnk).arg(ht).arg(wd)); imgNodes.append(newDoc.documentElement()); } for (const QDomNode &nd : qAsConst(imgNodes)) { svgs.at(i).parentNode().replaceChild(nd,svgs.at(i)); } } } // handle embedded videos QDomNodeList videoTags = dom.elementsByTagName(QStringLiteral("video")); while(!videoTags.isEmpty()) { QDomNodeList sourceTags = videoTags.at(0).toElement().elementsByTagName(QStringLiteral("source")); if(!sourceTags.isEmpty()) { QString lnk = sourceTags.at(0).toElement().attribute(QStringLiteral("src")); Okular::Movie *movie = new Okular::Movie(mTextDocument->loadResource(EpubDocument::MovieResource,QUrl(lnk)).toString()); movie->setSize(videoSize); movie->setShowControls(true); Okular::MovieAnnotation *annot = new Okular::MovieAnnotation; annot->setMovie(movie); movieAnnots.push_back(annot); QDomDocument tempDoc; tempDoc.setContent(QStringLiteral("

<video></video>
")); videoTags.at(0).parentNode().replaceChild(tempDoc.documentElement(),videoTags.at(0)); } } //handle embedded audio QDomNodeList audioTags = dom.elementsByTagName(QStringLiteral("audio")); while(!audioTags.isEmpty()) { QDomElement element = audioTags.at(0).toElement(); bool repeat = element.hasAttribute(QStringLiteral("loop")); QString lnk = element.attribute(QStringLiteral("src")); Okular::Sound *sound = new Okular::Sound(mTextDocument->loadResource( EpubDocument::AudioResource, QUrl(lnk)).toByteArray()); Okular::SoundAction *soundAction = new Okular::SoundAction(1.0,true,repeat,false,sound); soundActions.push_back(soundAction); QDomDocument tempDoc; tempDoc.setContent(QStringLiteral("
<audio></audio>
")); audioTags.at(0).parentNode().replaceChild(tempDoc.documentElement(),audioTags.at(0)); } htmlContent = dom.toString(); } // HACK BEGIN qApp->setPalette(p); // HACK END QTextBlock before; if(firstPage) { mTextDocument->setHtml(htmlContent); firstPage = false; before = mTextDocument->begin(); } else { before = _cursor->block(); _cursor->insertHtml(htmlContent); } // HACK BEGIN qApp->setPalette(orig); // HACK END QTextCursor csr(before); // a temporary cursor pointing at the begin of the last inserted block int index = 0; while( !movieAnnots.isEmpty() && !(csr = mTextDocument->find(QStringLiteral(""),csr)).isNull() ) { const int posStart = csr.position(); const QPoint startPoint = calculateXYPosition(mTextDocument, posStart); QImage img(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("okular/pics/okular-epub-movie.png"))); img = img.scaled(videoSize); csr.insertImage(img); const int posEnd = csr.position(); const QRect videoRect(startPoint,videoSize); movieAnnots[index]->setBoundingRectangle(Okular::NormalizedRect(videoRect,mTextDocument->pageSize().width(), mTextDocument->pageSize().height())); emit addAnnotation(movieAnnots[index++],posStart,posEnd); csr.movePosition(QTextCursor::NextWord); } csr = QTextCursor(before); index = 0; const QString keyToSearch(QStringLiteral("")); while( !soundActions.isEmpty() && !(csr = mTextDocument->find(keyToSearch, csr)).isNull() ) { const int posStart = csr.position() - keyToSearch.size(); const QImage img(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("okular/pics/okular-epub-sound-icon.png"))); csr.insertImage(img); const int posEnd = csr.position(); qDebug() << posStart << posEnd;; emit addAction(soundActions[index++],posStart,posEnd); csr.movePosition(QTextCursor::NextWord); } mSectionMap.insert(link, before); _handle_anchors(before, link); const int page = mTextDocument->pageCount(); // it will clear the previous format // useful when the last line had a bullet _cursor->insertBlock(QTextBlockFormat()); while(mTextDocument->pageCount() == page) _cursor->insertText(QStringLiteral("\n")); } while (epub_it_get_next(it)); epub_free_iterator(it); // handle toc struct titerator *tit; // FIXME: support other method beside NAVMAP and GUIDE tit = epub_get_titerator(mTextDocument->getEpub(), TITERATOR_NAVMAP, 0); if (!tit) tit = epub_get_titerator(mTextDocument->getEpub(), TITERATOR_GUIDE, 0); if (tit) { do { if (epub_tit_curr_valid(tit)) { char *clink = epub_tit_get_curr_link(tit); QString link = QString::fromUtf8(clink); char *label = epub_tit_get_curr_label(tit); QTextBlock block = mTextDocument->begin(); // must point somewhere if (mSectionMap.contains(link)) { block = mSectionMap.value(link); } else { // load missing resource char *data = nullptr; //epub_get_data can't handle whitespace url encodings QByteArray ba = link.replace(QLatin1String("%20"), QLatin1String(" ")).toLatin1(); const char *clinkClean = ba.data(); int size = epub_get_data(mTextDocument->getEpub(), clinkClean, &data); if (data) { _cursor->insertBlock(); // try to load as image and if not load as html block = _cursor->block(); QImage image; mSectionMap.insert(link, block); if (image.loadFromData((unsigned char *)data, size)) { mTextDocument->addResource(QTextDocument::ImageResource, QUrl(link), image); _cursor->insertImage(link); } else { _cursor->insertHtml(QString::fromUtf8(data)); // Add anchors to hashes _handle_anchors(block, link); } // Start new file in a new page int page = mTextDocument->pageCount(); while(mTextDocument->pageCount() == page) _cursor->insertText(QStringLiteral("\n")); } free(data); } if (block.isValid()) { // be sure we actually got a block emit addTitle(epub_tit_get_curr_depth(tit), QString::fromUtf8(label), block); } else { qDebug() << "Error: no block found for"<< link; } if (clink) free(clink); if (label) free(label); } } while (epub_tit_next(tit)); epub_free_titerator(tit); } else { qDebug() << "no toc found"; } // adding link actions QHashIterator > > hit(mLocalLinks); while (hit.hasNext()) { hit.next(); const QTextBlock block = mSectionMap.value(hit.key()); for (int i = 0; i < hit.value().size(); ++i) { if (block.isValid()) { // be sure we actually got a block Okular::DocumentViewport viewport = calculateViewport(mTextDocument, block); Okular::GotoAction *action = new Okular::GotoAction(QString(), viewport); emit addAction(action, hit.value()[i].first, hit.value()[i].second); } else { qDebug() << "Error: no block found for "<< hit.key(); } } } delete _cursor; return mTextDocument; } diff --git a/generators/epub/converter.h b/generators/epub/converter.h index 20dea8f88..4c19be005 100644 --- a/generators/epub/converter.h +++ b/generators/epub/converter.h @@ -1,42 +1,42 @@ /*************************************************************************** * Copyright (C) 2008 by Ely Levy * * * * 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. * ***************************************************************************/ #ifndef EPUB_CONVERTER_H #define EPUB_CONVERTER_H #include #include #include "epubdocument.h" namespace Epub { class Converter : public Okular::TextDocumentConverter { Q_OBJECT public: Converter(); ~Converter() override; QTextDocument *convert( const QString &fileName ) override; private: void _emitData(Okular::DocumentInfo::Key key, enum epub_metadata type); void _handle_anchors(const QTextBlock &start, const QString &name); - void _insert_local_links(const QString &key, const QPair &value); + void _insert_local_links(const QString &key, const QPair value); EpubDocument *mTextDocument; QHash mSectionMap; QHash > > mLocalLinks; }; } #endif diff --git a/generators/ooo/formatproperty.cpp b/generators/ooo/formatproperty.cpp index 4d1eed557..65c3e8587 100644 --- a/generators/ooo/formatproperty.cpp +++ b/generators/ooo/formatproperty.cpp @@ -1,441 +1,441 @@ /*************************************************************************** * Copyright (C) 2006 by Tobias Koenig * * * * 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. * ***************************************************************************/ #include "formatproperty.h" #include #include "styleinformation.h" using namespace OOO; FontFormatProperty::FontFormatProperty() : mFamily( QStringLiteral("Nimbus Sans L") ) { } void FontFormatProperty::apply( QTextFormat *format ) const { format->setProperty( QTextFormat::FontFamily, mFamily ); } void FontFormatProperty::setFamily( const QString &name ) { mFamily = name; } ParagraphFormatProperty::ParagraphFormatProperty() : mPageNumber( 0 ), mWritingMode( LRTB ), mAlignment( Qt::AlignLeft ), mHasAlignment( false ) { } void ParagraphFormatProperty::apply( QTextFormat *format ) const { if ( mWritingMode == LRTB || mWritingMode == TBLR || mWritingMode == LR || mWritingMode == TB ) format->setLayoutDirection( Qt::LeftToRight ); else format->setLayoutDirection( Qt::RightToLeft ); if ( mHasAlignment ) { static_cast( format )->setAlignment( mAlignment ); } format->setProperty( QTextFormat::FrameWidth, 595 ); static_cast( format )->setLeftMargin( mLeftMargin ); if ( mBackgroundColor.isValid() ) format->setBackground( mBackgroundColor ); } void ParagraphFormatProperty::setPageNumber( int number ) { mPageNumber = number; } void ParagraphFormatProperty::setWritingMode( WritingMode mode ) { mWritingMode = mode; } bool ParagraphFormatProperty::writingModeIsRightToLeft() const { return ( ( mWritingMode == RLTB ) || ( mWritingMode == TBRL ) || ( mWritingMode == RL ) ); } void ParagraphFormatProperty::setTextAlignment( Qt::Alignment alignment ) { mHasAlignment = true; mAlignment = alignment; } void ParagraphFormatProperty::setBackgroundColor( const QColor &color ) { mBackgroundColor = color; } void ParagraphFormatProperty::setLeftMargin( const qreal margin ) { mLeftMargin = margin; } TextFormatProperty::TextFormatProperty() : mStyleInformation( nullptr ), mHasFontSize( false ), mFontWeight( -1 ), mFontStyle( -1 ), mTextPosition( 0 ) { } TextFormatProperty::TextFormatProperty( const StyleInformation *information ) : mStyleInformation( information ), mHasFontSize( false ), mFontWeight( -1 ), mFontStyle( -1 ), mTextPosition( 0 ) { } void TextFormatProperty::apply( QTextCharFormat *format ) const { if ( !mFontName.isEmpty() ) { if ( mStyleInformation ) { const FontFormatProperty property = mStyleInformation->fontProperty( mFontName ); property.apply( format ); } } if ( mFontWeight != -1 ) { QFont font = format->font(); font.setWeight( mFontWeight ); format->setFont( font ); } if ( mHasFontSize ) { QFont font = format->font(); font.setPointSize( mFontSize ); format->setFont( font ); } if ( mFontStyle != -1 ) { QFont font = format->font(); font.setStyle( (QFont::Style)mFontStyle ); format->setFont( font ); } if ( mColor.isValid() ) format->setForeground( mColor ); if ( mBackgroundColor.isValid() ) format->setBackground( mBackgroundColor ); // TODO: get FontFormatProperty and apply it // TODO: how to set the base line?!? } void TextFormatProperty::setFontSize( int size ) { mHasFontSize = true; mFontSize = size; } void TextFormatProperty::setFontName( const QString &name ) { mFontName = name; } void TextFormatProperty::setFontWeight( int weight ) { mFontWeight = weight; } void TextFormatProperty::setFontStyle( int style ) { mFontStyle = style; } void TextFormatProperty::setTextPosition( int position ) { mTextPosition = position; } void TextFormatProperty::setColor( const QColor &color ) { mColor = color; } void TextFormatProperty::setBackgroundColor( const QColor &color ) { mBackgroundColor = color; } StyleFormatProperty::StyleFormatProperty() : mStyleInformation( nullptr ), mDefaultStyle( false ) { } StyleFormatProperty::StyleFormatProperty( const StyleInformation *information ) : mStyleInformation( information ), mDefaultStyle( false ) { } void StyleFormatProperty::applyBlock( QTextBlockFormat *format ) const { if ( !mDefaultStyle && !mFamily.isEmpty() && mStyleInformation ) { const StyleFormatProperty property = mStyleInformation->styleProperty( mFamily ); property.applyBlock( format ); } if ( !mParentStyleName.isEmpty() && mStyleInformation ) { const StyleFormatProperty property = mStyleInformation->styleProperty( mParentStyleName ); property.applyBlock( format ); } mParagraphFormat.apply( format ); } void StyleFormatProperty::applyText( QTextCharFormat *format ) const { if ( !mDefaultStyle && !mFamily.isEmpty() && mStyleInformation ) { const StyleFormatProperty property = mStyleInformation->styleProperty( mFamily ); property.applyText( format ); } if ( !mParentStyleName.isEmpty() && mStyleInformation ) { const StyleFormatProperty property = mStyleInformation->styleProperty( mParentStyleName ); property.applyText( format ); } mTextFormat.apply( format ); } void StyleFormatProperty::applyTableColumn( QTextTableFormat *format ) const { if ( !mDefaultStyle && !mFamily.isEmpty() && mStyleInformation ) { const StyleFormatProperty property = mStyleInformation->styleProperty( mFamily ); property.applyTableColumn( format ); } if ( !mParentStyleName.isEmpty() && mStyleInformation ) { const StyleFormatProperty property = mStyleInformation->styleProperty( mParentStyleName ); property.applyTableColumn( format ); } mTableColumnFormat.apply( format ); } void StyleFormatProperty::applyTableCell( QTextBlockFormat *format ) const { if ( !mDefaultStyle && !mFamily.isEmpty() && mStyleInformation ) { const StyleFormatProperty property = mStyleInformation->styleProperty( mFamily ); property.applyTableCell( format ); } if ( !mParentStyleName.isEmpty() && mStyleInformation ) { const StyleFormatProperty property = mStyleInformation->styleProperty( mParentStyleName ); property.applyTableCell( format ); } mTableCellFormat.apply( format ); } void StyleFormatProperty::setParentStyleName( const QString &parentStyleName ) { mParentStyleName = parentStyleName; } QString StyleFormatProperty::parentStyleName() const { return mParentStyleName; } void StyleFormatProperty::setFamily( const QString &family ) { mFamily = family; } void StyleFormatProperty::setDefaultStyle( bool defaultStyle ) { mDefaultStyle = defaultStyle; } void StyleFormatProperty::setMasterPageName( const QString &masterPageName ) { mMasterPageName = masterPageName; } void StyleFormatProperty::setParagraphFormat( const ParagraphFormatProperty &format ) { mParagraphFormat = format; } void StyleFormatProperty::setTextFormat( const TextFormatProperty &format ) { mTextFormat = format; } -void StyleFormatProperty::setTableColumnFormat( const TableColumnFormatProperty &format ) +void StyleFormatProperty::setTableColumnFormat( const TableColumnFormatProperty format ) { mTableColumnFormat = format; } void StyleFormatProperty::setTableCellFormat( const TableCellFormatProperty &format ) { mTableCellFormat = format; } PageFormatProperty::PageFormatProperty() : mHeight( 0.0 ), mWidth( 0.0 ) { } void PageFormatProperty::apply( QTextFormat *format ) const { format->setProperty( QTextFormat::BlockBottomMargin, mBottomMargin ); format->setProperty( QTextFormat::BlockLeftMargin, mLeftMargin ); format->setProperty( QTextFormat::BlockTopMargin, mTopMargin ); format->setProperty( QTextFormat::BlockRightMargin, mRightMargin ); format->setProperty( QTextFormat::FrameWidth, mWidth ); format->setProperty( QTextFormat::FrameHeight, mHeight ); } void PageFormatProperty::setPageUsage( PageUsage usage ) { mPageUsage = usage; } void PageFormatProperty::setBottomMargin( double margin ) { mBottomMargin = margin; } void PageFormatProperty::setLeftMargin( double margin ) { mLeftMargin = margin; } void PageFormatProperty::setTopMargin( double margin ) { mTopMargin = margin; } void PageFormatProperty::setRightMargin( double margin ) { mRightMargin = margin; } void PageFormatProperty::setHeight( double height ) { mHeight = height; } void PageFormatProperty::setWidth( double width ) { mWidth = width; } void PageFormatProperty::setPrintOrientation( PrintOrientation orientation ) { mPrintOrientation = orientation; } double PageFormatProperty::width() const { return mWidth; } double PageFormatProperty::height() const { return mHeight; } double PageFormatProperty::margin() const { return mLeftMargin; } ListFormatProperty::ListFormatProperty() : mType( Number ) { mIndents.resize( 10 ); } ListFormatProperty::ListFormatProperty( Type type ) : mType( type ) { mIndents.resize( 10 ); } void ListFormatProperty::apply( QTextListFormat *format, int level ) const { if ( mType == Number ) format->setStyle( QTextListFormat::ListDecimal ); else { format->setStyle( QTextListFormat::ListDisc ); if ( level > 0 && level < 10 ) format->setIndent( qRound( mIndents[ level ] ) ); } } void ListFormatProperty::addItem( int level, double indent ) { if ( level < 0 || level >= 10 ) return; mIndents[ level ] = indent; } TableColumnFormatProperty::TableColumnFormatProperty() : mWidth( 0 ), isValid( false ) { } void TableColumnFormatProperty::apply( QTextTableFormat *format ) const { if ( ! isValid ) { return; } QVector lengths = format->columnWidthConstraints(); lengths.append( QTextLength( QTextLength::FixedLength, mWidth ) ); format->setColumnWidthConstraints( lengths ); } void TableColumnFormatProperty::setWidth( double width ) { mWidth = width; isValid = true; } TableCellFormatProperty::TableCellFormatProperty() : mPadding( 0 ), mHasAlignment( false ) { } void TableCellFormatProperty::apply( QTextBlockFormat *format ) const { if ( mBackgroundColor.isValid() ) format->setBackground( mBackgroundColor ); if ( mHasAlignment ) format->setAlignment( mAlignment ); } void TableCellFormatProperty::setBackgroundColor( const QColor &color ) { mBackgroundColor = color; } void TableCellFormatProperty::setPadding( double padding ) { mPadding = padding; } void TableCellFormatProperty::setAlignment( Qt::Alignment alignment ) { mAlignment = alignment; mHasAlignment = true; } diff --git a/generators/ooo/formatproperty.h b/generators/ooo/formatproperty.h index 3b13f4d6e..3c08be452 100644 --- a/generators/ooo/formatproperty.h +++ b/generators/ooo/formatproperty.h @@ -1,239 +1,239 @@ /*************************************************************************** * Copyright (C) 2006 by Tobias Koenig * * * * 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. * ***************************************************************************/ #ifndef OOO_FORMATPROPERTY_H #define OOO_FORMATPROPERTY_H #include #include class QTextBlockFormat; class QTextCharFormat; class QTextFormat; class QTextListFormat; class QTextTableFormat; namespace OOO { class StyleInformation; class FontFormatProperty { public: FontFormatProperty(); void apply( QTextFormat *format ) const; void setFamily( const QString &name ); private: QString mFamily; }; class ParagraphFormatProperty { public: enum WritingMode { LRTB, RLTB, TBRL, TBLR, LR, RL, TB, PAGE }; ParagraphFormatProperty(); void apply( QTextFormat *format ) const; void setPageNumber( int number ); void setWritingMode( WritingMode mode ); void setTextAlignment( Qt::Alignment alignment ); void setBackgroundColor( const QColor &color ); void setLeftMargin( const qreal margin ); bool writingModeIsRightToLeft() const; private: int mPageNumber; WritingMode mWritingMode; Qt::Alignment mAlignment; bool mHasAlignment; QColor mBackgroundColor; qreal mLeftMargin; }; class TextFormatProperty { public: TextFormatProperty(); explicit TextFormatProperty( const StyleInformation *information ); void apply( QTextCharFormat *format ) const; void setFontSize( int size ); void setFontName( const QString &name ); void setFontWeight( int weight ); void setFontStyle( int style ); void setTextPosition( int position ); void setColor( const QColor &color ); void setBackgroundColor( const QColor &color ); private: const StyleInformation *mStyleInformation; int mFontSize; bool mHasFontSize; int mFontWeight; QString mFontName; int mFontStyle; int mTextPosition; QColor mColor; QColor mBackgroundColor; }; class PageFormatProperty { public: enum PageUsage { All, Left, Right, Mirrored }; enum PrintOrientation { Portrait, Landscape }; PageFormatProperty(); void apply( QTextFormat *format ) const; void setPageUsage( PageUsage usage ); void setBottomMargin( double margin ); void setLeftMargin( double margin ); void setTopMargin( double margin ); void setRightMargin( double margin ); void setHeight( double height ); void setWidth( double width ); void setPrintOrientation( PrintOrientation orientation ); double width() const; double height() const; double margin() const; private: PageUsage mPageUsage; double mBottomMargin; double mLeftMargin; double mTopMargin; double mRightMargin; double mHeight; double mWidth; PrintOrientation mPrintOrientation; }; class ListFormatProperty { public: enum Type { Number, Bullet }; ListFormatProperty(); explicit ListFormatProperty( Type type ); void apply( QTextListFormat *format, int level ) const; void addItem( int level, double indent = 0 ); private: Type mType; QVector mIndents; }; class TableColumnFormatProperty { public: TableColumnFormatProperty(); void apply( QTextTableFormat *format ) const; void setWidth( double width ); private: double mWidth; bool isValid; }; class TableCellFormatProperty { public: TableCellFormatProperty(); void apply( QTextBlockFormat *format ) const; void setBackgroundColor( const QColor &color ); void setPadding( double padding ); void setAlignment( Qt::Alignment alignment ); private: QColor mBackgroundColor; double mPadding; Qt::Alignment mAlignment; bool mHasAlignment; }; class StyleFormatProperty { public: StyleFormatProperty(); explicit StyleFormatProperty( const StyleInformation *information ); void applyBlock( QTextBlockFormat *format ) const; void applyText( QTextCharFormat *format ) const; void applyTableColumn( QTextTableFormat *format ) const; void applyTableCell( QTextBlockFormat *format ) const; void setParentStyleName( const QString &parentStyleName ); QString parentStyleName() const; void setFamily( const QString &family ); void setDefaultStyle( bool defaultStyle ); void setMasterPageName( const QString &masterPageName ); void setParagraphFormat( const ParagraphFormatProperty &format ); void setTextFormat( const TextFormatProperty &format ); - void setTableColumnFormat( const TableColumnFormatProperty &format ); + void setTableColumnFormat( const TableColumnFormatProperty format ); void setTableCellFormat( const TableCellFormatProperty &format ); private: QString mParentStyleName; QString mFamily; QString mMasterPageName; ParagraphFormatProperty mParagraphFormat; TextFormatProperty mTextFormat; TableColumnFormatProperty mTableColumnFormat; TableCellFormatProperty mTableCellFormat; const StyleInformation *mStyleInformation; bool mDefaultStyle; }; } #endif diff --git a/generators/xps/generator_xps.cpp b/generators/xps/generator_xps.cpp index 48b9feeae..ef5111a8c 100644 --- a/generators/xps/generator_xps.cpp +++ b/generators/xps/generator_xps.cpp @@ -1,2239 +1,2239 @@ /* Copyright (C) 2006, 2009 Brad Hards 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 "generator_xps.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include OKULAR_EXPORT_PLUGIN(XpsGenerator, "libokularGenerator_xps.json") Q_DECLARE_METATYPE( QGradient* ) Q_DECLARE_METATYPE( XpsPathFigure* ) Q_DECLARE_METATYPE( XpsPathGeometry* ) // From Qt4 static int hex2int(char hex) { QChar hexchar = QLatin1Char(hex); int v; if (hexchar.isDigit()) v = hexchar.digitValue(); else if (hexchar >= QLatin1Char('A') && hexchar <= QLatin1Char('F')) v = hexchar.cell() - 'A' + 10; else if (hexchar >= QLatin1Char('a') && hexchar <= QLatin1Char('f')) v = hexchar.cell() - 'a' + 10; else v = -1; return v; } // Modified from Qt4 static QColor hexToRgba(const QByteArray &name) { const int len = name.length(); if (len == 0 || name[0] != '#') return QColor(); int r, g, b; int a = 255; if (len == 7) { r = (hex2int(name[1]) << 4) + hex2int(name[2]); g = (hex2int(name[3]) << 4) + hex2int(name[4]); b = (hex2int(name[5]) << 4) + hex2int(name[6]); } else if (len == 9) { a = (hex2int(name[1]) << 4) + hex2int(name[2]); r = (hex2int(name[3]) << 4) + hex2int(name[4]); g = (hex2int(name[5]) << 4) + hex2int(name[6]); b = (hex2int(name[7]) << 4) + hex2int(name[8]); } else { r = g = b = -1; } if ((uint)r > 255 || (uint)g > 255 || (uint)b > 255) { return QColor(); } return QColor(r,g,b,a); } static QRectF stringToRectF( const QString &data ) { QStringList numbers = data.split(QLatin1Char(',')); QPointF origin( numbers.at(0).toDouble(), numbers.at(1).toDouble() ); QSizeF size( numbers.at(2).toDouble(), numbers.at(3).toDouble() ); return QRectF( origin, size ); } static bool parseGUID( const QString &guidString, unsigned short guid[16]) { if (guidString.length() <= 35) { return false; } // Maps bytes to positions in guidString const static int indexes[] = {6, 4, 2, 0, 11, 9, 16, 14, 19, 21, 24, 26, 28, 30, 32, 34}; for (int i = 0; i < 16; i++) { int hex1 = hex2int(guidString[indexes[i]].cell()); int hex2 = hex2int(guidString[indexes[i]+1].cell()); if ((hex1 < 0) || (hex2 < 0)) { return false; } guid[i] = hex1 * 16 + hex2; } return true; } // Read next token of abbreviated path data static bool nextAbbPathToken(AbbPathToken *token) { int *curPos = &token->curPos; QString data = token->data; while ((*curPos < data.length()) && (data.at(*curPos).isSpace())) { (*curPos)++; } if (*curPos == data.length()) { token->type = abtEOF; return true; } QChar ch = data.at(*curPos); if (ch.isNumber() || (ch == QLatin1Char('+')) || (ch == QLatin1Char('-'))) { int start = *curPos; while ((*curPos < data.length()) && (!data.at(*curPos).isSpace()) && (data.at(*curPos) != QLatin1Char(',') && (!data.at(*curPos).isLetter() || data.at(*curPos) == QLatin1Char('e')))) { (*curPos)++; } token->number = data.midRef(start, *curPos - start).toDouble(); token->type = abtNumber; } else if (ch == QLatin1Char(',')) { token->type = abtComma; (*curPos)++; } else if (ch.isLetter()) { token->type = abtCommand; token->command = data.at(*curPos).cell(); (*curPos)++; } else { (*curPos)++; return false; } return true; } /** Read point (two reals delimited by comma) from abbreviated path data */ -static QPointF getPointFromString(AbbPathToken *token, bool relative, const QPointF ¤tPosition) { +static QPointF getPointFromString(AbbPathToken *token, bool relative, const QPointF currentPosition) { //TODO Check grammar QPointF result; result.rx() = token->number; nextAbbPathToken(token); nextAbbPathToken(token); // , result.ry() = token->number; nextAbbPathToken(token); if (relative) { result += currentPosition; } return result; } /** Read point (two reals delimited by comma) from string */ static QPointF getPointFromString(const QString &string) { const int commaPos = string.indexOf(QLatin1Char(QLatin1Char(','))); if (commaPos == -1 || string.indexOf(QLatin1Char(QLatin1Char(',')), commaPos + 1) != -1) return QPointF(); QPointF result; bool ok = false; QStringRef ref = string.midRef(0, commaPos); result.setX(QString::fromRawData(ref.constData(), ref.count()).toDouble(&ok)); if (!ok) return QPointF(); ref = string.midRef(commaPos + 1); result.setY(QString::fromRawData(ref.constData(), ref.count()).toDouble(&ok)); if (!ok) return QPointF(); return result; } static Qt::FillRule fillRuleFromString( const QString &data, Qt::FillRule def = Qt::OddEvenFill ) { if ( data == QLatin1String( "EvenOdd" ) ) { return Qt::OddEvenFill; } else if ( data == QLatin1String( "NonZero" ) ) { return Qt::WindingFill; } return def; } /** Parse an abbreviated path "Data" description \param data the string containing the whitespace separated values \see XPS specification 4.2.3 and Appendix G */ static QPainterPath parseAbbreviatedPathData( const QString &data) { QPainterPath path; AbbPathToken token; token.data = data; token.curPos = 0; nextAbbPathToken(&token); // Used by Smooth cubic curve (command s) char lastCommand = ' '; QPointF lastSecondControlPoint; while (true) { if (token.type != abtCommand) { if (token.type != abtEOF) { qCWarning(OkularXpsDebug).nospace() << "Error in parsing abbreviated path data (" << token.type << "@" << token.curPos << "): " << data; } return path; } char command = QChar::fromLatin1(token.command).toLower().cell(); bool isRelative = QChar::fromLatin1(token.command).isLower(); QPointF currPos = path.currentPosition(); nextAbbPathToken(&token); switch (command) { case 'f': int rule; rule = (int)token.number; if (rule == 0) { path.setFillRule(Qt::OddEvenFill); } else if (rule == 1) { // In xps specs rule 1 means NonZero fill. I think it's equivalent to WindingFill but I'm not sure path.setFillRule(Qt::WindingFill); } nextAbbPathToken(&token); break; case 'm': // Move while (token.type == abtNumber) { QPointF point = getPointFromString(&token, isRelative, currPos); path.moveTo(point); } break; case 'l': // Line while (token.type == abtNumber) { QPointF point = getPointFromString(&token, isRelative, currPos); path.lineTo(point); } break; case 'h': // Horizontal line while (token.type == abtNumber) { double x = token.number; if ( isRelative ) x += path.currentPosition().x(); path.lineTo(x, path.currentPosition().y()); nextAbbPathToken(&token); } break; case 'v': // Vertical line while (token.type == abtNumber) { double y = token.number; if ( isRelative ) y += path.currentPosition().y(); path.lineTo(path.currentPosition().x(), y); nextAbbPathToken(&token); } break; case 'c': // Cubic bezier curve while (token.type == abtNumber) { QPointF firstControl = getPointFromString(&token, isRelative, currPos); QPointF secondControl = getPointFromString(&token, isRelative, currPos); QPointF endPoint = getPointFromString(&token, isRelative, currPos); path.cubicTo(firstControl, secondControl, endPoint); lastSecondControlPoint = secondControl; } break; case 'q': // Quadratic bezier curve while (token.type == abtNumber) { QPointF point1 = getPointFromString(&token, isRelative, currPos); QPointF point2 = getPointFromString(&token, isRelative, currPos); path.quadTo(point1, point2); } break; case 's': // Smooth cubic bezier curve while (token.type == abtNumber) { QPointF firstControl; if ((lastCommand == 's') || (lastCommand == 'c')) { firstControl = lastSecondControlPoint + (lastSecondControlPoint + path.currentPosition()); } else { firstControl = path.currentPosition(); } QPointF secondControl = getPointFromString(&token, isRelative, currPos); QPointF endPoint = getPointFromString(&token, isRelative, currPos); path.cubicTo(firstControl, secondControl, endPoint); } break; case 'a': // Arc //TODO Implement Arc drawing while (token.type == abtNumber) { /*QPointF rp =*/ getPointFromString(&token, isRelative, currPos); /*double r = token.number;*/ nextAbbPathToken(&token); /*double fArc = token.number; */ nextAbbPathToken(&token); /*double fSweep = token.number; */ nextAbbPathToken(&token); /*QPointF point = */getPointFromString(&token, isRelative, currPos); } break; case 'z': // Close path path.closeSubpath(); break; } lastCommand = command; } return path; } /** Parse a "Matrix" attribute string \param csv the comma separated list of values \return the QTransform corresponding to the affine transform given in the attribute \see XPS specification 7.4.1 */ static QTransform attsToMatrix( const QString &csv ) { QStringList values = csv.split( QLatin1Char(',') ); if ( values.count() != 6 ) { return QTransform(); // that is an identity matrix - no effect } return QTransform( values.at(0).toDouble(), values.at(1).toDouble(), values.at(2).toDouble(), values.at(3).toDouble(), values.at(4).toDouble(), values.at(5).toDouble() ); } /** \return Brush with given color or brush specified by reference to resource */ static QBrush parseRscRefColorForBrush( const QString &data ) { if (data[0] == QLatin1Char('{')) { //TODO qCWarning(OkularXpsDebug) << "Reference" << data; return QBrush(); } else { return QBrush( hexToRgba( data.toLatin1() ) ); } } /** \return Pen with given color or Pen specified by reference to resource */ static QPen parseRscRefColorForPen( const QString &data ) { if (data[0] == QLatin1Char('{')) { //TODO qCWarning(OkularXpsDebug) << "Reference" << data; return QPen(); } else { return QPen( hexToRgba( data.toLatin1() ) ); } } /** \return Matrix specified by given data or by referenced dictionary */ static QTransform parseRscRefMatrix( const QString &data ) { if (data[0] == QLatin1Char('{')) { //TODO qCWarning(OkularXpsDebug) << "Reference" << data; return QTransform(); } else { return attsToMatrix( data ); } } /** \return Path specified by given data or by referenced dictionary */ static QPainterPath parseRscRefPath( const QString &data ) { if (data[0] == QLatin1Char('{')) { //TODO qCWarning(OkularXpsDebug) << "Reference" << data; return QPainterPath(); } else { return parseAbbreviatedPathData( data ); } } /** \return The path of the entry */ static QString entryPath( const QString &entry ) { const QChar slash = QChar::fromLatin1( '/' ); const int index = entry.lastIndexOf( slash ); QString ret = entry.mid( 0, index ); if ( index > 0 ) { ret.append( slash ); } if ( !ret.startsWith( slash ) ) ret.prepend( slash ); return ret; } /** \return The path of the entry */ static QString entryPath( const KZipFileEntry* entry ) { return entryPath( entry->path() ); } /** \return The absolute path of the \p location, according to \p path if it's non-absolute */ static QString absolutePath( const QString &path, const QString &location ) { QString retPath; if ( location.startsWith(QLatin1Char( '/' ) )) { // already absolute retPath = location; } else { QUrl u = QUrl::fromLocalFile(path); QUrl u2 = QUrl(location); retPath = u.resolved(u2).toDisplayString(QUrl::PreferLocalFile); } // it seems paths & file names can also be percent-encoded // (XPS won't ever finish surprising me) if ( retPath.contains( QLatin1Char( '%' ) ) ) { retPath = QUrl::fromPercentEncoding( retPath.toUtf8() ); } return retPath; } /** Read the content of an archive entry in both the cases: a) single file + foobar b) directory + foobar/ + [0].piece + [1].piece + ... + [x].last.piece \see XPS specification 10.1.2 */ static QByteArray readFileOrDirectoryParts( const KArchiveEntry *entry, QString *pathOfFile = nullptr ) { QByteArray data; if ( entry->isDirectory() ) { const KArchiveDirectory* relDir = static_cast( entry ); QStringList entries = relDir->entries(); std::sort(entries.begin(), entries.end()); for ( const QString &entry : qAsConst(entries) ) { const KArchiveEntry* relSubEntry = relDir->entry( entry ); if ( !relSubEntry->isFile() ) continue; const KZipFileEntry* relSubFile = static_cast( relSubEntry ); data.append( relSubFile->data() ); } } else { const KZipFileEntry* relFile = static_cast( entry ); data.append( relFile->data() ); if ( pathOfFile ) { *pathOfFile = entryPath( relFile ); } } return data; } /** Load the resource \p fileName from the specified \p archive using the case sensitivity \p cs */ static const KArchiveEntry* loadEntry( KZip *archive, const QString &fileName, Qt::CaseSensitivity cs ) { // first attempt: loading the entry straight as requested const KArchiveEntry* entry = archive->directory()->entry( fileName ); // in case sensitive mode, or if we actually found something, return what we found if ( cs == Qt::CaseSensitive || entry ) { return entry; } QString path; QString entryName; const int index = fileName.lastIndexOf( QChar::fromLatin1( '/' ) ); if ( index > 0 ) { path = fileName.left( index ); entryName = fileName.mid( index + 1 ); } else { path = QLatin1Char('/'); entryName = fileName; } const KArchiveEntry * newEntry = archive->directory()->entry( path ); if ( newEntry->isDirectory() ) { const KArchiveDirectory* relDir = static_cast< const KArchiveDirectory * >( newEntry ); QStringList relEntries = relDir->entries(); std::sort(relEntries.begin(), relEntries.end()); for ( const QString &relEntry : qAsConst(relEntries) ) { if ( relEntry.compare( entryName, Qt::CaseInsensitive ) == 0 ) { return relDir->entry( relEntry ); } } } return nullptr; } static const KZipFileEntry* loadFile( KZip *archive, const QString &fileName, Qt::CaseSensitivity cs ) { const KArchiveEntry *entry = loadEntry( archive, fileName, cs ); return entry->isFile() ? static_cast< const KZipFileEntry * >( entry ) : nullptr; } /** \return The name of a resource from the \p fileName */ static QString resourceName( const QString &fileName ) { QString resource = fileName; const int slashPos = fileName.lastIndexOf( QLatin1Char( '/' ) ); const int dotPos = fileName.lastIndexOf( QLatin1Char( '.' ) ); if ( slashPos > -1 ) { if ( dotPos > -1 && dotPos > slashPos ) { resource = fileName.mid( slashPos + 1, dotPos - slashPos - 1 ); } else { resource = fileName.mid( slashPos + 1 ); } } return resource; } static QColor interpolatedColor( const QColor &c1, const QColor &c2 ) { QColor res; res.setAlpha( ( c1.alpha() + c2.alpha() ) / 2 ); res.setRed( ( c1.red() + c2.red() ) / 2 ); res.setGreen( ( c1.green() + c2.green() ) / 2 ); res.setBlue( ( c1.blue() + c2.blue() ) / 2 ); return res; } static bool xpsGradientLessThan( const XpsGradient &g1, const XpsGradient &g2 ) { return qFuzzyCompare( g1.offset, g2.offset ) ? g1.color.name() < g2.color.name() : g1.offset < g2.offset; } static int xpsGradientWithOffset( const QList &gradients, double offset ) { int i = 0; for ( const XpsGradient &grad : gradients ) { if ( grad.offset == offset ) { return i; } ++i; } return -1; } /** Preprocess a list of gradients. \see XPS specification 11.3.1.1 */ static void preprocessXpsGradients( QList &gradients ) { if ( gradients.isEmpty() ) return; // sort the gradients (case 1.) std::stable_sort( gradients.begin(), gradients.end(), xpsGradientLessThan ); // no gradient with stop 0.0 (case 2.) if ( xpsGradientWithOffset( gradients, 0.0 ) == -1 ) { int firstGreaterThanZero = 0; while ( firstGreaterThanZero < gradients.count() && gradients.at( firstGreaterThanZero ).offset < 0.0 ) ++firstGreaterThanZero; // case 2.a: no gradients with stop less than 0.0 if ( firstGreaterThanZero == 0 ) { gradients.prepend( XpsGradient( 0.0, gradients.first().color ) ); } // case 2.b: some gradients with stop more than 0.0 else if ( firstGreaterThanZero != gradients.count() ) { QColor col1 = gradients.at( firstGreaterThanZero - 1 ).color; QColor col2 = gradients.at( firstGreaterThanZero ).color; for ( int i = 0; i < firstGreaterThanZero; ++i ) { gradients.removeFirst(); } gradients.prepend( XpsGradient( 0.0, interpolatedColor( col1, col2 ) ) ); } // case 2.c: no gradients with stop more than 0.0 else { XpsGradient newGrad( 0.0, gradients.last().color ); gradients.clear(); gradients.append( newGrad ); } } if ( gradients.isEmpty() ) return; // no gradient with stop 1.0 (case 3.) if ( xpsGradientWithOffset( gradients, 1.0 ) == -1 ) { int firstLessThanOne = gradients.count() - 1; while ( firstLessThanOne >= 0 && gradients.at( firstLessThanOne ).offset > 1.0 ) --firstLessThanOne; // case 2.a: no gradients with stop greater than 1.0 if ( firstLessThanOne == gradients.count() - 1 ) { gradients.append( XpsGradient( 1.0, gradients.last().color ) ); } // case 2.b: some gradients with stop more than 1.0 else if ( firstLessThanOne != -1 ) { QColor col1 = gradients.at( firstLessThanOne ).color; QColor col2 = gradients.at( firstLessThanOne + 1 ).color; for ( int i = firstLessThanOne + 1; i < gradients.count(); ++i ) { gradients.removeLast(); } gradients.append( XpsGradient( 1.0, interpolatedColor( col1, col2 ) ) ); } // case 2.c: no gradients with stop less than 1.0 else { XpsGradient newGrad( 1.0, gradients.first().color ); gradients.clear(); gradients.append( newGrad ); } } } static void addXpsGradientsToQGradient( const QList &gradients, QGradient *qgrad ) { for ( const XpsGradient &grad : gradients ) { qgrad->setColorAt( grad.offset, grad.color ); } } static void applySpreadStyleToQGradient( const QString &style, QGradient *qgrad ) { if ( style.isEmpty() ) return; if ( style == QLatin1String( "Pad" ) ) { qgrad->setSpread( QGradient::PadSpread ); } else if ( style == QLatin1String( "Reflect" ) ) { qgrad->setSpread( QGradient::ReflectSpread ); } else if ( style == QLatin1String( "Repeat" ) ) { qgrad->setSpread( QGradient::RepeatSpread ); } } /** Read an UnicodeString \param raw the raw value of UnicodeString \see XPS specification 5.1.4 */ static QString unicodeString( const QString &raw ) { QString ret; if ( raw.startsWith( QLatin1String( "{}" ) ) ) { ret = raw.mid( 2 ); } else { ret = raw; } return ret; } XpsHandler::XpsHandler(XpsPage *page): m_page(page) { m_painter = nullptr; } XpsHandler::~XpsHandler() { } bool XpsHandler::startDocument() { qCWarning(OkularXpsDebug) << "start document" << m_page->m_fileName ; XpsRenderNode node; node.name = QStringLiteral("document"); m_nodes.push(node); return true; } bool XpsHandler::startElement( const QString &nameSpace, const QString &localName, const QString &qname, const QXmlAttributes & atts ) { Q_UNUSED( nameSpace ) Q_UNUSED( qname ) XpsRenderNode node; node.name = localName; node.attributes = atts; processStartElement( node ); m_nodes.push(node); return true; } bool XpsHandler::endElement( const QString &nameSpace, const QString &localName, const QString &qname) { Q_UNUSED( nameSpace ) Q_UNUSED( qname ) XpsRenderNode node = m_nodes.pop(); if (node.name != localName) { qCWarning(OkularXpsDebug) << "Name doesn't match"; } processEndElement( node ); node.children.clear(); m_nodes.top().children.append(node); return true; } void XpsHandler::processGlyph( XpsRenderNode &node ) { //TODO Currently ignored attributes: CaretStops, DeviceFontName, IsSideways, OpacityMask, Name, FixedPage.NavigateURI, xml:lang, x:key //TODO Indices is only partially implemented //TODO Currently ignored child elements: Clip, OpacityMask //Handled separately: RenderTransform QString att; m_painter->save(); // Get font (doesn't work well because qt doesn't allow to load font from file) // This works despite the fact that font size isn't specified in points as required by qt. It's because I set point size to be equal to drawing unit. float fontSize = node.attributes.value(QStringLiteral("FontRenderingEmSize")).toFloat(); // qCWarning(OkularXpsDebug) << "Font Rendering EmSize:" << fontSize; // a value of 0.0 means the text is not visible (see XPS specs, chapter 12, "Glyphs") if ( fontSize < 0.1 ) { m_painter->restore(); return; } const QString absoluteFileName = absolutePath( entryPath( m_page->fileName() ), node.attributes.value(QStringLiteral("FontUri")) ); QFont font = m_page->m_file->getFontByName( absoluteFileName, fontSize ); att = node.attributes.value( QStringLiteral("StyleSimulations") ); if ( !att.isEmpty() ) { if ( att == QLatin1String( "ItalicSimulation" ) ) { font.setItalic( true ); } else if ( att == QLatin1String( "BoldSimulation" ) ) { font.setBold( true ); } else if ( att == QLatin1String( "BoldItalicSimulation" ) ) { font.setItalic( true ); font.setBold( true ); } } m_painter->setFont(font); //Origin QPointF origin( node.attributes.value(QStringLiteral("OriginX")).toDouble(), node.attributes.value(QStringLiteral("OriginY")).toDouble() ); //Fill QBrush brush; att = node.attributes.value(QStringLiteral("Fill")); if (att.isEmpty()) { QVariant data = node.getChildData( QStringLiteral("Glyphs.Fill") ); if (data.canConvert()) { brush = data.value(); } else { // no "Fill" attribute and no "Glyphs.Fill" child, so show nothing // (see XPS specs, 5.10) m_painter->restore(); return; } } else { brush = parseRscRefColorForBrush( att ); if ( brush.style() > Qt::NoBrush && brush.style() < Qt::LinearGradientPattern && brush.color().alpha() == 0 ) { m_painter->restore(); return; } } m_painter->setBrush( brush ); m_painter->setPen( QPen( brush, 0 ) ); // Opacity att = node.attributes.value(QStringLiteral("Opacity")); if (! att.isEmpty()) { bool ok = true; double value = att.toDouble( &ok ); if ( ok && value >= 0.1 ) { m_painter->setOpacity( value ); } else { m_painter->restore(); return; } } //RenderTransform att = node.attributes.value(QStringLiteral("RenderTransform")); if (!att.isEmpty()) { m_painter->setWorldTransform( parseRscRefMatrix( att ), true); } // Clip att = node.attributes.value( QStringLiteral("Clip") ); if ( !att.isEmpty() ) { QPainterPath clipPath = parseRscRefPath( att ); if ( !clipPath.isEmpty() ) { m_painter->setClipPath( clipPath ); } } // BiDiLevel - default Left-to-Right m_painter->setLayoutDirection( Qt::LeftToRight ); att = node.attributes.value( QStringLiteral("BiDiLevel") ); if ( !att.isEmpty() ) { if ( (att.toInt() % 2) == 1 ) { // odd BiDiLevel, so Right-to-Left m_painter->setLayoutDirection( Qt::RightToLeft ); } } // Indices - partial handling only att = node.attributes.value( QStringLiteral("Indices") ); QList advanceWidths; if ( ! att.isEmpty() ) { QStringList indicesElements = att.split( QLatin1Char(';') ); for( int i = 0; i < indicesElements.size(); ++i ) { if ( indicesElements.at(i).contains( QStringLiteral(",") ) ) { QStringList parts = indicesElements.at(i).split( QLatin1Char(',') ); if (parts.size() == 2 ) { // regular advance case, no offsets advanceWidths.append( parts.at(1).toDouble() * fontSize / 100.0 ); } else if (parts.size() == 3 ) { // regular advance case, with uOffset qreal AdvanceWidth = parts.at(1).toDouble() * fontSize / 100.0 ; qreal uOffset = parts.at(2).toDouble() / 100.0; advanceWidths.append( AdvanceWidth + uOffset ); } else { // has vertical offset, but don't know how to handle that yet qCWarning(OkularXpsDebug) << "Unhandled Indices element: " << indicesElements.at(i); advanceWidths.append( -1.0 ); } } else { // no special advance case advanceWidths.append( -1.0 ); } } } // UnicodeString QString stringToDraw( unicodeString( node.attributes.value( QStringLiteral("UnicodeString") ) ) ); QPointF originAdvance(0, 0); QFontMetrics metrics = m_painter->fontMetrics(); for ( int i = 0; i < stringToDraw.size(); ++i ) { QChar thisChar = stringToDraw.at( i ); m_painter->drawText( origin + originAdvance, QString( thisChar ) ); const qreal advanceWidth = advanceWidths.value( i, qreal(-1.0) ); if ( advanceWidth > 0.0 ) { originAdvance.rx() += advanceWidth; } else { originAdvance.rx() += metrics.width( thisChar ); } } // qCWarning(OkularXpsDebug) << "Glyphs: " << atts.value("Fill") << ", " << atts.value("FontUri"); // qCWarning(OkularXpsDebug) << " Origin: " << atts.value("OriginX") << "," << atts.value("OriginY"); // qCWarning(OkularXpsDebug) << " Unicode: " << atts.value("UnicodeString"); m_painter->restore(); } void XpsHandler::processFill( XpsRenderNode &node ) { //TODO Ignored child elements: VirtualBrush if (node.children.size() != 1) { qCWarning(OkularXpsDebug) << "Fill element should have exactly one child"; } else { node.data = node.children[0].data; } } void XpsHandler::processStroke( XpsRenderNode &node ) { //TODO Ignored child elements: VirtualBrush if (node.children.size() != 1) { qCWarning(OkularXpsDebug) << "Stroke element should have exactly one child"; } else { node.data = node.children[0].data; } } void XpsHandler::processImageBrush( XpsRenderNode &node ) { //TODO Ignored attributes: Opacity, x:key, TileMode, ViewBoxUnits, ViewPortUnits //TODO Check whether transformation works for non standard situations (viewbox different that whole image, Transform different that simple move & scale, Viewport different than [0, 0, 1, 1] QString att; QBrush brush; QRectF viewport = stringToRectF( node.attributes.value( QStringLiteral("Viewport") ) ); QRectF viewbox = stringToRectF( node.attributes.value( QStringLiteral("Viewbox") ) ); QImage image = m_page->loadImageFromFile( node.attributes.value( QStringLiteral("ImageSource") ) ); // Matrix which can transform [0, 0, 1, 1] rectangle to given viewbox QTransform viewboxMatrix = QTransform( viewbox.width() * image.physicalDpiX() / 96, 0, 0, viewbox.height() * image.physicalDpiY() / 96, viewbox.x(), viewbox.y() ); // Matrix which can transform [0, 0, 1, 1] rectangle to given viewport //TODO Take ViewPort into account QTransform viewportMatrix; att = node.attributes.value( QStringLiteral("Transform") ); if ( att.isEmpty() ) { QVariant data = node.getChildData( QStringLiteral("ImageBrush.Transform") ); if (data.canConvert()) { viewportMatrix = data.value(); } else { viewportMatrix = QTransform(); } } else { viewportMatrix = parseRscRefMatrix( att ); } viewportMatrix = viewportMatrix * QTransform( viewport.width(), 0, 0, viewport.height(), viewport.x(), viewport.y() ); brush = QBrush( image ); brush.setTransform( viewboxMatrix.inverted() * viewportMatrix ); node.data = QVariant::fromValue( brush ); } void XpsHandler::processPath( XpsRenderNode &node ) { //TODO Ignored attributes: Clip, OpacityMask, StrokeEndLineCap, StorkeStartLineCap, Name, FixedPage.NavigateURI, xml:lang, x:key, AutomationProperties.Name, AutomationProperties.HelpText, SnapsToDevicePixels //TODO Ignored child elements: RenderTransform, Clip, OpacityMask // Handled separately: RenderTransform m_painter->save(); QString att; QVariant data; // Get path XpsPathGeometry * pathdata = node.getChildData( QStringLiteral("Path.Data") ).value< XpsPathGeometry * >(); att = node.attributes.value( QStringLiteral("Data") ); if (! att.isEmpty() ) { QPainterPath path = parseRscRefPath( att ); delete pathdata; pathdata = new XpsPathGeometry(); pathdata->paths.append( new XpsPathFigure( path, true ) ); } if ( !pathdata ) { // nothing to draw m_painter->restore(); return; } // Set Fill att = node.attributes.value( QStringLiteral("Fill") ); QBrush brush; if (! att.isEmpty() ) { brush = parseRscRefColorForBrush( att ); } else { data = node.getChildData( QStringLiteral("Path.Fill") ); if (data.canConvert()) { brush = data.value(); } } m_painter->setBrush( brush ); // Stroke (pen) att = node.attributes.value( QStringLiteral("Stroke") ); QPen pen( Qt::transparent ); if (! att.isEmpty() ) { pen = parseRscRefColorForPen( att ); } else { data = node.getChildData( QStringLiteral("Path.Stroke") ); if (data.canConvert()) { pen.setBrush( data.value() ); } } att = node.attributes.value( QStringLiteral("StrokeThickness") ); if (! att.isEmpty() ) { bool ok = false; int thickness = att.toInt( &ok ); if (ok) pen.setWidth( thickness ); } att = node.attributes.value( QStringLiteral("StrokeDashArray") ); if ( !att.isEmpty() ) { const QStringList pieces = att.split( QLatin1Char( ' ' ), QString::SkipEmptyParts ); QVector dashPattern( pieces.count() ); bool ok = false; for ( int i = 0; i < pieces.count(); ++i ) { qreal value = pieces.at( i ).toInt( &ok ); if ( ok ) { dashPattern[i] = value; } else { break; } } if ( ok ) { pen.setDashPattern( dashPattern ); } } att = node.attributes.value( QStringLiteral("StrokeDashOffset") ); if ( !att.isEmpty() ) { bool ok = false; int offset = att.toInt( &ok ); if ( ok ) pen.setDashOffset( offset ); } att = node.attributes.value( QStringLiteral("StrokeDashCap") ); if ( !att.isEmpty() ) { Qt::PenCapStyle cap = Qt::FlatCap; if ( att == QLatin1String( "Flat" ) ) { cap = Qt::FlatCap; } else if ( att == QLatin1String( "Round" ) ) { cap = Qt::RoundCap; } else if ( att == QLatin1String( "Square" ) ) { cap = Qt::SquareCap; } // ### missing "Triangle" pen.setCapStyle( cap ); } att = node.attributes.value( QStringLiteral("StrokeLineJoin") ); if ( !att.isEmpty() ) { Qt::PenJoinStyle joinStyle = Qt::MiterJoin; if ( att == QLatin1String( "Miter" ) ) { joinStyle = Qt::MiterJoin; } else if ( att == QLatin1String( "Bevel" ) ) { joinStyle = Qt::BevelJoin; } else if ( att == QLatin1String( "Round" ) ) { joinStyle = Qt::RoundJoin; } pen.setJoinStyle( joinStyle ); } att = node.attributes.value( QStringLiteral("StrokeMiterLimit") ); if ( !att.isEmpty() ) { bool ok = false; double limit = att.toDouble( &ok ); if ( ok ) { // we have to divide it by two, as XPS consider half of the stroke width, // while Qt the whole of it pen.setMiterLimit( limit / 2 ); } } m_painter->setPen( pen ); // Opacity att = node.attributes.value(QStringLiteral("Opacity")); if (! att.isEmpty()) { m_painter->setOpacity(att.toDouble()); } // RenderTransform att = node.attributes.value( QStringLiteral("RenderTransform") ); if (! att.isEmpty() ) { m_painter->setWorldTransform( parseRscRefMatrix( att ), true ); } if ( !pathdata->transform.isIdentity() ) { m_painter->setWorldTransform( pathdata->transform, true ); } for ( const XpsPathFigure *figure : qAsConst(pathdata->paths) ) { m_painter->setBrush( figure->isFilled ? brush : QBrush() ); m_painter->drawPath( figure->path ); } delete pathdata; m_painter->restore(); } void XpsHandler::processPathData( XpsRenderNode &node ) { if (node.children.size() != 1) { qCWarning(OkularXpsDebug) << "Path.Data element should have exactly one child"; } else { node.data = node.children[0].data; } } void XpsHandler::processPathGeometry( XpsRenderNode &node ) { XpsPathGeometry * geom = new XpsPathGeometry(); for ( const XpsRenderNode &child : qAsConst(node.children) ) { if ( child.data.canConvert() ) { XpsPathFigure *figure = child.data.value(); geom->paths.append( figure ); } } QString att; att = node.attributes.value( QStringLiteral("Figures") ); if ( !att.isEmpty() ) { QPainterPath path = parseRscRefPath( att ); qDeleteAll( geom->paths ); geom->paths.clear(); geom->paths.append( new XpsPathFigure( path, true ) ); } att = node.attributes.value( QStringLiteral("FillRule") ); if ( !att.isEmpty() ) { geom->fillRule = fillRuleFromString( att ); } // Transform att = node.attributes.value( QStringLiteral("Transform") ); if ( !att.isEmpty() ) { geom->transform = parseRscRefMatrix( att ); } if ( !geom->paths.isEmpty() ) { node.data = QVariant::fromValue( geom ); } else { delete geom; } } void XpsHandler::processPathFigure( XpsRenderNode &node ) { //TODO Ignored child elements: ArcSegment QString att; QPainterPath path; att = node.attributes.value( QStringLiteral("StartPoint") ); if ( !att.isEmpty() ) { QPointF point = getPointFromString( att ); path.moveTo( point ); } else { return; } for ( const XpsRenderNode &child : qAsConst(node.children) ) { bool isStroked = true; att = node.attributes.value( QStringLiteral("IsStroked") ); if ( !att.isEmpty() ) { isStroked = att == QLatin1String( "true" ); } if ( !isStroked ) { continue; } // PolyLineSegment if ( child.name == QLatin1String( "PolyLineSegment" ) ) { att = child.attributes.value( QStringLiteral("Points") ); if ( !att.isEmpty() ) { const QStringList points = att.split( QLatin1Char( ' ' ), QString::SkipEmptyParts ); for ( const QString &p : points ) { QPointF point = getPointFromString( p ); path.lineTo( point ); } } } // PolyBezierSegment else if ( child.name == QLatin1String( "PolyBezierSegment" ) ) { att = child.attributes.value( QStringLiteral("Points") ); if ( !att.isEmpty() ) { const QStringList points = att.split( QLatin1Char( ' ' ), QString::SkipEmptyParts ); if ( points.count() % 3 == 0 ) { for ( int i = 0; i < points.count(); ) { QPointF firstControl = getPointFromString( points.at( i++ ) ); QPointF secondControl = getPointFromString( points.at( i++ ) ); QPointF endPoint = getPointFromString( points.at( i++ ) ); path.cubicTo(firstControl, secondControl, endPoint); } } } } // PolyQuadraticBezierSegment else if ( child.name == QLatin1String( "PolyQuadraticBezierSegment" ) ) { att = child.attributes.value( QStringLiteral("Points") ); if ( !att.isEmpty() ) { const QStringList points = att.split( QLatin1Char( ' ' ), QString::SkipEmptyParts ); if ( points.count() % 2 == 0 ) { for ( int i = 0; i < points.count(); ) { QPointF point1 = getPointFromString( points.at( i++ ) ); QPointF point2 = getPointFromString( points.at( i++ ) ); path.quadTo( point1, point2 ); } } } } } bool closePath = false; att = node.attributes.value( QStringLiteral("IsClosed") ); if ( !att.isEmpty() ) { closePath = att == QLatin1String( "true" ); } if ( closePath ) { path.closeSubpath(); } bool isFilled = true; att = node.attributes.value( QStringLiteral("IsFilled") ); if ( !att.isEmpty() ) { isFilled = att == QLatin1String( "true" ); } if ( !path.isEmpty() ) { node.data = QVariant::fromValue( new XpsPathFigure( path, isFilled ) ); } } void XpsHandler::processStartElement( XpsRenderNode &node ) { if (node.name == QLatin1String("Canvas")) { m_painter->save(); QString att = node.attributes.value( QStringLiteral("RenderTransform") ); if ( !att.isEmpty() ) { m_painter->setWorldTransform( parseRscRefMatrix( att ), true ); } att = node.attributes.value( QStringLiteral("Opacity") ); if ( !att.isEmpty() ) { double value = att.toDouble(); if ( value > 0.0 && value <= 1.0 ) { m_painter->setOpacity( m_painter->opacity() * value ); } else { // setting manually to 0 is necessary to "disable" // all the stuff inside m_painter->setOpacity( 0.0 ); } } } } void XpsHandler::processEndElement( XpsRenderNode &node ) { if (node.name == QLatin1String("Glyphs")) { processGlyph( node ); } else if (node.name == QLatin1String("Path")) { processPath( node ); } else if (node.name == QLatin1String("MatrixTransform")) { //TODO Ignoring x:key node.data = QVariant::fromValue( QTransform( attsToMatrix( node.attributes.value( QStringLiteral("Matrix") ) ) ) ); } else if ((node.name == QLatin1String("Canvas.RenderTransform")) || (node.name == QLatin1String("Glyphs.RenderTransform")) || (node.name == QLatin1String("Path.RenderTransform"))) { QVariant data = node.getRequiredChildData( QStringLiteral("MatrixTransform") ); if (data.canConvert()) { m_painter->setWorldTransform( data.value(), true ); } } else if (node.name == QLatin1String("Canvas")) { m_painter->restore(); } else if ((node.name == QLatin1String("Path.Fill")) || (node.name == QLatin1String("Glyphs.Fill"))) { processFill( node ); } else if (node.name == QLatin1String("Path.Stroke")) { processStroke( node ); } else if (node.name == QLatin1String("SolidColorBrush")) { //TODO Ignoring opacity, x:key node.data = QVariant::fromValue( QBrush( QColor( hexToRgba( node.attributes.value( QStringLiteral("Color") ).toLatin1() ) ) ) ); } else if (node.name == QLatin1String("ImageBrush")) { processImageBrush( node ); } else if (node.name == QLatin1String("ImageBrush.Transform")) { node.data = node.getRequiredChildData( QStringLiteral("MatrixTransform") ); } else if (node.name == QLatin1String("LinearGradientBrush")) { const XpsRenderNode * gradients = node.findChild( QStringLiteral("LinearGradientBrush.GradientStops") ); if ( gradients && gradients->data.canConvert< QGradient * >() ) { QPointF start = getPointFromString( node.attributes.value( QStringLiteral("StartPoint") ) ); QPointF end = getPointFromString( node.attributes.value( QStringLiteral("EndPoint") ) ); QLinearGradient * qgrad = static_cast< QLinearGradient * >( gradients->data.value< QGradient * >() ); qgrad->setStart( start ); qgrad->setFinalStop( end ); applySpreadStyleToQGradient( node.attributes.value( QStringLiteral("SpreadMethod") ), qgrad ); node.data = QVariant::fromValue( QBrush( *qgrad ) ); delete qgrad; } } else if (node.name == QLatin1String("RadialGradientBrush")) { const XpsRenderNode * gradients = node.findChild( QStringLiteral("RadialGradientBrush.GradientStops") ); if ( gradients && gradients->data.canConvert< QGradient * >() ) { QPointF center = getPointFromString( node.attributes.value( QStringLiteral("Center") ) ); QPointF origin = getPointFromString( node.attributes.value( QStringLiteral("GradientOrigin") ) ); double radiusX = node.attributes.value( QStringLiteral("RadiusX") ).toDouble(); double radiusY = node.attributes.value( QStringLiteral("RadiusY") ).toDouble(); QRadialGradient * qgrad = static_cast< QRadialGradient * >( gradients->data.value< QGradient * >() ); qgrad->setCenter( center ); qgrad->setFocalPoint( origin ); // TODO what in case of different radii? qgrad->setRadius( qMin( radiusX, radiusY ) ); applySpreadStyleToQGradient( node.attributes.value( QStringLiteral("SpreadMethod") ), qgrad ); node.data = QVariant::fromValue( QBrush( *qgrad ) ); delete qgrad; } } else if (node.name == QLatin1String("LinearGradientBrush.GradientStops")) { QList gradients; for ( const XpsRenderNode &child : qAsConst(node.children) ) { double offset = child.attributes.value( QStringLiteral("Offset") ).toDouble(); QColor color = hexToRgba( child.attributes.value( QStringLiteral("Color") ).toLatin1() ); gradients.append( XpsGradient( offset, color ) ); } preprocessXpsGradients( gradients ); if ( !gradients.isEmpty() ) { QLinearGradient * qgrad = new QLinearGradient(); addXpsGradientsToQGradient( gradients, qgrad ); node.data = QVariant::fromValue< QGradient * >( qgrad ); } } else if (node.name == QLatin1String("RadialGradientBrush.GradientStops")) { QList gradients; for ( const XpsRenderNode &child : qAsConst(node.children) ) { double offset = child.attributes.value( QStringLiteral("Offset") ).toDouble(); QColor color = hexToRgba( child.attributes.value( QStringLiteral("Color") ).toLatin1() ); gradients.append( XpsGradient( offset, color ) ); } preprocessXpsGradients( gradients ); if ( !gradients.isEmpty() ) { QRadialGradient * qgrad = new QRadialGradient(); addXpsGradientsToQGradient( gradients, qgrad ); node.data = QVariant::fromValue< QGradient * >( qgrad ); } } else if (node.name == QLatin1String("PathFigure")) { processPathFigure( node ); } else if (node.name == QLatin1String("PathGeometry")) { processPathGeometry( node ); } else if (node.name == QLatin1String("Path.Data")) { processPathData( node ); } else { //qCWarning(OkularXpsDebug) << "Unknown element: " << node->name; } } XpsPage::XpsPage(XpsFile *file, const QString &fileName): m_file( file ), m_fileName( fileName ), m_pageIsRendered(false) { m_pageImage = nullptr; // qCWarning(OkularXpsDebug) << "page file name: " << fileName; const KZipFileEntry* pageFile = static_cast(m_file->xpsArchive()->directory()->entry( fileName )); QXmlStreamReader xml; xml.addData( readFileOrDirectoryParts( pageFile ) ); while ( !xml.atEnd() ) { xml.readNext(); if ( xml.isStartElement() && ( xml.name() == QStringLiteral("FixedPage") ) ) { QXmlStreamAttributes attributes = xml.attributes(); m_pageSize.setWidth( attributes.value( QStringLiteral("Width") ).toString().toDouble() ); m_pageSize.setHeight( attributes.value( QStringLiteral("Height") ).toString().toDouble() ); break; } } if ( xml.error() ) { qCWarning(OkularXpsDebug) << "Could not parse XPS page:" << xml.errorString(); } } XpsPage::~XpsPage() { delete m_pageImage; } bool XpsPage::renderToImage( QImage *p ) { if ((m_pageImage == nullptr) || (m_pageImage->size() != p->size())) { delete m_pageImage; m_pageImage = new QImage( p->size(), QImage::Format_ARGB32 ); // Set one point = one drawing unit. Useful for fonts, because xps specifies font size using drawing units, not points as usual m_pageImage->setDotsPerMeterX( 2835 ); m_pageImage->setDotsPerMeterY( 2835 ); m_pageIsRendered = false; } if (! m_pageIsRendered) { m_pageImage->fill( qRgba( 255, 255, 255, 255 ) ); QPainter painter( m_pageImage ); renderToPainter( &painter ); m_pageIsRendered = true; } *p = *m_pageImage; return true; } bool XpsPage::renderToPainter( QPainter *painter ) { XpsHandler handler( this ); handler.m_painter = painter; handler.m_painter->setWorldTransform(QTransform().scale((qreal)painter->device()->width() / size().width(), (qreal)painter->device()->height() / size().height())); QXmlSimpleReader parser; parser.setContentHandler( &handler ); parser.setErrorHandler( &handler ); const KZipFileEntry* pageFile = static_cast(m_file->xpsArchive()->directory()->entry( m_fileName )); QByteArray data = readFileOrDirectoryParts( pageFile ); QBuffer buffer( &data ); QXmlInputSource source( &buffer ); bool ok = parser.parse( source ); qCWarning(OkularXpsDebug) << "Parse result: " << ok; return true; } QSizeF XpsPage::size() const { return m_pageSize; } QFont XpsFile::getFontByName( const QString &absoluteFileName, float size ) { // qCWarning(OkularXpsDebug) << "trying to get font: " << fileName << ", size: " << size; int index = m_fontCache.value(absoluteFileName, -1); if (index == -1) { index = loadFontByName(absoluteFileName); m_fontCache[absoluteFileName] = index; } if ( index == -1 ) { qCWarning(OkularXpsDebug) << "Requesting unknown font:" << absoluteFileName; return QFont(); } const QStringList fontFamilies = m_fontDatabase.applicationFontFamilies( index ); if ( fontFamilies.isEmpty() ) { qCWarning(OkularXpsDebug) << "The unexpected has happened. No font family for a known font:" << absoluteFileName << index; return QFont(); } const QString fontFamily = fontFamilies[0]; const QStringList fontStyles = m_fontDatabase.styles( fontFamily ); if ( fontStyles.isEmpty() ) { qCWarning(OkularXpsDebug) << "The unexpected has happened. No font style for a known font family:" << absoluteFileName << index << fontFamily ; return QFont(); } const QString fontStyle = fontStyles[0]; return m_fontDatabase.font(fontFamily, fontStyle, qRound(size)); } int XpsFile::loadFontByName( const QString &absoluteFileName ) { // qCWarning(OkularXpsDebug) << "font file name: " << absoluteFileName; const KArchiveEntry* fontFile = loadEntry( m_xpsArchive, absoluteFileName, Qt::CaseInsensitive ); if ( !fontFile ) { return -1; } QByteArray fontData = readFileOrDirectoryParts( fontFile ); // once per file, according to the docs int result = m_fontDatabase.addApplicationFontFromData( fontData ); if (-1 == result) { // Try to deobfuscate font // TODO Use deobfuscation depending on font content type, don't do it always when standard loading fails const QString baseName = resourceName( absoluteFileName ); unsigned short guid[16]; if (!parseGUID(baseName, guid)) { qCWarning(OkularXpsDebug) << "File to load font - file name isn't a GUID"; } else { if (fontData.length() < 32) { qCWarning(OkularXpsDebug) << "Font file is too small"; } else { // Obfuscation - xor bytes in font binary with bytes from guid (font's filename) const static int mapping[] = {15, 14, 13, 12, 11, 10, 9, 8, 6, 7, 4, 5, 0, 1, 2, 3}; for (int i = 0; i < 16; i++) { fontData[i] = fontData[i] ^ guid[mapping[i]]; fontData[i+16] = fontData[i+16] ^ guid[mapping[i]]; } result = m_fontDatabase.addApplicationFontFromData( fontData ); } } } // qCWarning(OkularXpsDebug) << "Loaded font: " << m_fontDatabase.applicationFontFamilies( result ); return result; // a font ID } KZip * XpsFile::xpsArchive() { return m_xpsArchive; } QImage XpsPage::loadImageFromFile( const QString &fileName ) { // qCWarning(OkularXpsDebug) << "image file name: " << fileName; if ( fileName.at( 0 ) == QLatin1Char( '{' ) ) { // for example: '{ColorConvertedBitmap /Resources/bla.wdp /Resources/foobar.icc}' // TODO: properly read a ColorConvertedBitmap return QImage(); } QString absoluteFileName = absolutePath( entryPath( m_fileName ), fileName ); const KZipFileEntry* imageFile = loadFile( m_file->xpsArchive(), absoluteFileName, Qt::CaseInsensitive ); if ( !imageFile ) { // image not found return QImage(); } /* WORKAROUND: XPS standard requires to use 96dpi for images which doesn't have dpi specified (in file). When Qt loads such an image, it sets its dpi to qt_defaultDpi and doesn't allow to find out that it happend. To workaround this I used this procedure: load image, set its dpi to 96, load image again. When dpi isn't set in file, dpi set by me stays unchanged. Trolltech task ID: 159527. */ QImage image; QByteArray data = imageFile->data(); QBuffer buffer(&data); buffer.open(QBuffer::ReadOnly); QImageReader reader(&buffer); image = reader.read(); image.setDotsPerMeterX(qRound(96 / 0.0254)); image.setDotsPerMeterY(qRound(96 / 0.0254)); buffer.seek(0); reader.setDevice(&buffer); reader.read(&image); return image; } Okular::TextPage* XpsPage::textPage() { // qCWarning(OkularXpsDebug) << "Parsing XpsPage, text extraction"; Okular::TextPage* textPage = new Okular::TextPage(); const KZipFileEntry* pageFile = static_cast(m_file->xpsArchive()->directory()->entry( m_fileName )); QXmlStreamReader xml; xml.addData( readFileOrDirectoryParts( pageFile ) ); QTransform matrix = QTransform(); QStack matrices; matrices.push( QTransform() ); bool useMatrix = false; QXmlStreamAttributes glyphsAtts; while ( ! xml.atEnd() ) { xml.readNext(); if ( xml.isStartElement() ) { if ( xml.name() == QStringLiteral("Canvas")) { matrices.push(matrix); QString att = xml.attributes().value( QStringLiteral("RenderTransform") ).toString(); if (!att.isEmpty()) { matrix = parseRscRefMatrix( att ) * matrix; } } else if ((xml.name() == QStringLiteral("Canvas.RenderTransform")) || (xml.name() == QStringLiteral("Glyphs.RenderTransform"))) { useMatrix = true; } else if (xml.name() == QStringLiteral("MatrixTransform")) { if (useMatrix) { matrix = attsToMatrix( xml.attributes().value(QStringLiteral("Matrix")).toString() ) * matrix; } } else if (xml.name() == QStringLiteral("Glyphs")) { matrices.push( matrix ); glyphsAtts = xml.attributes(); } else if ( (xml.name() == QStringLiteral("Path")) || (xml.name() == QStringLiteral("Path.Fill")) || (xml.name() == QStringLiteral("SolidColorBrush")) || (xml.name() == QStringLiteral("ImageBrush")) || (xml.name() == QStringLiteral("ImageBrush.Transform")) || (xml.name() == QStringLiteral("Path.OpacityMask")) || (xml.name() == QStringLiteral("Path.Data")) || (xml.name() == QStringLiteral("PathGeometry")) || (xml.name() == QStringLiteral("PathFigure")) || (xml.name() == QStringLiteral("PolyLineSegment")) ) { // those are only graphical - no use in text handling } else if ( (xml.name() == QStringLiteral("FixedPage")) || (xml.name() == QStringLiteral("FixedPage.Resources")) ) { // not useful for text extraction } else { qCWarning(OkularXpsDebug) << "Unhandled element in Text Extraction start: " << xml.name().toString(); } } else if (xml.isEndElement() ) { if (xml.name() == QStringLiteral("Canvas")) { matrix = matrices.pop(); } else if ((xml.name() == QStringLiteral("Canvas.RenderTransform")) || (xml.name() == QStringLiteral("Glyphs.RenderTransform"))) { useMatrix = false; } else if (xml.name() == QStringLiteral("MatrixTransform")) { // not clear if we need to do anything here yet. } else if (xml.name() == QStringLiteral("Glyphs")) { QString att = glyphsAtts.value( QStringLiteral("RenderTransform") ).toString(); if (!att.isEmpty()) { matrix = parseRscRefMatrix( att ) * matrix; } QString text = unicodeString( glyphsAtts.value( QStringLiteral("UnicodeString") ).toString() ); // Get font (doesn't work well because qt doesn't allow to load font from file) const QString absoluteFileName = absolutePath( entryPath( m_fileName ), glyphsAtts.value( QStringLiteral("FontUri") ).toString() ); QFont font = m_file->getFontByName( absoluteFileName, glyphsAtts.value(QStringLiteral("FontRenderingEmSize")).toString().toFloat() * 72 / 96 ); QFontMetrics metrics = QFontMetrics( font ); // Origin QPointF origin( glyphsAtts.value(QStringLiteral("OriginX")).toString().toDouble(), glyphsAtts.value(QStringLiteral("OriginY")).toString().toDouble() ); int lastWidth = 0; for (int i = 0; i < text.length(); i++) { int width = metrics.width( text, i + 1 ); Okular::NormalizedRect * rect = new Okular::NormalizedRect( (origin.x() + lastWidth) / m_pageSize.width(), (origin.y() - metrics.height()) / m_pageSize.height(), (origin.x() + width) / m_pageSize.width(), origin.y() / m_pageSize.height() ); rect->transform( matrix ); textPage->append( text.mid(i, 1), rect ); lastWidth = width; } matrix = matrices.pop(); } else if ( (xml.name() == QStringLiteral("Path")) || (xml.name() == QStringLiteral("Path.Fill")) || (xml.name() == QStringLiteral("SolidColorBrush")) || (xml.name() == QStringLiteral("ImageBrush")) || (xml.name() == QStringLiteral("ImageBrush.Transform")) || (xml.name() == QStringLiteral("Path.OpacityMask")) || (xml.name() == QStringLiteral("Path.Data")) || (xml.name() == QStringLiteral("PathGeometry")) || (xml.name() == QStringLiteral("PathFigure")) || (xml.name() == QStringLiteral("PolyLineSegment")) ) { // those are only graphical - no use in text handling } else if ( (xml.name() == QStringLiteral("FixedPage")) || (xml.name() == QStringLiteral("FixedPage.Resources")) ) { // not useful for text extraction } else { qCWarning(OkularXpsDebug) << "Unhandled element in Text Extraction end: " << xml.name().toString(); } } } if ( xml.error() ) { qCWarning(OkularXpsDebug) << "Error parsing XpsPage text: " << xml.errorString(); } return textPage; } void XpsDocument::parseDocumentStructure( const QString &documentStructureFileName ) { qCWarning(OkularXpsDebug) << "document structure file name: " << documentStructureFileName; m_haveDocumentStructure = false; const KZipFileEntry* documentStructureFile = static_cast(m_file->xpsArchive()->directory()->entry( documentStructureFileName )); QXmlStreamReader xml; xml.addData( documentStructureFile->data() ); while ( !xml.atEnd() ) { xml.readNext(); if ( xml.isStartElement() ) { if ( xml.name() == QStringLiteral("DocumentStructure") ) { // just a container for optional outline and story elements - nothing to do here } else if ( xml.name() == QStringLiteral("DocumentStructure.Outline") ) { qCWarning(OkularXpsDebug) << "found DocumentStructure.Outline"; } else if ( xml.name() == QStringLiteral("DocumentOutline") ) { qCWarning(OkularXpsDebug) << "found DocumentOutline"; m_docStructure = new Okular::DocumentSynopsis; } else if ( xml.name() == QStringLiteral("OutlineEntry") ) { m_haveDocumentStructure = true; QXmlStreamAttributes attributes = xml.attributes(); int outlineLevel = attributes.value( QStringLiteral("OutlineLevel")).toString().toInt(); QString description = attributes.value(QStringLiteral("Description")).toString(); QDomElement synopsisElement = m_docStructure->createElement( description ); synopsisElement.setAttribute( QStringLiteral("OutlineLevel"), outlineLevel ); QString target = attributes.value(QStringLiteral("OutlineTarget")).toString(); int hashPosition = target.lastIndexOf( QLatin1Char('#') ); target = target.mid( hashPosition + 1 ); // qCWarning(OkularXpsDebug) << "target: " << target; Okular::DocumentViewport viewport; viewport.pageNumber = m_docStructurePageMap.value( target ); synopsisElement.setAttribute( QStringLiteral("Viewport"), viewport.toString() ); if ( outlineLevel == 1 ) { // qCWarning(OkularXpsDebug) << "Description: " // << outlineEntryElement.attribute( "Description" ); m_docStructure->appendChild( synopsisElement ); } else { // find the last next highest element (so it this is level 3, we need // to find the most recent level 2 node) QDomNode maybeParentNode = m_docStructure->lastChild(); while ( !maybeParentNode.isNull() ) { if ( maybeParentNode.toElement().attribute( QStringLiteral("OutlineLevel") ).toInt() == ( outlineLevel - 1 ) ) { // we have the right parent maybeParentNode.appendChild( synopsisElement ); break; } maybeParentNode = maybeParentNode.lastChild(); } } } else if ( xml.name() == QStringLiteral("Story") ) { // we need to handle Story here, but I have no idea what to do with it. } else if ( xml.name() == QStringLiteral("StoryFragment") ) { // we need to handle StoryFragment here, but I have no idea what to do with it. } else if ( xml.name() == QStringLiteral("StoryFragmentReference") ) { // we need to handle StoryFragmentReference here, but I have no idea what to do with it. } else { qCWarning(OkularXpsDebug) << "Unhandled entry in DocumentStructure: " << xml.name().toString(); } } } } const Okular::DocumentSynopsis * XpsDocument::documentStructure() { return m_docStructure; } bool XpsDocument::hasDocumentStructure() { return m_haveDocumentStructure; } XpsDocument::XpsDocument(XpsFile *file, const QString &fileName): m_file(file), m_haveDocumentStructure( false ), m_docStructure( nullptr ) { qCWarning(OkularXpsDebug) << "document file name: " << fileName; const KArchiveEntry* documentEntry = file->xpsArchive()->directory()->entry( fileName ); QString documentFilePath = fileName; const QString documentEntryPath = entryPath( fileName ); QXmlStreamReader docXml; docXml.addData( readFileOrDirectoryParts( documentEntry, &documentFilePath ) ); while( !docXml.atEnd() ) { docXml.readNext(); if ( docXml.isStartElement() ) { if ( docXml.name() == QStringLiteral("PageContent") ) { QString pagePath = docXml.attributes().value(QStringLiteral("Source")).toString(); qCWarning(OkularXpsDebug) << "Page Path: " << pagePath; XpsPage *page = new XpsPage( file, absolutePath( documentFilePath, pagePath ) ); m_pages.append(page); } else if ( docXml.name() == QStringLiteral("PageContent.LinkTargets") ) { // do nothing - wait for the real LinkTarget elements } else if ( docXml.name() == QStringLiteral("LinkTarget") ) { QString targetName = docXml.attributes().value( QStringLiteral("Name") ).toString(); if ( ! targetName.isEmpty() ) { m_docStructurePageMap[ targetName ] = m_pages.count() - 1; } } else if ( docXml.name() == QStringLiteral("FixedDocument") ) { // we just ignore this - it is just a container } else { qCWarning(OkularXpsDebug) << "Unhandled entry in FixedDocument: " << docXml.name().toString(); } } } if ( docXml.error() ) { qCWarning(OkularXpsDebug) << "Could not parse main XPS document file: " << docXml.errorString(); } // There might be a relationships entry for this document - typically used to tell us where to find the // content structure description // We should be able to find this using a reference from some other part of the document, but I can't see it. const int slashPosition = fileName.lastIndexOf( QLatin1Char('/') ); const QString documentRelationshipFile = absolutePath( documentEntryPath, QStringLiteral("_rels/") + fileName.mid( slashPosition + 1 ) + QStringLiteral(".rels") ); const KZipFileEntry* relFile = static_cast(file->xpsArchive()->directory()->entry(documentRelationshipFile)); QString documentStructureFile; if ( relFile ) { QXmlStreamReader xml; xml.addData( readFileOrDirectoryParts( relFile ) ); while ( !xml.atEnd() ) { xml.readNext(); if ( xml.isStartElement() && ( xml.name() == QStringLiteral("Relationship") ) ) { QXmlStreamAttributes attributes = xml.attributes(); if ( attributes.value( QStringLiteral("Type") ).toString() == QLatin1String("http://schemas.microsoft.com/xps/2005/06/documentstructure") ) { documentStructureFile = attributes.value( QStringLiteral("Target") ).toString(); } else { qCWarning(OkularXpsDebug) << "Unknown document relationships element: " << attributes.value( QStringLiteral("Type") ).toString() << " : " << attributes.value( QStringLiteral("Target") ).toString(); } } } if ( xml.error() ) { qCWarning(OkularXpsDebug) << "Could not parse XPS page relationships file ( " << documentRelationshipFile << " ) - " << xml.errorString(); } } else { // the page relationship file didn't exist in the zipfile // this isn't fatal qCWarning(OkularXpsDebug) << "Could not open Document relationship file from " << documentRelationshipFile; } if ( ! documentStructureFile.isEmpty() ) { // qCWarning(OkularXpsDebug) << "Document structure filename: " << documentStructureFile; // make the document path absolute documentStructureFile = absolutePath( documentEntryPath, documentStructureFile ); // qCWarning(OkularXpsDebug) << "Document structure absolute path: " << documentStructureFile; parseDocumentStructure( documentStructureFile ); } } XpsDocument::~XpsDocument() { qDeleteAll(m_pages); m_pages.clear(); if ( m_docStructure ) delete m_docStructure; } int XpsDocument::numPages() const { return m_pages.size(); } XpsPage* XpsDocument::page(int pageNum) const { return m_pages.at(pageNum); } XpsFile::XpsFile() { } XpsFile::~XpsFile() { for(int fontId : qAsConst(m_fontCache)) { m_fontDatabase.removeApplicationFont(fontId); } } bool XpsFile::loadDocument(const QString &filename) { m_xpsArchive = new KZip( filename ); if ( m_xpsArchive->open( QIODevice::ReadOnly ) == true ) { qCWarning(OkularXpsDebug) << "Successful open of " << m_xpsArchive->fileName(); } else { qCWarning(OkularXpsDebug) << "Could not open XPS archive: " << m_xpsArchive->fileName(); delete m_xpsArchive; return false; } // The only fixed entry in XPS is /_rels/.rels const KArchiveEntry* relEntry = m_xpsArchive->directory()->entry(QStringLiteral("_rels/.rels")); if ( !relEntry ) { // this might occur if we can't read the zip directory, or it doesn't have the relationships entry return false; } QXmlStreamReader relXml; relXml.addData( readFileOrDirectoryParts( relEntry ) ); QString fixedRepresentationFileName; // We work through the relationships document and pull out each element. while ( !relXml.atEnd() ) { relXml.readNext(); if ( relXml.isStartElement() ) { if ( relXml.name() == QStringLiteral("Relationship") ) { QXmlStreamAttributes attributes = relXml.attributes(); QString type = attributes.value( QStringLiteral("Type") ).toString(); QString target = attributes.value( QStringLiteral("Target") ).toString(); if ( QStringLiteral("http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail") == type ) { m_thumbnailFileName = target; } else if ( QStringLiteral("http://schemas.microsoft.com/xps/2005/06/fixedrepresentation") == type ) { fixedRepresentationFileName = target; } else if (QStringLiteral("http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties") == type) { m_corePropertiesFileName = target; } else if (QStringLiteral("http://schemas.openxmlformats.org/package/2006/relationships/digital-signature/origin") == type) { m_signatureOrigin = target; } else { qCWarning(OkularXpsDebug) << "Unknown relationships element: " << type << " : " << target; } } else if ( relXml.name() == QStringLiteral("Relationships") ) { // nothing to do here - this is just the container level } else { qCWarning(OkularXpsDebug) << "unexpected element in _rels/.rels: " << relXml.name().toString(); } } } if ( relXml.error() ) { qCWarning(OkularXpsDebug) << "Could not parse _rels/.rels: " << relXml.errorString(); return false; } if ( fixedRepresentationFileName.isEmpty() ) { // FixedRepresentation is a required part of the XPS document return false; } const KArchiveEntry* fixedRepEntry = m_xpsArchive->directory()->entry( fixedRepresentationFileName ); QString fixedRepresentationFilePath = fixedRepresentationFileName; QXmlStreamReader fixedRepXml; fixedRepXml.addData( readFileOrDirectoryParts( fixedRepEntry, &fixedRepresentationFileName ) ); while ( !fixedRepXml.atEnd() ) { fixedRepXml.readNext(); if ( fixedRepXml.isStartElement() ) { if ( fixedRepXml.name() == QStringLiteral("DocumentReference") ) { const QString source = fixedRepXml.attributes().value(QStringLiteral("Source")).toString(); XpsDocument *doc = new XpsDocument( this, absolutePath( fixedRepresentationFilePath, source ) ); for (int lv = 0; lv < doc->numPages(); ++lv) { // our own copy of the pages list m_pages.append( doc->page( lv ) ); } m_documents.append(doc); } else if ( fixedRepXml.name() == QStringLiteral("FixedDocumentSequence")) { // we don't do anything here - this is just a container for one or more DocumentReference elements } else { qCWarning(OkularXpsDebug) << "Unhandled entry in FixedDocumentSequence: " << fixedRepXml.name().toString(); } } } if ( fixedRepXml.error() ) { qCWarning(OkularXpsDebug) << "Could not parse FixedRepresentation file:" << fixedRepXml.errorString(); return false; } return true; } Okular::DocumentInfo XpsFile::generateDocumentInfo() const { Okular::DocumentInfo docInfo; docInfo.set( Okular::DocumentInfo::MimeType, QStringLiteral("application/oxps") ); if ( ! m_corePropertiesFileName.isEmpty() ) { const KZipFileEntry* corepropsFile = static_cast(m_xpsArchive->directory()->entry(m_corePropertiesFileName)); QXmlStreamReader xml; xml.addData( corepropsFile->data() ); while ( !xml.atEnd() ) { xml.readNext(); if ( xml.isEndElement() ) break; if ( xml.isStartElement() ) { if (xml.name() == QStringLiteral("title")) { docInfo.set( Okular::DocumentInfo::Title, xml.readElementText() ); } else if (xml.name() == QStringLiteral("subject")) { docInfo.set( Okular::DocumentInfo::Subject, xml.readElementText() ); } else if (xml.name() == QStringLiteral("description")) { docInfo.set( Okular::DocumentInfo::Description, xml.readElementText() ); } else if (xml.name() == QStringLiteral("creator")) { docInfo.set( Okular::DocumentInfo::Creator, xml.readElementText() ); } else if (xml.name() == QStringLiteral("category")) { docInfo.set( Okular::DocumentInfo::Category, xml.readElementText() ); } else if (xml.name() == QStringLiteral("created")) { QDateTime createdDate = QDateTime::fromString( xml.readElementText(), QStringLiteral("yyyy-MM-ddThh:mm:ssZ") ); docInfo.set( Okular::DocumentInfo::CreationDate, QLocale().toString( createdDate, QLocale::LongFormat ) ); } else if (xml.name() == QStringLiteral("modified")) { QDateTime modifiedDate = QDateTime::fromString( xml.readElementText(), QStringLiteral("yyyy-MM-ddThh:mm:ssZ") ); docInfo.set( Okular::DocumentInfo::ModificationDate, QLocale().toString( modifiedDate, QLocale::LongFormat ) ); } else if (xml.name() == QStringLiteral("keywords")) { docInfo.set( Okular::DocumentInfo::Keywords, xml.readElementText() ); } else if (xml.name() == QStringLiteral("revision")) { docInfo.set( QStringLiteral("revision"), xml.readElementText(), i18n( "Revision" ) ); } } } if ( xml.error() ) { qCWarning(OkularXpsDebug) << "Could not parse XPS core properties:" << xml.errorString(); } } else { qCWarning(OkularXpsDebug) << "No core properties filename"; } docInfo.set( Okular::DocumentInfo::Pages, QString::number(numPages()) ); return docInfo; } bool XpsFile::closeDocument() { qDeleteAll( m_documents ); m_documents.clear(); delete m_xpsArchive; return true; } int XpsFile::numPages() const { return m_pages.size(); } int XpsFile::numDocuments() const { return m_documents.size(); } XpsDocument* XpsFile::document(int documentNum) const { return m_documents.at( documentNum ); } XpsPage* XpsFile::page(int pageNum) const { return m_pages.at( pageNum ); } XpsGenerator::XpsGenerator( QObject *parent, const QVariantList &args ) : Okular::Generator( parent, args ), m_xpsFile( nullptr ) { setFeature( TextExtraction ); setFeature( PrintNative ); setFeature( PrintToFile ); setFeature( Threaded ); userMutex(); } XpsGenerator::~XpsGenerator() { } bool XpsGenerator::loadDocument( const QString & fileName, QVector & pagesVector ) { m_xpsFile = new XpsFile(); m_xpsFile->loadDocument( fileName ); pagesVector.resize( m_xpsFile->numPages() ); int pagesVectorOffset = 0; for (int docNum = 0; docNum < m_xpsFile->numDocuments(); ++docNum ) { XpsDocument *doc = m_xpsFile->document( docNum ); for (int pageNum = 0; pageNum < doc->numPages(); ++pageNum ) { QSizeF pageSize = doc->page( pageNum )->size(); pagesVector[pagesVectorOffset] = new Okular::Page( pagesVectorOffset, pageSize.width(), pageSize.height(), Okular::Rotation0 ); ++pagesVectorOffset; } } return true; } bool XpsGenerator::doCloseDocument() { m_xpsFile->closeDocument(); delete m_xpsFile; m_xpsFile = nullptr; return true; } QImage XpsGenerator::image( Okular::PixmapRequest * request ) { QMutexLocker lock( userMutex() ); QSize size( (int)request->width(), (int)request->height() ); QImage image( size, QImage::Format_RGB32 ); XpsPage *pageToRender = m_xpsFile->page( request->page()->number() ); pageToRender->renderToImage( &image ); return image; } Okular::TextPage* XpsGenerator::textPage( Okular::TextRequest * request ) { QMutexLocker lock( userMutex() ); XpsPage * xpsPage = m_xpsFile->page( request->page()->number() ); return xpsPage->textPage(); } Okular::DocumentInfo XpsGenerator::generateDocumentInfo( const QSet &keys ) const { Q_UNUSED(keys); qCWarning(OkularXpsDebug) << "generating document metadata"; return m_xpsFile->generateDocumentInfo(); } const Okular::DocumentSynopsis * XpsGenerator::generateDocumentSynopsis() { qCWarning(OkularXpsDebug) << "generating document synopsis"; // we only generate the synopsis for the first file. if ( !m_xpsFile || !m_xpsFile->document( 0 ) ) return nullptr; if ( m_xpsFile->document( 0 )->hasDocumentStructure() ) return m_xpsFile->document( 0 )->documentStructure(); return nullptr; } Okular::ExportFormat::List XpsGenerator::exportFormats() const { static Okular::ExportFormat::List formats; if ( formats.isEmpty() ) { formats.append( Okular::ExportFormat::standardFormat( Okular::ExportFormat::PlainText ) ); } return formats; } bool XpsGenerator::exportTo( const QString &fileName, const Okular::ExportFormat &format ) { if ( format.mimeType().inherits( QStringLiteral( "text/plain" ) ) ) { QFile f( fileName ); if ( !f.open( QIODevice::WriteOnly ) ) return false; QTextStream ts( &f ); for ( int i = 0; i < m_xpsFile->numPages(); ++i ) { Okular::TextPage* textPage = m_xpsFile->page(i)->textPage(); QString text = textPage->text(); ts << text; ts << QLatin1Char('\n'); delete textPage; } f.close(); return true; } return false; } bool XpsGenerator::print( QPrinter &printer ) { QList pageList = Okular::FilePrinter::pageList( printer, document()->pages(), document()->currentPage() + 1, document()->bookmarkedPageList() ); QPainter painter( &printer ); for ( int i = 0; i < pageList.count(); ++i ) { if ( i != 0 ) printer.newPage(); const int page = pageList.at( i ) - 1; XpsPage *pageToRender = m_xpsFile->page( page ); pageToRender->renderToPainter( &painter ); } return true; } const XpsRenderNode * XpsRenderNode::findChild( const QString &name ) const { for (const XpsRenderNode &child : children) { if (child.name == name) { return &child; } } return nullptr; } QVariant XpsRenderNode::getRequiredChildData( const QString &name ) const { const XpsRenderNode * child = findChild( name ); if (child == nullptr) { qCWarning(OkularXpsDebug) << "Required element " << name << " is missing in " << this->name; return QVariant(); } return child->data; } QVariant XpsRenderNode::getChildData( const QString &name ) const { const XpsRenderNode * child = findChild( name ); if (child == nullptr) { return QVariant(); } else { return child->data; } } Q_LOGGING_CATEGORY(OkularXpsDebug, "org.kde.okular.generators.xps", QtWarningMsg) #include "generator_xps.moc" diff --git a/part.cpp b/part.cpp index ddc09f39c..514abe1c1 100644 --- a/part.cpp +++ b/part.cpp @@ -1,3726 +1,3726 @@ /*************************************************************************** * Copyright (C) 2002 by Wilco Greven * * Copyright (C) 2002 by Chris Cheney * * Copyright (C) 2002 by Malcolm Hunter * * Copyright (C) 2003-2004 by Christophe Devriese * * * * Copyright (C) 2003 by Daniel Molkentin * * Copyright (C) 2003 by Andy Goossens * * Copyright (C) 2003 by Dirk Mueller * * Copyright (C) 2003 by Laurent Montel * * Copyright (C) 2004 by Dominique Devriese * * Copyright (C) 2004 by Christoph Cullmann * * Copyright (C) 2004 by Henrique Pinto * * Copyright (C) 2004 by Waldo Bastian * * Copyright (C) 2004-2008 by Albert Astals Cid * * Copyright (C) 2004 by Antti Markus * * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * * company, info@kdab.com. Work sponsored by the * * LiMux project of the city of Munich * * * * 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. * ***************************************************************************/ #include "part.h" // qt/kde includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef WITH_KWALLET #include #endif #include #include #if PURPOSE_FOUND #include #include #endif #if 0 #include #endif // local includes #include "aboutdata.h" #include "extensions.h" #include "ui/debug_ui.h" #include "ui/drawingtoolactions.h" #include "ui/pageview.h" #include "ui/toc.h" #include "ui/searchwidget.h" #include "ui/thumbnaillist.h" #include "ui/side_reviews.h" #include "ui/minibar.h" #include "ui/embeddedfilesdialog.h" #include "ui/propertiesdialog.h" #include "ui/presentationwidget.h" #include "ui/pagesizelabel.h" #include "ui/bookmarklist.h" #include "ui/findbar.h" #include "ui/sidebar.h" #include "ui/fileprinterpreview.h" #include "ui/guiutils.h" #include "ui/layers.h" #include "ui/okmenutitle.h" #include "ui/signaturepanel.h" #include "conf/preferencesdialog.h" #include "settings.h" #include "core/action.h" #include "core/annotations.h" #include "core/bookmarkmanager.h" #include "core/document.h" #include "core/document_p.h" #include "core/generator.h" #include "core/page.h" #include "core/fileprinter.h" #include "core/printoptionswidget.h" #include #ifdef OKULAR_KEEP_FILE_OPEN class FileKeeper { public: FileKeeper() : m_handle( nullptr ) { } ~FileKeeper() { } void open( const QString & path ) { if ( !m_handle ) m_handle = std::fopen( QFile::encodeName( path ).constData(), "r" ); } void close() { if ( m_handle ) { int ret = std::fclose( m_handle ); Q_UNUSED( ret ) m_handle = nullptr; } } QTemporaryFile* copyToTemporary() const { if ( !m_handle ) return nullptr; QTemporaryFile * retFile = new QTemporaryFile; retFile->open(); std::rewind( m_handle ); int c = -1; do { c = std::fgetc( m_handle ); if ( c == EOF ) break; if ( !retFile->putChar( (char)c ) ) break; } while ( !feof( m_handle ) ); retFile->flush(); return retFile; } private: std::FILE * m_handle; }; #endif K_PLUGIN_FACTORY(OkularPartFactory, registerPlugin();) static QAction* actionForExportFormat( const Okular::ExportFormat& format, QObject *parent = Q_NULLPTR ) { QAction *act = new QAction( format.description(), parent ); if ( !format.icon().isNull() ) { act->setIcon( format.icon() ); } return act; } static KFilterDev::CompressionType compressionTypeFor( const QString& mime_to_check ) { // The compressedMimeMap is here in case you have a very old shared mime database // that doesn't have inheritance info for things like gzeps, etc // Otherwise the "is()" calls below are just good enough static QHash< QString, KFilterDev::CompressionType > compressedMimeMap; static bool supportBzip = false; static bool supportXz = false; const QString app_gzip( QStringLiteral( "application/x-gzip" ) ); const QString app_bzip( QStringLiteral( "application/x-bzip" ) ); const QString app_xz( QStringLiteral( "application/x-xz" ) ); if ( compressedMimeMap.isEmpty() ) { std::unique_ptr< KFilterBase > f; compressedMimeMap[ QStringLiteral( "image/x-gzeps" ) ] = KFilterDev::GZip; // check we can read bzip2-compressed files f.reset( KCompressionDevice::filterForCompressionType( KCompressionDevice::BZip2 ) ); if ( f.get() ) { supportBzip = true; compressedMimeMap[ QStringLiteral( "application/x-bzpdf" ) ] = KFilterDev::BZip2; compressedMimeMap[ QStringLiteral( "application/x-bzpostscript" ) ] = KFilterDev::BZip2; compressedMimeMap[ QStringLiteral( "application/x-bzdvi" ) ] = KFilterDev::BZip2; compressedMimeMap[ QStringLiteral( "image/x-bzeps" ) ] = KFilterDev::BZip2; } // check if we can read XZ-compressed files f.reset( KCompressionDevice::filterForCompressionType( KCompressionDevice::Xz ) ); if ( f.get() ) { supportXz = true; } } QHash< QString, KFilterDev::CompressionType >::const_iterator it = compressedMimeMap.constFind( mime_to_check ); if ( it != compressedMimeMap.constEnd() ) return it.value(); QMimeDatabase db; QMimeType mime = db.mimeTypeForName( mime_to_check ); if ( mime.isValid() ) { if ( mime.inherits( app_gzip ) ) return KFilterDev::GZip; else if ( supportBzip && mime.inherits( app_bzip ) ) return KFilterDev::BZip2; else if ( supportXz && mime.inherits( app_xz ) ) return KFilterDev::Xz; } return KFilterDev::None; } static Okular::EmbedMode detectEmbedMode( QWidget *parentWidget, QObject *parent, const QVariantList &args ) { Q_UNUSED( parentWidget ); if ( parent && ( parent->objectName().startsWith( QLatin1String( "okular::Shell" ) ) || parent->objectName().startsWith( QLatin1String( "okular/okular__Shell" ) ) ) ) return Okular::NativeShellMode; if ( parent && ( QByteArray( "KHTMLPart" ) == parent->metaObject()->className() ) ) return Okular::KHTMLPartMode; for ( const QVariant &arg : args ) { if ( arg.type() == QVariant::String ) { if ( arg.toString() == QLatin1String( "Print/Preview" ) ) { return Okular::PrintPreviewMode; } else if ( arg.toString() == QLatin1String( "ViewerWidget" ) ) { return Okular::ViewerWidgetMode; } } } return Okular::UnknownEmbedMode; } static QString detectConfigFileName( const QVariantList &args ) { for ( const QVariant &arg : args ) { if ( arg.type() == QVariant::String ) { QString argString = arg.toString(); int separatorIndex = argString.indexOf( QStringLiteral("=") ); if ( separatorIndex >= 0 && argString.left( separatorIndex ) == QLatin1String( "ConfigFileName" ) ) { return argString.mid( separatorIndex + 1 ); } } } return QString(); } #undef OKULAR_KEEP_FILE_OPEN #ifdef OKULAR_KEEP_FILE_OPEN static bool keepFileOpen() { static bool keep_file_open = !qgetenv("OKULAR_NO_KEEP_FILE_OPEN").toInt(); return keep_file_open; } #endif int Okular::Part::numberOfParts = 0; namespace Okular { Part::Part(QWidget *parentWidget, QObject *parent, const QVariantList &args) : KParts::ReadWritePart(parent), m_tempfile( nullptr ), m_documentOpenWithPassword( false ), m_swapInsteadOfOpening( false ), m_isReloading( false ), m_fileWasRemoved( false ), m_showMenuBarAction( nullptr ), m_showFullScreenAction( nullptr ), m_actionsSearched( false ), m_cliPresentation(false), m_cliPrint(false), m_cliPrintAndExit(false), m_embedMode(detectEmbedMode(parentWidget, parent, args)), m_generatorGuiClient(nullptr), m_keeper( nullptr ) { // make sure that the component name is okular otherwise the XMLGUI .rc files are not found // when this part is used in an application other than okular (e.g. unit tests) setComponentName(QStringLiteral("okular"), QString()); const QLatin1String configFileName("okularpartrc"); // first, we check if a config file name has been specified QString configFilePath = detectConfigFileName( args ); if ( configFilePath.isEmpty() ) { configFilePath = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QLatin1Char('/') + configFileName; } // Migrate old config if ( !QFile::exists( configFilePath ) ) { qCDebug(OkularUiDebug) << "Did not find a config file, attempting to look for old config"; // Migrate old config + UI Kdelibs4ConfigMigrator configMigrator( componentName() ); // UI file is handled automatically, we only need to specify config name because we're a part configMigrator.setConfigFiles( QStringList( configFileName ) ); // If there's no old okular config to migrate, look for kpdf if ( !configMigrator.migrate() ) { qCDebug(OkularUiDebug) << "Did not find an old okular config file, attempting to look for kpdf config"; // First try the automatic detection, using $KDEHOME etc. Kdelibs4Migration migration; QString kpdfConfig = migration.locateLocal( "config", QStringLiteral("kpdfpartrc") ); // Fallback just in case it tried e. g. ~/.kde4 if ( kpdfConfig.isEmpty() ) { kpdfConfig = QDir::homePath() + QStringLiteral("/.kde/share/config/kpdfpartrc"); } if ( QFile::exists( kpdfConfig ) ) { qCDebug(OkularUiDebug) << "Found old kpdf config" << kpdfConfig << "copying to" << configFilePath; QFile::copy( kpdfConfig, configFilePath ); } else { qCDebug(OkularUiDebug) << "Did not find an old kpdf config file"; } } else { qCDebug(OkularUiDebug) << "Migrated old okular config"; } } Okular::Settings::instance( configFilePath ); numberOfParts++; if (numberOfParts == 1) { m_registerDbusName = QStringLiteral("/okular"); } else { m_registerDbusName = QStringLiteral("/okular%1").arg(numberOfParts); } QDBusConnection::sessionBus().registerObject(m_registerDbusName, this, QDBusConnection::ExportScriptableSlots); // connect the started signal to tell the job the mimetypes we like, // and get some more information from it connect(this, &KParts::ReadOnlyPart::started, this, &Part::slotJobStarted); // connect the completed signal so we can put the window caption when loading remote files connect(this, QOverload<>::of(&Part::completed), this, &Part::setWindowTitleFromDocument); connect(this, &KParts::ReadOnlyPart::canceled, this, &Part::loadCancelled); // create browser extension (for printing when embedded into browser) m_bExtension = new BrowserExtension(this); // create live connect extension (for integrating with browser scripting) new OkularLiveConnectExtension( this ); GuiUtils::addIconLoader( iconLoader() ); m_sidebar = new Sidebar( parentWidget ); setWidget( m_sidebar ); connect( m_sidebar, &Sidebar::urlsDropped, this, &Part::handleDroppedUrls ); // build the document m_document = new Okular::Document(widget()); connect( m_document, &Document::linkFind, this, &Part::slotFind ); connect( m_document, &Document::linkGoToPage, this, &Part::slotGoToPage ); connect( m_document, &Document::linkPresentation, this, &Part::slotShowPresentation ); connect( m_document, &Document::linkEndPresentation, this, &Part::slotHidePresentation ); connect( m_document, &Document::openUrl, this, &Part::openUrlFromDocument ); connect( m_document->bookmarkManager(), &BookmarkManager::openUrl, this, &Part::openUrlFromBookmarks ); connect( m_document, &Document::close, this, &Part::close ); connect( m_document, &Document::undoHistoryCleanChanged, this, [this](bool clean) { setModified( !clean ); setWindowTitleFromDocument(); } ); if ( parent && parent->metaObject()->indexOfSlot( QMetaObject::normalizedSignature( "slotQuit()" ).constData() ) != -1 ) connect( m_document, SIGNAL(quit()), parent, SLOT(slotQuit()) ); // clazy:exclude=old-style-connect else connect( m_document, &Document::quit, this, &Part::cannotQuit ); // widgets: ^searchbar (toolbar containing label and SearchWidget) // m_searchToolBar = new KToolBar( parentWidget, "searchBar" ); // m_searchToolBar->boxLayout()->setSpacing( KDialog::spacingHint() ); // QLabel * sLabel = new QLabel( i18n( "&Search:" ), m_searchToolBar, "kde toolbar widget" ); // m_searchWidget = new SearchWidget( m_searchToolBar, m_document ); // sLabel->setBuddy( m_searchWidget ); // m_searchToolBar->setStretchableWidget( m_searchWidget ); // [left toolbox: Table of Contents] | [] m_toc = new TOC( nullptr, m_document ); connect( m_toc.data(), &TOC::hasTOC, this, &Part::enableTOC ); connect( m_toc.data(), &TOC::rightClick, this, &Part::slotShowTOCMenu ); m_sidebar->addItem( m_toc, QIcon::fromTheme(QApplication::isLeftToRight() ? QStringLiteral("format-justify-left") : QStringLiteral("format-justify-right")), i18n("Contents") ); enableTOC( false ); // [left toolbox: Layers] | [] m_layers = new Layers( nullptr, m_document ); connect( m_layers.data(), &Layers::hasLayers, this, &Part::enableLayers ); m_sidebar->addItem( m_layers, QIcon::fromTheme( QStringLiteral("format-list-unordered") ), i18n( "Layers" ) ); enableLayers( false ); // [left toolbox: Thumbnails and Bookmarks] | [] QWidget * thumbsBox = new ThumbnailsBox( nullptr ); thumbsBox->layout()->setSpacing( 6 ); m_searchWidget = new SearchWidget( thumbsBox, m_document ); thumbsBox->layout()->addWidget(m_searchWidget); m_thumbnailList = new ThumbnailList( thumbsBox, m_document ); thumbsBox->layout()->addWidget(m_thumbnailList); // ThumbnailController * m_tc = new ThumbnailController( thumbsBox, m_thumbnailList ); connect( m_thumbnailList.data(), &ThumbnailList::rightClick, this, &Part::slotShowMenu ); m_sidebar->addItem( thumbsBox, QIcon::fromTheme( QStringLiteral("view-preview") ), i18n("Thumbnails") ); m_sidebar->setCurrentItem( thumbsBox ); // [left toolbox: Reviews] | [] m_reviewsWidget = new Reviews( nullptr, m_document ); m_sidebar->addItem( m_reviewsWidget, QIcon::fromTheme(QStringLiteral("draw-freehand")), i18n("Reviews") ); m_sidebar->setItemEnabled( m_reviewsWidget, false ); // [left toolbox: Bookmarks] | [] m_bookmarkList = new BookmarkList( m_document, nullptr ); m_sidebar->addItem( m_bookmarkList, QIcon::fromTheme(QStringLiteral("bookmarks")), i18n("Bookmarks") ); m_sidebar->setItemEnabled( m_bookmarkList, false ); // [left toolbox: Signature Panel] | [] m_signaturePanel = new SignaturePanel( m_document, nullptr ); connect( m_signaturePanel.data(), &SignaturePanel::documentHasSignatures, this, &Part::showSidebarSignaturesItem ); m_sidebar->addItem( m_signaturePanel, QIcon::fromTheme(QStringLiteral("application-pkcs7-signature")), i18n("Signatures") ); showSidebarSignaturesItem( false ); // widgets: [../miniBarContainer] | [] #ifdef OKULAR_ENABLE_MINIBAR QWidget * miniBarContainer = new QWidget( 0 ); m_sidebar->setBottomWidget( miniBarContainer ); QVBoxLayout * miniBarLayout = new QVBoxLayout( miniBarContainer ); miniBarLayout->setContentsMargins( 0, 0, 0, 0 ); // widgets: [../[spacer/..]] | [] miniBarLayout->addItem( new QSpacerItem( 6, 6, QSizePolicy::Fixed, QSizePolicy::Fixed ) ); // widgets: [../[../MiniBar]] | [] QFrame * bevelContainer = new QFrame( miniBarContainer ); bevelContainer->setFrameStyle( QFrame::StyledPanel | QFrame::Sunken ); QVBoxLayout * bevelContainerLayout = new QVBoxLayout( bevelContainer ); bevelContainerLayout->setContentsMargins( 4, 4, 4, 4 ); m_progressWidget = new ProgressWidget( bevelContainer, m_document ); bevelContainerLayout->addWidget( m_progressWidget ); miniBarLayout->addWidget( bevelContainer ); miniBarLayout->addItem( new QSpacerItem( 6, 6, QSizePolicy::Fixed, QSizePolicy::Fixed ) ); #endif // widgets: [] | [right 'pageView'] QWidget * rightContainer = new QWidget( nullptr ); m_sidebar->setMainWidget( rightContainer ); QVBoxLayout * rightLayout = new QVBoxLayout( rightContainer ); rightLayout->setContentsMargins( 0, 0, 0, 0 ); rightLayout->setSpacing( 0 ); // KToolBar * rtb = new KToolBar( rightContainer, "mainToolBarSS" ); // rightLayout->addWidget( rtb ); m_migrationMessage = new KMessageWidget( rightContainer ); m_migrationMessage->setVisible( false ); m_migrationMessage->setWordWrap( true ); m_migrationMessage->setMessageType( KMessageWidget::Warning ); m_migrationMessage->setText( i18n( "This document contains annotations or form data that were saved internally by a previous Okular version. Internal storage is no longer supported.
Please save to a file in order to move them if you want to continue to edit the document." ) ); rightLayout->addWidget( m_migrationMessage ); m_topMessage = new KMessageWidget( rightContainer ); m_topMessage->setVisible( false ); m_topMessage->setWordWrap( true ); m_topMessage->setMessageType( KMessageWidget::Information ); m_topMessage->setText( i18n( "This document has embedded files. Click here to see them or go to File -> Embedded Files." ) ); m_topMessage->setIcon( QIcon::fromTheme( QStringLiteral("mail-attachment") ) ); connect( m_topMessage, &KMessageWidget::linkActivated, this, &Part::slotShowEmbeddedFiles ); rightLayout->addWidget( m_topMessage ); m_formsMessage = new KMessageWidget( rightContainer ); m_formsMessage->setVisible( false ); m_formsMessage->setWordWrap( true ); m_formsMessage->setMessageType( KMessageWidget::Information ); rightLayout->addWidget( m_formsMessage ); m_infoMessage = new KMessageWidget( rightContainer ); m_infoMessage->setVisible( false ); m_infoMessage->setWordWrap( true ); m_infoMessage->setMessageType( KMessageWidget::Information ); rightLayout->addWidget( m_infoMessage ); m_infoTimer = new QTimer(); m_infoTimer->setSingleShot( true ); connect( m_infoTimer, &QTimer::timeout, m_infoMessage, &KMessageWidget::animatedHide ); m_signatureMessage = new KMessageWidget( rightContainer ); m_signatureMessage->setVisible( false ); m_signatureMessage->setWordWrap( true ); m_signatureMessage->setMessageType( KMessageWidget::Information ); rightLayout->addWidget( m_signatureMessage ); m_pageView = new PageView( rightContainer, m_document ); QMetaObject::invokeMethod( m_pageView, "setFocus", Qt::QueuedConnection ); //usability setting // m_splitter->setFocusProxy(m_pageView); connect( m_pageView.data(), &PageView::rightClick, this, &Part::slotShowMenu ); connect( m_pageView, &PageView::triggerSearch, this, [this] (const QString& searchText){ m_findBar->startSearch(searchText); slotShowFindBar(); } ); connect( m_document, &Document::error, this, &Part::errorMessage ); connect( m_document, &Document::warning, this, &Part::warningMessage ); connect( m_document, &Document::notice, this, &Part::noticeMessage ); connect( m_document, &Document::sourceReferenceActivated, this, &Part::slotHandleActivatedSourceReference ); connect( m_pageView.data(), &PageView::fitWindowToPage, this, &Part::fitWindowToPage ); rightLayout->addWidget( m_pageView ); m_layers->setPageView( m_pageView ); m_signaturePanel->setPageView( m_pageView ); m_findBar = new FindBar( m_document, rightContainer ); rightLayout->addWidget( m_findBar ); m_bottomBar = new QWidget( rightContainer ); QHBoxLayout * bottomBarLayout = new QHBoxLayout( m_bottomBar ); m_pageSizeLabel = new PageSizeLabel( m_bottomBar, m_document ); bottomBarLayout->setContentsMargins( 0, 0, 0, 0 ); bottomBarLayout->setSpacing( 0 ); bottomBarLayout->addItem( new QSpacerItem( 5, 5, QSizePolicy::Expanding, QSizePolicy::Minimum ) ); m_miniBarLogic = new MiniBarLogic( this, m_document ); m_miniBar = new MiniBar( m_bottomBar, m_miniBarLogic ); bottomBarLayout->addWidget( m_miniBar ); bottomBarLayout->addWidget( m_pageSizeLabel ); rightLayout->addWidget( m_bottomBar ); m_pageNumberTool = new MiniBar( nullptr, m_miniBarLogic ); connect( m_findBar, &FindBar::forwardKeyPressEvent, m_pageView, &PageView::externalKeyPressEvent ); connect( m_findBar, &FindBar::onCloseButtonPressed, m_pageView, QOverload<>::of(&PageView::setFocus) ); connect( m_miniBar, &MiniBar::forwardKeyPressEvent, m_pageView, &PageView::externalKeyPressEvent ); connect( m_pageView.data(), &PageView::escPressed, m_findBar, &FindBar::resetSearch ); connect( m_pageNumberTool, &MiniBar::forwardKeyPressEvent, m_pageView, &PageView::externalKeyPressEvent); connect( m_reviewsWidget.data(), &Reviews::openAnnotationWindow, m_pageView.data(), &PageView::openAnnotationWindow ); // add document observers m_document->addObserver( this ); m_document->addObserver( m_thumbnailList ); m_document->addObserver( m_pageView ); m_document->registerView( m_pageView ); m_document->addObserver( m_toc ); m_document->addObserver( m_miniBarLogic ); #ifdef OKULAR_ENABLE_MINIBAR m_document->addObserver( m_progressWidget ); #endif m_document->addObserver( m_reviewsWidget ); m_document->addObserver( m_pageSizeLabel ); m_document->addObserver( m_bookmarkList ); m_document->addObserver( m_signaturePanel ); connect( m_document->bookmarkManager(), &BookmarkManager::saved, this, &Part::slotRebuildBookmarkMenu ); setupViewerActions(); if ( m_embedMode != ViewerWidgetMode ) { setupActions(); } else { setViewerShortcuts(); } // document watcher and reloader m_watcher = new KDirWatch( this ); connect( m_watcher, &KDirWatch::dirty, this, &Part::slotFileDirty ); connect( m_watcher, &KDirWatch::created, this, &Part::slotFileDirty ); connect( m_watcher, &KDirWatch::deleted, this, &Part::slotFileDirty ); m_dirtyHandler = new QTimer( this ); m_dirtyHandler->setSingleShot( true ); connect( m_dirtyHandler, &QTimer::timeout, this, [this] { slotAttemptReload(); } ); slotNewConfig(); // keep us informed when the user changes settings connect( Okular::Settings::self(), &KCoreConfigSkeleton::configChanged, this, &Part::slotNewConfig ); #ifdef HAVE_SPEECH // [SPEECH] check for TTS presence and usability Okular::Settings::setUseTTS( true ); Okular::Settings::self()->save(); #endif rebuildBookmarkMenu( false ); if ( m_embedMode == ViewerWidgetMode ) { // set the XML-UI resource file for the viewer mode setXMLFile(QStringLiteral("part-viewermode.rc")); } else { // set our main XML-UI resource file setXMLFile(QStringLiteral("part.rc")); } m_pageView->setupBaseActions( actionCollection() ); m_sidebar->setSidebarVisibility( false ); if ( m_embedMode != PrintPreviewMode ) { // now set up actions that are required for all remaining modes m_pageView->setupViewerActions( actionCollection() ); // and if we are not in viewer mode, we want the full GUI if ( m_embedMode != ViewerWidgetMode ) { unsetDummyMode(); } } // ensure history actions are in the correct state updateViewActions(); // also update the state of the actions in the page view m_pageView->updateActionState( false, false, false ); if ( m_embedMode == NativeShellMode ) m_sidebar->setAutoFillBackground( false ); #ifdef OKULAR_KEEP_FILE_OPEN m_keeper = new FileKeeper(); #endif } void Part::setupViewerActions() { // ACTIONS KActionCollection * ac = actionCollection(); // Page Traversal actions m_gotoPage = KStandardAction::gotoPage( this, SLOT(slotGoToPage()), ac ); ac->setDefaultShortcuts(m_gotoPage, KStandardShortcut::gotoLine()); // dirty way to activate gotopage when pressing miniBar's button connect( m_miniBar.data(), &MiniBar::gotoPage, m_gotoPage, &QAction::trigger ); connect( m_pageNumberTool.data(), &MiniBar::gotoPage, m_gotoPage, &QAction::trigger ); m_prevPage = KStandardAction::prior(this, SLOT(slotPreviousPage()), ac); m_prevPage->setIconText( i18nc( "Previous page", "Previous" ) ); m_prevPage->setToolTip( i18n( "Go back to the Previous Page" ) ); m_prevPage->setWhatsThis( i18n( "Moves to the previous page of the document" ) ); ac->setDefaultShortcut(m_prevPage, QKeySequence()); // dirty way to activate prev page when pressing miniBar's button connect( m_miniBar.data(), &MiniBar::prevPage, m_prevPage, &QAction::trigger ); connect( m_pageNumberTool.data(), &MiniBar::prevPage, m_prevPage, &QAction::trigger ); #ifdef OKULAR_ENABLE_MINIBAR connect( m_progressWidget, SIGNAL(prevPage()), m_prevPage, SLOT(trigger()) ); #endif m_nextPage = KStandardAction::next(this, SLOT(slotNextPage()), ac ); m_nextPage->setIconText( i18nc( "Next page", "Next" ) ); m_nextPage->setToolTip( i18n( "Advance to the Next Page" ) ); m_nextPage->setWhatsThis( i18n( "Moves to the next page of the document" ) ); ac->setDefaultShortcut(m_nextPage, QKeySequence()); // dirty way to activate next page when pressing miniBar's button connect( m_miniBar.data(), &MiniBar::nextPage, m_nextPage, &QAction::trigger ); connect( m_pageNumberTool.data(), &MiniBar::nextPage, m_nextPage, &QAction::trigger ); #ifdef OKULAR_ENABLE_MINIBAR connect( m_progressWidget, SIGNAL(nextPage()), m_nextPage, SLOT(trigger()) ); #endif m_beginningOfDocument = KStandardAction::firstPage( this, SLOT(slotGotoFirst()), ac ); ac->addAction(QStringLiteral("first_page"), m_beginningOfDocument); m_beginningOfDocument->setText(i18n( "Beginning of the document")); m_beginningOfDocument->setWhatsThis( i18n( "Moves to the beginning of the document" ) ); m_endOfDocument = KStandardAction::lastPage( this, SLOT(slotGotoLast()), ac ); ac->addAction(QStringLiteral("last_page"),m_endOfDocument); m_endOfDocument->setText(i18n( "End of the document")); m_endOfDocument->setWhatsThis( i18n( "Moves to the end of the document" ) ); // we do not want back and next in history in the dummy mode m_historyBack = nullptr; m_historyNext = nullptr; m_addBookmark = KStandardAction::addBookmark( this, SLOT(slotAddBookmark()), ac ); m_addBookmarkText = m_addBookmark->text(); m_addBookmarkIcon = m_addBookmark->icon(); m_renameBookmark = ac->addAction(QStringLiteral("rename_bookmark")); m_renameBookmark->setText(i18n( "Rename Bookmark" )); m_renameBookmark->setIcon(QIcon::fromTheme( QStringLiteral("edit-rename") )); m_renameBookmark->setWhatsThis( i18n( "Rename the current bookmark" ) ); connect( m_renameBookmark, &QAction::triggered, this, &Part::slotRenameCurrentViewportBookmark ); m_prevBookmark = ac->addAction(QStringLiteral("previous_bookmark")); m_prevBookmark->setText(i18n( "Previous Bookmark" )); m_prevBookmark->setIcon(QIcon::fromTheme( QStringLiteral("go-up-search") )); m_prevBookmark->setWhatsThis( i18n( "Go to the previous bookmark" ) ); connect( m_prevBookmark, &QAction::triggered, this, &Part::slotPreviousBookmark ); m_nextBookmark = ac->addAction(QStringLiteral("next_bookmark")); m_nextBookmark->setText(i18n( "Next Bookmark" )); m_nextBookmark->setIcon(QIcon::fromTheme( QStringLiteral("go-down-search") )); m_nextBookmark->setWhatsThis( i18n( "Go to the next bookmark" ) ); connect( m_nextBookmark, &QAction::triggered, this, &Part::slotNextBookmark ); m_copy = nullptr; m_selectAll = nullptr; m_selectCurrentPage = nullptr; // Find and other actions m_find = KStandardAction::find( this, SLOT(slotShowFindBar()), ac ); QList s = m_find->shortcuts(); s.append( QKeySequence( Qt::Key_Slash ) ); ac->setDefaultShortcuts(m_find, s); m_find->setEnabled( false ); m_findNext = KStandardAction::findNext( this, SLOT(slotFindNext()), ac); m_findNext->setEnabled( false ); m_findPrev = KStandardAction::findPrev( this, SLOT(slotFindPrev()), ac ); m_findPrev->setEnabled( false ); m_save = nullptr; m_saveAs = nullptr; m_openContainingFolder = nullptr; QAction * prefs = KStandardAction::preferences( this, SLOT(slotPreferences()), ac); if ( m_embedMode == NativeShellMode ) { prefs->setText( i18n( "Configure Okular..." ) ); } else { // TODO: improve this message prefs->setText( i18n( "Configure Viewer..." ) ); } QAction * genPrefs = new QAction( ac ); ac->addAction(QStringLiteral("options_configure_generators"), genPrefs); if ( m_embedMode == ViewerWidgetMode ) { genPrefs->setText( i18n( "Configure Viewer Backends..." ) ); } else { genPrefs->setText( i18n( "Configure Backends..." ) ); } genPrefs->setIcon( QIcon::fromTheme( QStringLiteral("configure") ) ); genPrefs->setEnabled( m_document->configurableGenerators() > 0 ); connect( genPrefs, &QAction::triggered, this, &Part::slotGeneratorPreferences ); m_printPreview = KStandardAction::printPreview( this, SLOT(slotPrintPreview()), ac ); m_printPreview->setEnabled( false ); m_showLeftPanel = nullptr; m_showBottomBar = nullptr; m_showSignaturePanel = nullptr; m_showProperties = ac->addAction(QStringLiteral("properties")); m_showProperties->setText(i18n("&Properties")); m_showProperties->setIcon(QIcon::fromTheme(QStringLiteral("document-properties"))); connect(m_showProperties, &QAction::triggered, this, &Part::slotShowProperties); m_showProperties->setEnabled( false ); m_showEmbeddedFiles = nullptr; m_showPresentation = nullptr; m_exportAs = nullptr; m_exportAsMenu = nullptr; m_exportAsText = nullptr; m_exportAsDocArchive = nullptr; #if PURPOSE_FOUND m_share = nullptr; m_shareMenu = nullptr; #endif m_presentationDrawingActions = nullptr; m_aboutBackend = ac->addAction(QStringLiteral("help_about_backend")); m_aboutBackend->setText(i18n("About Backend")); m_aboutBackend->setEnabled( false ); connect(m_aboutBackend, &QAction::triggered, this, &Part::slotAboutBackend); QAction *reload = ac->add( QStringLiteral("file_reload") ); reload->setText( i18n( "Reloa&d" ) ); reload->setIcon( QIcon::fromTheme( QStringLiteral("view-refresh") ) ); reload->setWhatsThis( i18n( "Reload the current document from disk." ) ); connect( reload, &QAction::triggered, this, &Part::slotReload ); ac->setDefaultShortcuts(reload, KStandardShortcut::reload()); m_reload = reload; m_closeFindBar = ac->addAction( QStringLiteral("close_find_bar"), this, SLOT(slotHideFindBar()) ); m_closeFindBar->setText( i18n("Close &Find Bar") ); ac->setDefaultShortcut(m_closeFindBar, QKeySequence(Qt::Key_Escape)); m_closeFindBar->setEnabled( false ); QWidgetAction *pageno = new QWidgetAction( ac ); pageno->setText( i18n( "Page Number" ) ); pageno->setDefaultWidget( m_pageNumberTool ); ac->addAction( QStringLiteral("page_number"), pageno ); } void Part::setViewerShortcuts() { KActionCollection * ac = actionCollection(); ac->setDefaultShortcut(m_gotoPage, QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_G)); ac->setDefaultShortcut(m_find, QKeySequence()); ac->setDefaultShortcut(m_findNext, QKeySequence()); ac->setDefaultShortcut(m_findPrev, QKeySequence()); ac->setDefaultShortcut(m_addBookmark, QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_B)); ac->setDefaultShortcut(m_beginningOfDocument, QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_Home)); ac->setDefaultShortcut(m_endOfDocument, QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_End)); QAction *action = static_cast( ac->action( QStringLiteral("file_reload") ) ); if (action) { ac->setDefaultShortcut(action, QKeySequence(Qt::ALT + Qt::Key_F5)); } } void Part::setupActions() { KActionCollection * ac = actionCollection(); m_copy = KStandardAction::create( KStandardAction::Copy, m_pageView, SLOT(copyTextSelection()), ac ); m_selectAll = KStandardAction::selectAll( m_pageView, SLOT(selectAll()), ac ); // Setup select all action for the current page m_selectCurrentPage = ac->addAction(QStringLiteral("edit_select_all_current_page")); m_selectCurrentPage->setText(i18n("Select All Text on Current Page")); connect( m_selectCurrentPage, &QAction::triggered, m_pageView, &PageView::slotSelectPage ); m_selectCurrentPage->setEnabled( false ); m_save = KStandardAction::save( this, [this] { saveFile(); }, ac ); m_save->setEnabled( false ); m_saveAs = KStandardAction::saveAs( this, SLOT(slotSaveFileAs()), ac ); m_saveAs->setEnabled( false ); m_migrationMessage->addAction( m_saveAs ); m_showLeftPanel = ac->add(QStringLiteral("show_leftpanel")); m_showLeftPanel->setText(i18n( "Show &Navigation Panel")); m_showLeftPanel->setIcon(QIcon::fromTheme( QStringLiteral("view-sidetree") )); connect( m_showLeftPanel, &QAction::toggled, this, &Part::slotShowLeftPanel ); ac->setDefaultShortcut(m_showLeftPanel, QKeySequence(Qt::Key_F7)); m_showLeftPanel->setChecked( Okular::Settings::showLeftPanel() ); slotShowLeftPanel(); m_showBottomBar = ac->add(QStringLiteral("show_bottombar")); m_showBottomBar->setText(i18n( "Show &Page Bar")); connect( m_showBottomBar, &QAction::toggled, this, &Part::slotShowBottomBar ); m_showBottomBar->setChecked( Okular::Settings::showBottomBar() ); slotShowBottomBar(); m_showSignaturePanel = ac->add(QStringLiteral("show_signatures")); m_showSignaturePanel->setText(i18n("Show &Signatures Panel")); connect( m_showSignaturePanel, &QAction::triggered, this, [this] { if ( m_sidebar->currentItem() != m_signaturePanel) { m_sidebar->setCurrentItem( m_signaturePanel ); } }); m_showEmbeddedFiles = ac->addAction(QStringLiteral("embedded_files")); m_showEmbeddedFiles->setText(i18n("&Embedded Files")); m_showEmbeddedFiles->setIcon( QIcon::fromTheme( QStringLiteral("mail-attachment") ) ); connect(m_showEmbeddedFiles, &QAction::triggered, this, &Part::slotShowEmbeddedFiles); m_showEmbeddedFiles->setEnabled( false ); m_exportAs = ac->addAction(QStringLiteral("file_export_as")); m_exportAs->setText(i18n("E&xport As")); m_exportAs->setIcon( QIcon::fromTheme( QStringLiteral("document-export") ) ); m_exportAsMenu = new QMenu(); connect(m_exportAsMenu, &QMenu::triggered, this, &Part::slotExportAs); m_exportAs->setMenu( m_exportAsMenu ); m_exportAsText = actionForExportFormat( Okular::ExportFormat::standardFormat( Okular::ExportFormat::PlainText ), m_exportAsMenu ); m_exportAsMenu->addAction( m_exportAsText ); m_exportAs->setEnabled( false ); m_exportAsText->setEnabled( false ); #if PURPOSE_FOUND m_share = ac->addAction( QStringLiteral("file_share") ); m_share->setText( i18n("S&hare") ); m_share->setIcon( QIcon::fromTheme( QStringLiteral("document-share") ) ); m_share->setEnabled( false ); m_shareMenu = new Purpose::Menu(); connect(m_shareMenu, &Purpose::Menu::finished, this, &Part::slotShareActionFinished); m_share->setMenu( m_shareMenu ); #endif m_showPresentation = ac->addAction(QStringLiteral("presentation")); m_showPresentation->setText(i18n("P&resentation")); m_showPresentation->setIcon( QIcon::fromTheme( QStringLiteral("view-presentation") ) ); connect(m_showPresentation, &QAction::triggered, this, &Part::slotShowPresentation); ac->setDefaultShortcut(m_showPresentation, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_P)); m_showPresentation->setEnabled( false ); m_openContainingFolder = ac->addAction(QStringLiteral("open_containing_folder")); m_openContainingFolder->setText(i18n("Open Con&taining Folder")); m_openContainingFolder->setIcon( QIcon::fromTheme( QStringLiteral("document-open-folder") ) ); connect(m_openContainingFolder, &QAction::triggered, this, &Part::slotOpenContainingFolder); m_openContainingFolder->setEnabled( false ); QAction * importPS = ac->addAction(QStringLiteral("import_ps")); importPS->setText(i18n("&Import PostScript as PDF...")); importPS->setIcon(QIcon::fromTheme(QStringLiteral("document-import"))); connect(importPS, &QAction::triggered, this, &Part::slotImportPSFile); #if 0 QAction * ghns = ac->addAction("get_new_stuff"); ghns->setText(i18n("&Get Books From Internet...")); ghns->setIcon(QIcon::fromTheme("get-hot-new-stuff")); connect(ghns, SIGNAL(triggered()), this, SLOT(slotGetNewStuff())); #endif KToggleAction *blackscreenAction = new KToggleAction( i18n( "Switch Blackscreen Mode" ), ac ); ac->addAction( QStringLiteral("switch_blackscreen_mode"), blackscreenAction ); ac->setDefaultShortcut(blackscreenAction, QKeySequence(Qt::Key_B)); blackscreenAction->setIcon( QIcon::fromTheme( QStringLiteral("view-presentation") ) ); blackscreenAction->setEnabled( false ); m_presentationDrawingActions = new DrawingToolActions( ac ); QAction *eraseDrawingAction = new QAction( i18n( "Erase Drawing" ), ac ); ac->addAction( QStringLiteral("presentation_erase_drawings"), eraseDrawingAction ); eraseDrawingAction->setIcon( QIcon::fromTheme( QStringLiteral("draw-eraser-delete-objects") ) ); eraseDrawingAction->setEnabled( false ); QAction *configureAnnotations = new QAction( i18n( "Configure Annotations..." ), ac ); ac->addAction( QStringLiteral("options_configure_annotations"), configureAnnotations ); configureAnnotations->setIcon( QIcon::fromTheme( QStringLiteral("configure") ) ); connect(configureAnnotations, &QAction::triggered, this, &Part::slotAnnotationPreferences); QAction *playPauseAction = new QAction( i18n( "Play/Pause Presentation" ), ac ); ac->addAction( QStringLiteral("presentation_play_pause"), playPauseAction ); playPauseAction->setEnabled( false ); } Part::~Part() { QDBusConnection::sessionBus().unregisterObject(m_registerDbusName); GuiUtils::removeIconLoader( iconLoader() ); m_document->removeObserver( this ); if ( m_document->isOpened() ) Part::closeUrl( false ); delete m_toc; delete m_layers; delete m_pageView; delete m_thumbnailList; delete m_miniBar; delete m_pageNumberTool; delete m_miniBarLogic; delete m_bottomBar; #ifdef OKULAR_ENABLE_MINIBAR delete m_progressWidget; #endif delete m_pageSizeLabel; delete m_reviewsWidget; delete m_bookmarkList; delete m_infoTimer; delete m_signaturePanel; delete m_document; delete m_tempfile; qDeleteAll( m_bookmarkActions ); delete m_exportAsMenu; #if PURPOSE_FOUND delete m_shareMenu; #endif #ifdef OKULAR_KEEP_FILE_OPEN delete m_keeper; #endif } bool Part::openDocument(const QUrl& url, uint page) { Okular::DocumentViewport vp( page - 1 ); vp.rePos.enabled = true; vp.rePos.normalizedX = 0; vp.rePos.normalizedY = 0; vp.rePos.pos = Okular::DocumentViewport::TopLeft; if ( vp.isValid() ) m_document->setNextDocumentViewport( vp ); return openUrl( url ); } void Part::startPresentation() { m_cliPresentation = true; } QStringList Part::supportedMimeTypes() const { return m_document->supportedMimeTypes(); } QUrl Part::realUrl() const { if ( !m_realUrl.isEmpty() ) return m_realUrl; return url(); } // ViewerInterface void Part::showSourceLocation(const QString& fileName, int line, int column, bool showGraphically) { Q_UNUSED(column); const QString u = QStringLiteral( "src:%1 %2" ).arg( line + 1 ).arg( fileName ); GotoAction action( QString(), u ); m_document->processAction( &action ); if( showGraphically ) { m_pageView->setLastSourceLocationViewport( m_document->viewport() ); } } void Part::clearLastShownSourceLocation() { m_pageView->clearLastSourceLocationViewport(); } bool Part::isWatchFileModeEnabled() const { return !m_watcher->signalsBlocked(); } void Part::setWatchFileModeEnabled(bool enabled) { // Don't call 'KDirWatch::stopScan()' in here (as of KDE Frameworks 5.51.0, see bug 400541)! // 'KDirWatch::stopScan' has a bug that may affect other code paths that make use of KDirWatch // (other loaded KParts, for example). if( isWatchFileModeEnabled() == enabled ) { return; } m_watcher->blockSignals(!enabled); if( !enabled ) { m_dirtyHandler->stop(); } } bool Part::areSourceLocationsShownGraphically() const { return m_pageView->areSourceLocationsShownGraphically(); } void Part::setShowSourceLocationsGraphically(bool show) { m_pageView->setShowSourceLocationsGraphically(show); } bool Part::openNewFilesInTabs() const { return Okular::Settings::self()->shellOpenFileInTabs(); } void Part::slotHandleActivatedSourceReference(const QString& absFileName, int line, int col, bool *handled) { emit openSourceReference( absFileName, line, col ); if ( m_embedMode == Okular::ViewerWidgetMode ) { *handled = true; } } void Part::openUrlFromDocument(const QUrl &url) { if ( m_embedMode == PrintPreviewMode ) return; if (url.isLocalFile()) { if (!QFile::exists(url.toLocalFile())) { KMessageBox::error( widget(), i18n("Could not open '%1'. File does not exist", url.toDisplayString() ) ); return; } } else { KIO::StatJob *statJob = KIO::stat(url, KIO::StatJob::SourceSide, 0); KJobWidgets::setWindow(statJob, widget()); if (!statJob->exec() || statJob->error()) { KMessageBox::error( widget(), i18n("Could not open '%1' (%2) ", url.toDisplayString(), statJob->errorString() ) ); return; } } emit m_bExtension->openUrlNotify(); emit m_bExtension->setLocationBarUrl(url.toDisplayString()); openUrl(url); } void Part::openUrlFromBookmarks(const QUrl &_url) { QUrl url = _url; Okular::DocumentViewport vp( _url.fragment(QUrl::FullyDecoded) ); if ( vp.isValid() ) m_document->setNextDocumentViewport( vp ); url.setFragment( QString() ); if ( m_document->currentDocument() == url ) { if ( vp.isValid() ) m_document->setViewport( vp ); } else openUrl( url ); } void Part::handleDroppedUrls( const QList& urls ) { if ( urls.isEmpty() ) return; if ( m_embedMode != NativeShellMode || !openNewFilesInTabs() ) { openUrlFromDocument( urls.first() ); return; } emit urlsDropped( urls ); } void Part::slotJobStarted(KIO::Job *job) { if (job) { QStringList supportedMimeTypes = m_document->supportedMimeTypes(); job->addMetaData(QStringLiteral("accept"), supportedMimeTypes.join(QStringLiteral(", ")) + QStringLiteral(", */*;q=0.5")); connect(job, &KJob::result, this, &Part::slotJobFinished); } } void Part::slotJobFinished(KJob *job) { if ( job->error() == KIO::ERR_USER_CANCELED ) { m_pageView->displayMessage( i18n( "The loading of %1 has been canceled.", realUrl().toDisplayString(QUrl::PreferLocalFile) ) ); } } void Part::loadCancelled(const QString &reason) { emit setWindowCaption( QString() ); resetStartArguments(); // when m_viewportDirty.pageNumber != -1 we come from slotAttemptReload // so we don't want to show an ugly messagebox just because the document is // taking more than usual to be recreated if (m_viewportDirty.pageNumber == -1) { if (!reason.isEmpty()) { KMessageBox::error( widget(), i18n("Could not open %1. Reason: %2", url().toDisplayString(), reason ) ); } } } void Part::setWindowTitleFromDocument() { // If 'DocumentTitle' should be used, check if the document has one. If // either case is false, use the file name. QString title = Okular::Settings::displayDocumentNameOrPath() == Okular::Settings::EnumDisplayDocumentNameOrPath::Path ? realUrl().toDisplayString(QUrl::PreferLocalFile) : realUrl().fileName(); if ( Okular::Settings::displayDocumentTitle() ) { const QString docTitle = m_document->metaData( QStringLiteral("DocumentTitle") ).toString(); if ( !docTitle.isEmpty() && !docTitle.trimmed().isEmpty() ) { title = docTitle; } } emit setWindowCaption( title ); } KConfigDialog * Part::slotGeneratorPreferences( ) { // Create dialog KConfigDialog * dialog = new Okular::BackendConfigDialog( m_pageView, QStringLiteral("generator_prefs"), Okular::Settings::self() ); dialog->setAttribute( Qt::WA_DeleteOnClose ); if( m_embedMode == ViewerWidgetMode ) { dialog->setWindowTitle( i18n( "Configure Viewer Backends" ) ); } else { dialog->setWindowTitle( i18n( "Configure Backends" ) ); } m_document->fillConfigDialog( dialog ); // Show it dialog->setWindowModality( Qt::ApplicationModal ); dialog->show(); return dialog; } void Part::notifySetup( const QVector< Okular::Page * > & /*pages*/, int setupFlags ) { // Hide the migration message if the user has just migrated. Otherwise, // if m_migrationMessage is already hidden, this does nothing. if ( !m_document->isDocdataMigrationNeeded() ) m_migrationMessage->animatedHide(); if ( !( setupFlags & Okular::DocumentObserver::DocumentChanged ) ) return; rebuildBookmarkMenu(); updateAboutBackendAction(); m_findBar->resetSearch(); m_searchWidget->setEnabled( m_document->supportsSearching() ); } void Part::notifyViewportChanged( bool /*smoothMove*/ ) { updateViewActions(); } void Part::notifyPageChanged( int page, int flags ) { if ( !(flags & Okular::DocumentObserver::Bookmark ) ) return; rebuildBookmarkMenu(); if ( page == m_document->viewport().pageNumber ) updateBookmarksActions(); } void Part::goToPage(uint page) { if ( page <= m_document->pages() ) m_document->setViewportPage( page - 1 ); } void Part::openDocument( const QString &doc ) { openUrl( QUrl::fromUserInput( doc ) ); } uint Part::pages() { return m_document->pages(); } uint Part::currentPage() { return m_document->pages() ? m_document->currentPage() + 1 : 0; } QString Part::currentDocument() { return m_document->currentDocument().toDisplayString(QUrl::PreferLocalFile); } QString Part::documentMetaData( const QString &metaData ) const { const Okular::DocumentInfo info = m_document->documentInfo(); return info.get( metaData ); } bool Part::slotImportPSFile() { QString app = QStandardPaths::findExecutable(QStringLiteral("ps2pdf") ); if ( app.isEmpty() ) { // TODO point the user to their distro packages? KMessageBox::error( widget(), i18n( "The program \"ps2pdf\" was not found, so Okular can not import PS files using it." ), i18n("ps2pdf not found") ); return false; } QMimeDatabase mimeDatabase; QString filter = i18n("PostScript files (%1)", mimeDatabase.mimeTypeForName(QStringLiteral("application/postscript")).globPatterns().join(QLatin1Char(' '))); QUrl url = QFileDialog::getOpenFileUrl( widget(), QString(), QUrl(), filter ); if ( url.isLocalFile() ) { QTemporaryFile tf(QDir::tempPath() + QLatin1String("/okular_XXXXXX.pdf")); tf.setAutoRemove( false ); if ( !tf.open() ) return false; m_temporaryLocalFile = tf.fileName(); tf.close(); setLocalFilePath( url.toLocalFile() ); QStringList args; QProcess *p = new QProcess(); args << url.toLocalFile() << m_temporaryLocalFile; m_pageView->displayMessage(i18n("Importing PS file as PDF (this may take a while)...")); connect(p, QOverload::of(&QProcess::finished), this, &Part::psTransformEnded); p->start(app, args); return true; } m_temporaryLocalFile.clear(); return false; } void Part::setFileToWatch( const QString &filePath ) { if ( !m_watchedFilePath.isEmpty() ) unsetFileToWatch(); const QFileInfo fi(filePath); m_watchedFilePath = filePath; m_watcher->addFile( m_watchedFilePath ); if ( fi.isSymLink() ) { m_watchedFileSymlinkTarget = fi.symLinkTarget(); m_watcher->addFile( m_watchedFileSymlinkTarget ); } else { m_watchedFileSymlinkTarget.clear(); } } void Part::unsetFileToWatch() { if ( m_watchedFilePath.isEmpty() ) return; m_watcher->removeFile( m_watchedFilePath ); if ( !m_watchedFileSymlinkTarget.isEmpty() ) m_watcher->removeFile( m_watchedFileSymlinkTarget ); m_watchedFilePath.clear(); m_watchedFileSymlinkTarget.clear(); } Document::OpenResult Part::doOpenFile( const QMimeType &mimeA, const QString &fileNameToOpenA, bool *isCompressedFile ) { QMimeDatabase db; Document::OpenResult openResult = Document::OpenError; bool uncompressOk = true; QMimeType mime = mimeA; QString fileNameToOpen = fileNameToOpenA; KFilterDev::CompressionType compressionType = compressionTypeFor( mime.name() ); if ( compressionType != KFilterDev::None ) { *isCompressedFile = true; uncompressOk = handleCompressed( fileNameToOpen, localFilePath(), compressionType ); mime = db.mimeTypeForFile( fileNameToOpen ); } else { *isCompressedFile = false; } if ( m_swapInsteadOfOpening ) { m_swapInsteadOfOpening = false; if ( !uncompressOk ) return Document::OpenError; if ( mime.inherits( QStringLiteral("application/vnd.kde.okular-archive") ) ) { isDocumentArchive = true; if (!m_document->swapBackingFileArchive( fileNameToOpen, url() )) return Document::OpenError; } else { isDocumentArchive = false; if (!m_document->swapBackingFile( fileNameToOpen, url() )) return Document::OpenError; } m_fileLastModified = QFileInfo( localFilePath() ).lastModified(); return Document::OpenSuccess; } isDocumentArchive = false; if ( uncompressOk ) { if ( mime.inherits( QStringLiteral("application/vnd.kde.okular-archive") ) ) { openResult = m_document->openDocumentArchive( fileNameToOpen, url() ); isDocumentArchive = true; } else { openResult = m_document->openDocument( fileNameToOpen, url(), mime ); } m_documentOpenWithPassword = false; #ifdef WITH_KWALLET // if the file didn't open correctly it might be encrypted, so ask for a pass QString walletName, walletFolder, walletKey; m_document->walletDataForFile(fileNameToOpen, &walletName, &walletFolder, &walletKey); bool firstInput = true; bool triedWallet = false; KWallet::Wallet * wallet = nullptr; bool keep = true; while ( openResult == Document::OpenNeedsPassword ) { QString password; // 1.A. try to retrieve the first password from the kde wallet system if ( !triedWallet && !walletKey.isNull() ) { const WId parentwid = widget()->effectiveWinId(); wallet = KWallet::Wallet::openWallet( walletName, parentwid ); if ( wallet ) { // use the KPdf folder (and create if missing) if ( !wallet->hasFolder( walletFolder ) ) wallet->createFolder( walletFolder ); wallet->setFolder( walletFolder ); // look for the pass in that folder QString retrievedPass; if ( !wallet->readPassword( walletKey, retrievedPass ) ) password = retrievedPass; } triedWallet = true; } // 1.B. if not retrieved, ask the password using the kde password dialog if ( password.isNull() ) { QString prompt; if ( firstInput ) prompt = i18n( "Please enter the password to read the document:" ); else prompt = i18n( "Incorrect password. Try again:" ); firstInput = false; // if the user presses cancel, abort opening KPasswordDialog dlg( widget(), wallet ? KPasswordDialog::ShowKeepPassword : KPasswordDialog::KPasswordDialogFlags() ); dlg.setWindowTitle( i18n( "Document Password" ) ); dlg.setPrompt( prompt ); if( !dlg.exec() ) break; password = dlg.password(); if ( wallet ) keep = dlg.keepPassword(); } // 2. reopen the document using the password if ( mime.inherits( QStringLiteral("application/vnd.kde.okular-archive") ) ) { openResult = m_document->openDocumentArchive( fileNameToOpen, url(), password ); isDocumentArchive = true; } else { openResult = m_document->openDocument( fileNameToOpen, url(), mime, password ); } if ( openResult == Document::OpenSuccess ) { m_documentOpenWithPassword = true; // 3. if the password is correct and the user chose to remember it, store it to the wallet if (wallet && /*safety check*/ wallet->isOpen() && keep ) { wallet->writePassword( walletKey, password ); } } } #endif } if ( openResult == Document::OpenSuccess ) { m_fileLastModified = QFileInfo( localFilePath() ).lastModified(); } return openResult; } bool Part::openFile() { QList mimes; QString fileNameToOpen = localFilePath(); const bool isstdin = url().isLocalFile() && url().fileName() == QLatin1String( "-" ); const QFileInfo fileInfo( fileNameToOpen ); if ( (!isstdin) && (!fileInfo.exists()) ) return false; QMimeDatabase db; QMimeType pathMime = db.mimeTypeForFile( fileNameToOpen ); if ( !arguments().mimeType().isEmpty() ) { QMimeType argMime = db.mimeTypeForName( arguments().mimeType() ); // Select the "childmost" mimetype, if none of them // inherits the other trust more what pathMime says // but still do a second try if that one fails if ( argMime.inherits( pathMime.name() ) ) { mimes << argMime; } else if ( pathMime.inherits( argMime.name() ) ) { mimes << pathMime; } else { mimes << pathMime << argMime; } if (mimes[0].name() == QLatin1String("text/plain")) { QMimeType contentMime = db.mimeTypeForFile(fileNameToOpen, QMimeDatabase::MatchContent); mimes.prepend( contentMime ); } } else { mimes << pathMime; } QMimeType mime; Document::OpenResult openResult = Document::OpenError; bool isCompressedFile = false; while ( !mimes.isEmpty() && openResult == Document::OpenError ) { mime = mimes.takeFirst(); openResult = doOpenFile( mime, fileNameToOpen, &isCompressedFile ); } bool canSearch = m_document->supportsSearching(); emit mimeTypeChanged( mime ); // update one-time actions const bool ok = openResult == Document::OpenSuccess; emit enableCloseAction( ok ); m_find->setEnabled( ok && canSearch ); m_findNext->setEnabled( ok && canSearch ); m_findPrev->setEnabled( ok && canSearch ); if( m_save ) m_save->setEnabled( ok && !( isstdin || mime.inherits( QStringLiteral("inode/directory") ) ) ); if( m_saveAs ) m_saveAs->setEnabled( ok && !( isstdin || mime.inherits( QStringLiteral("inode/directory") ) ) ); emit enablePrintAction( ok && m_document->printingSupport() != Okular::Document::NoPrinting ); m_printPreview->setEnabled( ok && m_document->printingSupport() != Okular::Document::NoPrinting ); m_showProperties->setEnabled( ok ); if( m_openContainingFolder ) m_openContainingFolder->setEnabled( ok ); bool hasEmbeddedFiles = ok && m_document->embeddedFiles() && m_document->embeddedFiles()->count() > 0; if ( m_showEmbeddedFiles ) m_showEmbeddedFiles->setEnabled( hasEmbeddedFiles ); m_topMessage->setVisible( hasEmbeddedFiles && Okular::Settings::showOSD() ); m_migrationMessage->setVisible( m_document->isDocdataMigrationNeeded() ); // Warn the user that XFA forms are not supported yet (NOTE: poppler generator only) if ( ok && m_document->metaData( QStringLiteral("HasUnsupportedXfaForm") ).toBool() == true ) { m_formsMessage->setText( i18n( "This document has XFA forms, which are currently unsupported." ) ); m_formsMessage->setIcon( QIcon::fromTheme( QStringLiteral("dialog-warning") ) ); m_formsMessage->setMessageType( KMessageWidget::Warning ); m_formsMessage->setVisible( true ); } // m_pageView->toggleFormsAction() may be null on dummy mode else if ( ok && m_pageView->toggleFormsAction() && m_pageView->toggleFormsAction()->isEnabled() ) { m_formsMessage->setText( i18n( "This document has forms. Click on the button to interact with them, or use View -> Show Forms." ) ); m_formsMessage->setMessageType( KMessageWidget::Information ); m_formsMessage->setVisible( true ); } else { m_formsMessage->setVisible( false ); } if ( ok && m_document->metaData( QStringLiteral("IsDigitallySigned") ).toBool() ) { if ( m_embedMode == PrintPreviewMode ) { m_signatureMessage->setText( i18n( "All editing and interactive features for this document are disabled. Please save a copy and reopen to edit this document." ) ); } else { m_signatureMessage->setText( i18n( "This document is digitally signed." ) ); } m_signatureMessage->setVisible( true ); } if ( m_showPresentation ) m_showPresentation->setEnabled( ok ); if ( ok ) { if ( m_exportAs ) { m_exportFormats = m_document->exportFormats(); QList::ConstIterator it = m_exportFormats.constBegin(); QList::ConstIterator itEnd = m_exportFormats.constEnd(); QMenu *menu = m_exportAs->menu(); for ( ; it != itEnd; ++it ) { menu->addAction( actionForExportFormat( *it ) ); } } #if PURPOSE_FOUND if ( m_share ) { m_shareMenu->model()->setInputData(QJsonObject{ { QStringLiteral("mimeType"), mime.name() }, { QStringLiteral("urls"), QJsonArray{ url().toString() } } }); m_shareMenu->model()->setPluginType( QStringLiteral("Export") ); m_shareMenu->reload(); } #endif if ( isCompressedFile ) { m_realUrl = url(); } #ifdef OKULAR_KEEP_FILE_OPEN if ( keepFileOpen() ) m_keeper->open( fileNameToOpen ); #endif // Tries to find the text passed from terminal after the file is open if(!m_textToFindOnOpen.isEmpty()) { m_findBar->startSearch(m_textToFindOnOpen); m_textToFindOnOpen = QString(); } } if ( m_exportAsText ) m_exportAsText->setEnabled( ok && m_document->canExportToText() ); if ( m_exportAs ) m_exportAs->setEnabled( ok ); #if PURPOSE_FOUND if ( m_share ) m_share->setEnabled( ok ); #endif // update viewing actions updateViewActions(); m_fileWasRemoved = false; if ( !ok ) { // if can't open document, update windows so they display blank contents m_pageView->viewport()->update(); m_thumbnailList->update(); setUrl( QUrl() ); return false; } // set the file to the fileWatcher if ( url().isLocalFile() ) setFileToWatch( localFilePath() ); // if the 'OpenTOC' flag is set, open the TOC if ( m_document->metaData( QStringLiteral("OpenTOC") ).toBool() && m_sidebar->isItemEnabled( m_toc ) && !m_sidebar->isCollapsed() && m_sidebar->currentItem() != m_toc ) { m_sidebar->setCurrentItem( m_toc, Sidebar::DoNotUncollapseIfCollapsed ); } // if the 'StartFullScreen' flag is set and we're not in viewer widget mode, or the command line flag was // specified, start presentation const bool presentationBecauseOfDocumentMetadata = ( m_embedMode != ViewerWidgetMode ) && m_document->metaData( QStringLiteral("StartFullScreen") ).toBool(); if ( presentationBecauseOfDocumentMetadata || m_cliPresentation ) { bool goAheadWithPresentationMode = true; if ( !m_cliPresentation ) { const QString text = i18n( "This document wants to be shown full screen.\n" "Leave normal mode and enter presentation mode?" ); const QString caption = i18n( "Request to Change Viewing Mode" ); const KGuiItem yesItem = KGuiItem( i18n( "Enter Presentation Mode" ), QStringLiteral("dialog-ok") ); const KGuiItem noItem = KGuiItem( i18n( "Deny Request" ), QStringLiteral("dialog-cancel") ); const int result = KMessageBox::questionYesNo( widget(), text, caption, yesItem, noItem ); if ( result == KMessageBox::No ) goAheadWithPresentationMode = false; } m_cliPresentation = false; if ( goAheadWithPresentationMode ) QMetaObject::invokeMethod( this, "slotShowPresentation", Qt::QueuedConnection ); } m_generatorGuiClient = factory() ? m_document->guiClient() : nullptr; if ( m_generatorGuiClient ) factory()->addClient( m_generatorGuiClient ); if ( m_cliPrint ) { m_cliPrint = false; slotPrint(); } else if ( m_cliPrintAndExit ) { slotPrint(); } return true; } bool Part::openUrl( const QUrl &url ) { return openUrl( url, false /* swapInsteadOfOpening */ ); } bool Part::openUrl( const QUrl &_url, bool swapInsteadOfOpening ) { /* Store swapInsteadOfOpening, so that closeUrl and openFile will be able * to read it */ m_swapInsteadOfOpening = swapInsteadOfOpening; // The subsequent call to closeUrl clears the arguments. // We want to save them and restore them later. const KParts::OpenUrlArguments args = arguments(); // Close current document if any if ( !closeUrl() ) return false; setArguments(args); QUrl url( _url ); if ( url.hasFragment() ) { const QString dest = url.fragment(QUrl::FullyDecoded); bool ok = true; const int page = dest.toInt( &ok ); if ( ok ) { Okular::DocumentViewport vp( page - 1 ); vp.rePos.enabled = true; vp.rePos.normalizedX = 0; vp.rePos.normalizedY = 0; vp.rePos.pos = Okular::DocumentViewport::TopLeft; m_document->setNextDocumentViewport( vp ); } else { m_document->setNextDocumentDestination( dest ); } url.setFragment( QString() ); } // this calls in sequence the 'closeUrl' and 'openFile' methods bool openOk = KParts::ReadWritePart::openUrl( url ); if ( openOk ) { m_viewportDirty.pageNumber = -1; setWindowTitleFromDocument(); } else { resetStartArguments(); /* TRANSLATORS: Adding the reason (%2) why the opening failed (if any). */ QString errorMessage = i18n("Could not open %1. %2", url.toDisplayString(), QString("\n%1").arg(m_document->openError()) ); KMessageBox::error( widget(), errorMessage ); } return openOk; } bool Part::queryClose() { if ( !isReadWrite() || !isModified() ) return true; // TODO When we get different saving backends we need to query the backend // as to if it can save changes even if the open file has been modified, // since we only have poppler as saving backend for now we're skipping that check if ( m_fileLastModified != QFileInfo( localFilePath() ).lastModified() ) { int res; if ( m_isReloading ) { res = KMessageBox::warningYesNo( widget(), i18n( "There are unsaved changes, and the file '%1' has been modified by another program. Your changes will be lost, because the file can no longer be saved.
Do you want to continue reloading the file?", url().fileName() ), i18n( "File Changed" ), KGuiItem( i18n( "Continue Reloading" ) ), // <- KMessageBox::Yes KGuiItem( i18n( "Abort Reloading" ) )); } else { res = KMessageBox::warningYesNo( widget(), i18n( "There are unsaved changes, and the file '%1' has been modified by another program. Your changes will be lost, because the file can no longer be saved.
Do you want to continue closing the file?", url().fileName() ), i18n( "File Changed" ), KGuiItem( i18n( "Continue Closing" ) ), // <- KMessageBox::Yes KGuiItem( i18n( "Abort Closing" ) )); } return res == KMessageBox::Yes; } const int res = KMessageBox::warningYesNoCancel( widget(), i18n( "Do you want to save your changes to \"%1\" or discard them?", url().fileName() ), i18n( "Close Document" ), KStandardGuiItem::save(), KStandardGuiItem::discard() ); switch ( res ) { case KMessageBox::Yes: // Save saveFile(); return !isModified(); // Only allow closing if file was really saved case KMessageBox::No: // Discard return true; default: // Cancel return false; } } bool Part::closeUrl(bool promptToSave) { if ( promptToSave && !queryClose() ) return false; if ( m_swapInsteadOfOpening ) { // If we're swapping the backing file, we don't want to close the // current one when openUrl() calls us internally return true; // pretend it worked } m_document->setHistoryClean( true ); if (!m_temporaryLocalFile.isNull() && m_temporaryLocalFile != localFilePath()) { QFile::remove( m_temporaryLocalFile ); m_temporaryLocalFile.clear(); } slotHidePresentation(); emit enableCloseAction( false ); m_find->setEnabled( false ); m_findNext->setEnabled( false ); m_findPrev->setEnabled( false ); if( m_save ) m_save->setEnabled( false ); if( m_saveAs ) m_saveAs->setEnabled( false ); m_printPreview->setEnabled( false ); m_showProperties->setEnabled( false ); if ( m_showEmbeddedFiles ) m_showEmbeddedFiles->setEnabled( false ); if ( m_exportAs ) m_exportAs->setEnabled( false ); if ( m_exportAsText ) m_exportAsText->setEnabled( false ); m_exportFormats.clear(); if ( m_exportAs ) { QMenu *menu = m_exportAs->menu(); QList acts = menu->actions(); int num = acts.count(); for ( int i = 1; i < num; ++i ) { menu->removeAction( acts.at(i) ); delete acts.at(i); } } #if PURPOSE_FOUND if ( m_share ) { m_share->setEnabled(false); m_shareMenu->clear(); } #endif if ( m_showPresentation ) m_showPresentation->setEnabled( false ); emit setWindowCaption(QLatin1String("")); emit enablePrintAction(false); m_realUrl = QUrl(); if ( url().isLocalFile() ) unsetFileToWatch(); m_fileWasRemoved = false; if ( m_generatorGuiClient ) factory()->removeClient( m_generatorGuiClient ); m_generatorGuiClient = nullptr; m_document->closeDocument(); m_fileLastModified = QDateTime(); updateViewActions(); delete m_tempfile; m_tempfile = nullptr; if ( widget() ) { m_searchWidget->clearText(); m_migrationMessage->setVisible( false ); m_topMessage->setVisible( false ); m_formsMessage->setVisible( false ); m_signatureMessage->setVisible( false ); } #ifdef OKULAR_KEEP_FILE_OPEN m_keeper->close(); #endif bool r = KParts::ReadWritePart::closeUrl(); setUrl(QUrl()); return r; } bool Part::closeUrl() { return closeUrl( true ); } void Part::guiActivateEvent(KParts::GUIActivateEvent *event) { updateViewActions(); KParts::ReadWritePart::guiActivateEvent(event); setWindowTitleFromDocument(); } void Part::close() { if ( m_embedMode == NativeShellMode ) { closeUrl(); } else KMessageBox::information( widget(), i18n( "This link points to a close document action that does not work when using the embedded viewer." ), QString(), QStringLiteral("warnNoCloseIfNotInOkular") ); } void Part::cannotQuit() { KMessageBox::information( widget(), i18n( "This link points to a quit application action that does not work when using the embedded viewer." ), QString(), QStringLiteral("warnNoQuitIfNotInOkular") ); } void Part::slotShowLeftPanel() { bool showLeft = m_showLeftPanel->isChecked(); Okular::Settings::setShowLeftPanel( showLeft ); Okular::Settings::self()->save(); // show/hide left panel m_sidebar->setSidebarVisibility( showLeft ); } void Part::slotShowBottomBar() { const bool showBottom = m_showBottomBar->isChecked(); Okular::Settings::setShowBottomBar( showBottom ); Okular::Settings::self()->save(); // show/hide bottom bar m_bottomBar->setVisible( showBottom ); } void Part::slotFileDirty( const QString& path ) { // The beauty of this is that each start cancels the previous one. // This means that timeout() is only fired when there have // no changes to the file for the last 750 millisecs. // This ensures that we don't update on every other byte that gets // written to the file. if ( path == localFilePath() ) { // Only start watching the file in case if it wasn't removed if (QFile::exists(localFilePath())) m_dirtyHandler->start( 750 ); else m_fileWasRemoved = true; } else { const QFileInfo fi(localFilePath()); if ( fi.absolutePath() == path ) { // Our parent has been dirtified if (!QFile::exists(localFilePath())) { m_fileWasRemoved = true; } else if (m_fileWasRemoved && QFile::exists(localFilePath())) { // we need to watch the new file unsetFileToWatch(); setFileToWatch( localFilePath() ); m_dirtyHandler->start( 750 ); } } else if ( fi.isSymLink() && fi.symLinkTarget() == path ) { if ( QFile::exists( fi.symLinkTarget() )) m_dirtyHandler->start( 750 ); else m_fileWasRemoved = true; } } } // Attempt to reload the document, one or more times, optionally from a different URL bool Part::slotAttemptReload( bool oneShot, const QUrl &newUrl ) { // Skip reload when another reload is already in progress if ( m_isReloading ) { return false; } QScopedValueRollback rollback(m_isReloading, true); bool tocReloadPrepared = false; // do the following the first time the file is reloaded if ( m_viewportDirty.pageNumber == -1 ) { // store the url of the current document m_oldUrl = newUrl.isEmpty() ? url() : newUrl; // store the current viewport m_viewportDirty = m_document->viewport(); // store the current toolbox pane m_dirtyToolboxItem = m_sidebar->currentItem(); m_wasSidebarVisible = m_sidebar->isSidebarVisible(); m_wasSidebarCollapsed = m_sidebar->isCollapsed(); // store if presentation view was open m_wasPresentationOpen = ((PresentationWidget*)m_presentationWidget != nullptr); // preserves the toc state after reload m_toc->prepareForReload(); tocReloadPrepared = true; // store the page rotation m_dirtyPageRotation = m_document->rotation(); // inform the user about the operation in progress // TODO: Remove this line and integrate reload info in queryClose m_pageView->displayMessage( i18n("Reloading the document...") ); } // close and (try to) reopen the document if ( !closeUrl() ) { m_viewportDirty.pageNumber = -1; if ( tocReloadPrepared ) { m_toc->rollbackReload(); } return false; } if ( tocReloadPrepared ) m_toc->finishReload(); // inform the user about the operation in progress m_pageView->displayMessage( i18n("Reloading the document...") ); bool reloadSucceeded = false; if ( KParts::ReadWritePart::openUrl( m_oldUrl ) ) { // on successful opening, restore the previous viewport if ( m_viewportDirty.pageNumber >= (int) m_document->pages() ) m_viewportDirty.pageNumber = (int) m_document->pages() - 1; m_document->setViewport( m_viewportDirty ); m_oldUrl = QUrl(); m_viewportDirty.pageNumber = -1; m_document->setRotation( m_dirtyPageRotation ); if ( m_sidebar->currentItem() != m_dirtyToolboxItem && m_sidebar->isItemEnabled( m_dirtyToolboxItem ) && !m_sidebar->isCollapsed() ) { m_sidebar->setCurrentItem( m_dirtyToolboxItem ); } if ( m_sidebar->isSidebarVisible() != m_wasSidebarVisible ) { m_sidebar->setSidebarVisibility( m_wasSidebarVisible ); } if ( m_sidebar->isCollapsed() != m_wasSidebarCollapsed ) { m_sidebar->setCollapsed( m_wasSidebarCollapsed ); } if (m_wasPresentationOpen) slotShowPresentation(); emit enablePrintAction(true && m_document->printingSupport() != Okular::Document::NoPrinting); reloadSucceeded = true; } else if ( !oneShot ) { // start watching the file again (since we dropped it on close) setFileToWatch( localFilePath() ); m_dirtyHandler->start( 750 ); } return reloadSucceeded; } void Part::updateViewActions() { bool opened = m_document->pages() > 0; if ( opened ) { m_gotoPage->setEnabled( m_document->pages() > 1 ); // Check if you are at the beginning or not if (m_document->currentPage() != 0) { m_beginningOfDocument->setEnabled( true ); m_prevPage->setEnabled( true ); } else { if (m_pageView->verticalScrollBar()->value() != 0) { // The page isn't at the very beginning m_beginningOfDocument->setEnabled( true ); } else { // The page is at the very beginning of the document m_beginningOfDocument->setEnabled( false ); } // The document is at the first page, you can go to a page before m_prevPage->setEnabled( false ); } if (m_document->pages() == m_document->currentPage() + 1 ) { // If you are at the end, disable go to next page m_nextPage->setEnabled( false ); if (m_pageView->verticalScrollBar()->value() == m_pageView->verticalScrollBar()->maximum()) { // If you are the end of the page of the last document, you can't go to the last page m_endOfDocument->setEnabled( false ); } else { // Otherwise you can move to the endif m_endOfDocument->setEnabled( true ); } } else { // If you are not at the end, enable go to next page m_nextPage->setEnabled( true ); m_endOfDocument->setEnabled( true ); } if (m_historyBack) m_historyBack->setEnabled( !m_document->historyAtBegin() ); if (m_historyNext) m_historyNext->setEnabled( !m_document->historyAtEnd() ); m_reload->setEnabled( true ); if (m_copy) m_copy->setEnabled( true ); if (m_selectAll) m_selectAll->setEnabled( true ); if (m_selectCurrentPage) m_selectCurrentPage->setEnabled( true ); } else { m_gotoPage->setEnabled( false ); m_beginningOfDocument->setEnabled( false ); m_endOfDocument->setEnabled( false ); m_prevPage->setEnabled( false ); m_nextPage->setEnabled( false ); if (m_historyBack) m_historyBack->setEnabled( false ); if (m_historyNext) m_historyNext->setEnabled( false ); m_reload->setEnabled( false ); if (m_copy) m_copy->setEnabled( false ); if (m_selectAll) m_selectAll->setEnabled( false ); if (m_selectCurrentPage) m_selectCurrentPage->setEnabled( false ); } if ( factory() ) { QWidget *menu = factory()->container(QStringLiteral("menu_okular_part_viewer"), this); if (menu) menu->setEnabled( opened ); menu = factory()->container(QStringLiteral("view_orientation"), this); if (menu) menu->setEnabled( opened ); } emit viewerMenuStateChange( opened ); updateBookmarksActions(); } void Part::updateBookmarksActions() { bool opened = m_document->pages() > 0; if ( opened ) { m_addBookmark->setEnabled( true ); if ( m_document->bookmarkManager()->isBookmarked( m_document->viewport() ) ) { m_addBookmark->setText( i18n( "Remove Bookmark" ) ); m_addBookmark->setIcon( QIcon::fromTheme( QStringLiteral("edit-delete-bookmark") ) ); m_renameBookmark->setEnabled( true ); } else { m_addBookmark->setText( m_addBookmarkText ); m_addBookmark->setIcon( m_addBookmarkIcon ); m_renameBookmark->setEnabled( false ); } } else { m_addBookmark->setEnabled( false ); m_addBookmark->setText( m_addBookmarkText ); m_addBookmark->setIcon( m_addBookmarkIcon ); m_renameBookmark->setEnabled( false ); } } void Part::enableTOC(bool enable) { m_sidebar->setItemEnabled(m_toc, enable); // If present, show the TOC when a document is opened if ( enable && m_sidebar->currentItem() != m_toc ) { m_sidebar->setCurrentItem( m_toc, Sidebar::DoNotUncollapseIfCollapsed ); } } void Part::slotRebuildBookmarkMenu() { rebuildBookmarkMenu(); } void Part::enableLayers(bool enable) { m_sidebar->setItemVisible( m_layers, enable ); } void Part::showSidebarSignaturesItem( bool show ) { m_sidebar->setItemVisible( m_signaturePanel, show ); } void Part::slotShowFindBar() { m_findBar->show(); m_findBar->focusAndSetCursor(); m_closeFindBar->setEnabled( true ); } void Part::slotHideFindBar() { if ( m_findBar->maybeHide() ) { m_pageView->setFocus(); m_closeFindBar->setEnabled( false ); } } //BEGIN go to page dialog class GotoPageDialog : public QDialog { Q_OBJECT public: GotoPageDialog(QWidget *p, int current, int max) : QDialog(p) { setWindowTitle(i18n("Go to Page")); buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); QVBoxLayout *topLayout = new QVBoxLayout(this); topLayout->setContentsMargins(6, 6, 6, 6); QHBoxLayout *midLayout = new QHBoxLayout(); spinbox = new QSpinBox(this); spinbox->setRange(1, max); spinbox->setValue(current); spinbox->setFocus(); slider = new QSlider(Qt::Horizontal, this); slider->setRange(1, max); slider->setValue(current); slider->setSingleStep(1); slider->setTickPosition(QSlider::TicksBelow); slider->setTickInterval(max/10); connect(slider, &QSlider::valueChanged, spinbox, &QSpinBox::setValue); connect(spinbox, static_cast(&QSpinBox::valueChanged), slider, &QSlider::setValue); QLabel *label = new QLabel(i18n("&Page:"), this); label->setBuddy(spinbox); topLayout->addWidget(label); topLayout->addLayout(midLayout); midLayout->addWidget(slider); midLayout->addWidget(spinbox); // A little bit extra space topLayout->addStretch(10); topLayout->addWidget(buttonBox); spinbox->setFocus(); } int getPage() const { return spinbox->value(); } protected: QSpinBox *spinbox; QSlider *slider; QDialogButtonBox *buttonBox; }; //END go to page dialog void Part::slotGoToPage() { GotoPageDialog pageDialog( m_pageView, m_document->currentPage() + 1, m_document->pages() ); if ( pageDialog.exec() == QDialog::Accepted ) m_document->setViewportPage( pageDialog.getPage() - 1, nullptr, true ); } void Part::slotPreviousPage() { if ( m_document->isOpened() && !(m_document->currentPage() < 1) ) m_document->setViewportPage( m_document->currentPage() - 1, nullptr, true ); } void Part::slotNextPage() { if ( m_document->isOpened() && m_document->currentPage() < (m_document->pages() - 1) ) m_document->setViewportPage( m_document->currentPage() + 1, nullptr, true ); } void Part::slotGotoFirst() { if ( m_document->isOpened() ) { m_document->setViewportPage( 0, nullptr, true); m_beginningOfDocument->setEnabled( false ); } } void Part::slotGotoLast() { if ( m_document->isOpened() ) { DocumentViewport endPage(m_document->pages() -1 ); endPage.rePos.enabled = true; endPage.rePos.normalizedX = 0; endPage.rePos.normalizedY = 1; endPage.rePos.pos = Okular::DocumentViewport::TopLeft; m_document->setViewport(endPage, nullptr, true); m_endOfDocument->setEnabled(false); } } void Part::slotHistoryBack() { m_document->setPrevViewport(); } void Part::slotHistoryNext() { m_document->setNextViewport(); } void Part::slotAddBookmark() { DocumentViewport vp = m_document->viewport(); if ( m_document->bookmarkManager()->isBookmarked( vp ) ) { m_document->bookmarkManager()->removeBookmark( vp ); } else { m_document->bookmarkManager()->addBookmark( vp ); } } void Part::slotRenameBookmark( const DocumentViewport &viewport ) { Q_ASSERT(m_document->bookmarkManager()->isBookmarked( viewport )); if ( m_document->bookmarkManager()->isBookmarked( viewport ) ) { KBookmark bookmark = m_document->bookmarkManager()->bookmark( viewport ); const QString newName = QInputDialog::getText(widget(), i18n( "Rename Bookmark" ), i18n( "Enter the new name of the bookmark:" ), QLineEdit::Normal, bookmark.fullText()); if (!newName.isEmpty()) { m_document->bookmarkManager()->renameBookmark(&bookmark, newName); } } } void Part::slotRenameBookmarkFromMenu() { QAction *action = dynamic_cast(sender()); Q_ASSERT( action ); if ( action ) { DocumentViewport vp( action->data().toString() ); slotRenameBookmark( vp ); } } void Part::slotRemoveBookmarkFromMenu() { QAction *action = dynamic_cast(sender()); Q_ASSERT( action ); if ( action ) { DocumentViewport vp ( action->data().toString() ); slotRemoveBookmark( vp ); } } void Part::slotRemoveBookmark(const DocumentViewport &viewport) { Q_ASSERT(m_document->bookmarkManager()->isBookmarked( viewport )); if ( m_document->bookmarkManager()->isBookmarked( viewport ) ) { m_document->bookmarkManager()->removeBookmark( viewport ); } } void Part::slotRenameCurrentViewportBookmark() { slotRenameBookmark( m_document->viewport() ); } bool Part::aboutToShowContextMenu(QMenu * /*menu*/, QAction *action, QMenu *contextMenu) { KBookmarkAction *ba = dynamic_cast(action); if (ba != nullptr) { QAction *separatorAction = contextMenu->addSeparator(); separatorAction->setObjectName(QStringLiteral("OkularPrivateRenameBookmarkActions")); QAction *renameAction = contextMenu->addAction( QIcon::fromTheme( QStringLiteral("edit-rename") ), i18n( "Rename this Bookmark" ), this, &Part::slotRenameBookmarkFromMenu ); renameAction->setData(ba->property("htmlRef").toString()); renameAction->setObjectName(QStringLiteral("OkularPrivateRenameBookmarkActions")); QAction *deleteAction = contextMenu->addAction( QIcon::fromTheme( QStringLiteral("list-remove") ), i18n("Remove this Bookmark"), this, &Part::slotRemoveBookmarkFromMenu); deleteAction->setData(ba->property("htmlRef").toString()); deleteAction->setObjectName(QStringLiteral("OkularPrivateRenameBookmarkActions")); } return ba; } void Part::slotPreviousBookmark() { const KBookmark bookmark = m_document->bookmarkManager()->previousBookmark( m_document->viewport() ); if ( !bookmark.isNull() ) { DocumentViewport vp( bookmark.url().fragment(QUrl::FullyDecoded) ); m_document->setViewport( vp, nullptr, true ); } } void Part::slotNextBookmark() { const KBookmark bookmark = m_document->bookmarkManager()->nextBookmark( m_document->viewport() ); if ( !bookmark.isNull() ) { DocumentViewport vp( bookmark.url().fragment(QUrl::FullyDecoded) ); m_document->setViewport( vp, nullptr, true ); } } void Part::slotFind() { // when in presentation mode, there's already a search bar, taking care of // the 'find' requests if ( (PresentationWidget*)m_presentationWidget != nullptr ) { m_presentationWidget->slotFind(); } else { slotShowFindBar(); } } void Part::slotFindNext() { if (m_findBar->isHidden()) slotShowFindBar(); else m_findBar->findNext(); } void Part::slotFindPrev() { if (m_findBar->isHidden()) slotShowFindBar(); else m_findBar->findPrev(); } bool Part::saveFile() { if ( !isModified() ) return true; else return saveAs( url() ); } bool Part::slotSaveFileAs( bool showOkularArchiveAsDefaultFormat ) { if ( m_embedMode == PrintPreviewMode ) return false; // Determine the document's mimetype QMimeDatabase db; QMimeType originalMimeType; const QString typeName = m_document->documentInfo().get( DocumentInfo::MimeType ); if ( !typeName.isEmpty() ) originalMimeType = db.mimeTypeForName( typeName ); // What data would we lose if we saved natively? bool wontSaveForms, wontSaveAnnotations; checkNativeSaveDataLoss(&wontSaveForms, &wontSaveAnnotations); const QMimeType okularArchiveMimeType = db.mimeTypeForName( QStringLiteral("application/vnd.kde.okular-archive") ); // Prepare "Save As" dialog const QString originalMimeTypeFilter = i18nc("File type name and pattern", "%1 (%2)", originalMimeType.comment(), originalMimeType.globPatterns().join(QLatin1Char(' '))); const QString okularArchiveMimeTypeFilter = i18nc("File type name and pattern", "%1 (%2)", okularArchiveMimeType.comment(), okularArchiveMimeType.globPatterns().join(QLatin1Char(' '))); // What format choice should we show as default? QString selectedFilter = (isDocumentArchive || showOkularArchiveAsDefaultFormat || wontSaveForms || wontSaveAnnotations) ? okularArchiveMimeTypeFilter : originalMimeTypeFilter; QString filter = originalMimeTypeFilter + QStringLiteral(";;") + okularArchiveMimeTypeFilter; const QUrl saveUrl = QFileDialog::getSaveFileUrl(widget(), i18n("Save As"), url(), filter, &selectedFilter); if ( !saveUrl.isValid() || saveUrl.isEmpty() ) return false; // Has the user chosen to save in .okular archive format? const bool saveAsOkularArchive = ( selectedFilter == okularArchiveMimeTypeFilter ); return saveAs( saveUrl, saveAsOkularArchive ? SaveAsOkularArchive : NoSaveAsFlags ); } bool Part::saveAs(const QUrl & saveUrl) { // Save in the same format (.okular vs native) as the current file return saveAs( saveUrl, isDocumentArchive ? SaveAsOkularArchive : NoSaveAsFlags ); } static QUrl resolveSymlinksIfFileExists( const QUrl &saveUrl ) { if ( saveUrl.isLocalFile() ) { const QFileInfo fi( saveUrl.toLocalFile() ); return fi.exists() ? QUrl::fromLocalFile( fi.canonicalFilePath() ) : saveUrl; } else { return saveUrl; } } bool Part::saveAs( const QUrl & saveUrl, SaveAsFlags flags ) { // TODO When we get different saving backends we need to query the backend // as to if it can save changes even if the open file has been modified, // since we only have poppler as saving backend for now we're skipping that check if ( m_fileLastModified != QFileInfo( localFilePath() ).lastModified() ) { KMessageBox::sorry( widget(), i18n( "The file '%1' has been modified by another program, which means it can no longer be saved.", url().fileName() ), i18n( "File Changed" ) ); return false; } bool hasUserAcceptedReload = false; if ( m_documentOpenWithPassword ) { const int res = KMessageBox::warningYesNo( widget(), i18n( "The current document is protected with a password.
In order to save, the file needs to be reloaded. You will be asked for the password again and your undo/redo history will be lost.
Do you want to continue?" ), i18n( "Save - Warning" ) ); switch ( res ) { case KMessageBox::Yes: hasUserAcceptedReload = true; // do nothing break; case KMessageBox::No: // User said no to continue, so return true even if save didn't happen otherwise we will get an error return true; } } bool setModifiedAfterSave = false; QTemporaryFile tf; QString fileName; if ( !tf.open() ) { KMessageBox::information( widget(), i18n("Could not open the temporary file for saving." ) ); return false; } fileName = tf.fileName(); tf.close(); // Figure out the real save url, for symlinks we don't want to copy over the symlink but over the target file const QUrl realSaveUrl = resolveSymlinksIfFileExists( saveUrl ); QScopedPointer tempFile; KIO::Job *copyJob = nullptr; // this will be filled with the job that writes to saveUrl // Does the user want a .okular archive? if ( flags & SaveAsOkularArchive ) { if ( !hasUserAcceptedReload && !m_document->canSwapBackingFile() ) { const int res = KMessageBox::warningYesNo( widget(), i18n( "After saving, the current document format requires the file to be reloaded. Your undo/redo history will be lost.
Do you want to continue?" ), i18n( "Save - Warning" ) ); switch ( res ) { case KMessageBox::Yes: // do nothing break; case KMessageBox::No: // User said no to continue, so return true even if save didn't happen otherwise we will get an error return true; } } if ( !m_document->saveDocumentArchive( fileName ) ) { KMessageBox::information( widget(), i18n("File could not be saved in '%1'. Try to save it to another location.", fileName ) ); return false; } copyJob = KIO::file_copy( QUrl::fromLocalFile( fileName ), realSaveUrl, -1, KIO::Overwrite ); } else { bool wontSaveForms, wontSaveAnnotations; checkNativeSaveDataLoss(&wontSaveForms, &wontSaveAnnotations); // If something can't be saved in this format, ask for confirmation QStringList listOfwontSaves; if ( wontSaveForms ) listOfwontSaves << i18n( "Filled form contents" ); if ( wontSaveAnnotations ) listOfwontSaves << i18n( "User annotations" ); if ( !listOfwontSaves.isEmpty() ) { if ( saveUrl == url() ) { // Save const QString warningMessage = i18n( "You are about to save changes, but the current file format does not support saving the following elements. Please use the Okular document archive format to preserve them." ); const int result = KMessageBox::warningYesNoList( widget(), warningMessage, listOfwontSaves, i18n( "Warning" ), KGuiItem( i18n( "Save as Okular document archive..." ), QStringLiteral("document-save-as") ), // <- KMessageBox::Yes KStandardGuiItem::cancel() ); switch (result) { case KMessageBox::Yes: // -> Save as Okular document archive return slotSaveFileAs( true /* showOkularArchiveAsDefaultFormat */ ); default: return false; } } else { // Save as const QString warningMessage = m_document->canSwapBackingFile() ? i18n( "You are about to save changes, but the current file format does not support saving the following elements. Please use the Okular document archive format to preserve them. Click Continue to save the document and discard these elements." ) : i18n( "You are about to save changes, but the current file format does not support saving the following elements. Please use the Okular document archive format to preserve them. Click Continue to save, but you will lose these elements as well as the undo/redo history." ); const QString continueMessage = m_document->canSwapBackingFile() ? i18n( "Continue" ) : i18n( "Continue losing changes" ); const int result = KMessageBox::warningYesNoCancelList( widget(), warningMessage, listOfwontSaves, i18n( "Warning" ), KGuiItem( i18n( "Save as Okular document archive..." ), QStringLiteral("document-save-as") ), // <- KMessageBox::Yes KGuiItem( continueMessage, QStringLiteral("arrow-right") ) ); // <- KMessageBox::NO switch (result) { case KMessageBox::Yes: // -> Save as Okular document archive return slotSaveFileAs( true /* showOkularArchiveAsDefaultFormat */ ); case KMessageBox::No: // -> Continue setModifiedAfterSave = m_document->canSwapBackingFile(); break; case KMessageBox::Cancel: return false; } } } if ( m_document->canSaveChanges() ) { // If the generator supports saving changes, save them QString errorText; if ( !m_document->saveChanges( fileName, &errorText ) ) { if (errorText.isEmpty()) KMessageBox::information( widget(), i18n("File could not be saved in '%1'. Try to save it to another location.", fileName ) ); else KMessageBox::information( widget(), i18n("File could not be saved in '%1'. %2", fileName, errorText ) ); return false; } copyJob = KIO::file_copy( QUrl::fromLocalFile( fileName ), realSaveUrl, -1, KIO::Overwrite ); } else { // If the generators doesn't support saving changes, we will // just copy the original file. if ( isDocumentArchive ) { // Special case: if the user is extracting the contents of a // .okular archive back to the native format, we can't just copy // the open file (which is a .okular). So let's ask to core to // extract and give us the real file if ( !m_document->extractArchivedFile( fileName ) ) { KMessageBox::information( widget(), i18n("File could not be saved in '%1'. Try to save it to another location.", fileName ) ); return false; } copyJob = KIO::file_copy( QUrl::fromLocalFile( fileName ), realSaveUrl, -1, KIO::Overwrite ); } else { // Otherwise just copy the open file. // make use of the already downloaded (in case of remote URLs) file, // no point in downloading that again QUrl srcUrl = QUrl::fromLocalFile( localFilePath() ); // duh, our local file disappeared... if ( !QFile::exists( localFilePath() ) ) { if ( url().isLocalFile() ) { #ifdef OKULAR_KEEP_FILE_OPEN // local file: try to get it back from the open handle on it tempFile.reset( m_keeper->copyToTemporary() ); if ( tempFile ) srcUrl = KUrl::fromPath( tempFile->fileName() ); #else const QString msg = i18n( "Okular cannot copy %1 to the specified location.\n\nThe document does not exist anymore.", localFilePath() ); KMessageBox::sorry( widget(), msg ); return false; #endif } else { // we still have the original remote URL of the document, // so copy the document from there srcUrl = url(); } } if ( srcUrl != saveUrl ) { copyJob = KIO::file_copy( srcUrl, realSaveUrl, -1, KIO::Overwrite ); } else { // Don't do a real copy in this case, just update the timestamps copyJob = KIO::setModificationTime( realSaveUrl, QDateTime::currentDateTime() ); } } } } // Stop watching for changes while we write the new file (useful when // overwriting) if ( url().isLocalFile() ) unsetFileToWatch(); KJobWidgets::setWindow(copyJob, widget()); if ( !copyJob->exec() ) { KMessageBox::information( widget(), i18n("File could not be saved in '%1'. Error: '%2'. Try to save it to another location.", saveUrl.toDisplayString(), copyJob->errorString() ) ); // Restore watcher if ( url().isLocalFile() ) setFileToWatch( localFilePath() ); return false; } m_document->setHistoryClean( true ); if ( m_document->isDocdataMigrationNeeded() ) m_document->docdataMigrationDone(); bool reloadedCorrectly = true; // Make the generator use the new file instead of the old one if ( m_document->canSwapBackingFile() && !m_documentOpenWithPassword ) { QWidget *currentSidebarItem = m_sidebar->currentItem(); // this calls openFile internally, which in turn actually calls // m_document->swapBackingFile() instead of the regular loadDocument if ( openUrl( saveUrl, true /* swapInsteadOfOpening */ ) ) { if ( setModifiedAfterSave ) { m_document->setHistoryClean( false ); } } else { reloadedCorrectly = false; } if ( m_sidebar->currentItem() != currentSidebarItem ) m_sidebar->setCurrentItem( currentSidebarItem ); } else { // If the generator doesn't support swapping file, then just reload // the document from the new location if ( !slotAttemptReload( true, saveUrl ) ) reloadedCorrectly = false; } // In case of file swapping errors, close the document to avoid inconsistencies if ( !reloadedCorrectly ) { qWarning() << "The document hasn't been reloaded/swapped correctly"; closeUrl(); } // Restore watcher if ( url().isLocalFile() ) setFileToWatch( localFilePath() ); //Set correct permission taking into account the umask value #ifndef Q_OS_WIN const QString saveFilePath = saveUrl.toLocalFile(); if ( QFile::exists( saveFilePath ) ) { const mode_t mask = umask( 0 ); umask( mask ); const mode_t fileMode = 0666 & ~mask; chmod( QFile::encodeName( saveFilePath ).constData(), fileMode ); } #endif return true; } // If the user wants to save in the original file's format, some features might // not be available. Find out what cannot be saved in this format void Part::checkNativeSaveDataLoss(bool *out_wontSaveForms, bool *out_wontSaveAnnotations) const { bool wontSaveForms = false; bool wontSaveAnnotations = false; if ( !m_document->canSaveChanges( Document::SaveFormsCapability ) ) { /* Set wontSaveForms only if there are forms */ const int pagecount = m_document->pages(); for ( int pageno = 0; pageno < pagecount; ++pageno ) { const Okular::Page *page = m_document->page( pageno ); if ( !page->formFields().empty() ) { wontSaveForms = true; break; } } } if ( !m_document->canSaveChanges( Document::SaveAnnotationsCapability ) ) { /* Set wontSaveAnnotations only if there are local annotations */ const int pagecount = m_document->pages(); for ( int pageno = 0; pageno < pagecount; ++pageno ) { const QLinkedList< Okular::Annotation* > annotations = m_document->page( pageno )->annotations(); for ( const Okular::Annotation *ann : annotations ) { if ( !(ann->flags() & Okular::Annotation::External) ) { wontSaveAnnotations = true; break; } } if ( wontSaveAnnotations ) break; } } *out_wontSaveForms = wontSaveForms; *out_wontSaveAnnotations = wontSaveAnnotations; } void Part::slotGetNewStuff() { #if 0 KNS::Engine engine(widget()); engine.init( "okular.knsrc" ); // show the modal dialog over pageview and execute it KNS::Entry::List entries = engine.downloadDialogModal( m_pageView ); Q_UNUSED( entries ) #endif } void Part::slotPreferences() { // Create dialog PreferencesDialog * dialog = new PreferencesDialog( m_pageView, Okular::Settings::self(), m_embedMode ); dialog->setAttribute( Qt::WA_DeleteOnClose ); // Show it dialog->show(); } void Part::slotToggleChangeColors() { m_pageView->slotToggleChangeColors(); } void Part::slotSetChangeColors(bool active) { m_pageView->slotSetChangeColors(active); } void Part::slotAnnotationPreferences() { // Create dialog PreferencesDialog * dialog = new PreferencesDialog( m_pageView, Okular::Settings::self(), m_embedMode ); dialog->setAttribute( Qt::WA_DeleteOnClose ); // Show it dialog->switchToAnnotationsPage(); dialog->show(); } void Part::slotNewConfig() { // Apply settings here. A good policy is to check whether the setting has // changed before applying changes. // Watch File setWatchFileModeEnabled(Okular::Settings::watchFile()); // Main View (pageView) m_pageView->reparseConfig(); // update document settings m_document->reparseConfig(); // update TOC settings if ( m_sidebar->isItemEnabled(m_toc) ) m_toc->reparseConfig(); // update ThumbnailList contents if ( Okular::Settings::showLeftPanel() && !m_thumbnailList->isHidden() ) m_thumbnailList->updateWidgets(); // update Reviews settings if ( m_sidebar->isItemEnabled(m_reviewsWidget) ) m_reviewsWidget->reparseConfig(); setWindowTitleFromDocument (); if ( m_presentationDrawingActions ) { m_presentationDrawingActions->reparseConfig(); if (factory()) { factory()->refreshActionProperties(); } } } void Part::slotPrintPreview() { if (m_document->pages() == 0) return; QPrinter printer; QString tempFilePattern; if ( m_document->printingSupport() == Okular::Document::PostscriptPrinting ) { tempFilePattern = (QDir::tempPath() + QLatin1String("/okular_XXXXXX.ps")); } else if ( m_document->printingSupport() == Okular::Document::NativePrinting ) { tempFilePattern = (QDir::tempPath() + QLatin1String("/okular_XXXXXX.pdf")); } else { return; } // Generate a temp filename for Print to File, then release the file so generator can write to it QTemporaryFile tf(tempFilePattern); tf.setAutoRemove( true ); tf.open(); printer.setOutputFileName( tf.fileName() ); tf.close(); setupPrint( printer ); doPrint( printer ); if ( QFile::exists( printer.outputFileName() ) ) { Okular::FilePrinterPreview previewdlg( printer.outputFileName(), widget() ); previewdlg.exec(); } } -void Part::slotShowTOCMenu(const Okular::DocumentViewport &vp, const QPoint &point, const QString &title) +void Part::slotShowTOCMenu(const Okular::DocumentViewport &vp, const QPoint point, const QString &title) { showMenu(m_document->page(vp.pageNumber), point, title, vp, true); } -void Part::slotShowMenu(const Okular::Page *page, const QPoint &point) +void Part::slotShowMenu(const Okular::Page *page, const QPoint point) { showMenu(page, point); } -void Part::showMenu(const Okular::Page *page, const QPoint &point, const QString &bookmarkTitle, const Okular::DocumentViewport &vp, bool showTOCActions) +void Part::showMenu(const Okular::Page *page, const QPoint point, const QString &bookmarkTitle, const Okular::DocumentViewport &vp, bool showTOCActions) { if ( m_embedMode == PrintPreviewMode ) return; bool reallyShow = false; const bool currentPage = page && page->number() == m_document->viewport().pageNumber; if (!m_actionsSearched) { // the quest for options_show_menubar KActionCollection *ac; QAction *act; if (factory()) { const QList clients(factory()->clients()); for(int i = 0 ; (!m_showMenuBarAction || !m_showFullScreenAction) && i < clients.size(); ++i) { ac = clients.at(i)->actionCollection(); // show_menubar act = ac->action(QStringLiteral("options_show_menubar")); if (act && qobject_cast(act)) m_showMenuBarAction = qobject_cast(act); // fullscreen act = ac->action(QStringLiteral("fullscreen")); if (act && qobject_cast(act)) m_showFullScreenAction = qobject_cast(act); } } m_actionsSearched = true; } QMenu *popup = new QMenu( widget() ); if (showTOCActions) { popup->addAction( i18n("Expand whole section"), m_toc.data(), &TOC::expandRecursively ); popup->addAction( i18n("Collapse whole section"), m_toc.data(), &TOC::collapseRecursively ); popup->addAction( i18n("Expand all"), m_toc.data(), &TOC::expandAll ); popup->addAction( i18n("Collapse all"), m_toc.data(), &TOC::collapseAll ); reallyShow = true; } QAction *addBookmark = nullptr; QAction *removeBookmark = nullptr; QAction *fitPageWidth = nullptr; if (page) { popup->addAction( new OKMenuTitle( popup, i18n( "Page %1", page->number() + 1 ) ) ); if ( ( !currentPage && m_document->bookmarkManager()->isBookmarked( page->number() ) ) || ( currentPage && m_document->bookmarkManager()->isBookmarked( m_document->viewport() ) ) ) removeBookmark = popup->addAction( QIcon::fromTheme(QStringLiteral("edit-delete-bookmark")), i18n("Remove Bookmark") ); else addBookmark = popup->addAction( QIcon::fromTheme(QStringLiteral("bookmark-new")), i18n("Add Bookmark") ); if ( m_pageView->canFitPageWidth() ) fitPageWidth = popup->addAction( QIcon::fromTheme(QStringLiteral("zoom-fit-best")), i18n("Fit Width") ); popup->addAction( m_prevBookmark ); popup->addAction( m_nextBookmark ); reallyShow = true; } if ((m_showMenuBarAction && !m_showMenuBarAction->isChecked()) || (m_showFullScreenAction && m_showFullScreenAction->isChecked())) { popup->addAction( new OKMenuTitle( popup, i18n( "Tools" ) ) ); if (m_showMenuBarAction && !m_showMenuBarAction->isChecked()) popup->addAction(m_showMenuBarAction); if (m_showFullScreenAction && m_showFullScreenAction->isChecked()) popup->addAction(m_showFullScreenAction); reallyShow = true; } if (reallyShow) { QAction *res = popup->exec(point); if (res) { if (res == addBookmark) { if ( currentPage && bookmarkTitle.isEmpty() ) m_document->bookmarkManager()->addBookmark( m_document->viewport() ); else if ( !bookmarkTitle.isEmpty() ) m_document->bookmarkManager()->addBookmark( m_document->currentDocument(), vp, bookmarkTitle ); else m_document->bookmarkManager()->addBookmark( page->number() ); } else if (res == removeBookmark) { if (currentPage) m_document->bookmarkManager()->removeBookmark( m_document->viewport() ); else m_document->bookmarkManager()->removeBookmark( page->number() ); } else if (res == fitPageWidth) { m_pageView->fitPageWidth( page->number() ); } } } delete popup; } void Part::slotShowProperties() { PropertiesDialog *d = new PropertiesDialog(widget(), m_document); connect(d, &QDialog::finished, d, &QObject::deleteLater); d->open(); } void Part::slotShowEmbeddedFiles() { EmbeddedFilesDialog *d = new EmbeddedFilesDialog(widget(), m_document); connect(d, &QDialog::finished, d, &QObject::deleteLater); d->open(); } void Part::slotShowPresentation() { if ( !m_presentationWidget ) { m_presentationWidget = new PresentationWidget( widget(), m_document, m_presentationDrawingActions, actionCollection() ); } } void Part::slotHidePresentation() { if ( m_presentationWidget ) delete (PresentationWidget*) m_presentationWidget; } void Part::slotTogglePresentation() { if ( m_document->isOpened() ) { if ( !m_presentationWidget ) m_presentationWidget = new PresentationWidget( widget(), m_document, m_presentationDrawingActions, actionCollection() ); else delete (PresentationWidget*) m_presentationWidget; } } void Part::reload() { if ( m_document->isOpened() ) { slotReload(); } } void Part::enableStartWithPrint() { m_cliPrint = true; } void Part::enableExitAfterPrint() { m_cliPrintAndExit = true; } void Part::slotAboutBackend() { const KPluginMetaData data = m_document->generatorInfo(); if (!data.isValid()) return; KAboutData aboutData = KAboutData::fromPluginMetaData(data); QIcon icon = QIcon::fromTheme(data.iconName()); // fall back to mime type icon if (icon.isNull()) { const Okular::DocumentInfo documentInfo = m_document->documentInfo(QSet() << DocumentInfo::MimeType); const QString mimeTypeName = documentInfo.get(DocumentInfo::MimeType); if (!mimeTypeName.isEmpty()) { QMimeDatabase db; QMimeType type = db.mimeTypeForName(mimeTypeName); if (type.isValid()) { icon = QIcon::fromTheme(type.iconName()); } } } const QString extraDescription = m_document->metaData( QStringLiteral("GeneratorExtraDescription") ).toString(); if (!extraDescription.isEmpty()) { aboutData.setShortDescription(aboutData.shortDescription() + QStringLiteral("\n\n") + extraDescription); } if (!icon.isNull()) { // 48x48 is what KAboutApplicationDialog wants, which doesn't match any default so we hardcode it aboutData.setProgramLogo(icon.pixmap(48, 48)); } KAboutApplicationDialog dlg(aboutData, widget()); dlg.exec(); } void Part::slotExportAs(QAction * act) { QList acts = m_exportAs->menu() ? m_exportAs->menu()->actions() : QList(); int id = acts.indexOf( act ); if ( ( id < 0 ) || ( id >= acts.count() ) ) return; QMimeDatabase mimeDatabase; QMimeType mimeType; switch ( id ) { case 0: mimeType = mimeDatabase.mimeTypeForName(QStringLiteral("text/plain")); break; default: mimeType = m_exportFormats.at( id - 1 ).mimeType(); break; } QString filter = i18nc("File type name and pattern", "%1 (%2)", mimeType.comment(), mimeType.globPatterns().join(QLatin1Char(' '))); QString fileName = QFileDialog::getSaveFileName( widget(), QString(), QString(), filter); if ( !fileName.isEmpty() ) { bool saved = false; switch ( id ) { case 0: saved = m_document->exportToText( fileName ); break; default: saved = m_document->exportTo( fileName, m_exportFormats.at( id - 1 ) ); break; } if ( !saved ) KMessageBox::information( widget(), i18n("File could not be saved in '%1'. Try to save it to another location.", fileName ) ); } } void Part::slotReload() { // stop the dirty handler timer, otherwise we may conflict with the // auto-refresh system m_dirtyHandler->stop(); slotAttemptReload(); } void Part::slotPrint() { if (m_document->pages() == 0) return; #ifdef Q_OS_WIN QPrinter printer(QPrinter::HighResolution); #else QPrinter printer; #endif QPrintDialog *printDialog = nullptr; QWidget *printConfigWidget = nullptr; // Must do certain QPrinter setup before creating QPrintDialog setupPrint( printer ); // Create the Print Dialog with extra config widgets if required if ( m_document->canConfigurePrinter() ) { printConfigWidget = m_document->printConfigurationWidget(); } else { printConfigWidget = new DefaultPrintOptionsWidget(); } printDialog = new QPrintDialog(&printer, widget()); printDialog->setWindowTitle(i18nc("@title:window", "Print")); QList options; if (printConfigWidget) { options << printConfigWidget; } printDialog->setOptionTabs(options); if ( printDialog ) { // Set the available Print Range printDialog->setMinMax( 1, m_document->pages() ); printDialog->setFromTo( 1, m_document->pages() ); // If the user has bookmarked pages for printing, then enable Selection if ( !m_document->bookmarkedPageRange().isEmpty() ) { printDialog->addEnabledOption( QAbstractPrintDialog::PrintSelection ); } // If the Document type doesn't support print to both PS & PDF then disable the Print Dialog option if ( printDialog->isOptionEnabled( QAbstractPrintDialog::PrintToFile ) && !m_document->supportsPrintToFile() ) { printDialog->setEnabledOptions( printDialog->enabledOptions() ^ QAbstractPrintDialog::PrintToFile ); } // Enable the Current Page option in the dialog. if ( m_document->pages() > 1 && currentPage() > 0 ) { printDialog->setOption( QAbstractPrintDialog::PrintCurrentPage ); } bool success = true; if ( printDialog->exec() ) { // set option for margins if widget is of corresponding type that holds this information PrintOptionsWidget *optionWidget = dynamic_cast(printConfigWidget); if (optionWidget != nullptr) printer.setFullPage( optionWidget->ignorePrintMargins() ); else { // printConfigurationWidget() method should always return an object of type Okular::PrintOptionsWidget, // (signature does not (yet) require it for ABI stability reasons), so emit a warning if the object is of another type qWarning() << "printConfigurationWidget() method did not return an Okular::PrintOptionsWidget. This is strongly discouraged!"; } success = doPrint( printer ); } delete printDialog; if ( m_cliPrintAndExit ) exit ( success ? EXIT_SUCCESS : EXIT_FAILURE ); } } void Part::setupPrint( QPrinter &printer ) { printer.setOrientation(m_document->orientation()); // title QString title = m_document->metaData( QStringLiteral("DocumentTitle") ).toString(); if ( title.isEmpty() ) { title = m_document->currentDocument().fileName(); } if ( !title.isEmpty() ) { printer.setDocName( title ); } } bool Part::doPrint(QPrinter &printer) { if (!m_document->isAllowed(Okular::AllowPrint)) { KMessageBox::error(widget(), i18n("Printing this document is not allowed.")); return false; } if (!m_document->print(printer)) { const QString error = m_document->printError(); if (error.isEmpty()) { KMessageBox::error(widget(), i18n("Could not print the document. Unknown error. Please report to bugs.kde.org")); } else { KMessageBox::error(widget(), i18n("Could not print the document. Detailed error is \"%1\". Please report to bugs.kde.org", error)); } return false; } return true; } void Part::psTransformEnded(int exit, QProcess::ExitStatus status) { Q_UNUSED( exit ) if ( status != QProcess::NormalExit ) return; QProcess *senderobj = sender() ? qobject_cast< QProcess * >( sender() ) : 0; if ( senderobj ) { senderobj->close(); senderobj->deleteLater(); } setLocalFilePath( m_temporaryLocalFile ); openUrl( QUrl::fromLocalFile(m_temporaryLocalFile) ); m_temporaryLocalFile.clear(); } void Part::displayInfoMessage( const QString &message, KMessageWidget::MessageType messageType, int duration ) { if ( !Okular::Settings::showOSD() ) { if (messageType == KMessageWidget::Error) { KMessageBox::error( widget(), message ); } return; } // hide messageWindow if string is empty if ( message.isEmpty() ) m_infoMessage->animatedHide(); // display message (duration is length dependent) if ( duration < 0 ) { duration = 500 + 100 * message.length(); } m_infoTimer->start( duration ); m_infoMessage->setText( message ); m_infoMessage->setMessageType( messageType ); m_infoMessage->setVisible( true ); } void Part::errorMessage( const QString &message, int duration ) { displayInfoMessage( message, KMessageWidget::Error, duration ); } void Part::warningMessage( const QString &message, int duration ) { displayInfoMessage( message, KMessageWidget::Warning, duration ); } void Part::noticeMessage( const QString &message, int duration ) { // less important message -> simpler display widget in the PageView m_pageView->displayMessage( message, QString(), PageViewMessage::Info, duration ); } void Part::moveSplitter(int sideWidgetSize) { m_sidebar->moveSplitter( sideWidgetSize ); } void Part::unsetDummyMode() { if ( m_embedMode == PrintPreviewMode ) return; m_sidebar->setItemEnabled( m_reviewsWidget, true ); m_sidebar->setItemEnabled( m_bookmarkList, true ); m_sidebar->setItemEnabled( m_signaturePanel, true ); m_sidebar->setSidebarVisibility( Okular::Settings::showLeftPanel() ); // add back and next in history m_historyBack = KStandardAction::documentBack( this, SLOT(slotHistoryBack()), actionCollection() ); m_historyBack->setWhatsThis( i18n( "Go to the place you were before" ) ); connect(m_pageView.data(), &PageView::mouseBackButtonClick, m_historyBack, &QAction::trigger); m_historyNext = KStandardAction::documentForward( this, SLOT(slotHistoryNext()), actionCollection()); m_historyNext->setWhatsThis( i18n( "Go to the place you were after" ) ); connect(m_pageView.data(), &PageView::mouseForwardButtonClick, m_historyNext, &QAction::trigger); m_pageView->setupActions( actionCollection() ); // attach the actions of the children widgets too m_formsMessage->addAction( m_pageView->toggleFormsAction() ); m_signatureMessage->addAction( m_showSignaturePanel ); // ensure history actions are in the correct state updateViewActions(); } bool Part::handleCompressed( QString &destpath, const QString &path, KFilterDev::CompressionType compressionType) { m_tempfile = nullptr; // we are working with a compressed file, decompressing // temporary file for decompressing QTemporaryFile *newtempfile = new QTemporaryFile(); newtempfile->setAutoRemove(true); if ( !newtempfile->open() ) { KMessageBox::error( widget(), i18n("File Error! Could not create temporary file " "%1.", newtempfile->errorString())); delete newtempfile; return false; } // decompression filer KCompressionDevice dev( path, compressionType ); if ( !dev.open(QIODevice::ReadOnly) ) { KMessageBox::detailedError( widget(), i18n("File Error! Could not open the file " "%1 for uncompression. " "The file will not be loaded.", path), i18n("This error typically occurs if you do " "not have enough permissions to read the file. " "You can check ownership and permissions if you " "right-click on the file in the Dolphin " "file manager, then choose the 'Properties' option, " "and select 'Permissions' tab in the opened window.")); delete newtempfile; return false; } char buf[65536]; int read = 0, wrtn = 0; while ((read = dev.read(buf, sizeof(buf))) > 0) { wrtn = newtempfile->write(buf, read); if ( read != wrtn ) break; } if ((read != 0) || (newtempfile->size() == 0)) { KMessageBox::detailedError(widget(), i18n("File Error! Could not uncompress " "the file %1. " "The file will not be loaded.", path ), i18n("This error typically occurs if the file is corrupt. " "If you want to be sure, try to decompress the file manually " "using command-line tools.")); delete newtempfile; return false; } m_tempfile = newtempfile; destpath = m_tempfile->fileName(); return true; } void Part::rebuildBookmarkMenu( bool unplugActions ) { if ( unplugActions ) { unplugActionList( QStringLiteral("bookmarks_currentdocument") ); qDeleteAll( m_bookmarkActions ); m_bookmarkActions.clear(); } QUrl u = m_document->currentDocument(); if ( u.isValid() ) { m_bookmarkActions = m_document->bookmarkManager()->actionsForUrl( u ); } bool havebookmarks = true; if ( m_bookmarkActions.isEmpty() ) { havebookmarks = false; QAction * a = new QAction( nullptr ); a->setText( i18n( "No Bookmarks" ) ); a->setEnabled( false ); m_bookmarkActions.append( a ); } plugActionList( QStringLiteral("bookmarks_currentdocument"), m_bookmarkActions ); if (factory()) { const QList clients(factory()->clients()); bool containerFound = false; for (int i = 0; !containerFound && i < clients.size(); ++i) { QMenu *container = dynamic_cast(factory()->container(QStringLiteral("bookmarks"), clients[i])); if (container && container->actions().contains(m_bookmarkActions.first())) { container->installEventFilter(this); containerFound = true; } } } m_prevBookmark->setEnabled( havebookmarks ); m_nextBookmark->setEnabled( havebookmarks ); } bool Part::eventFilter(QObject * watched, QEvent * event) { switch (event->type()) { case QEvent::ContextMenu: { QContextMenuEvent *e = static_cast(event); QMenu *menu = static_cast(watched); QScopedPointer ctxMenu(new QMenu); QPoint pos; bool ret = false; if (e->reason() == QContextMenuEvent::Mouse) { pos = e->pos(); ret = aboutToShowContextMenu(menu, menu->actionAt(e->pos()), ctxMenu.data()); } else if (menu->activeAction()) { pos = menu->actionGeometry(menu->activeAction()).center(); ret = aboutToShowContextMenu(menu, menu->activeAction(), ctxMenu.data()); } ctxMenu->exec(menu->mapToGlobal(pos)); if (ret) { event->accept(); } return ret; } default: break; } - return false; + return KParts::ReadWritePart::eventFilter(watched, event); } void Part::updateAboutBackendAction() { const KPluginMetaData data = m_document->generatorInfo(); m_aboutBackend->setEnabled(data.isValid()); } void Part::resetStartArguments() { m_cliPrint = false; m_cliPrintAndExit = false; } #if PURPOSE_FOUND void Part::slotShareActionFinished(const QJsonObject &output, int error, const QString &message) { if (error) { KMessageBox::error(widget(), i18n("There was a problem sharing the document: %1", message), i18n("Share")); } else { const QString url = output[QStringLiteral("url")].toString(); if (url.isEmpty()) { m_pageView->displayMessage(i18n("Document shared successfully")); } else { KMessageBox::information(widget(), i18n("You can find the shared document at: %1", url), i18n("Share"), QString(), KMessageBox::Notify | KMessageBox::AllowLink); } } } #endif void Part::setReadWrite(bool readwrite) { m_document->setAnnotationEditingEnabled( readwrite ); ReadWritePart::setReadWrite( readwrite ); } void Part::enableStartWithFind(const QString &text) { m_textToFindOnOpen = QString(text); } void Part::slotOpenContainingFolder() { KIO::highlightInFileManager( { QUrl(localFilePath()) } ); } } // namespace Okular #include "part.moc" /* kate: replace-tabs on; indent-width 4; */ diff --git a/part.h b/part.h index 37d1c10e0..ff3f7cb6c 100644 --- a/part.h +++ b/part.h @@ -1,428 +1,428 @@ /*************************************************************************** * Copyright (C) 2002 by Wilco Greven * * Copyright (C) 2003-2004 by Christophe Devriese * * * * Copyright (C) 2003 by Andy Goossens * * Copyright (C) 2003 by Laurent Montel * * Copyright (C) 2004 by Dominique Devriese * * Copyright (C) 2004-2007 by Albert Astals Cid * * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * * company, info@kdab.com. Work sponsored by the * * LiMux project of the city of Munich * * * * 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. * ***************************************************************************/ #ifndef _PART_H_ #define _PART_H_ #include #include #include #include #include // krazy:exclude=includes #include #include #include #include #include #include #include "core/observer.h" #include "core/document.h" #include "kdocumentviewer.h" #include "interfaces/viewerinterface.h" #include "okularpart_export.h" #include class QAction; class QWidget; class QPrinter; class QMenu; class KConfigDialog; class KDirWatch; class KToggleAction; class KToggleFullScreenAction; class QTemporaryFile; class QAction; class QJsonObject; namespace KParts { class GUIActivateEvent; } class FindBar; class ThumbnailList; class PageSizeLabel; class PageView; class PresentationWidget; class ProgressWidget; class SearchWidget; class Sidebar; class TOC; class MiniBar; class MiniBarLogic; class FileKeeper; class Reviews; class BookmarkList; class DrawingToolActions; class Layers; class SignaturePanel; #if PURPOSE_FOUND namespace Purpose { class Menu; } #endif namespace Okular { class BrowserExtension; class ExportFormat; /** * Describes the possible embedding modes of the part * * @since 0.14 (KDE 4.8) */ enum EmbedMode { UnknownEmbedMode, NativeShellMode, // embedded in the native Okular' shell PrintPreviewMode, // embedded to show the print preview of a document KHTMLPartMode, // embedded in KHTML ViewerWidgetMode // the part acts as a widget that can display all kinds of documents }; /** * This is a "Part". It that does all the real work in a KPart * application. * * @short Main Part * @author Wilco Greven * @version 0.2 */ class OKULARPART_EXPORT Part : public KParts::ReadWritePart, public Okular::DocumentObserver, public KDocumentViewer, public Okular::ViewerInterface { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.okular") Q_INTERFACES(KDocumentViewer) Q_INTERFACES(Okular::ViewerInterface) friend class PartTest; public: // Default constructor /** * If one element of 'args' contains one of the strings "Print/Preview" or "ViewerWidget", * the part will be set up in the corresponding mode. Additionally, it is possible to specify * which config file should be used by adding a string containing "ConfigFileName=" * to 'args'. **/ Part(QWidget* parentWidget, QObject* parent, const QVariantList& args); // Destructor ~Part() override; // inherited from DocumentObserver void notifySetup( const QVector< Okular::Page * > &pages, int setupFlags ) override; void notifyViewportChanged( bool smoothMove ) override; void notifyPageChanged( int page, int flags ) override; bool openDocument(const QUrl &url, uint page) override; void startPresentation() override; QStringList supportedMimeTypes() const override; QUrl realUrl() const; void showSourceLocation(const QString& fileName, int line, int column, bool showGraphically = true) override; void clearLastShownSourceLocation() override; bool isWatchFileModeEnabled() const override; void setWatchFileModeEnabled(bool enable) override; bool areSourceLocationsShownGraphically() const override; void setShowSourceLocationsGraphically(bool show) override; bool openNewFilesInTabs() const override; public Q_SLOTS: // dbus Q_SCRIPTABLE Q_NOREPLY void goToPage(uint page) override; Q_SCRIPTABLE Q_NOREPLY void openDocument( const QString &doc ); Q_SCRIPTABLE uint pages(); Q_SCRIPTABLE uint currentPage(); Q_SCRIPTABLE QString currentDocument(); Q_SCRIPTABLE QString documentMetaData( const QString &metaData ) const; Q_SCRIPTABLE void slotPreferences(); Q_SCRIPTABLE void slotFind(); Q_SCRIPTABLE void slotPrintPreview(); Q_SCRIPTABLE void slotPreviousPage(); Q_SCRIPTABLE void slotNextPage(); Q_SCRIPTABLE void slotGotoFirst(); Q_SCRIPTABLE void slotGotoLast(); Q_SCRIPTABLE void slotTogglePresentation(); Q_SCRIPTABLE void slotToggleChangeColors(); Q_SCRIPTABLE void slotSetChangeColors(bool active); Q_SCRIPTABLE Q_NOREPLY void reload(); Q_SCRIPTABLE Q_NOREPLY void enableStartWithPrint(); Q_SCRIPTABLE Q_NOREPLY void enableExitAfterPrint(); Q_SCRIPTABLE Q_NOREPLY void enableStartWithFind(const QString &text); Q_SCRIPTABLE void slotOpenContainingFolder(); Q_SIGNALS: void enablePrintAction(bool enable); void openSourceReference(const QString& absFileName, int line, int column); void viewerMenuStateChange(bool enabled); void enableCloseAction(bool enable); - void mimeTypeChanged(QMimeType mimeType); + void mimeTypeChanged(const QMimeType &mimeType); void urlsDropped( const QList& urls ); - void fitWindowToPage( const QSize& pageViewPortSize, const QSize& pageSize ); + void fitWindowToPage( const QSize pageViewPortSize, const QSize pageSize ); protected: // reimplemented from KParts::ReadWritePart bool openFile() override; bool openUrl(const QUrl &url) override; void guiActivateEvent(KParts::GUIActivateEvent *event) override; void displayInfoMessage( const QString &message, KMessageWidget::MessageType messageType = KMessageWidget::Information, int duration = -1 ); public: bool queryClose() override; bool closeUrl() override; bool closeUrl(bool promptToSave) override; void setReadWrite(bool readwrite) override; bool saveAs(const QUrl & saveUrl) override; protected Q_SLOTS: // connected to actions void openUrlFromDocument(const QUrl &url); void openUrlFromBookmarks(const QUrl &url); void handleDroppedUrls( const QList& urls ); void slotGoToPage(); void slotHistoryBack(); void slotHistoryNext(); void slotAddBookmark(); void slotRenameBookmarkFromMenu(); void slotRemoveBookmarkFromMenu(); void slotRenameCurrentViewportBookmark(); void slotPreviousBookmark(); void slotNextBookmark(); void slotFindNext(); void slotFindPrev(); bool slotSaveFileAs(bool showOkularArchiveAsDefaultFormat = false); void slotGetNewStuff(); void slotNewConfig(); - void slotShowMenu(const Okular::Page *page, const QPoint &point); - void slotShowTOCMenu(const Okular::DocumentViewport &vp, const QPoint &point, const QString &title); + void slotShowMenu(const Okular::Page *page, const QPoint point); + void slotShowTOCMenu(const Okular::DocumentViewport &vp, const QPoint point, const QString &title); void slotShowProperties(); void slotShowEmbeddedFiles(); void slotShowLeftPanel(); void slotShowBottomBar(); void slotShowPresentation(); void slotHidePresentation(); void slotExportAs(QAction *); bool slotImportPSFile(); void slotAboutBackend(); void slotReload(); void close(); void cannotQuit(); void slotShowFindBar(); void slotHideFindBar(); void slotJobStarted(KIO::Job *job); void slotJobFinished(KJob *job); void loadCancelled(const QString &reason); void setWindowTitleFromDocument(); // can be connected to widget elements void updateViewActions(); void updateBookmarksActions(); void enableTOC(bool enable); void slotRebuildBookmarkMenu(); void enableLayers( bool enable ); void showSidebarSignaturesItem( bool show ); public Q_SLOTS: bool saveFile() override; // connected to Shell action (and browserExtension), not local one void slotPrint(); void slotFileDirty( const QString& ); bool slotAttemptReload( bool oneShot = false, const QUrl &newUrl = QUrl() ); void psTransformEnded(int, QProcess::ExitStatus); KConfigDialog * slotGeneratorPreferences(); void errorMessage( const QString &message, int duration = 0 ); void warningMessage( const QString &message, int duration = -1 ); void noticeMessage( const QString &message, int duration = -1 ); void moveSplitter( const int sideWidgetSize ); private: bool aboutToShowContextMenu(QMenu *menu, QAction *action, QMenu *contextMenu); - void showMenu(const Okular::Page *page, const QPoint &point, const QString &bookmarkTitle = QString(), const Okular::DocumentViewport &vp = DocumentViewport(), bool showTOCActions = false); + void showMenu(const Okular::Page *page, const QPoint point, const QString &bookmarkTitle = QString(), const Okular::DocumentViewport &vp = DocumentViewport(), bool showTOCActions = false); bool eventFilter(QObject * watched, QEvent * event) override; Document::OpenResult doOpenFile(const QMimeType &mime, const QString &fileNameToOpen, bool *isCompressedFile); bool openUrl( const QUrl &url, bool swapInsteadOfOpening ); void setupViewerActions(); void setViewerShortcuts(); void setupActions(); void setupPrint( QPrinter &printer ); bool doPrint( QPrinter &printer ); bool handleCompressed(QString &destpath, const QString &path, KCompressionDevice::CompressionType compressionType ); void rebuildBookmarkMenu( bool unplugActions = true ); void updateAboutBackendAction(); void unsetDummyMode(); void slotRenameBookmark( const DocumentViewport &viewport ); void slotRemoveBookmark( const DocumentViewport &viewport ); void resetStartArguments(); void checkNativeSaveDataLoss(bool *out_wontSaveForms, bool *out_wontSaveAnnotations) const; enum SaveAsFlag { NoSaveAsFlags = 0, ///< No options SaveAsOkularArchive = 1 ///< Save as Okular Archive (.okular) instead of document's native format }; Q_DECLARE_FLAGS( SaveAsFlags, SaveAsFlag ) bool saveAs( const QUrl & saveUrl, SaveAsFlags flags ); void setFileToWatch( const QString &filePath ); void unsetFileToWatch(); #if PURPOSE_FOUND void slotShareActionFinished(const QJsonObject &output, int error, const QString &message); #endif static int numberOfParts; QTemporaryFile *m_tempfile; // the document Okular::Document * m_document; QDateTime m_fileLastModified; QString m_temporaryLocalFile; bool isDocumentArchive; bool m_documentOpenWithPassword; bool m_swapInsteadOfOpening; // if set, the next open operation will replace the backing file (used when reloading just saved files) // main widgets Sidebar *m_sidebar; SearchWidget *m_searchWidget; FindBar * m_findBar; KMessageWidget * m_migrationMessage; KMessageWidget * m_topMessage; KMessageWidget * m_formsMessage; KMessageWidget * m_infoMessage; KMessageWidget * m_signatureMessage; QPointer m_thumbnailList; QPointer m_pageView; QPointer m_toc; QPointer m_miniBarLogic; QPointer m_miniBar; QPointer m_pageNumberTool; QPointer m_bottomBar; QPointer m_presentationWidget; QPointer m_progressWidget; QPointer m_pageSizeLabel; QPointer m_reviewsWidget; QPointer m_bookmarkList; QPointer m_layers; QPointer m_signaturePanel; // document watcher (and reloader) variables KDirWatch *m_watcher; QString m_watchedFilePath, m_watchedFileSymlinkTarget; QTimer *m_dirtyHandler; QUrl m_oldUrl; Okular::DocumentViewport m_viewportDirty; bool m_isReloading; bool m_wasPresentationOpen; QWidget *m_dirtyToolboxItem; bool m_wasSidebarVisible; bool m_wasSidebarCollapsed; bool m_fileWasRemoved; Rotation m_dirtyPageRotation; // Remember the search history QStringList m_searchHistory; // actions QAction *m_gotoPage; QAction *m_prevPage; QAction *m_nextPage; QAction *m_beginningOfDocument; QAction *m_endOfDocument; QAction *m_historyBack; QAction *m_historyNext; QAction *m_addBookmark; QAction *m_renameBookmark; QAction *m_prevBookmark; QAction *m_nextBookmark; QAction *m_copy; QAction *m_selectAll; QAction *m_selectCurrentPage; QAction *m_find; QAction *m_findNext; QAction *m_findPrev; QAction *m_save; QAction *m_saveAs; QAction *m_saveCopyAs; QAction *m_printPreview; QAction *m_showProperties; QAction *m_showEmbeddedFiles; QAction *m_exportAs; QAction *m_exportAsText; QAction *m_exportAsDocArchive; #if PURPOSE_FOUND QAction *m_share; #endif QAction *m_showPresentation; QAction *m_openContainingFolder; KToggleAction* m_showMenuBarAction; KToggleAction* m_showLeftPanel; KToggleAction* m_showBottomBar; QAction* m_showSignaturePanel; KToggleFullScreenAction* m_showFullScreenAction; QAction *m_aboutBackend; QAction *m_reload; QMenu *m_exportAsMenu; #if PURPOSE_FOUND Purpose::Menu *m_shareMenu; #endif QAction *m_closeFindBar; DrawingToolActions *m_presentationDrawingActions; bool m_actionsSearched; BrowserExtension *m_bExtension; QList m_exportFormats; QList m_bookmarkActions; bool m_cliPresentation; bool m_cliPrint; bool m_cliPrintAndExit; QString m_addBookmarkText; QIcon m_addBookmarkIcon; EmbedMode m_embedMode; QUrl m_realUrl; KXMLGUIClient *m_generatorGuiClient; FileKeeper *m_keeper; // Timer for m_infoMessage QTimer *m_infoTimer; QString m_registerDbusName; // String to search in document startup QString m_textToFindOnOpen; private Q_SLOTS: void slotAnnotationPreferences(); void slotHandleActivatedSourceReference(const QString& absFileName, int line, int col, bool *handled); }; } #endif /* kate: replace-tabs on; indent-width 4; */ diff --git a/shell/shell.cpp b/shell/shell.cpp index c484407c3..0bdb710c3 100644 --- a/shell/shell.cpp +++ b/shell/shell.cpp @@ -1,846 +1,846 @@ /*************************************************************************** * Copyright (C) 2002 by Wilco Greven * * Copyright (C) 2002 by Chris Cheney * * Copyright (C) 2003 by Benjamin Meyer * * Copyright (C) 2003-2004 by Christophe Devriese * * * * Copyright (C) 2003 by Laurent Montel * * Copyright (C) 2003-2004 by Albert Astals Cid * * Copyright (C) 2003 by Luboš Luňák * * Copyright (C) 2003 by Malcolm Hunter * * Copyright (C) 2004 by Dominique Devriese * * Copyright (C) 2004 by Dirk Mueller * * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * * company, info@kdab.com. Work sponsored by the * * LiMux project of the city of Munich * * * * 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. * ***************************************************************************/ #include "shell.h" // qt/kde includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef WITH_KACTIVITIES #include #endif // local includes #include "kdocumentviewer.h" #include "../interfaces/viewerinterface.h" #include "shellutils.h" static const char *shouldShowMenuBarComingFromFullScreen = "shouldShowMenuBarComingFromFullScreen"; static const char *shouldShowToolBarComingFromFullScreen = "shouldShowToolBarComingFromFullScreen"; static const char* const SESSION_URL_KEY = "Urls"; static const char* const SESSION_TAB_KEY = "ActiveTab"; Shell::Shell( const QString &serializedOptions ) : KParts::MainWindow(), m_menuBarWasShown(true), m_toolBarWasShown(true) #ifndef Q_OS_WIN , m_activityResource(nullptr) #endif , m_isValid(true) { setObjectName( QStringLiteral( "okular::Shell#" ) ); setContextMenuPolicy( Qt::NoContextMenu ); // otherwise .rc file won't be found by unit test setComponentName(QStringLiteral("okular"), QString()); // set the shell's ui resource file setXMLFile(QStringLiteral("shell.rc")); m_fileformatsscanned = false; m_showMenuBarAction = nullptr; // this routine will find and load our Part. it finds the Part by // name which is a bad idea usually.. but it's alright in this // case since our Part is made for this Shell KPluginLoader loader(QStringLiteral("okularpart")); m_partFactory = loader.factory(); if (!m_partFactory) { // if we couldn't find our Part, we exit since the Shell by // itself can't do anything useful m_isValid = false; KMessageBox::error(this, i18n("Unable to find the Okular component: %1", loader.errorString())); return; } // now that the Part plugin is loaded, create the part KParts::ReadWritePart* const firstPart = m_partFactory->create< KParts::ReadWritePart >( this ); if (firstPart) { // Setup tab bar m_tabWidget = new QTabWidget( this ); m_tabWidget->setTabsClosable( true ); m_tabWidget->setElideMode( Qt::ElideRight ); m_tabWidget->tabBar()->hide(); m_tabWidget->setDocumentMode( true ); m_tabWidget->setMovable( true ); m_tabWidget->setAcceptDrops(true); m_tabWidget->installEventFilter(this); connect( m_tabWidget, &QTabWidget::currentChanged, this, &Shell::setActiveTab ); connect( m_tabWidget, &QTabWidget::tabCloseRequested, this, &Shell::closeTab ); connect( m_tabWidget->tabBar(), &QTabBar::tabMoved, this, &Shell::moveTabData ); setCentralWidget( m_tabWidget ); // then, setup our actions setupActions(); connect( QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &QObject::deleteLater ); // and integrate the part's GUI with the shell's setupGUI(Keys | ToolBar | Save); createGUI(firstPart); connectPart( firstPart ); m_tabs.append( firstPart ); m_tabWidget->addTab( firstPart->widget(), QString() ); readSettings(); m_unique = ShellUtils::unique(serializedOptions); if (m_unique) { m_unique = QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.okular")); if (!m_unique) KMessageBox::information(this, i18n("There is already a unique Okular instance running. This instance won't be the unique one.")); } else { QString serviceName = QStringLiteral("org.kde.okular-") + QString::number(qApp->applicationPid()); QDBusConnection::sessionBus().registerService(serviceName); } if (ShellUtils::noRaise(serializedOptions)) { setAttribute(Qt::WA_ShowWithoutActivating); } QDBusConnection::sessionBus().registerObject(QStringLiteral("/okularshell"), this, QDBusConnection::ExportScriptableSlots); } else { m_isValid = false; KMessageBox::error(this, i18n("Unable to find the Okular component.")); } } bool Shell::eventFilter(QObject *obj, QEvent *event) { Q_UNUSED(obj); QDragMoveEvent* dmEvent = dynamic_cast(event); if (dmEvent) { bool accept = dmEvent->mimeData()->hasUrls(); event->setAccepted(accept); return accept; } QDropEvent* dEvent = dynamic_cast(event); if (dEvent) { const QList list = KUrlMimeData::urlsFromMimeData(dEvent->mimeData()); handleDroppedUrls(list); dEvent->setAccepted(true); return true; } // Handle middle button click events on the tab bar if (obj == m_tabWidget && event->type() == QEvent::MouseButtonRelease) { QMouseEvent* mEvent = static_cast(event); if (mEvent->button() == Qt::MiddleButton) { int tabIndex = m_tabWidget->tabBar()->tabAt(mEvent->pos()); if (tabIndex != -1) { closeTab(tabIndex); return true; } } } - return false; + return KParts::MainWindow::eventFilter(obj, event); } bool Shell::isValid() const { return m_isValid; } void Shell::showOpenRecentMenu() { m_recent->menu()->popup(QCursor::pos()); } Shell::~Shell() { if( !m_tabs.empty() ) { writeSettings(); for( const TabState &tab : qAsConst( m_tabs ) ) { tab.part->closeUrl( false ); } m_tabs.clear(); } if (m_unique) QDBusConnection::sessionBus().unregisterService(QStringLiteral("org.kde.okular")); delete m_tabWidget; } // Open a new document if we have space for it // This can hang if called on a unique instance and openUrl pops a messageBox bool Shell::openDocument( const QUrl& url, const QString &serializedOptions ) { if( m_tabs.size() <= 0 ) return false; KParts::ReadWritePart* const part = m_tabs[0].part; // Return false if we can't open new tabs and the only part is occupied if ( !dynamic_cast(part)->openNewFilesInTabs() && !part->url().isEmpty() && !ShellUtils::unique(serializedOptions)) { return false; } openUrl( url, serializedOptions ); return true; } bool Shell::openDocument( const QString& urlString, const QString &serializedOptions ) { return openDocument(QUrl(urlString), serializedOptions); } bool Shell::canOpenDocs( int numDocs, int desktop ) { if( m_tabs.size() <= 0 || numDocs <= 0 || m_unique ) return false; KParts::ReadWritePart* const part = m_tabs[0].part; const bool allowTabs = dynamic_cast(part)->openNewFilesInTabs(); if( !allowTabs && (numDocs > 1 || !part->url().isEmpty()) ) return false; const KWindowInfo winfo( window()->effectiveWinId(), KWindowSystem::WMDesktop ); if( winfo.desktop() != desktop ) return false; return true; } void Shell::openUrl( const QUrl & url, const QString &serializedOptions ) { const int activeTab = m_tabWidget->currentIndex(); if ( activeTab < m_tabs.size() ) { KParts::ReadWritePart* const activePart = m_tabs[activeTab].part; if( !activePart->url().isEmpty() ) { if( m_unique ) { applyOptionsToPart( activePart, serializedOptions ); activePart->openUrl( url ); } else { if( dynamic_cast(activePart)->openNewFilesInTabs() ) { openNewTab( url, serializedOptions ); } else { Shell* newShell = new Shell( serializedOptions ); newShell->show(); newShell->openUrl( url, serializedOptions ); } } } else { m_tabWidget->setTabText( activeTab, url.fileName() ); applyOptionsToPart( activePart, serializedOptions ); bool openOk = activePart->openUrl( url ); const bool isstdin = url.fileName() == QLatin1String( "-" ) || url.scheme() == QLatin1String( "fd" ); if ( !isstdin ) { if ( openOk ) { #ifdef WITH_KACTIVITIES if ( !m_activityResource ) m_activityResource = new KActivities::ResourceInstance( window()->winId(), this ); m_activityResource->setUri( url ); #endif m_recent->addUrl( url ); } else { m_recent->removeUrl( url ); closeTab( activeTab ); } } } } } void Shell::closeUrl() { closeTab( m_tabWidget->currentIndex() ); } void Shell::readSettings() { m_recent->loadEntries( KSharedConfig::openConfig()->group( "Recent Files" ) ); m_recent->setEnabled( true ); // force enabling const KConfigGroup group = KSharedConfig::openConfig()->group( "Desktop Entry" ); bool fullScreen = group.readEntry( "FullScreen", false ); setFullScreen( fullScreen ); if (fullScreen) { m_menuBarWasShown = group.readEntry( shouldShowMenuBarComingFromFullScreen, true ); m_toolBarWasShown = group.readEntry( shouldShowToolBarComingFromFullScreen, true ); } } void Shell::writeSettings() { m_recent->saveEntries( KSharedConfig::openConfig()->group( "Recent Files" ) ); KConfigGroup group = KSharedConfig::openConfig()->group( "Desktop Entry" ); group.writeEntry( "FullScreen", m_fullScreenAction->isChecked() ); if (m_fullScreenAction->isChecked()) { group.writeEntry( shouldShowMenuBarComingFromFullScreen, m_menuBarWasShown ); group.writeEntry( shouldShowToolBarComingFromFullScreen, m_toolBarWasShown ); } KSharedConfig::openConfig()->sync(); } void Shell::setupActions() { KStandardAction::open(this, SLOT(fileOpen()), actionCollection()); m_recent = KStandardAction::openRecent( this, SLOT(openUrl(QUrl)), actionCollection() ); m_recent->setToolBarMode( KRecentFilesAction::MenuMode ); connect( m_recent, &QAction::triggered, this, &Shell::showOpenRecentMenu ); m_recent->setToolTip( i18n("Click to open a file\nClick and hold to open a recent file") ); m_recent->setWhatsThis( i18n( "Click to open a file or Click and hold to select a recent file" ) ); m_printAction = KStandardAction::print( this, SLOT(print()), actionCollection() ); m_printAction->setEnabled( false ); m_closeAction = KStandardAction::close( this, SLOT(closeUrl()), actionCollection() ); m_closeAction->setEnabled( false ); KStandardAction::quit(this, SLOT(close()), actionCollection()); setStandardToolBarMenuEnabled(true); m_showMenuBarAction = KStandardAction::showMenubar( this, SLOT(slotShowMenubar()), actionCollection()); m_fullScreenAction = KStandardAction::fullScreen( this, SLOT(slotUpdateFullScreen()), this,actionCollection() ); m_nextTabAction = actionCollection()->addAction(QStringLiteral("tab-next")); m_nextTabAction->setText( i18n("Next Tab") ); actionCollection()->setDefaultShortcuts(m_nextTabAction, KStandardShortcut::tabNext()); m_nextTabAction->setEnabled( false ); connect( m_nextTabAction, &QAction::triggered, this, &Shell::activateNextTab ); m_prevTabAction = actionCollection()->addAction(QStringLiteral("tab-previous")); m_prevTabAction->setText( i18n("Previous Tab") ); actionCollection()->setDefaultShortcuts(m_prevTabAction, KStandardShortcut::tabPrev()); m_prevTabAction->setEnabled( false ); connect( m_prevTabAction, &QAction::triggered, this, &Shell::activatePrevTab ); m_undoCloseTab = actionCollection()->addAction(QStringLiteral("undo-close-tab")); m_undoCloseTab->setText( i18n("Undo close tab") ); actionCollection()->setDefaultShortcut(m_undoCloseTab, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_T)); m_undoCloseTab->setIcon(QIcon::fromTheme(QStringLiteral("edit-undo"))); m_undoCloseTab->setEnabled( false ); connect( m_undoCloseTab, &QAction::triggered, this, &Shell::undoCloseTab ); } void Shell::saveProperties(KConfigGroup &group) { if ( !m_isValid ) // part couldn't be loaded, nothing to save return; // Gather lists of settings to preserve QStringList urls; for( const TabState &tab : qAsConst( m_tabs ) ) { urls.append( tab.part->url().url() ); } group.writePathEntry( SESSION_URL_KEY, urls ); group.writeEntry( SESSION_TAB_KEY, m_tabWidget->currentIndex() ); } void Shell::readProperties(const KConfigGroup &group) { // Reopen documents based on saved settings QStringList urls = group.readPathEntry( SESSION_URL_KEY, QStringList() ); int desiredTab = group.readEntry( SESSION_TAB_KEY, 0 ); while( !urls.isEmpty() ) { openUrl( QUrl(urls.takeFirst()) ); } if( desiredTab < m_tabs.size() ) { setActiveTab( desiredTab ); } } QStringList Shell::fileFormats() const { QStringList supportedPatterns; QString constraint( QStringLiteral("(Library == 'okularpart')") ); QLatin1String basePartService( "KParts/ReadOnlyPart" ); KService::List offers = KServiceTypeTrader::self()->query( basePartService, constraint ); KService::List::ConstIterator it = offers.constBegin(), itEnd = offers.constEnd(); for ( ; it != itEnd; ++it ) { KService::Ptr service = *it; QStringList mimeTypes = service->mimeTypes(); supportedPatterns += mimeTypes; } return supportedPatterns; } void Shell::fileOpen() { // this slot is called whenever the File->Open menu is selected, // the Open shortcut is pressed (usually CTRL+O) or the Open toolbar // button is clicked const int activeTab = m_tabWidget->currentIndex(); if ( !m_fileformatsscanned ) { const KDocumentViewer* const doc = qobject_cast(m_tabs[activeTab].part); if ( doc ) m_fileformats = doc->supportedMimeTypes(); if ( m_fileformats.isEmpty() ) m_fileformats = fileFormats(); m_fileformatsscanned = true; } QUrl startDir; const KParts::ReadWritePart* const curPart = m_tabs[activeTab].part; if ( curPart->url().isLocalFile() ) startDir = KIO::upUrl(curPart->url()); QPointer dlg( new QFileDialog( this )); dlg->setDirectoryUrl( startDir ); dlg->setAcceptMode( QFileDialog::AcceptOpen ); dlg->setOption( QFileDialog::HideNameFilterDetails, true ); dlg->setFileMode( QFileDialog::ExistingFiles ); // Allow selection of more than one file QMimeDatabase mimeDatabase; QSet globPatterns; QMap namedGlobs; for ( const QString &mimeName : qAsConst(m_fileformats) ) { QMimeType mimeType = mimeDatabase.mimeTypeForName( mimeName ); const QStringList globs( mimeType.globPatterns() ); if ( globs.isEmpty() ) { continue; } globPatterns.unite( globs.toSet() ) ; namedGlobs[ mimeType.comment() ].append( globs ); } QStringList namePatterns; for ( auto it = namedGlobs.cbegin(); it != namedGlobs.cend(); ++it ) { namePatterns.append( it.key() + QLatin1String(" (") + it.value().join( QLatin1Char(' ') ) + QLatin1Char(')') ); } const QStringList allGlobPatterns = globPatterns.values(); namePatterns.prepend( i18n("All files (*)") ); namePatterns.prepend( i18n("All supported files (%1)", allGlobPatterns.join( QLatin1Char(' ') ) ) ); dlg->setNameFilters( namePatterns ); dlg->setWindowTitle( i18n("Open Document") ); if ( dlg->exec() && dlg ) { const QList urlList = dlg->selectedUrls(); for (const QUrl &url : urlList) { openUrl( url ); } } if ( dlg ) { delete dlg.data(); } } void Shell::tryRaise() { KWindowSystem::forceActiveWindow( window()->effectiveWinId() ); } // only called when starting the program void Shell::setFullScreen( bool useFullScreen ) { if( useFullScreen ) setWindowState( windowState() | Qt::WindowFullScreen ); // set else setWindowState( windowState() & ~Qt::WindowFullScreen ); // reset } void Shell::setCaption( const QString &caption ) { bool modified = false; const int activeTab = m_tabWidget->currentIndex(); if ( activeTab >= 0 && activeTab < m_tabs.size() ) { KParts::ReadWritePart* const activePart = m_tabs[activeTab].part; QString tabCaption = activePart->url().fileName(); if ( activePart->isModified() ) { modified = true; if ( !tabCaption.isEmpty() ) { tabCaption.append( QStringLiteral( " *" ) ); } } m_tabWidget->setTabText( activeTab, tabCaption ); } setCaption( caption, modified ); } void Shell::showEvent(QShowEvent *e) { if (!menuBar()->isNativeMenuBar() && m_showMenuBarAction) m_showMenuBarAction->setChecked( menuBar()->isVisible() ); KParts::MainWindow::showEvent(e); } void Shell::slotUpdateFullScreen() { if(m_fullScreenAction->isChecked()) { m_menuBarWasShown = !menuBar()->isHidden(); menuBar()->hide(); m_toolBarWasShown = !toolBar()->isHidden(); toolBar()->hide(); KToggleFullScreenAction::setFullScreen(this, true); } else { if (m_menuBarWasShown) { menuBar()->show(); } if (m_toolBarWasShown) { toolBar()->show(); } KToggleFullScreenAction::setFullScreen(this, false); } } void Shell::slotShowMenubar() { if ( menuBar()->isHidden() ) menuBar()->show(); else menuBar()->hide(); } QSize Shell::sizeHint() const { return QApplication::desktop()->availableGeometry( this ).size() * 0.75; } bool Shell::queryClose() { if (m_tabs.count() > 1) { const QString dontAskAgainName = QStringLiteral("ShowTabWarning"); KMessageBox::ButtonCode dummy = KMessageBox::Yes; if (shouldBeShownYesNo(dontAskAgainName, dummy)) { QDialog *dialog = new QDialog(this); dialog->setWindowTitle(i18n("Confirm Close")); QDialogButtonBox *buttonBox = new QDialogButtonBox(dialog); buttonBox->setStandardButtons(QDialogButtonBox::Yes | QDialogButtonBox::No); KGuiItem::assign(buttonBox->button(QDialogButtonBox::Yes), KGuiItem(i18n("Close Tabs"), QStringLiteral("tab-close"))); KGuiItem::assign(buttonBox->button(QDialogButtonBox::No), KStandardGuiItem::cancel()); bool checkboxResult = true; const int result = KMessageBox::createKMessageBox(dialog, buttonBox, QMessageBox::Question, i18n("You are about to close %1 tabs. Are you sure you want to continue?", m_tabs.count()), QStringList(), i18n("Warn me when I attempt to close multiple tabs"), &checkboxResult, KMessageBox::Notify); if (!checkboxResult) { saveDontShowAgainYesNo(dontAskAgainName, dummy); } if (result != QDialogButtonBox::Yes) { return false; } } } for( int i = 0; i < m_tabs.size(); ++i ) { KParts::ReadWritePart* const part = m_tabs[i].part; // To resolve confusion about multiple modified docs, switch to relevant tab if( part->isModified() ) setActiveTab( i ); if( !part->queryClose() ) return false; } return true; } void Shell::setActiveTab( int tab ) { m_tabWidget->setCurrentIndex( tab ); createGUI( m_tabs[tab].part ); m_printAction->setEnabled( m_tabs[tab].printEnabled ); m_closeAction->setEnabled( m_tabs[tab].closeEnabled ); } void Shell::closeTab( int tab ) { KParts::ReadWritePart* const part = m_tabs[tab].part; QUrl url = part->url(); if( part->closeUrl() && m_tabs.count() > 1 ) { if( part->factory() ) part->factory()->removeClient( part ); part->disconnect(); part->deleteLater(); m_tabs.removeAt( tab ); m_tabWidget->removeTab( tab ); m_undoCloseTab->setEnabled( true ); m_closedTabUrls.append( url ); if( m_tabWidget->count() == 1 ) { m_tabWidget->tabBar()->hide(); m_nextTabAction->setEnabled( false ); m_prevTabAction->setEnabled( false ); } } } void Shell::openNewTab( const QUrl& url, const QString &serializedOptions ) { // Tabs are hidden when there's only one, so show it if( m_tabs.size() == 1 ) { m_tabWidget->tabBar()->show(); m_nextTabAction->setEnabled( true ); m_prevTabAction->setEnabled( true ); } const int newIndex = m_tabs.size(); // Make new part m_tabs.append( m_partFactory->create(this) ); connectPart( m_tabs[newIndex].part ); // Update GUI KParts::ReadWritePart* const part = m_tabs[newIndex].part; m_tabWidget->addTab( part->widget(), url.fileName() ); applyOptionsToPart(part, serializedOptions); int previousActiveTab = m_tabWidget->currentIndex(); setActiveTab( m_tabs.size() - 1 ); if( part->openUrl(url) ) { m_recent->addUrl( url ); } else { setActiveTab( previousActiveTab ); closeTab( m_tabs.size() - 1 ); m_recent->removeUrl( url ); } } void Shell::applyOptionsToPart( QObject* part, const QString &serializedOptions ) { KDocumentViewer* const doc = qobject_cast(part); const QString find = ShellUtils::find(serializedOptions); if ( ShellUtils::startInPresentation(serializedOptions) ) doc->startPresentation(); if ( ShellUtils::showPrintDialog(serializedOptions) ) QMetaObject::invokeMethod( part, "enableStartWithPrint" ); if ( ShellUtils::showPrintDialogAndExit(serializedOptions) ) QMetaObject::invokeMethod( part, "enableExitAfterPrint" ); if(!find.isEmpty()) QMetaObject::invokeMethod( part, "enableStartWithFind", Q_ARG(QString, find )); } void Shell::connectPart( QObject* part ) { // We're abusing the fact we know the part is our part here connect( this, SIGNAL(moveSplitter(int)), part, SLOT(moveSplitter(int)) ); // clazy:exclude=old-style-connect connect( part, SIGNAL(enablePrintAction(bool)), this, SLOT(setPrintEnabled(bool))); // clazy:exclude=old-style-connect connect( part, SIGNAL(enableCloseAction(bool)), this, SLOT(setCloseEnabled(bool))); // clazy:exclude=old-style-connect connect( part, SIGNAL(mimeTypeChanged(QMimeType)), this, SLOT(setTabIcon(QMimeType))); // clazy:exclude=old-style-connect connect( part, SIGNAL(urlsDropped(QList)), this, SLOT(handleDroppedUrls(QList)) ); // clazy:exclude=old-style-connect connect( part, SIGNAL(fitWindowToPage(QSize,QSize)), this, SLOT(slotFitWindowToPage(QSize,QSize)) ); // clazy:exclude=old-style-connect } void Shell::print() { QMetaObject::invokeMethod( m_tabs[m_tabWidget->currentIndex()].part, "slotPrint" ); } void Shell::setPrintEnabled( bool enabled ) { int i = findTabIndex( sender() ); if( i != -1 ) { m_tabs[i].printEnabled = enabled; if( i == m_tabWidget->currentIndex() ) m_printAction->setEnabled( enabled ); } } void Shell::setCloseEnabled( bool enabled ) { int i = findTabIndex( sender() ); if( i != -1 ) { m_tabs[i].closeEnabled = enabled; if( i == m_tabWidget->currentIndex() ) m_closeAction->setEnabled( enabled ); } } void Shell::activateNextTab() { if( m_tabs.size() < 2 ) return; const int activeTab = m_tabWidget->currentIndex(); const int nextTab = (activeTab == m_tabs.size()-1) ? 0 : activeTab+1; setActiveTab( nextTab ); } void Shell::activatePrevTab() { if( m_tabs.size() < 2 ) return; const int activeTab = m_tabWidget->currentIndex(); const int prevTab = (activeTab == 0) ? m_tabs.size()-1 : activeTab-1; setActiveTab( prevTab ); } void Shell::undoCloseTab() { if ( m_closedTabUrls.isEmpty() ) { return; } const QUrl lastTabUrl = m_closedTabUrls.takeLast(); if ( m_closedTabUrls.isEmpty() ) { m_undoCloseTab->setEnabled(false); } openUrl(lastTabUrl); } void Shell::setTabIcon( const QMimeType& mimeType ) { int i = findTabIndex( sender() ); if( i != -1 ) { m_tabWidget->setTabIcon( i, QIcon::fromTheme(mimeType.iconName()) ); } } int Shell::findTabIndex( QObject* sender ) { for( int i = 0; i < m_tabs.size(); ++i ) { if( m_tabs[i].part == sender ) { return i; } } return -1; } void Shell::handleDroppedUrls( const QList& urls ) { for ( const QUrl &url : urls ) { openUrl( url ); } } void Shell::moveTabData( int from, int to ) { m_tabs.move( from, to ); } -void Shell::slotFitWindowToPage(const QSize& pageViewSize, const QSize& pageSize ) +void Shell::slotFitWindowToPage(const QSize pageViewSize, const QSize pageSize ) { const int xOffset = pageViewSize.width() - pageSize.width(); const int yOffset = pageViewSize.height() - pageSize.height(); showNormal(); resize( width() - xOffset, height() - yOffset); emit moveSplitter(pageSize.width()); } /* kate: replace-tabs on; indent-width 4; */ diff --git a/shell/shell.h b/shell/shell.h index 10573345e..6b9016a64 100644 --- a/shell/shell.h +++ b/shell/shell.h @@ -1,186 +1,186 @@ /*************************************************************************** * Copyright (C) 2002 by Wilco Greven * * Copyright (C) 2003 by Benjamin Meyer * * Copyright (C) 2003 by Laurent Montel * * Copyright (C) 2003 by Luboš Luňák * * Copyright (C) 2004 by Christophe Devriese * * * * Copyright (C) 2004 by Albert Astals Cid * * * * 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. * ***************************************************************************/ #ifndef _OKULAR_SHELL_H_ #define _OKULAR_SHELL_H_ #include #include #include #include #include #include #include // krazy:exclude=includes class KRecentFilesAction; class KToggleAction; class QTabWidget; class KPluginFactory; #ifndef Q_OS_WIN namespace KActivities { class ResourceInstance; } #endif /** * This is the application "Shell". It has a menubar and a toolbar * but relies on the "Part" to do all the real work. * * @short Application Shell * @author Wilco Greven * @version 0.1 */ class Shell : public KParts::MainWindow { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.okular") friend class MainShellTest; public: /** * Constructor */ explicit Shell( const QString &serializedOptions = QString() ); /** * Default Destructor */ ~Shell() override; QSize sizeHint() const override; /** * Returns false if Okular component wasn't found **/ bool isValid() const; bool openDocument(const QUrl &url, const QString &serializedOptions); public Q_SLOTS: Q_SCRIPTABLE Q_NOREPLY void tryRaise(); Q_SCRIPTABLE bool openDocument(const QString &urlString, const QString &serializedOptions = QString() ); Q_SCRIPTABLE bool canOpenDocs( int numDocs, int desktop ); protected: /** * This method is called when it is time for the app to save its * properties for session management purposes. */ void saveProperties(KConfigGroup&) override; /** * This method is called when this app is restored. The KConfig * object points to the session management config file that was saved * with @ref saveProperties */ void readProperties(const KConfigGroup&) override; /** * Expose internal functions for session restore testing */ void savePropertiesInternal(KConfig* config, int num) {KMainWindow::savePropertiesInternal(config,num);} void readPropertiesInternal(KConfig* config, int num) {KMainWindow::readPropertiesInternal(config,num);} void readSettings(); void writeSettings(); void setFullScreen( bool ); using KParts::MainWindow::setCaption; void setCaption( const QString &caption ) override; bool queryClose() override; void showEvent(QShowEvent *event) override; private Q_SLOTS: void fileOpen(); void slotUpdateFullScreen(); void slotShowMenubar(); void openUrl( const QUrl & url, const QString &serializedOptions = QString() ); void showOpenRecentMenu(); void closeUrl(); void print(); void setPrintEnabled( bool enabled ); void setCloseEnabled( bool enabled ); void setTabIcon(const QMimeType& mimeType ); void handleDroppedUrls( const QList& urls ); // Tab event handlers void setActiveTab( int tab ); void closeTab( int tab ); void activateNextTab(); void activatePrevTab(); void undoCloseTab(); void moveTabData( int from, int to ); - void slotFitWindowToPage( const QSize& pageViewSize, const QSize& pageSize ); + void slotFitWindowToPage( const QSize pageViewSize, const QSize pageSize ); Q_SIGNALS: void moveSplitter(int sideWidgetSize); private: void setupAccel(); void setupActions(); QStringList fileFormats() const; void openNewTab( const QUrl& url, const QString &serializedOptions ); void applyOptionsToPart( QObject* part, const QString &serializedOptions ); void connectPart( QObject* part ); int findTabIndex( QObject* sender ); private: bool eventFilter(QObject *obj, QEvent *event) override; KPluginFactory* m_partFactory; KRecentFilesAction* m_recent; QStringList m_fileformats; bool m_fileformatsscanned; QAction* m_printAction; QAction* m_closeAction; KToggleAction* m_fullScreenAction; KToggleAction* m_showMenuBarAction; bool m_menuBarWasShown, m_toolBarWasShown; bool m_unique; QTabWidget* m_tabWidget; KToggleAction* m_openInTab; struct TabState { TabState( KParts::ReadWritePart* p ) : part(p), printEnabled(false), closeEnabled(false) {} KParts::ReadWritePart* part; bool printEnabled; bool closeEnabled; }; QList m_tabs; QList m_closedTabUrls; QAction* m_nextTabAction; QAction* m_prevTabAction; QAction* m_undoCloseTab; #ifndef Q_OS_WIN KActivities::ResourceInstance* m_activityResource; #endif bool m_isValid; }; #endif // vim:ts=2:sw=2:tw=78:et diff --git a/ui/annotationpopup.cpp b/ui/annotationpopup.cpp index e1a989e04..86c5acf54 100644 --- a/ui/annotationpopup.cpp +++ b/ui/annotationpopup.cpp @@ -1,188 +1,188 @@ /*************************************************************************** * Copyright (C) 2006 by Tobias Koenig * * * * 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. * ***************************************************************************/ #include "annotationpopup.h" #include #include #include #include "annotationpropertiesdialog.h" #include "core/annotations.h" #include "core/document.h" #include "guiutils.h" #include "okmenutitle.h" Q_DECLARE_METATYPE( AnnotationPopup::AnnotPagePair ) namespace { bool annotationHasFileAttachment( Okular::Annotation *annotation ) { return ( annotation->subType() == Okular::Annotation::AFileAttachment || annotation->subType() == Okular::Annotation::ARichMedia ); } Okular::EmbeddedFile* embeddedFileFromAnnotation( Okular::Annotation *annotation ) { if ( annotation->subType() == Okular::Annotation::AFileAttachment ) { const Okular::FileAttachmentAnnotation *fileAttachAnnot = static_cast( annotation ); return fileAttachAnnot->embeddedFile(); } else if ( annotation->subType() == Okular::Annotation::ARichMedia ) { const Okular::RichMediaAnnotation *richMediaAnnot = static_cast( annotation ); return richMediaAnnot->embeddedFile(); } else { return nullptr; } } } AnnotationPopup::AnnotationPopup( Okular::Document *document, MenuMode mode, QWidget *parent ) : mParent( parent ), mDocument( document ), mMenuMode( mode ) { } void AnnotationPopup::addAnnotation( Okular::Annotation* annotation, int pageNumber ) { AnnotPagePair pair( annotation, pageNumber ); if ( !mAnnotations.contains( pair ) ) mAnnotations.append( pair ); } -void AnnotationPopup::exec( const QPoint &point ) +void AnnotationPopup::exec( const QPoint point ) { if ( mAnnotations.isEmpty() ) return; QMenu menu( mParent ); QAction *action = nullptr; const char *actionTypeId = "actionType"; const QString openId = QStringLiteral( "open" ); const QString deleteId = QStringLiteral( "delete" ); const QString deleteAllId = QStringLiteral( "deleteAll" ); const QString propertiesId = QStringLiteral( "properties" ); const QString saveId = QStringLiteral( "save" ); if ( mMenuMode == SingleAnnotationMode ) { const bool onlyOne = (mAnnotations.count() == 1); const AnnotPagePair &pair = mAnnotations.at(0); menu.addAction( new OKMenuTitle( &menu, i18np( "Annotation", "%1 Annotations", mAnnotations.count() ) ) ); action = menu.addAction( QIcon::fromTheme( QStringLiteral("comment") ), i18n( "&Open Pop-up Note" ) ); action->setData( QVariant::fromValue( pair ) ); action->setEnabled( onlyOne ); action->setProperty( actionTypeId, openId ); action = menu.addAction( QIcon::fromTheme( QStringLiteral("list-remove") ), i18n( "&Delete" ) ); action->setEnabled( mDocument->isAllowed( Okular::AllowNotes ) ); action->setProperty( actionTypeId, deleteAllId ); for ( const AnnotPagePair &pair : qAsConst(mAnnotations) ) { if ( !mDocument->canRemovePageAnnotation( pair.annotation ) ) action->setEnabled( false ); } action = menu.addAction( QIcon::fromTheme( QStringLiteral("configure") ), i18n( "&Properties" ) ); action->setData( QVariant::fromValue( pair ) ); action->setEnabled( onlyOne ); action->setProperty( actionTypeId, propertiesId ); if ( onlyOne && annotationHasFileAttachment( pair.annotation ) ) { const Okular::EmbeddedFile *embeddedFile = embeddedFileFromAnnotation( pair.annotation ); if ( embeddedFile ) { const QString saveText = i18nc( "%1 is the name of the file to save", "&Save '%1'...", embeddedFile->name() ); menu.addSeparator(); action = menu.addAction( QIcon::fromTheme( QStringLiteral("document-save") ), saveText ); action->setData( QVariant::fromValue( pair ) ); action->setProperty( actionTypeId, saveId ); } } } else { for ( const AnnotPagePair &pair : qAsConst(mAnnotations) ) { menu.addAction( new OKMenuTitle( &menu, GuiUtils::captionForAnnotation( pair.annotation ) ) ); action = menu.addAction( QIcon::fromTheme( QStringLiteral("comment") ), i18n( "&Open Pop-up Note" ) ); action->setData( QVariant::fromValue( pair ) ); action->setProperty( actionTypeId, openId ); action = menu.addAction( QIcon::fromTheme( QStringLiteral("list-remove") ), i18n( "&Delete" ) ); action->setEnabled( mDocument->isAllowed( Okular::AllowNotes ) && mDocument->canRemovePageAnnotation( pair.annotation ) ); action->setData( QVariant::fromValue( pair ) ); action->setProperty( actionTypeId, deleteId ); action = menu.addAction( QIcon::fromTheme( QStringLiteral("configure") ), i18n( "&Properties" ) ); action->setData( QVariant::fromValue( pair ) ); action->setProperty( actionTypeId, propertiesId ); if ( annotationHasFileAttachment( pair.annotation ) ) { const Okular::EmbeddedFile *embeddedFile = embeddedFileFromAnnotation( pair.annotation ); if ( embeddedFile ) { const QString saveText = i18nc( "%1 is the name of the file to save", "&Save '%1'...", embeddedFile->name() ); menu.addSeparator(); action = menu.addAction( QIcon::fromTheme( QStringLiteral("document-save") ), saveText ); action->setData( QVariant::fromValue( pair ) ); action->setProperty( actionTypeId, saveId ); } } } } QAction *choice = menu.exec( point.isNull() ? QCursor::pos() : point ); // check if the user really selected an action if ( choice ) { const AnnotPagePair pair = choice->data().value(); const QString actionType = choice->property( actionTypeId ).toString(); if ( actionType == openId ) { emit openAnnotationWindow( pair.annotation, pair.pageNumber ); } else if( actionType == deleteId ) { if ( pair.pageNumber != -1 ) mDocument->removePageAnnotation( pair.pageNumber, pair.annotation ); } else if( actionType == deleteAllId ) { for ( const AnnotPagePair &pair : qAsConst(mAnnotations) ) { if ( pair.pageNumber != -1 ) mDocument->removePageAnnotation( pair.pageNumber, pair.annotation ); } } else if( actionType == propertiesId ) { if ( pair.pageNumber != -1 ) { AnnotsPropertiesDialog propdialog( mParent, mDocument, pair.pageNumber, pair.annotation ); propdialog.exec(); } } else if( actionType == saveId ) { Okular::EmbeddedFile *embeddedFile = embeddedFileFromAnnotation( pair.annotation ); GuiUtils::saveEmbeddedFile( embeddedFile, mParent ); } } } diff --git a/ui/annotationpopup.h b/ui/annotationpopup.h index fcb0d5077..47e6c2812 100644 --- a/ui/annotationpopup.h +++ b/ui/annotationpopup.h @@ -1,74 +1,70 @@ /*************************************************************************** * Copyright (C) 2006 by Tobias Koenig * * * * 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. * ***************************************************************************/ #ifndef ANNOTATIONPOPUP_H #define ANNOTATIONPOPUP_H #include #include #include #include namespace Okular { class Annotation; class Document; } class AnnotationPopup : public QObject { Q_OBJECT public: /** * Describes the structure of the popup menu. */ enum MenuMode { SingleAnnotationMode, ///< The menu shows only entries to manipulate a single annotation, or multiple annotations as a group. MultiAnnotationMode ///< The menu shows entries to manipulate multiple annotations. }; AnnotationPopup( Okular::Document *document, MenuMode mode, QWidget *parent = nullptr ); void addAnnotation( Okular::Annotation* annotation, int pageNumber ); - void exec( const QPoint &point = QPoint() ); + void exec( const QPoint point = QPoint() ); Q_SIGNALS: void openAnnotationWindow( Okular::Annotation *annotation, int pageNumber ); public: struct AnnotPagePair { AnnotPagePair() : annotation( nullptr ), pageNumber( -1 ) { } AnnotPagePair( Okular::Annotation *a, int pn ) : annotation( a ), pageNumber( pn ) { } - ~AnnotPagePair() = default; - AnnotPagePair( const AnnotPagePair & pair ) = default; - AnnotPagePair &operator=( const AnnotPagePair & pair ) = default; - - bool operator==( const AnnotPagePair & pair ) const + bool operator==( const AnnotPagePair pair ) const { return annotation == pair.annotation && pageNumber == pair.pageNumber; } Okular::Annotation* annotation; int pageNumber; }; private: QWidget *mParent; QList< AnnotPagePair > mAnnotations; Okular::Document *mDocument; MenuMode mMenuMode; }; #endif diff --git a/ui/annotwindow.cpp b/ui/annotwindow.cpp index ea5591b2c..b16fd86b9 100644 --- a/ui/annotwindow.cpp +++ b/ui/annotwindow.cpp @@ -1,444 +1,444 @@ /*************************************************************************** * Copyright (C) 2006 by Chu Xiaodong * * Copyright (C) 2006 by Pino Toscano * * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * * company, info@kdab.com. Work sponsored by the * * LiMux project of the city of Munich * * * * 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. * ***************************************************************************/ #include "annotwindow.h" // qt/kde includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // local includes #include "core/annotations.h" #include "core/document.h" #include "latexrenderer.h" #include #include class CloseButton : public QPushButton { Q_OBJECT public: CloseButton( QWidget * parent = Q_NULLPTR ) : QPushButton( parent ) { setSizePolicy( QSizePolicy::Fixed, QSizePolicy::Fixed ); QSize size = QSize( 14, 14 ).expandedTo( QApplication::globalStrut() ); setFixedSize( size ); setIcon( style()->standardIcon( QStyle::SP_DockWidgetCloseButton ) ); setIconSize( size ); setToolTip( i18n( "Close this note" ) ); setCursor( Qt::ArrowCursor ); } }; class MovableTitle : public QWidget { Q_OBJECT public: MovableTitle( AnnotWindow * parent ) : QWidget( parent ) { QVBoxLayout * mainlay = new QVBoxLayout( this ); mainlay->setContentsMargins( 0, 0, 0, 0 ); mainlay->setSpacing( 0 ); // close button row QHBoxLayout * buttonlay = new QHBoxLayout(); mainlay->addLayout( buttonlay ); titleLabel = new QLabel( this ); QFont f = titleLabel->font(); f.setBold( true ); titleLabel->setFont( f ); titleLabel->setCursor( Qt::SizeAllCursor ); buttonlay->addWidget( titleLabel ); dateLabel = new QLabel( this ); dateLabel->setAlignment( Qt::AlignTop | Qt::AlignRight ); f = dateLabel->font(); f.setPointSize( QFontInfo( f ).pointSize() - 2 ); dateLabel->setFont( f ); dateLabel->setCursor( Qt::SizeAllCursor ); buttonlay->addWidget( dateLabel ); CloseButton * close = new CloseButton( this ); connect( close, &QAbstractButton::clicked, parent, &QWidget::close ); buttonlay->addWidget( close ); // option button row QHBoxLayout * optionlay = new QHBoxLayout(); mainlay->addLayout( optionlay ); authorLabel = new QLabel( this ); authorLabel->setCursor( Qt::SizeAllCursor ); authorLabel->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Minimum ); optionlay->addWidget( authorLabel ); optionButton = new QToolButton( this ); QString opttext = i18n( "Options" ); optionButton->setText( opttext ); optionButton->setAutoRaise( true ); QSize s = QFontMetrics( optionButton->font() ).boundingRect( opttext ).size() + QSize( 8, 8 ); optionButton->setSizePolicy( QSizePolicy::Fixed, QSizePolicy::Fixed ); optionButton->setFixedSize( s ); optionlay->addWidget( optionButton ); // ### disabled for now optionButton->hide(); latexButton = new QToolButton( this ); QHBoxLayout * latexlay = new QHBoxLayout(); QString latextext = i18n ( "This annotation may contain LaTeX code.\nClick here to render." ); latexButton->setText( latextext ); latexButton->setAutoRaise( true ); s = QFontMetrics( latexButton->font() ).boundingRect(0, 0, this->width(), this->height(), 0, latextext ).size() + QSize( 8, 8 ); latexButton->setSizePolicy( QSizePolicy::Fixed, QSizePolicy::Fixed ); latexButton->setFixedSize( s ); latexButton->setCheckable( true ); latexButton->setVisible( false ); latexlay->addSpacing( 1 ); latexlay->addWidget( latexButton ); latexlay->addSpacing( 1 ); mainlay->addLayout( latexlay ); connect(latexButton, &QToolButton::clicked, parent, &AnnotWindow::renderLatex); connect(parent, &AnnotWindow::containsLatex, latexButton, &QWidget::setVisible); titleLabel->installEventFilter( this ); dateLabel->installEventFilter( this ); authorLabel->installEventFilter( this ); } bool eventFilter( QObject * obj, QEvent * e ) override { if ( obj != titleLabel && obj != authorLabel && obj != dateLabel ) return false; QMouseEvent * me = nullptr; switch ( e->type() ) { case QEvent::MouseButtonPress: me = (QMouseEvent*)e; mousePressPos = me->pos(); parentWidget()->raise(); break; case QEvent::MouseButtonRelease: mousePressPos = QPoint(); break; case QEvent::MouseMove: me = (QMouseEvent*)e; parentWidget()->move( me->pos() - mousePressPos + parentWidget()->pos() ); break; default: return false; } return true; } void setTitle( const QString& title ) { titleLabel->setText( QStringLiteral( " " ) + title ); } void setDate( const QDateTime& dt ) { dateLabel->setText( QLocale().toString( dt.toTimeSpec(Qt::LocalTime), QLocale::ShortFormat ) + QLatin1Char(' ') ); } void setAuthor( const QString& author ) { authorLabel->setText( QStringLiteral( " " ) + author ); } void connectOptionButton( QObject * recv, const char* method ) { connect( optionButton, SIGNAL(clicked()), recv, method ); } void uncheckLatexButton() { latexButton->setChecked( false ); } private: QLabel * titleLabel; QLabel * dateLabel; QLabel * authorLabel; QPoint mousePressPos; QToolButton * optionButton; QToolButton * latexButton; }; // Qt::SubWindow is needed to make QSizeGrip work AnnotWindow::AnnotWindow( QWidget * parent, Okular::Annotation * annot, Okular::Document *document, int page ) : QFrame( parent, Qt::SubWindow ), m_annot( annot ), m_document( document ), m_page( page ) { setAutoFillBackground( true ); setFrameStyle( Panel | Raised ); setAttribute( Qt::WA_DeleteOnClose ); setObjectName(QStringLiteral("AnnotWindow")); const bool canEditAnnotation = m_document->canModifyPageAnnotation( annot ); textEdit = new KTextEdit( this ); textEdit->setAcceptRichText( false ); textEdit->setPlainText( m_annot->contents() ); textEdit->installEventFilter( this ); textEdit->setUndoRedoEnabled( false ); m_prevCursorPos = textEdit->textCursor().position(); m_prevAnchorPos = textEdit->textCursor().anchor(); connect(textEdit, &KTextEdit::textChanged, this, &AnnotWindow::slotsaveWindowText); connect(textEdit, &KTextEdit::cursorPositionChanged, this, &AnnotWindow::slotsaveWindowText); connect(textEdit, &KTextEdit::aboutToShowContextMenu, this, &AnnotWindow::slotUpdateUndoAndRedoInContextMenu); connect(m_document, &Okular::Document::annotationContentsChangedByUndoRedo, this, &AnnotWindow::slotHandleContentsChangedByUndoRedo); if (!canEditAnnotation) textEdit->setReadOnly(true); QVBoxLayout * mainlay = new QVBoxLayout( this ); mainlay->setContentsMargins( 2, 2, 2, 2 ); mainlay->setSpacing( 0 ); m_title = new MovableTitle( this ); mainlay->addWidget( m_title ); mainlay->addWidget( textEdit ); QHBoxLayout * lowerlay = new QHBoxLayout(); mainlay->addLayout( lowerlay ); lowerlay->addItem( new QSpacerItem( 5, 5, QSizePolicy::Expanding, QSizePolicy::Fixed ) ); QSizeGrip * sb = new QSizeGrip( this ); lowerlay->addWidget( sb ); m_latexRenderer = new GuiUtils::LatexRenderer(); // The emit below is not wrong even if emitting signals from the constructor it's usually wrong // in this case the signal it's connected to inside MovableTitle constructor a few lines above emit containsLatex( GuiUtils::LatexRenderer::mightContainLatex( m_annot->contents() ) ); // clazy:exclude=incorrect-emit m_title->setTitle( m_annot->window().summary() ); m_title->connectOptionButton( this, SLOT(slotOptionBtn()) ); setGeometry(10,10,300,300 ); reloadInfo(); } AnnotWindow::~AnnotWindow() { delete m_latexRenderer; } Okular::Annotation * AnnotWindow::annotation() const { return m_annot; } void AnnotWindow::updateAnnotation( Okular::Annotation * a ) { m_annot = a; } void AnnotWindow::reloadInfo() { QColor newcolor; if ( m_annot->subType() == Okular::Annotation::AText ) { Okular::TextAnnotation * textAnn = static_cast< Okular::TextAnnotation * >( m_annot ); if ( textAnn->textType() == Okular::TextAnnotation::InPlace && textAnn->inplaceIntent() == Okular::TextAnnotation::TypeWriter ) newcolor = QColor(0xfd, 0xfd, 0x96); } if ( !newcolor.isValid() ) newcolor = m_annot->style().color().isValid() ? QColor(m_annot->style().color().red(), m_annot->style().color().green(), m_annot->style().color().blue(), 255) : Qt::yellow; if ( newcolor != m_color ) { m_color = newcolor; setPalette( QPalette( m_color ) ); QPalette pl = textEdit->palette(); pl.setColor( QPalette::Base, m_color ); textEdit->setPalette( pl ); } m_title->setAuthor( m_annot->author() ); m_title->setDate( m_annot->modificationDate() ); } int AnnotWindow::pageNumber() const { return m_page; } void AnnotWindow::showEvent( QShowEvent * event ) { QFrame::showEvent( event ); // focus the content area by default textEdit->setFocus(); } -bool AnnotWindow::eventFilter(QObject *, QEvent *e) +bool AnnotWindow::eventFilter(QObject *o, QEvent *e) { if ( e->type () == QEvent::ShortcutOverride ) { QKeyEvent * keyEvent = static_cast< QKeyEvent * >( e ); if ( keyEvent->key() == Qt::Key_Escape ) { close(); return true; } } else if (e->type() == QEvent::KeyPress) { QKeyEvent *keyEvent = static_cast(e); if (keyEvent == QKeySequence::Undo) { m_document->undo(); return true; } else if (keyEvent == QKeySequence::Redo) { m_document->redo(); return true; } } else if (e->type() == QEvent::FocusIn) { raise(); } - return false; + return QFrame::eventFilter(o, e); } void AnnotWindow::slotUpdateUndoAndRedoInContextMenu(QMenu* menu) { if (!menu) return; QList actionList = menu->actions(); enum { UndoAct, RedoAct, CutAct, CopyAct, PasteAct, ClearAct, SelectAllAct, NCountActs }; QAction *kundo = KStandardAction::create( KStandardAction::Undo, m_document, SLOT(undo()), menu); QAction *kredo = KStandardAction::create( KStandardAction::Redo, m_document, SLOT(redo()), menu); connect(m_document, &Okular::Document::canUndoChanged, kundo, &QAction::setEnabled); connect(m_document, &Okular::Document::canRedoChanged, kredo, &QAction::setEnabled); kundo->setEnabled(m_document->canUndo()); kredo->setEnabled(m_document->canRedo()); QAction *oldUndo, *oldRedo; oldUndo = actionList[UndoAct]; oldRedo = actionList[RedoAct]; menu->insertAction(oldUndo, kundo); menu->insertAction(oldRedo, kredo); menu->removeAction(oldUndo); menu->removeAction(oldRedo); } void AnnotWindow::slotOptionBtn() { //TODO: call context menu in pageview //emit sig... } void AnnotWindow::slotsaveWindowText() { const QString contents = textEdit->toPlainText(); const int cursorPos = textEdit->textCursor().position(); if (contents != m_annot->contents()) { m_document->editPageAnnotationContents( m_page, m_annot, contents, cursorPos, m_prevCursorPos, m_prevAnchorPos); emit containsLatex( GuiUtils::LatexRenderer::mightContainLatex( textEdit->toPlainText() ) ); } m_prevCursorPos = cursorPos; m_prevAnchorPos = textEdit->textCursor().anchor(); } void AnnotWindow::renderLatex( bool render ) { if (render) { textEdit->setReadOnly( true ); disconnect(textEdit, &KTextEdit::textChanged, this, &AnnotWindow::slotsaveWindowText); disconnect(textEdit, &KTextEdit::cursorPositionChanged, this, &AnnotWindow::slotsaveWindowText); textEdit->setAcceptRichText( true ); QString contents = m_annot->contents(); contents = Qt::convertFromPlainText( contents ); QColor fontColor = textEdit->textColor(); int fontSize = textEdit->fontPointSize(); QString latexOutput; GuiUtils::LatexRenderer::Error errorCode = m_latexRenderer->renderLatexInHtml( contents, fontColor, fontSize, Okular::Utils::realDpi(nullptr).width(), latexOutput ); switch ( errorCode ) { case GuiUtils::LatexRenderer::LatexNotFound: KMessageBox::sorry( this, i18n( "Cannot find latex executable." ), i18n( "LaTeX rendering failed" ) ); m_title->uncheckLatexButton(); renderLatex( false ); break; case GuiUtils::LatexRenderer::DvipngNotFound: KMessageBox::sorry( this, i18n( "Cannot find dvipng executable." ), i18n( "LaTeX rendering failed" ) ); m_title->uncheckLatexButton(); renderLatex( false ); break; case GuiUtils::LatexRenderer::LatexFailed: KMessageBox::detailedSorry( this, i18n( "A problem occurred during the execution of the 'latex' command." ), latexOutput, i18n( "LaTeX rendering failed" ) ); m_title->uncheckLatexButton(); renderLatex( false ); break; case GuiUtils::LatexRenderer::DvipngFailed: KMessageBox::sorry( this, i18n( "A problem occurred during the execution of the 'dvipng' command." ), i18n( "LaTeX rendering failed" ) ); m_title->uncheckLatexButton(); renderLatex( false ); break; case GuiUtils::LatexRenderer::NoError: default: textEdit->setHtml( contents ); break; } } else { textEdit->setAcceptRichText( false ); textEdit->setPlainText( m_annot->contents() ); connect(textEdit, &KTextEdit::textChanged, this, &AnnotWindow::slotsaveWindowText); connect(textEdit, &KTextEdit::cursorPositionChanged, this, &AnnotWindow::slotsaveWindowText); textEdit->setReadOnly( false ); } } void AnnotWindow::slotHandleContentsChangedByUndoRedo(Okular::Annotation* annot, const QString &contents, int cursorPos, int anchorPos) { if ( annot != m_annot ) { return; } textEdit->setPlainText(contents); QTextCursor c = textEdit->textCursor(); c.setPosition(anchorPos); c.setPosition(cursorPos,QTextCursor::KeepAnchor); m_prevCursorPos = cursorPos; m_prevAnchorPos = anchorPos; textEdit->setTextCursor(c); textEdit->setFocus(); emit containsLatex( GuiUtils::LatexRenderer::mightContainLatex( m_annot->contents() ) ); } #include "annotwindow.moc" diff --git a/ui/bookmarklist.cpp b/ui/bookmarklist.cpp index 00f38c73a..002fa7daf 100644 --- a/ui/bookmarklist.cpp +++ b/ui/bookmarklist.cpp @@ -1,472 +1,472 @@ /*************************************************************************** * Copyright (C) 2006 by Pino Toscano * * * * 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. * ***************************************************************************/ #include "bookmarklist.h" // qt/kde includes #include #include #include #include #include #include #include #include #include #include #include #include "pageitemdelegate.h" #include "core/action.h" #include "core/bookmarkmanager.h" #include "core/document.h" static const int BookmarkItemType = QTreeWidgetItem::UserType + 1; static const int FileItemType = QTreeWidgetItem::UserType + 2; static const int UrlRole = Qt::UserRole + 1; class BookmarkItem : public QTreeWidgetItem { public: BookmarkItem( const KBookmark& bm ) : QTreeWidgetItem( BookmarkItemType ), m_bookmark( bm ) { setFlags( Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable ); m_url = m_bookmark.url(); m_viewport = Okular::DocumentViewport( m_url.fragment(QUrl::FullyDecoded) ); m_url.setFragment( QString() ); setText( 0, m_bookmark.fullText() ); if ( m_viewport.isValid() ) setData( 0, PageItemDelegate::PageRole, QString::number( m_viewport.pageNumber + 1 ) ); } QVariant data( int column, int role ) const override { switch ( role ) { case Qt::ToolTipRole: return m_bookmark.fullText(); } return QTreeWidgetItem::data( column, role ); } bool operator<( const QTreeWidgetItem& other ) const override { if ( other.type() == BookmarkItemType ) { const BookmarkItem *cmp = static_cast< const BookmarkItem* >( &other ); return m_viewport < cmp->m_viewport; } return QTreeWidgetItem::operator<( other ); } KBookmark& bookmark() { return m_bookmark; } const Okular::DocumentViewport& viewport() const { return m_viewport; } QUrl url() const { return m_url; } private: KBookmark m_bookmark; QUrl m_url; Okular::DocumentViewport m_viewport; }; class FileItem : public QTreeWidgetItem { public: FileItem( const QUrl & url, QTreeWidget *tree, Okular::Document *document ) : QTreeWidgetItem( tree, FileItemType ) { setFlags( Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable ); const QString fileString = document->bookmarkManager()->titleForUrl( url ); setText( 0, fileString ); setData( 0, UrlRole, QVariant::fromValue( url ) ); } QVariant data( int column, int role ) const override { switch ( role ) { case Qt::ToolTipRole: return i18ncp( "%1 is the file name", "%1\n\nOne bookmark", "%1\n\n%2 bookmarks", text( 0 ), childCount() ); } return QTreeWidgetItem::data( column, role ); } }; BookmarkList::BookmarkList( Okular::Document *document, QWidget *parent ) : QWidget( parent ), m_document( document ), m_currentDocumentItem( nullptr ) { QVBoxLayout *mainlay = new QVBoxLayout( this ); mainlay->setContentsMargins( 0, 0, 0, 0 ); mainlay->setSpacing( 6 ); m_searchLine = new KTreeWidgetSearchLine( this ); mainlay->addWidget( m_searchLine ); m_searchLine->setPlaceholderText(i18n( "Search..." )); m_tree = new QTreeWidget( this ); mainlay->addWidget( m_tree ); QStringList cols; cols.append( QStringLiteral("Bookmarks") ); m_tree->setContextMenuPolicy( Qt::CustomContextMenu ); m_tree->setHeaderLabels( cols ); m_tree->setSortingEnabled( false ); m_tree->setRootIsDecorated( true ); m_tree->setAlternatingRowColors( true ); m_tree->setItemDelegate( new PageItemDelegate( m_tree ) ); m_tree->header()->hide(); m_tree->setSelectionBehavior( QAbstractItemView::SelectRows ); m_tree->setEditTriggers( QAbstractItemView::EditKeyPressed ); connect(m_tree, &QTreeWidget::itemActivated, this, &BookmarkList::slotExecuted); connect(m_tree, &QTreeWidget::customContextMenuRequested, this, &BookmarkList::slotContextMenu); m_searchLine->addTreeWidget( m_tree ); QToolBar * bookmarkController = new QToolBar( this ); mainlay->addWidget( bookmarkController ); bookmarkController->setObjectName( QStringLiteral( "BookmarkControlBar" ) ); // change toolbar appearance bookmarkController->setIconSize( QSize( 16, 16 ) ); bookmarkController->setMovable( false ); QSizePolicy sp = bookmarkController->sizePolicy(); sp.setVerticalPolicy( QSizePolicy::Minimum ); bookmarkController->setSizePolicy( sp ); // insert a togglebutton [show only bookmarks in the current document] m_showBoomarkOnlyAction = bookmarkController->addAction( QIcon::fromTheme( QStringLiteral("bookmarks") ), i18n( "Current document only" ) ); m_showBoomarkOnlyAction->setCheckable( true ); connect(m_showBoomarkOnlyAction, &QAction::toggled, this, &BookmarkList::slotFilterBookmarks); connect( m_document->bookmarkManager(), &Okular::BookmarkManager::bookmarksChanged, this, &BookmarkList::slotBookmarksChanged ); rebuildTree( m_showBoomarkOnlyAction->isChecked() ); } BookmarkList::~BookmarkList() { m_document->removeObserver( this ); } void BookmarkList::notifySetup( const QVector< Okular::Page * > & pages, int setupFlags ) { Q_UNUSED( pages ); if ( !( setupFlags & Okular::DocumentObserver::UrlChanged ) ) return; // clear contents m_searchLine->clear(); if ( m_showBoomarkOnlyAction->isChecked() ) { rebuildTree( m_showBoomarkOnlyAction->isChecked() ); } else { disconnect(m_tree, &QTreeWidget::itemChanged, this, &BookmarkList::slotChanged); if ( m_currentDocumentItem && m_currentDocumentItem != m_tree->invisibleRootItem() ) { m_currentDocumentItem->setIcon( 0, QIcon() ); } m_currentDocumentItem = itemForUrl( m_document->currentDocument() ); if ( m_currentDocumentItem && m_currentDocumentItem != m_tree->invisibleRootItem() ) { m_currentDocumentItem->setIcon( 0, QIcon::fromTheme( QStringLiteral("bookmarks") ) ); m_currentDocumentItem->setExpanded( true ); } connect(m_tree, &QTreeWidget::itemChanged, this, &BookmarkList::slotChanged); } } void BookmarkList::slotFilterBookmarks( bool on ) { rebuildTree( on ); } void BookmarkList::slotExecuted( QTreeWidgetItem * item ) { BookmarkItem* bmItem = dynamic_cast( item ); if ( !bmItem || !bmItem->viewport().isValid() ) return; goTo( bmItem ); } void BookmarkList::slotChanged( QTreeWidgetItem * item ) { BookmarkItem* bmItem = dynamic_cast( item ); if ( bmItem && bmItem->viewport().isValid() ) { bmItem->bookmark().setFullText( bmItem->text( 0 ) ); m_document->bookmarkManager()->save(); } FileItem* fItem = dynamic_cast( item ); if ( fItem ) { const QUrl url = fItem->data( 0, UrlRole ).value< QUrl >(); m_document->bookmarkManager()->renameBookmark( url, fItem->text( 0 ) ); m_document->bookmarkManager()->save(); } } -void BookmarkList::slotContextMenu( const QPoint& p ) +void BookmarkList::slotContextMenu( const QPoint p ) { QTreeWidgetItem * item = m_tree->itemAt( p ); BookmarkItem* bmItem = item ? dynamic_cast( item ) : nullptr; if ( bmItem ) contextMenuForBookmarkItem( p, bmItem ); else if ( FileItem* fItem = dynamic_cast< FileItem * >( item ) ) contextMenuForFileItem( p, fItem ); } -void BookmarkList::contextMenuForBookmarkItem( const QPoint& p, BookmarkItem* bmItem ) +void BookmarkList::contextMenuForBookmarkItem( const QPoint p, BookmarkItem* bmItem ) { Q_UNUSED( p ); if ( !bmItem || !bmItem->viewport().isValid() ) return; QMenu menu( this ); QAction * gotobm = menu.addAction( i18n( "Go to This Bookmark" ) ); QAction * editbm = menu.addAction( QIcon::fromTheme( QStringLiteral("edit-rename") ), i18n( "Rename Bookmark" ) ); QAction * removebm = menu.addAction( QIcon::fromTheme( QStringLiteral("list-remove") ), i18n( "Remove Bookmark" ) ); QAction * res = menu.exec( QCursor::pos() ); if ( !res ) return; if ( res == gotobm ) goTo( bmItem ); else if ( res == editbm ) m_tree->editItem( bmItem, 0 ); else if ( res == removebm ) m_document->bookmarkManager()->removeBookmark( bmItem->url(), bmItem->bookmark() ); } -void BookmarkList::contextMenuForFileItem( const QPoint& p, FileItem* fItem ) +void BookmarkList::contextMenuForFileItem( const QPoint p, FileItem* fItem ) { Q_UNUSED( p ); if ( !fItem ) return; const QUrl itemurl = fItem->data( 0, UrlRole ).value< QUrl >(); const bool thisdoc = itemurl == m_document->currentDocument(); QMenu menu( this ); QAction * open = nullptr; if ( !thisdoc ) open = menu.addAction( i18nc( "Opens the selected document", "Open Document" ) ); QAction * editbm = menu.addAction( QIcon::fromTheme( QStringLiteral("edit-rename") ), i18n( "Rename Bookmark" ) ); QAction * removebm = menu.addAction( QIcon::fromTheme( QStringLiteral("list-remove") ), i18n( "Remove Bookmarks" ) ); QAction * res = menu.exec( QCursor::pos() ); if ( !res ) return; if ( res == open ) { Okular::GotoAction action( itemurl.toDisplayString(QUrl::PreferLocalFile), Okular::DocumentViewport() ); m_document->processAction( &action ); } else if ( res == editbm ) m_tree->editItem( fItem, 0 ); else if ( res == removebm ) { KBookmark::List list; for ( int i = 0; i < fItem->childCount(); ++i ) { list.append( static_cast( fItem->child( i ) )->bookmark() ); } m_document->bookmarkManager()->removeBookmarks( itemurl, list ); } } void BookmarkList::slotBookmarksChanged(const QUrl &url ) { // special case here, as m_currentDocumentItem could represent // the invisible root item if ( url == m_document->currentDocument() ) { selectiveUrlUpdate( m_document->currentDocument(), m_currentDocumentItem ); return; } // we are showing the bookmarks for the current document only if ( m_showBoomarkOnlyAction->isChecked() ) return; QTreeWidgetItem *item = itemForUrl( url ); selectiveUrlUpdate( url, item ); } QList createItems( const QUrl& baseurl, const KBookmark::List& bmlist ) { Q_UNUSED(baseurl) QList ret; for ( const KBookmark &bm : bmlist ) { // qCDebug(OkularUiDebug).nospace() << "checking '" << tmp << "'"; // qCDebug(OkularUiDebug).nospace() << " vs '" << baseurl << "'"; // TODO check that bm and baseurl are the same (#ref excluded) QTreeWidgetItem * item = new BookmarkItem( bm ); ret.append( item ); } return ret; } void BookmarkList::rebuildTree( bool filter ) { // disconnect and reconnect later, otherwise we'll get many itemChanged() // signals for all the current items disconnect(m_tree, &QTreeWidget::itemChanged, this, &BookmarkList::slotChanged); m_currentDocumentItem = nullptr; m_tree->clear(); const QList urls = m_document->bookmarkManager()->files(); if ( filter ) { if ( m_document->isOpened() ) { for ( const QUrl &url : urls ) { if ( url == m_document->currentDocument() ) { m_tree->addTopLevelItems( createItems( url, m_document->bookmarkManager()->bookmarks( url ) ) ); m_currentDocumentItem = m_tree->invisibleRootItem(); break; } } } } else { QTreeWidgetItem * currenturlitem = nullptr; for ( const QUrl &url : urls ) { QList subitems = createItems( url, m_document->bookmarkManager()->bookmarks( url ) ); if ( !subitems.isEmpty() ) { FileItem * item = new FileItem( url, m_tree, m_document ); item->addChildren( subitems ); if ( !currenturlitem && url == m_document->currentDocument() ) { currenturlitem = item; } } } if ( currenturlitem ) { currenturlitem->setExpanded( true ); currenturlitem->setIcon( 0, QIcon::fromTheme( QStringLiteral("bookmarks") ) ); m_tree->scrollToItem( currenturlitem, QAbstractItemView::PositionAtTop ); m_currentDocumentItem = currenturlitem; } } m_tree->sortItems( 0, Qt::AscendingOrder ); connect(m_tree, &QTreeWidget::itemChanged, this, &BookmarkList::slotChanged); } void BookmarkList::goTo( BookmarkItem * item ) { if ( item->url() == m_document->currentDocument() ) { m_document->setViewport( item->viewport(), nullptr, true ); } else { Okular::GotoAction action( item->url().toDisplayString(QUrl::PreferLocalFile), item->viewport() ); m_document->processAction( &action ); } } void BookmarkList::selectiveUrlUpdate( const QUrl& url, QTreeWidgetItem*& item ) { disconnect(m_tree, &QTreeWidget::itemChanged, this, &BookmarkList::slotChanged); const KBookmark::List urlbookmarks = m_document->bookmarkManager()->bookmarks( url ); if ( urlbookmarks.isEmpty() ) { if ( item != m_tree->invisibleRootItem() ) { m_tree->invisibleRootItem()->removeChild( item ); item = nullptr; } else if ( item ) { for ( int i = item->childCount(); i >= 0; --i ) { item->removeChild( item->child( i ) ); } } } else { bool fileitem_created = false; if ( item ) { for ( int i = item->childCount() - 1; i >= 0; --i ) { item->removeChild( item->child( i ) ); } } else { item = new FileItem( url, m_tree, m_document ); fileitem_created = true; } if ( m_document->isOpened() && url == m_document->currentDocument() ) { item->setIcon( 0, QIcon::fromTheme( QStringLiteral("bookmarks") ) ); item->setExpanded( true ); } item->addChildren( createItems( url, urlbookmarks ) ); if ( fileitem_created ) { // we need to sort also the parent of the new file item, // so it can be properly shown in the correct place m_tree->invisibleRootItem()->sortChildren( 0, Qt::AscendingOrder ); } item->sortChildren( 0, Qt::AscendingOrder ); } connect(m_tree, &QTreeWidget::itemChanged, this, &BookmarkList::slotChanged); } QTreeWidgetItem* BookmarkList::itemForUrl( const QUrl& url ) const { const int count = m_tree->topLevelItemCount(); for ( int i = 0; i < count; ++i ) { QTreeWidgetItem *item = m_tree->topLevelItem( i ); const QUrl itemurl = item->data( 0, UrlRole ).value< QUrl >(); if ( itemurl.isValid() && itemurl == url ) { return item; } } return nullptr; } #include "moc_bookmarklist.cpp" diff --git a/ui/bookmarklist.h b/ui/bookmarklist.h index 728d5031b..2c931fdeb 100644 --- a/ui/bookmarklist.h +++ b/ui/bookmarklist.h @@ -1,62 +1,62 @@ /*************************************************************************** * Copyright (C) 2006 by Pino Toscano * * * * 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. * ***************************************************************************/ #ifndef BOOKMARKLIST_H #define BOOKMARKLIST_H #include #include "core/observer.h" class QAction; class QTreeWidget; class QTreeWidgetItem; class KTreeWidgetSearchLine; class QUrl; class BookmarkItem; class FileItem; namespace Okular { class Document; } class BookmarkList : public QWidget, public Okular::DocumentObserver { Q_OBJECT public: explicit BookmarkList( Okular::Document *document, QWidget *parent = nullptr ); ~BookmarkList() override; // inherited from DocumentObserver void notifySetup( const QVector< Okular::Page * > & pages, int setupFlags ) override; private Q_SLOTS: void slotFilterBookmarks( bool ); void slotExecuted( QTreeWidgetItem * item ); void slotChanged( QTreeWidgetItem * item ); - void slotContextMenu( const QPoint& p ); + void slotContextMenu( const QPoint p ); void slotBookmarksChanged( const QUrl& url ); private: void rebuildTree( bool filter ); void goTo( BookmarkItem * item ); void selectiveUrlUpdate( const QUrl& url, QTreeWidgetItem*& item ); QTreeWidgetItem* itemForUrl(const QUrl &url ) const; - void contextMenuForBookmarkItem( const QPoint& p, BookmarkItem* bmItem ); - void contextMenuForFileItem( const QPoint& p, FileItem* fItem ); + void contextMenuForBookmarkItem( const QPoint p, BookmarkItem* bmItem ); + void contextMenuForFileItem( const QPoint p, FileItem* fItem ); Okular::Document * m_document; QTreeWidget * m_tree; KTreeWidgetSearchLine * m_searchLine; QAction * m_showBoomarkOnlyAction; QTreeWidgetItem * m_currentDocumentItem; }; #endif diff --git a/ui/embeddedfilesdialog.cpp b/ui/embeddedfilesdialog.cpp index 2f071d08c..0583ec886 100644 --- a/ui/embeddedfilesdialog.cpp +++ b/ui/embeddedfilesdialog.cpp @@ -1,206 +1,206 @@ /*************************************************************************** * Copyright (C) 2006 by Albert Astals Cid * * * * 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. * ***************************************************************************/ #include "embeddedfilesdialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "core/document.h" #include "guiutils.h" Q_DECLARE_METATYPE( Okular::EmbeddedFile* ) static const int EmbeddedFileRole = Qt::UserRole + 100; static QString dateToString( const QDateTime & date ) { return date.isValid() ? QLocale().toString( date, QLocale::LongFormat ) : i18nc( "Unknown date", "Unknown" ); } EmbeddedFilesDialog::EmbeddedFilesDialog(QWidget *parent, const Okular::Document *document) : QDialog(parent) { setWindowTitle(i18nc("@title:window", "Embedded Files")); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Close); QVBoxLayout *mainLayout = new QVBoxLayout; setLayout(mainLayout); mUser1Button = new QPushButton; buttonBox->addButton(mUser1Button, QDialogButtonBox::ActionRole); connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); buttonBox->button(QDialogButtonBox::Close)->setDefault(true); KGuiItem::assign(mUser1Button, KStandardGuiItem::save()); mUser1Button->setEnabled(false); mUser2Button = new QPushButton; buttonBox->addButton(mUser2Button, QDialogButtonBox::ActionRole); KGuiItem::assign(mUser2Button, KGuiItem(i18nc("@action:button", "View"), QStringLiteral("document-open"))); mUser2Button->setEnabled(false); m_tw = new QTreeWidget(this); mainLayout->addWidget(m_tw); mainLayout->addWidget(buttonBox); QStringList header; header.append(i18nc("@title:column", "Name")); header.append(i18nc("@title:column", "Description")); header.append(i18nc("@title:column", "Size")); header.append(i18nc("@title:column", "Created")); header.append(i18nc("@title:column", "Modified")); m_tw->setHeaderLabels(header); m_tw->setRootIsDecorated(false); m_tw->setSelectionMode(QAbstractItemView::ExtendedSelection); m_tw->setContextMenuPolicy(Qt::CustomContextMenu); // embeddedFiles() returns a const QList for (Okular::EmbeddedFile *ef : *document->embeddedFiles()) { QTreeWidgetItem *twi = new QTreeWidgetItem(); twi->setText(0, ef->name()); QMimeDatabase db; QMimeType mime = db.mimeTypeForFile( ef->name(), QMimeDatabase::MatchExtension); if (mime.isValid()) { twi->setIcon(0, QIcon::fromTheme(mime.iconName())); } twi->setText(1, ef->description()); twi->setText(2, ef->size() <= 0 ? i18nc("Not available size", "N/A") : KFormat().formatByteSize(ef->size())); twi->setText(3, dateToString( ef->creationDate() ) ); twi->setText(4, dateToString( ef->modificationDate() ) ); twi->setData( 0, EmbeddedFileRole, QVariant::fromValue( ef ) ); m_tw->addTopLevelItem(twi); } // Having filled the columns, it is nice to resize them to be able to read the contents for (int lv = 0; lv < m_tw->columnCount(); ++lv) { m_tw->resizeColumnToContents(lv); } // This is a bit dubious, but I'm not seeing a nice way to say "expand to fit contents" m_tw->setMinimumWidth(640); m_tw->updateGeometry(); connect(mUser1Button, &QPushButton::clicked, this, &EmbeddedFilesDialog::saveFileFromButton); connect(mUser2Button, &QPushButton::clicked, this, &EmbeddedFilesDialog::viewFileFromButton); connect(m_tw, &QWidget::customContextMenuRequested, this, &EmbeddedFilesDialog::attachViewContextMenu); connect(m_tw, &QTreeWidget::itemSelectionChanged, this, &EmbeddedFilesDialog::updateSaveButton); connect(m_tw, &QTreeWidget::itemDoubleClicked, this, &EmbeddedFilesDialog::viewFileItem); } void EmbeddedFilesDialog::updateSaveButton() { bool enable = (m_tw->selectedItems().count() > 0); mUser1Button->setEnabled(enable); mUser2Button->setEnabled(enable); } void EmbeddedFilesDialog::saveFileFromButton() { const QList selected = m_tw->selectedItems(); for (const QTreeWidgetItem *twi : selected) { Okular::EmbeddedFile* ef = qvariant_cast< Okular::EmbeddedFile* >( twi->data( 0, EmbeddedFileRole ) ); saveFile(ef); } } void EmbeddedFilesDialog::viewFileFromButton() { const QList selected = m_tw->selectedItems(); for (QTreeWidgetItem *twi : selected) { Okular::EmbeddedFile* ef = qvariant_cast< Okular::EmbeddedFile* >( twi->data( 0, EmbeddedFileRole ) ); viewFile( ef ); } } void EmbeddedFilesDialog::viewFileItem( QTreeWidgetItem* item, int /*column*/ ) { Okular::EmbeddedFile* ef = qvariant_cast< Okular::EmbeddedFile* >( item->data( 0, EmbeddedFileRole ) ); viewFile( ef ); } -void EmbeddedFilesDialog::attachViewContextMenu( const QPoint& /*pos*/ ) +void EmbeddedFilesDialog::attachViewContextMenu() { QList selected = m_tw->selectedItems(); if ( selected.isEmpty() ) return; if ( selected.size() > 1 ) return; QMenu menu( this ); QAction* saveAsAct = menu.addAction( QIcon::fromTheme( QStringLiteral("document-save-as") ), i18nc( "@action:inmenu", "&Save As..." ) ); QAction* viewAct = menu.addAction( QIcon::fromTheme( QStringLiteral("document-open" ) ), i18nc( "@action:inmenu", "&View..." ) ); QAction* act = menu.exec( QCursor::pos() ); if ( !act ) return; Okular::EmbeddedFile* ef = qvariant_cast< Okular::EmbeddedFile* >( selected.at( 0 )->data( 0, EmbeddedFileRole ) ); if ( act == saveAsAct ) { saveFile( ef ); } else if ( act == viewAct ) { viewFile( ef ); } } void EmbeddedFilesDialog::viewFile( Okular::EmbeddedFile* ef ) { // get name and extension QFileInfo fileInfo(ef->name()); // save in temporary directory with a unique name resembling the attachment name, // using QTemporaryFile's XXXXXX placeholder QTemporaryFile *tmpFile = new QTemporaryFile( QDir::tempPath() + QDir::separator() + fileInfo.baseName() + ".XXXXXX" + (fileInfo.completeSuffix().isEmpty() ? QLatin1String("") : QString("." + fileInfo.completeSuffix())) ); GuiUtils::writeEmbeddedFile( ef, this, *tmpFile ); // set readonly to prevent the viewer application from modifying it tmpFile->setPermissions( QFile::ReadOwner ); // keep temporary file alive while the dialog is open m_openedFiles.push_back( QSharedPointer< QTemporaryFile >( tmpFile ) ); // view the temporary file with the default application new KRun( QUrl( "file://" + tmpFile->fileName() ), this ); } void EmbeddedFilesDialog::saveFile( Okular::EmbeddedFile* ef ) { GuiUtils::saveEmbeddedFile( ef, this ); } #include "moc_embeddedfilesdialog.cpp" diff --git a/ui/embeddedfilesdialog.h b/ui/embeddedfilesdialog.h index ee059f34e..0b35e4317 100644 --- a/ui/embeddedfilesdialog.h +++ b/ui/embeddedfilesdialog.h @@ -1,50 +1,50 @@ /*************************************************************************** * Copyright (C) 2006 by Albert Astals Cid * * * * 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. * ***************************************************************************/ #ifndef _EMBEDDEDFILESDIALOG_H_ #define _EMBEDDEDFILESDIALOG_H_ #include class QTreeWidget; class QPushButton; class QTemporaryFile; class QTreeWidgetItem; namespace Okular { class Document; class EmbeddedFile; } class EmbeddedFilesDialog : public QDialog { Q_OBJECT public: EmbeddedFilesDialog(QWidget *parent, const Okular::Document *document); private Q_SLOTS: void saveFileFromButton(); - void attachViewContextMenu( const QPoint& pos ); + void attachViewContextMenu(); void updateSaveButton(); void viewFileFromButton(); void viewFileItem( QTreeWidgetItem* item, int column ); private: void saveFile( Okular::EmbeddedFile* ); void viewFile( Okular::EmbeddedFile* ); QTreeWidget *m_tw; QPushButton *mUser1Button; QPushButton *mUser2Button; QList< QSharedPointer > m_openedFiles; }; #endif diff --git a/ui/okmenutitle.cpp b/ui/okmenutitle.cpp index cc44a5480..f316d5be2 100644 --- a/ui/okmenutitle.cpp +++ b/ui/okmenutitle.cpp @@ -1,65 +1,65 @@ /* This file was part of the KDE libraries (copied partially from kmenu.cpp) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "okmenutitle.h" #include #include #include #include #include OKMenuTitle::OKMenuTitle(QMenu *menu, const QString &text, const QIcon &icon) : QWidgetAction(menu) { QAction *buttonAction = new QAction(menu); QFont font = buttonAction->font(); font.setBold(true); buttonAction->setFont(font); buttonAction->setText(text); buttonAction->setIcon(icon); QToolButton *titleButton = new QToolButton(menu); titleButton->installEventFilter(this); // prevent clicks on the title of the menu titleButton->setDefaultAction(buttonAction); titleButton->setDown(true); // prevent hover style changes in some styles titleButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); setDefaultWidget(titleButton); } bool OKMenuTitle::eventFilter(QObject *object, QEvent *event) { Q_UNUSED(object); if (event->type() == QEvent::Paint) { - return false; + return QWidgetAction::eventFilter(object, event); } else if (event->type() == QEvent::KeyRelease) { // If we're receiving the key release event is because we just gained // focus though a key event, use the same key to move it to the next action if (static_cast(parentWidget())->activeAction() == this) { QKeyEvent *ke = static_cast(event); QKeyEvent newKe(QEvent::KeyPress, ke->key(), ke->modifiers(), ke->text(), ke->isAutoRepeat(), ke->count()); QApplication::sendEvent(parentWidget(), &newKe); } // TODO What happens when there's multiple OKMenuTitle or only OKMenuTitle in a menu } event->accept(); return true; } diff --git a/ui/pagepainter.cpp b/ui/pagepainter.cpp index 9fb50ff8a..e016665a7 100644 --- a/ui/pagepainter.cpp +++ b/ui/pagepainter.cpp @@ -1,1226 +1,1226 @@ /*************************************************************************** * Copyright (C) 2005 by Enrico Ros * * * * 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. * ***************************************************************************/ #include "pagepainter.h" // qt / kde includes #include #include #include #include #include #include #include #include #include #include // system includes #include // local includes #include "core/area.h" #include "core/page.h" #include "core/page_p.h" #include "core/annotations.h" #include "core/utils.h" #include "guiutils.h" #include "settings.h" #include "core/observer.h" #include "core/tile.h" #include "settings_core.h" #include "ui/debug_ui.h" Q_GLOBAL_STATIC_WITH_ARGS( QPixmap, busyPixmap, ( KIconLoader::global()->loadIcon(QLatin1String("okular"), KIconLoader::NoGroup, IconSize(KIconLoader::Desktop), KIconLoader::DefaultState, QStringList(), 0, true) ) ) #define TEXTANNOTATION_ICONSIZE 24 inline QPen buildPen( const Okular::Annotation *ann, double width, const QColor &color ) { QPen p( QBrush( color ), width, ann->style().lineStyle() == Okular::Annotation::Dashed ? Qt::DashLine : Qt::SolidLine, Qt::SquareCap, Qt::MiterJoin ); return p; } void PagePainter::paintPageOnPainter( QPainter * destPainter, const Okular::Page * page, - Okular::DocumentObserver *observer, int flags, int scaledWidth, int scaledHeight, const QRect &limits ) + Okular::DocumentObserver *observer, int flags, int scaledWidth, int scaledHeight, const QRect limits ) { paintCroppedPageOnPainter( destPainter, page, observer, flags, scaledWidth, scaledHeight, limits, Okular::NormalizedRect( 0, 0, 1, 1 ), nullptr ); } void PagePainter::paintCroppedPageOnPainter( QPainter * destPainter, const Okular::Page * page, - Okular::DocumentObserver *observer, int flags, int scaledWidth, int scaledHeight, const QRect &limits, + Okular::DocumentObserver *observer, int flags, int scaledWidth, int scaledHeight, const QRect limits, const Okular::NormalizedRect &crop, Okular::NormalizedPoint *viewPortPoint ) { qreal dpr = destPainter->device()->devicePixelRatioF(); /* Calculate the cropped geometry of the page */ QRect scaledCrop = crop.geometry( scaledWidth, scaledHeight ); /* variables prefixed with d are in the device pixels coordinate system, which translates to the rendered output - that means, * multiplied with the device pixel ratio of the target PaintDevice */ const QRect dScaledCrop(QRectF(scaledCrop.x() * dpr, scaledCrop.y() * dpr, scaledCrop.width() * dpr, scaledCrop.height() * dpr).toAlignedRect()); int croppedWidth = scaledCrop.width(); int croppedHeight = scaledCrop.height(); int dScaledWidth = ceil(scaledWidth * dpr); int dScaledHeight = ceil(scaledHeight * dpr); const QRect dLimits(QRectF(limits.x() * dpr, limits.y() * dpr, limits.width() * dpr, limits.height() * dpr).toAlignedRect()); QColor paperColor = Qt::white; QColor backgroundColor = paperColor; if ( Okular::SettingsCore::changeColors() ) { switch ( Okular::SettingsCore::renderMode() ) { case Okular::SettingsCore::EnumRenderMode::Inverted: backgroundColor = Qt::black; break; case Okular::SettingsCore::EnumRenderMode::Paper: paperColor = Okular::SettingsCore::paperColor(); backgroundColor = paperColor; break; case Okular::SettingsCore::EnumRenderMode::Recolor: backgroundColor = Okular::Settings::recolorBackground(); break; default: ; } } destPainter->fillRect( limits, backgroundColor ); const bool hasTilesManager = page->hasTilesManager( observer ); QPixmap pixmap; if ( !hasTilesManager ) { /** 1 - RETRIEVE THE 'PAGE+ID' PIXMAP OR A SIMILAR 'PAGE' ONE **/ const QPixmap *p = page->_o_nearestPixmap( observer, dScaledWidth, dScaledHeight ); if (p != nullptr) { pixmap = *p; pixmap.setDevicePixelRatio( qApp->devicePixelRatio() ); } /** 1B - IF NO PIXMAP, DRAW EMPTY PAGE **/ double pixmapRescaleRatio = !pixmap.isNull() ? dScaledWidth / (double)pixmap.width() : -1; long pixmapPixels = !pixmap.isNull() ? (long)pixmap.width() * (long)pixmap.height() : 0; if ( pixmap.isNull() || pixmapRescaleRatio > 20.0 || pixmapRescaleRatio < 0.25 || (dScaledWidth > pixmap.width() && pixmapPixels > 60000000L) ) { // draw something on the blank page: the okular icon or a cross (as a fallback) if ( !busyPixmap()->isNull() ) { busyPixmap->setDevicePixelRatio(dpr); destPainter->drawPixmap( QPoint( 10, 10 ), *busyPixmap() ); } else { destPainter->setPen( Qt::gray ); destPainter->drawLine( 0, 0, croppedWidth-1, croppedHeight-1 ); destPainter->drawLine( 0, croppedHeight-1, croppedWidth-1, 0 ); } return; } } /** 2 - FIND OUT WHAT TO PAINT (Flags + Configuration + Presence) **/ bool canDrawHighlights = (flags & Highlights) && !page->m_highlights.isEmpty(); bool canDrawTextSelection = (flags & TextSelection) && page->textSelection(); bool canDrawAnnotations = (flags & Annotations) && !page->m_annotations.isEmpty(); bool enhanceLinks = (flags & EnhanceLinks) && Okular::Settings::highlightLinks(); bool enhanceImages = (flags & EnhanceImages) && Okular::Settings::highlightImages(); // vectors containing objects to draw // make this a qcolor, rect map, since we don't need // to know s_id here! we are only drawing this right? QList< QPair > * bufferedHighlights = nullptr; QList< Okular::Annotation * > * bufferedAnnotations = nullptr; QList< Okular::Annotation * > * unbufferedAnnotations = nullptr; Okular::Annotation *boundingRectOnlyAnn = nullptr; // Paint the bounding rect of this annotation // fill up lists with visible annotation/highlight objects/text selections if ( canDrawHighlights || canDrawTextSelection || canDrawAnnotations ) { // precalc normalized 'limits rect' for intersection double nXMin = ( (double)limits.left() / scaledWidth ) + crop.left, nXMax = ( (double)limits.right() / scaledWidth ) + crop.left, nYMin = ( (double)limits.top() / scaledHeight ) + crop.top, nYMax = ( (double)limits.bottom() / scaledHeight ) + crop.top; // append all highlights inside limits to their list if ( canDrawHighlights ) { if ( !bufferedHighlights ) bufferedHighlights = new QList< QPair >(); /* else {*/ Okular::NormalizedRect* limitRect = new Okular::NormalizedRect(nXMin, nYMin, nXMax, nYMax ); QLinkedList< Okular::HighlightAreaRect * >::const_iterator h2It = page->m_highlights.constBegin(), hEnd = page->m_highlights.constEnd(); Okular::HighlightAreaRect::const_iterator hIt; for ( ; h2It != hEnd; ++h2It ) for (hIt=(*h2It)->constBegin(); hIt!=(*h2It)->constEnd(); ++hIt) { if ((*hIt).intersects(limitRect)) bufferedHighlights->append( qMakePair((*h2It)->color,*hIt) ); } delete limitRect; //} } if ( canDrawTextSelection ) { if ( !bufferedHighlights ) bufferedHighlights = new QList< QPair >(); /* else {*/ Okular::NormalizedRect* limitRect = new Okular::NormalizedRect(nXMin, nYMin, nXMax, nYMax ); const Okular::RegularAreaRect *textSelection = page->textSelection(); Okular::HighlightAreaRect::const_iterator hIt = textSelection->constBegin(), hEnd = textSelection->constEnd(); for ( ; hIt != hEnd; ++hIt ) { if ( (*hIt).intersects( limitRect ) ) bufferedHighlights->append( qMakePair( page->textSelectionColor(), *hIt ) ); } delete limitRect; //} } // append annotations inside limits to the un/buffered list if ( canDrawAnnotations ) { QLinkedList< Okular::Annotation * >::const_iterator aIt = page->m_annotations.constBegin(), aEnd = page->m_annotations.constEnd(); for ( ; aIt != aEnd; ++aIt ) { Okular::Annotation * ann = *aIt; int flags = ann->flags(); if ( flags & Okular::Annotation::Hidden ) continue; if ( flags & Okular::Annotation::ExternallyDrawn ) { // ExternallyDrawn annots are never rendered by PagePainter. // Just paint the boundingRect if the annot is moved or resized. if ( flags & (Okular::Annotation::BeingMoved | Okular::Annotation::BeingResized) ) { boundingRectOnlyAnn = ann; } continue; } bool intersects = ann->transformedBoundingRectangle().intersects( nXMin, nYMin, nXMax, nYMax ); if ( ann->subType() == Okular::Annotation::AText ) { Okular::TextAnnotation * ta = static_cast< Okular::TextAnnotation * >( ann ); if ( ta->textType() == Okular::TextAnnotation::Linked ) { Okular::NormalizedRect iconrect( ann->transformedBoundingRectangle().left, ann->transformedBoundingRectangle().top, ann->transformedBoundingRectangle().left + TEXTANNOTATION_ICONSIZE / page->width(), ann->transformedBoundingRectangle().top + TEXTANNOTATION_ICONSIZE / page->height() ); intersects = iconrect.intersects( nXMin, nYMin, nXMax, nYMax ); } } if ( intersects ) { Okular::Annotation::SubType type = ann->subType(); if ( type == Okular::Annotation::ALine || type == Okular::Annotation::AHighlight || type == Okular::Annotation::AInk /*|| (type == Annotation::AGeom && ann->style().opacity() < 0.99)*/ ) { if ( !bufferedAnnotations ) bufferedAnnotations = new QList< Okular::Annotation * >(); bufferedAnnotations->append( ann ); } else { if ( !unbufferedAnnotations ) unbufferedAnnotations = new QList< Okular::Annotation * >(); unbufferedAnnotations->append( ann ); } } } } // end of intersections checking } /** 3 - ENABLE BACKBUFFERING IF DIRECT IMAGE MANIPULATION IS NEEDED **/ bool bufferAccessibility = (flags & Accessibility) && Okular::SettingsCore::changeColors() && (Okular::SettingsCore::renderMode() != Okular::SettingsCore::EnumRenderMode::Paper); bool useBackBuffer = bufferAccessibility || bufferedHighlights || bufferedAnnotations || viewPortPoint; QPixmap * backPixmap = nullptr; QPainter * mixedPainter = nullptr; QRect limitsInPixmap = limits.translated( scaledCrop.topLeft() ); QRect dLimitsInPixmap = dLimits.translated( dScaledCrop.topLeft() ); // limits within full (scaled but uncropped) pixmap /** 4A -- REGULAR FLOW. PAINT PIXMAP NORMAL OR RESCALED USING GIVEN QPAINTER **/ if ( !useBackBuffer ) { if ( hasTilesManager ) { const Okular::NormalizedRect normalizedLimits( limitsInPixmap, scaledWidth, scaledHeight ); const QList tiles = page->tilesAt( observer, normalizedLimits ); QList::const_iterator tIt = tiles.constBegin(), tEnd = tiles.constEnd(); while ( tIt != tEnd ) { const Okular::Tile &tile = *tIt; QRect tileRect = tile.rect().geometry( scaledWidth, scaledHeight ).translated( -scaledCrop.topLeft() ); QRect dTileRect = QRectF(tileRect.x() * dpr, tileRect.y() * dpr, tileRect.width() * dpr, tileRect.height() * dpr).toAlignedRect(); QRect limitsInTile = limits & tileRect; QRectF dLimitsInTile = dLimits & dTileRect; if ( !limitsInTile.isEmpty() ) { QPixmap* tilePixmap = tile.pixmap(); tilePixmap->setDevicePixelRatio( qApp->devicePixelRatio() ); if ( tilePixmap->width() == dTileRect.width() && tilePixmap->height() == dTileRect.height() ) { destPainter->drawPixmap( limitsInTile.topLeft(), *tilePixmap, dLimitsInTile.translated( -dTileRect.topLeft() ) ); } else { destPainter->drawPixmap( tileRect, *tilePixmap ); } } tIt++; } } else { QPixmap scaledCroppedPixmap = pixmap.scaled(dScaledWidth, dScaledHeight).copy(dLimitsInPixmap); scaledCroppedPixmap.setDevicePixelRatio(dpr); destPainter->drawPixmap( limits.topLeft(), scaledCroppedPixmap, QRectF(0, 0, dLimits.width(),dLimits.height())); } // 4A.2. active painter is the one passed to this method mixedPainter = destPainter; } /** 4B -- BUFFERED FLOW. IMAGE PAINTING + OPERATIONS. QPAINTER OVER PIXMAP **/ else { // the image over which we are going to draw QImage backImage = QImage( dLimits.width(), dLimits.height(), QImage::Format_ARGB32_Premultiplied ); backImage.setDevicePixelRatio(dpr); backImage.fill( paperColor ); QPainter p( &backImage ); if ( hasTilesManager ) { const Okular::NormalizedRect normalizedLimits( limitsInPixmap, scaledWidth, scaledHeight ); const QList tiles = page->tilesAt( observer, normalizedLimits ); QList::const_iterator tIt = tiles.constBegin(), tEnd = tiles.constEnd(); while ( tIt != tEnd ) { const Okular::Tile &tile = *tIt; QRect tileRect = tile.rect().geometry( scaledWidth, scaledHeight ).translated( -scaledCrop.topLeft() ); QRect dTileRect(QRectF(tileRect.x() * dpr, tileRect.y() * dpr, tileRect.width() * dpr, tileRect.height() * dpr).toAlignedRect()); QRect limitsInTile = limits & tileRect; QRect dLimitsInTile = dLimits & dTileRect; if ( !limitsInTile.isEmpty() ) { QPixmap* tilePixmap = tile.pixmap(); tilePixmap->setDevicePixelRatio( qApp->devicePixelRatio() ); if ( tilePixmap->width() == dTileRect.width() && tilePixmap->height() == dTileRect.height() ) { p.drawPixmap( limitsInTile.translated( -limits.topLeft() ).topLeft(), *tilePixmap, dLimitsInTile.translated( -dTileRect.topLeft() ) ); } else { double xScale = tilePixmap->width() / (double)dTileRect.width(); double yScale = tilePixmap->height() / (double)dTileRect.height(); QTransform transform( xScale, 0, 0, yScale, 0, 0 ); p.drawPixmap( limitsInTile.translated( -limits.topLeft() ), *tilePixmap, transform.mapRect( dLimitsInTile ).translated( -transform.mapRect( dTileRect ).topLeft() ) ); } } ++tIt; } } else { // 4B.1. draw the page pixmap: normal or scaled QPixmap scaledCroppedPixmap = pixmap.scaled(dScaledWidth, dScaledHeight).copy(dLimitsInPixmap); scaledCroppedPixmap.setDevicePixelRatio(dpr); p.drawPixmap( 0, 0, scaledCroppedPixmap ); } p.end(); // 4B.2. modify pixmap following accessibility settings if ( bufferAccessibility ) { switch ( Okular::SettingsCore::renderMode() ) { case Okular::SettingsCore::EnumRenderMode::Inverted: // Invert image pixels using QImage internal function backImage.invertPixels(QImage::InvertRgb); break; case Okular::SettingsCore::EnumRenderMode::Recolor: recolor(&backImage, Okular::Settings::recolorForeground(), Okular::Settings::recolorBackground()); break; case Okular::SettingsCore::EnumRenderMode::BlackWhite: // Manual Gray and Contrast unsigned int * data = reinterpret_cast(backImage.bits()); int val, pixels = backImage.width() * backImage.height(), con = Okular::Settings::bWContrast(), thr = 255 - Okular::Settings::bWThreshold(); for( int i = 0; i < pixels; ++i ) { val = qGray( data[i] ); if ( val > thr ) val = 128 + (127 * (val - thr)) / (255 - thr); else if ( val < thr ) val = (128 * val) / thr; if ( con > 2 ) { val = con * ( val - thr ) / 2 + thr; if ( val > 255 ) val = 255; else if ( val < 0 ) val = 0; } data[i] = qRgba( val, val, val, 255 ); } break; } } // 4B.3. highlight rects in page if ( bufferedHighlights ) { // draw highlights that are inside the 'limits' paint region for (const auto& highlight : qAsConst(*bufferedHighlights)) { const Okular::NormalizedRect & r = highlight.second; // find out the rect to highlight on pixmap QRect highlightRect = r.geometry( scaledWidth, scaledHeight ).translated( -scaledCrop.topLeft() ).intersected( limits ); highlightRect.translate( -limits.left(), -limits.top() ); const QColor highlightColor = highlight.first; QPainter painter(&backImage); painter.setCompositionMode(QPainter::CompositionMode_Multiply); painter.fillRect(highlightRect, highlightColor); auto frameColor = highlightColor.darker(150); const QRect frameRect = r.geometry( scaledWidth, scaledHeight ).translated( -scaledCrop.topLeft() ).translated( -limits.left(), -limits.top() ); painter.setPen(frameColor); painter.drawRect(frameRect); } } // 4B.4. paint annotations [COMPOSITED ONES] if ( bufferedAnnotations ) { // Albert: This is quite "heavy" but all the backImage that reach here are QImage::Format_ARGB32_Premultiplied // and have to be so that the QPainter::CompositionMode_Multiply works // we could also put a // backImage = backImage.convertToFormat(QImage::Format_ARGB32_Premultiplied) // that would be almost a noop, but we'll leave the assert for now Q_ASSERT(backImage.format() == QImage::Format_ARGB32_Premultiplied); // precalc constants for normalizing [0,1] page coordinates into normalized [0,1] limit rect coordinates double pageScale = (double)croppedWidth / page->width(); double xOffset = (double)limits.left() / (double)scaledWidth + crop.left, xScale = (double)scaledWidth / (double)limits.width(), yOffset = (double)limits.top() / (double)scaledHeight + crop.top, yScale = (double)scaledHeight / (double)limits.height(); // paint all buffered annotations in the page QList< Okular::Annotation * >::const_iterator aIt = bufferedAnnotations->constBegin(), aEnd = bufferedAnnotations->constEnd(); for ( ; aIt != aEnd; ++aIt ) { Okular::Annotation * a = *aIt; Okular::Annotation::SubType type = a->subType(); QColor acolor = a->style().color(); if ( !acolor.isValid() ) acolor = Qt::yellow; acolor.setAlphaF( a->style().opacity() ); // draw LineAnnotation MISSING: caption, dash pattern, endings for multipoint lines if ( type == Okular::Annotation::ALine ) { LineAnnotPainter linepainter { (Okular::LineAnnotation *) a, { page->width(), page->height() }, pageScale, { xScale, 0., 0., yScale, -xOffset * xScale, -yOffset * yScale } }; linepainter.draw( backImage ); } // draw HighlightAnnotation MISSING: under/strike width, feather, capping else if ( type == Okular::Annotation::AHighlight ) { // get the annotation Okular::HighlightAnnotation * ha = (Okular::HighlightAnnotation *) a; Okular::HighlightAnnotation::HighlightType type = ha->highlightType(); // draw each quad of the annotation int quads = ha->highlightQuads().size(); for ( int q = 0; q < quads; q++ ) { NormalizedPath path; const Okular::HighlightAnnotation::Quad & quad = ha->highlightQuads()[ q ]; // normalize page point to image for ( int i = 0; i < 4; i++ ) { Okular::NormalizedPoint point; point.x = (quad.transformedPoint( i ).x - xOffset) * xScale; point.y = (quad.transformedPoint( i ).y - yOffset) * yScale; path.append( point ); } // draw the normalized path into image switch ( type ) { // highlight the whole rect case Okular::HighlightAnnotation::Highlight: drawShapeOnImage( backImage, path, true, Qt::NoPen, acolor, pageScale, Multiply ); break; // highlight the bottom part of the rect case Okular::HighlightAnnotation::Squiggly: path[ 3 ].x = ( path[ 0 ].x + path[ 3 ].x ) / 2.0; path[ 3 ].y = ( path[ 0 ].y + path[ 3 ].y ) / 2.0; path[ 2 ].x = ( path[ 1 ].x + path[ 2 ].x ) / 2.0; path[ 2 ].y = ( path[ 1 ].y + path[ 2 ].y ) / 2.0; drawShapeOnImage( backImage, path, true, Qt::NoPen, acolor, pageScale, Multiply ); break; // make a line at 3/4 of the height case Okular::HighlightAnnotation::Underline: path[ 0 ].x = ( 3 * path[ 0 ].x + path[ 3 ].x ) / 4.0; path[ 0 ].y = ( 3 * path[ 0 ].y + path[ 3 ].y ) / 4.0; path[ 1 ].x = ( 3 * path[ 1 ].x + path[ 2 ].x ) / 4.0; path[ 1 ].y = ( 3 * path[ 1 ].y + path[ 2 ].y ) / 4.0; path.pop_back(); path.pop_back(); drawShapeOnImage( backImage, path, false, QPen( acolor, 2 ), QBrush(), pageScale ); break; // make a line at 1/2 of the height case Okular::HighlightAnnotation::StrikeOut: path[ 0 ].x = ( path[ 0 ].x + path[ 3 ].x ) / 2.0; path[ 0 ].y = ( path[ 0 ].y + path[ 3 ].y ) / 2.0; path[ 1 ].x = ( path[ 1 ].x + path[ 2 ].x ) / 2.0; path[ 1 ].y = ( path[ 1 ].y + path[ 2 ].y ) / 2.0; path.pop_back(); path.pop_back(); drawShapeOnImage( backImage, path, false, QPen( acolor, 2 ), QBrush(), pageScale ); break; } } } // draw InkAnnotation MISSING:invar width, PENTRACER else if ( type == Okular::Annotation::AInk ) { // get the annotation Okular::InkAnnotation * ia = (Okular::InkAnnotation *) a; // draw each ink path const QList< QLinkedList > transformedInkPaths = ia->transformedInkPaths(); const QPen inkPen = buildPen( a, a->style().width(), acolor ); int paths = transformedInkPaths.size(); for ( int p = 0; p < paths; p++ ) { NormalizedPath path; const QLinkedList & inkPath = transformedInkPaths[ p ]; // normalize page point to image QLinkedList::const_iterator pIt = inkPath.constBegin(), pEnd = inkPath.constEnd(); for ( ; pIt != pEnd; ++pIt ) { const Okular::NormalizedPoint & inkPoint = *pIt; Okular::NormalizedPoint point; point.x = (inkPoint.x - xOffset) * xScale; point.y = (inkPoint.y - yOffset) * yScale; path.append( point ); } // draw the normalized path into image drawShapeOnImage( backImage, path, false, inkPen, QBrush(), pageScale ); } } } // end current annotation drawing } if(viewPortPoint) { QPainter painter(&backImage); painter.translate( -limits.left(), -limits.top() ); painter.setPen( QApplication::palette().color( QPalette::Active, QPalette::Highlight ) ); painter.drawLine( 0, viewPortPoint->y * scaledHeight + 1, scaledWidth - 1, viewPortPoint->y * scaledHeight + 1 ); // ROTATION CURRENTLY NOT IMPLEMENTED /* if( page->rotation() == Okular::Rotation0) { } else if(page->rotation() == Okular::Rotation270) { painter.drawLine( viewPortPoint->y * scaledHeight + 1, 0, viewPortPoint->y * scaledHeight + 1, scaledWidth - 1); } else if(page->rotation() == Okular::Rotation180) { painter.drawLine( 0, (1.0 - viewPortPoint->y) * scaledHeight - 1, scaledWidth - 1, (1.0 - viewPortPoint->y) * scaledHeight - 1 ); } else if(page->rotation() == Okular::Rotation90) // not right, rotation clock-wise { painter.drawLine( scaledWidth - (viewPortPoint->y * scaledHeight + 1), 0, scaledWidth - (viewPortPoint->y * scaledHeight + 1), scaledWidth - 1); } */ } // 4B.5. create the back pixmap converting from the local image backPixmap = new QPixmap( QPixmap::fromImage( backImage ) ); backPixmap->setDevicePixelRatio(dpr); // 4B.6. create a painter over the pixmap and set it as the active one mixedPainter = new QPainter( backPixmap ); mixedPainter->translate( -limits.left(), -limits.top() ); } /** 5 -- MIXED FLOW. Draw ANNOTATIONS [OPAQUE ONES] on ACTIVE PAINTER **/ if ( unbufferedAnnotations ) { // iterate over annotations and paint AText, AGeom, AStamp QList< Okular::Annotation * >::const_iterator aIt = unbufferedAnnotations->constBegin(), aEnd = unbufferedAnnotations->constEnd(); for ( ; aIt != aEnd; ++aIt ) { Okular::Annotation * a = *aIt; // honor opacity settings on supported types unsigned int opacity = (unsigned int)( a->style().color().alpha() * a->style().opacity() ); // skip the annotation drawing if all the annotation is fully // transparent, but not with text annotations if ( opacity <= 0 && a->subType() != Okular::Annotation::AText ) continue; QColor acolor = a->style().color(); if ( !acolor.isValid() ) acolor = Qt::yellow; acolor.setAlpha( opacity ); // get annotation boundary and drawn rect QRect annotBoundary = a->transformedBoundingRectangle().geometry( scaledWidth, scaledHeight ).translated( -scaledCrop.topLeft() ); QRect annotRect = annotBoundary.intersected( limits ); QRect innerRect( annotRect.left() - annotBoundary.left(), annotRect.top() - annotBoundary.top(), annotRect.width(), annotRect.height() ); QRectF dInnerRect(innerRect.x() * dpr, innerRect.y() * dpr, innerRect.width() * dpr, innerRect.height() * dpr); Okular::Annotation::SubType type = a->subType(); // draw TextAnnotation if ( type == Okular::Annotation::AText ) { Okular::TextAnnotation * text = (Okular::TextAnnotation *)a; if ( text->textType() == Okular::TextAnnotation::InPlace ) { QImage image( annotBoundary.size(), QImage::Format_ARGB32 ); image.fill( acolor.rgba() ); QPainter painter( &image ); painter.setFont( text->textFont() ); painter.setPen( text->textColor() ); Qt::AlignmentFlag halign = ( text->inplaceAlignment() == 1 ? Qt::AlignHCenter : ( text->inplaceAlignment() == 2 ? Qt::AlignRight : Qt::AlignLeft ) ); const double invXScale = (double)page->width() / scaledWidth; const double invYScale = (double)page->height() / scaledHeight; const double borderWidth = text->style().width(); painter.scale( 1 / invXScale, 1 / invYScale ); painter.drawText( borderWidth * invXScale, borderWidth * invYScale, (image.width() - 2 * borderWidth) * invXScale, (image.height() - 2 * borderWidth) * invYScale, Qt::AlignTop | halign | Qt::TextWordWrap, text->contents() ); painter.resetTransform(); //Required as asking for a zero width pen results //in a default width pen (1.0) being created if ( borderWidth != 0 ) { QPen pen( Qt::black, borderWidth ); painter.setPen( pen ); painter.drawRect( 0, 0, image.width() - 1, image.height() - 1 ); } painter.end(); mixedPainter->drawImage( annotBoundary.topLeft(), image ); } else if ( text->textType() == Okular::TextAnnotation::Linked ) { // get pixmap, colorize and alpha-blend it QString path; QPixmap pixmap = GuiUtils::iconLoader()->loadIcon( text->textIcon().toLower(), KIconLoader::User, 32, KIconLoader::DefaultState, QStringList(), &path, true ); if ( path.isEmpty() ) pixmap = GuiUtils::iconLoader()->loadIcon( text->textIcon().toLower(), KIconLoader::NoGroup, 32 ); QPixmap scaledCroppedPixmap = pixmap.scaled(TEXTANNOTATION_ICONSIZE * dpr, TEXTANNOTATION_ICONSIZE * dpr).copy(dInnerRect.toAlignedRect()); scaledCroppedPixmap.setDevicePixelRatio(dpr); QImage scaledCroppedImage = scaledCroppedPixmap.toImage(); // if the annotation color is valid (ie it was set), then // use it to colorize the icon, otherwise the icon will be // "gray" if ( a->style().color().isValid() ) GuiUtils::colorizeImage( scaledCroppedImage, a->style().color(), opacity ); pixmap = QPixmap::fromImage( scaledCroppedImage ); // draw the mangled image to painter mixedPainter->drawPixmap( annotRect.topLeft(), pixmap); } } // draw StampAnnotation else if ( type == Okular::Annotation::AStamp ) { Okular::StampAnnotation * stamp = (Okular::StampAnnotation *)a; // get pixmap and alpha blend it if needed QPixmap pixmap = GuiUtils::loadStamp( stamp->stampIconName(), annotBoundary.width() ); if ( !pixmap.isNull() ) // should never happen but can happen on huge sizes { const QRect dInnerRect(QRectF(innerRect.x() * dpr, innerRect.y() * dpr, innerRect.width() * dpr, innerRect.height() * dpr).toAlignedRect()); QPixmap scaledCroppedPixmap = pixmap.scaled(annotBoundary.width() * dpr, annotBoundary.height() * dpr).copy(dInnerRect); scaledCroppedPixmap.setDevicePixelRatio(dpr); QImage scaledCroppedImage = scaledCroppedPixmap.toImage(); if ( opacity < 255 ) changeImageAlpha( scaledCroppedImage, opacity ); pixmap = QPixmap::fromImage( scaledCroppedImage ); // draw the scaled and al mixedPainter->drawPixmap( annotRect.topLeft(), pixmap ); } } // draw GeomAnnotation else if ( type == Okular::Annotation::AGeom ) { Okular::GeomAnnotation * geom = (Okular::GeomAnnotation *)a; // check whether there's anything to draw if ( geom->style().width() || geom->geometricalInnerColor().isValid() ) { mixedPainter->save(); const double width = geom->style().width() * Okular::Utils::realDpi(nullptr).width() / ( 72.0 * 2.0 ) * scaledWidth / page->width(); QRectF r( .0, .0, annotBoundary.width(), annotBoundary.height() ); r.adjust( width, width, -width, -width ); r.translate( annotBoundary.topLeft() ); if ( geom->geometricalInnerColor().isValid() ) { r.adjust( width, width, -width, -width ); const QColor color = geom->geometricalInnerColor(); mixedPainter->setPen( Qt::NoPen ); mixedPainter->setBrush( QColor( color.red(), color.green(), color.blue(), opacity ) ); if ( geom->geometricalType() == Okular::GeomAnnotation::InscribedSquare ) mixedPainter->drawRect( r ); else mixedPainter->drawEllipse( r ); r.adjust( -width, -width, width, width ); } if ( geom->style().width() ) // need to check the original size here.. { mixedPainter->setPen( buildPen( a, width * 2, acolor ) ); mixedPainter->setBrush( Qt::NoBrush ); if ( geom->geometricalType() == Okular::GeomAnnotation::InscribedSquare ) mixedPainter->drawRect( r ); else mixedPainter->drawEllipse( r ); } mixedPainter->restore(); } } // draw extents rectangle if ( Okular::Settings::debugDrawAnnotationRect() ) { mixedPainter->setPen( a->style().color() ); mixedPainter->drawRect( annotBoundary ); } } } if ( boundingRectOnlyAnn ) { QRect annotBoundary = boundingRectOnlyAnn->transformedBoundingRectangle().geometry( scaledWidth, scaledHeight ).translated( -scaledCrop.topLeft() ); mixedPainter->setPen( Qt::DashLine ); mixedPainter->drawRect( annotBoundary ); } /** 6 -- MIXED FLOW. Draw LINKS+IMAGES BORDER on ACTIVE PAINTER **/ if ( enhanceLinks || enhanceImages ) { mixedPainter->save(); mixedPainter->scale( scaledWidth, scaledHeight ); mixedPainter->translate( -crop.left, -crop.top ); QColor normalColor = QApplication::palette().color( QPalette::Active, QPalette::Highlight ); // enlarging limits for intersection is like growing the 'rectGeometry' below QRect limitsEnlarged = limits; limitsEnlarged.adjust( -2, -2, 2, 2 ); // draw rects that are inside the 'limits' paint region as opaque rects QLinkedList< Okular::ObjectRect * >::const_iterator lIt = page->m_rects.constBegin(), lEnd = page->m_rects.constEnd(); for ( ; lIt != lEnd; ++lIt ) { Okular::ObjectRect * rect = *lIt; if ( (enhanceLinks && rect->objectType() == Okular::ObjectRect::Action) || (enhanceImages && rect->objectType() == Okular::ObjectRect::Image) ) { if ( limitsEnlarged.intersects( rect->boundingRect( scaledWidth, scaledHeight ).translated( -scaledCrop.topLeft() ) ) ) { mixedPainter->strokePath( rect->region(), QPen( normalColor, 0 ) ); } } } mixedPainter->restore(); } /** 7 -- BUFFERED FLOW. Copy BACKPIXMAP on DESTINATION PAINTER **/ if ( useBackBuffer ) { delete mixedPainter; destPainter->drawPixmap( limits.left(), limits.top(), *backPixmap ); delete backPixmap; } // delete object containers delete bufferedHighlights; delete bufferedAnnotations; delete unbufferedAnnotations; } /** Private Helpers :: Pixmap conversion **/ -void PagePainter::cropPixmapOnImage( QImage & dest, const QPixmap * src, const QRect & r ) +void PagePainter::cropPixmapOnImage( QImage & dest, const QPixmap * src, const QRect r ) { qreal dpr = src->devicePixelRatioF(); // handle quickly the case in which the whole pixmap has to be converted if ( r == QRect( 0, 0, src->width() / dpr, src->height() / dpr ) ) { dest = src->toImage(); dest = dest.convertToFormat(QImage::Format_ARGB32_Premultiplied); } // else copy a portion of the src to an internal pixmap (smaller) and convert it else { QImage croppedImage( r.width() * dpr, r.height() * dpr, QImage::Format_ARGB32_Premultiplied ); croppedImage.setDevicePixelRatio(dpr); QPainter p( &croppedImage ); p.drawPixmap( 0, 0, *src, r.left(), r.top(), r.width(), r.height() ); p.end(); dest = croppedImage; } } void PagePainter::recolor(QImage *image, const QColor &foreground, const QColor &background) { if (image->format() != QImage::Format_ARGB32_Premultiplied) { qCWarning(OkularUiDebug) << "Wrong image format! Converting..."; *image = image->convertToFormat(QImage::Format_ARGB32_Premultiplied); } Q_ASSERT(image->format() == QImage::Format_ARGB32_Premultiplied); const float scaleRed = background.redF() - foreground.redF(); const float scaleGreen = background.greenF() - foreground.greenF(); const float scaleBlue = background.blueF() - foreground.blueF(); for (int y=0; yheight(); y++) { QRgb *pixels = reinterpret_cast(image->scanLine(y)); for (int x=0; xwidth(); x++) { const int lightness = qGray(pixels[x]); pixels[x] = qRgba(scaleRed * lightness + foreground.red(), scaleGreen * lightness + foreground.green(), scaleBlue * lightness + foreground.blue(), qAlpha(pixels[x])); } } } /** Private Helpers :: Image Drawing **/ // from Arthur - qt4 static inline int qt_div_255(int x) { return (x + (x>>8) + 0x80) >> 8; } void PagePainter::changeImageAlpha( QImage & image, unsigned int destAlpha ) { // iterate over all pixels changing the alpha component value unsigned int * data = reinterpret_cast(image.bits()); unsigned int pixels = image.width() * image.height(); int source, sourceAlpha; for( unsigned int i = 0; i < pixels; ++i ) { // optimize this loop keeping byte order into account source = data[i]; if ( (sourceAlpha = qAlpha( source )) == 255 ) { // use destAlpha data[i] = qRgba( qRed(source), qGreen(source), qBlue(source), destAlpha ); } else { // use destAlpha * sourceAlpha product sourceAlpha = qt_div_255( destAlpha * sourceAlpha ); data[i] = qRgba( qRed(source), qGreen(source), qBlue(source), sourceAlpha ); } } } void PagePainter::drawShapeOnImage( QImage & image, const NormalizedPath & normPath, bool closeShape, const QPen & pen, const QBrush & brush, double penWidthMultiplier, RasterOperation op //float antiAliasRadius ) { // safety checks int pointsNumber = normPath.size(); if ( pointsNumber < 2 ) return; int imageWidth = image.width(); int imageHeight = image.height(); double fImageWidth = (double)imageWidth; double fImageHeight = (double)imageHeight; // stroke outline double penWidth = (double)pen.width() * penWidthMultiplier; QPainter painter(&image); painter.setRenderHint(QPainter::Antialiasing); QPen pen2 = pen; pen2.setWidthF(penWidth); painter.setPen(pen2); painter.setBrush(brush); if (op == Multiply) { painter.setCompositionMode(QPainter::CompositionMode_Multiply); } if ( brush.style() == Qt::NoBrush ) { // create a polygon QPolygonF poly( closeShape ? pointsNumber + 1 : pointsNumber ); for ( int i = 0; i < pointsNumber; ++i ) { poly[ i ] = QPointF( normPath[ i ].x * fImageWidth, normPath[ i ].y * fImageHeight ); } if ( closeShape ) poly[ pointsNumber ] = poly[ 0 ]; painter.drawPolyline( poly ); } else { // create a 'path' QPainterPath path; path.setFillRule( Qt::WindingFill ); path.moveTo( normPath[ 0 ].x * fImageWidth, normPath[ 0 ].y * fImageHeight ); for ( int i = 1; i < pointsNumber; i++ ) { path.lineTo( normPath[ i ].x * fImageWidth, normPath[ i ].y * fImageHeight ); } if ( closeShape ) path.closeSubpath(); painter.drawPath( path ); } } void PagePainter::drawEllipseOnImage( QImage & image, const NormalizedPath & rect, const QPen & pen, const QBrush & brush, double penWidthMultiplier, RasterOperation op ) { const double fImageWidth = (double) image.width(); const double fImageHeight = (double) image.height(); // stroke outline const double penWidth = (double)pen.width() * penWidthMultiplier; QPainter painter(&image); painter.setRenderHint(QPainter::Antialiasing); QPen pen2 = pen; pen2.setWidthF(penWidth); painter.setPen(pen2); painter.setBrush(brush); if ( op == Multiply ) { painter.setCompositionMode(QPainter::CompositionMode_Multiply); } const QPointF &topLeft { rect[0].x * fImageWidth, rect[0].y * fImageHeight }; const QSizeF &size { (rect[1].x - rect[0].x) * fImageWidth, (rect[1].y - rect[0].y) * fImageHeight }; const QRectF imgRect { topLeft, size }; if ( brush.style() == Qt::NoBrush ) { painter.drawArc( imgRect, 0, 16*360 ); } else { painter.drawEllipse( imgRect ); } } LineAnnotPainter::LineAnnotPainter( const Okular::LineAnnotation * a, QSizeF pageSize, double pageScale, const QTransform &toNormalizedImage ) : la { a } , pageSize { pageSize } , pageScale { pageScale } , toNormalizedImage { toNormalizedImage } , aspectRatio { pageSize.height() / pageSize.width() } , linePen { buildPen( a, a->style().width(), a->style().color() ) } { if ( ( la->lineClosed() || la->transformedLinePoints().count() == 2 ) && la->lineInnerColor().isValid() ) { fillBrush = QBrush( la->lineInnerColor() ); } } void LineAnnotPainter::draw( QImage &image ) const { const QLinkedList transformedLinePoints = la->transformedLinePoints(); if ( transformedLinePoints.count() == 2 ) { const Okular::NormalizedPoint delta { transformedLinePoints.last().x - transformedLinePoints.first().x, transformedLinePoints.first().y - transformedLinePoints.last().y }; const double angle { atan2( delta.y * aspectRatio, delta.x ) }; const double cosA { cos( -angle ) }; const double sinA { sin( -angle ) }; const QTransform tmpMatrix = QTransform { cosA, sinA / aspectRatio, -sinA, cosA / aspectRatio, transformedLinePoints.first().x, transformedLinePoints.first().y }; const double deaspectedY { delta.y * aspectRatio }; const double mainSegmentLength { sqrt( delta.x * delta.x + deaspectedY * deaspectedY ) }; const double lineendSize { std::min( 6. * la->style().width() / pageSize.width(), mainSegmentLength / 2. ) }; drawShortenedLine( mainSegmentLength, lineendSize, image, tmpMatrix ); drawLineEnds( mainSegmentLength, lineendSize, image, tmpMatrix ); drawLeaderLine( 0., image, tmpMatrix ); drawLeaderLine( mainSegmentLength, image, tmpMatrix ); } else if ( transformedLinePoints.count() > 2 ) { drawMainLine( image ); } } void LineAnnotPainter::drawMainLine( QImage &image ) const { // draw the line as normalized path into image PagePainter::drawShapeOnImage( image, transformPath( la->transformedLinePoints(), toNormalizedImage ), la->lineClosed(), linePen, fillBrush, pageScale, PagePainter::Multiply ); } void LineAnnotPainter::drawShortenedLine( double mainSegmentLength, double size, QImage &image, const QTransform& toNormalizedPage ) const { const QTransform combinedTransform { toNormalizedPage * toNormalizedImage }; const QList path { { shortenForArrow(size, la->lineStartStyle()), 0 }, { mainSegmentLength - shortenForArrow(size, la->lineEndStyle()), 0 } }; PagePainter::drawShapeOnImage( image, transformPath(path, combinedTransform), la->lineClosed(), linePen, fillBrush, pageScale, PagePainter::Multiply ); } void LineAnnotPainter::drawLineEnds( double mainSegmentLength, double size, QImage &image, const QTransform& transform ) const { switch ( la->lineStartStyle() ) { case Okular::LineAnnotation::Square: drawLineEndSquare( 0, -size, transform, image ); break; case Okular::LineAnnotation::Circle: drawLineEndCircle( 0, -size, transform, image ); break; case Okular::LineAnnotation::Diamond: drawLineEndDiamond( 0, -size, transform, image ); break; case Okular::LineAnnotation::OpenArrow: drawLineEndArrow( 0, -size, 1., false, transform, image ); break; case Okular::LineAnnotation::ClosedArrow: drawLineEndArrow( 0, -size, 1., true, transform, image ); break; case Okular::LineAnnotation::None: break; case Okular::LineAnnotation::Butt: drawLineEndButt( 0, size, transform, image ); break; case Okular::LineAnnotation::ROpenArrow: drawLineEndArrow( 0, size, 1., false, transform, image ); break; case Okular::LineAnnotation::RClosedArrow: drawLineEndArrow( 0, size, 1., true, transform, image ); break; case Okular::LineAnnotation::Slash: drawLineEndSlash( 0, -size, transform, image ); break; } switch ( la->lineEndStyle() ) { case Okular::LineAnnotation::Square: drawLineEndSquare( mainSegmentLength, size, transform, image ); break; case Okular::LineAnnotation::Circle: drawLineEndCircle( mainSegmentLength, size, transform, image ); break; case Okular::LineAnnotation::Diamond: drawLineEndDiamond( mainSegmentLength, size, transform, image ); break; case Okular::LineAnnotation::OpenArrow: drawLineEndArrow( mainSegmentLength, size, 1., false, transform, image ); break; case Okular::LineAnnotation::ClosedArrow: drawLineEndArrow( mainSegmentLength, size, 1., true, transform, image ); break; case Okular::LineAnnotation::None: break; case Okular::LineAnnotation::Butt: drawLineEndButt( mainSegmentLength, size, transform, image ); break; case Okular::LineAnnotation::ROpenArrow: drawLineEndArrow( mainSegmentLength, size, -1., false, transform, image ); break; case Okular::LineAnnotation::RClosedArrow: drawLineEndArrow( mainSegmentLength, size, -1., true, transform, image ); break; case Okular::LineAnnotation::Slash: drawLineEndSlash( mainSegmentLength, size, transform, image ); break; } } void LineAnnotPainter::drawLineEndArrow( double xEndPos, double size, double flipX, bool close, const QTransform& toNormalizedPage, QImage &image ) const { const QTransform combinedTransform { toNormalizedPage * toNormalizedImage }; const QList path { { xEndPos - size * flipX, size / 2. }, { xEndPos, 0 }, { xEndPos - size * flipX, -size / 2. }, }; PagePainter::drawShapeOnImage( image, transformPath(path, combinedTransform), close, linePen, fillBrush, pageScale, PagePainter::Multiply); } void LineAnnotPainter::drawLineEndButt( double xEndPos, double size, const QTransform& toNormalizedPage, QImage &image ) const { const QTransform combinedTransform { toNormalizedPage * toNormalizedImage }; const double halfSize { size / 2. }; const QList path { { xEndPos, halfSize }, { xEndPos, -halfSize }, }; PagePainter::drawShapeOnImage( image, transformPath(path, combinedTransform), true, linePen, fillBrush, pageScale, PagePainter::Multiply); } void LineAnnotPainter::drawLineEndCircle( double xEndPos, double size, const QTransform& toNormalizedPage, QImage &image ) const { /* transform the circle midpoint to intermediate normalized coordinates * where it's easy to construct the bounding rect of the circle */ Okular::NormalizedPoint center; toNormalizedPage.map( xEndPos - size / 2., 0, ¢er.x, ¢er.y ); const double halfSize { size / 2. }; const QList path { { center.x - halfSize, center.y - halfSize / aspectRatio }, { center.x + halfSize, center.y + halfSize / aspectRatio }, }; /* then transform bounding rect with toNormalizedImage */ PagePainter::drawEllipseOnImage( image, transformPath(path, toNormalizedImage), linePen, fillBrush, pageScale, PagePainter::Multiply); } void LineAnnotPainter::drawLineEndSquare( double xEndPos, double size, const QTransform& toNormalizedPage, QImage &image ) const { const QTransform combinedTransform { toNormalizedPage * toNormalizedImage }; const QList path { { xEndPos, size / 2. }, { xEndPos - size, size / 2. }, { xEndPos - size, -size / 2. }, { xEndPos, -size / 2. } }; PagePainter::drawShapeOnImage( image, transformPath(path, combinedTransform), true, linePen, fillBrush, pageScale, PagePainter::Multiply); } void LineAnnotPainter::drawLineEndDiamond( double xEndPos, double size, const QTransform& toNormalizedPage, QImage &image ) const { const QTransform combinedTransform { toNormalizedPage * toNormalizedImage }; const QList path { { xEndPos, 0 }, { xEndPos - size / 2., size / 2. }, { xEndPos - size, 0 }, { xEndPos - size / 2., -size / 2. } }; PagePainter::drawShapeOnImage( image, transformPath(path, combinedTransform), true, linePen, fillBrush, pageScale, PagePainter::Multiply); } void LineAnnotPainter::drawLineEndSlash( double xEndPos, double size, const QTransform& toNormalizedPage, QImage &image ) const { const QTransform combinedTransform { toNormalizedPage * toNormalizedImage }; const double halfSize { size / 2. }; const double xOffset { cos(M_PI/3.) * halfSize }; const QList path { { xEndPos - xOffset, halfSize }, { xEndPos + xOffset, -halfSize }, }; PagePainter::drawShapeOnImage( image, transformPath(path, combinedTransform), true, linePen, fillBrush, pageScale, PagePainter::Multiply); } void LineAnnotPainter::drawLeaderLine( double xEndPos, QImage &image, const QTransform& toNormalizedPage ) const { const QTransform combinedTransform = toNormalizedPage * toNormalizedImage; const double ll = aspectRatio * la->lineLeadingForwardPoint() / pageSize.height(); const double lle = aspectRatio * la->lineLeadingBackwardPoint() / pageSize.height(); const int sign { ll > 0 ? -1 : 1 }; QList path; if ( fabs( ll ) > 0 ) { path.append( { xEndPos, ll } ); // do we have the extension on the "back"? if ( fabs( lle ) > 0 ) { path.append( { xEndPos, sign * lle } ); } else { path.append( { xEndPos, 0 } ); } } PagePainter::drawShapeOnImage( image, transformPath(path, combinedTransform), false, linePen, fillBrush, pageScale, PagePainter::Multiply); } double LineAnnotPainter::shortenForArrow( double size, Okular::LineAnnotation::TermStyle endStyle ) { double shortenBy { 0 }; if ( endStyle == Okular::LineAnnotation::Square || endStyle == Okular::LineAnnotation::Circle || endStyle == Okular::LineAnnotation::Diamond || endStyle == Okular::LineAnnotation::ClosedArrow ) { shortenBy = size; } return shortenBy; } /* kate: replace-tabs on; indent-width 4; */ diff --git a/ui/pagepainter.h b/ui/pagepainter.h index f4e41a0a4..e35bd2f52 100644 --- a/ui/pagepainter.h +++ b/ui/pagepainter.h @@ -1,128 +1,128 @@ /*************************************************************************** * Copyright (C) 2005 by Enrico Ros * * * * 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. * ***************************************************************************/ #ifndef _OKULAR_PAGEPAINTER_H_ #define _OKULAR_PAGEPAINTER_H_ #include #include #include #include "core/annotations.h" #include "core/area.h" // for NormalizedPoint class QPainter; class QRect; namespace Okular { class DocumentObserver; class Page; } /** * @short Paints a Okular::Page to an open painter using given flags. */ class Q_DECL_EXPORT PagePainter { public: // list of flags passed to the painting function. by OR-ing those flags // you can decide whether or not to permit drawing of a certain feature. enum PagePainterFlags { Accessibility = 1, EnhanceLinks = 2, EnhanceImages = 4, Highlights = 8, TextSelection = 16, Annotations = 32 }; // draw (using painter 'destPainter') the 'page' requested by 'observer' using features // in 'flags'. 'limits' is the bounding rect of the paint operation, // 'scaledWidth' and 'scaledHeight' the expected size of page contents static void paintPageOnPainter( QPainter * destPainter, const Okular::Page * page, Okular::DocumentObserver *observer, - int flags, int scaledWidth, int scaledHeight, const QRect & pageLimits ); + int flags, int scaledWidth, int scaledHeight, const QRect pageLimits ); // draw (using painter 'destPainter') the 'page' requested by 'observer' using features // in 'flags'. // 'pageLimits' is the bounding rect of the paint operation relative to the // top left of the (cropped) page. // 'scaledWidth' and 'scaledHeight' the size of the page pixmap (before cropping). // 'crop' is the (normalized) cropped rectangle within the page. // The painter's (0,0) is assumed to be top left of the painted ('pageLimits') rect. static void paintCroppedPageOnPainter( QPainter * destPainter, const Okular::Page * page, Okular::DocumentObserver *observer, - int flags, int scaledWidth, int scaledHeight, const QRect & pageLimits, + int flags, int scaledWidth, int scaledHeight, const QRect pageLimits, const Okular::NormalizedRect & crop, Okular::NormalizedPoint *viewPortPoint ); private: - static void cropPixmapOnImage( QImage & dest, const QPixmap * src, const QRect & r ); + static void cropPixmapOnImage( QImage & dest, const QPixmap * src, const QRect r ); static void recolor(QImage *image, const QColor &foreground, const QColor &background); // set the alpha component of the image to a given value static void changeImageAlpha( QImage & image, unsigned int alpha ); // my pretty dear raster function typedef QList< Okular::NormalizedPoint > NormalizedPath; enum RasterOperation { Normal, Multiply }; static void drawShapeOnImage( QImage & image, const NormalizedPath & normPath, bool closeShape, const QPen & pen, const QBrush & brush = QBrush(), double penWidthMultiplier = 1.0, RasterOperation op = Normal //float antiAliasRadius = 1.0 ); static void drawEllipseOnImage( QImage & image, const NormalizedPath & rect, const QPen & pen, const QBrush & brush, double penWidthMultiplier, RasterOperation op ); friend class LineAnnotPainter; }; class LineAnnotPainter { public: LineAnnotPainter( const Okular::LineAnnotation * a, QSizeF pageSizeA, double pageScale, const QTransform &toNormalizedImage ); void draw( QImage &image ) const; private: void drawMainLine( QImage &image ) const; void drawShortenedLine( double mainSegmentLength, double size, QImage &image, const QTransform& toNormalizedPage ) const; void drawLineEnds( double mainSegmentLength, double size, QImage &image, const QTransform& transform ) const; void drawLineEndArrow( double xEndPos, double size, double flipX, bool close, const QTransform& toNormalizedPage, QImage &image ) const; void drawLineEndButt( double xEndPos, double size, const QTransform& toNormalizedPage, QImage &image ) const; void drawLineEndCircle( double xEndPos, double size, const QTransform& toNormalizedPage, QImage &image ) const; void drawLineEndSquare( double xEndPos, double size, const QTransform& toNormalizedPage, QImage &image ) const; void drawLineEndDiamond( double xEndPos, double size, const QTransform& toNormalizedPage, QImage &image ) const; void drawLineEndSlash( double xEndPos, double size, const QTransform& toNormalizedPage, QImage &image ) const; void drawLeaderLine( double xEndPos, QImage &image, const QTransform& toNormalizedPage ) const; template QList transformPath( const T& path, const QTransform& transform ) const { QList transformedPath; for( const Okular::NormalizedPoint &item : path ) { Okular::NormalizedPoint p; transform.map( item.x, item.y, &p.x, &p.y ); transformedPath.append(p); } return transformedPath; } static double shortenForArrow( double size, Okular::LineAnnotation::TermStyle endStyle ); private: const Okular::LineAnnotation* la; QSizeF pageSize; double pageScale; QTransform toNormalizedImage; double aspectRatio; const QPen linePen; QBrush fillBrush; }; #endif diff --git a/ui/pageview.cpp b/ui/pageview.cpp index ff3968891..10d568aa7 100644 --- a/ui/pageview.cpp +++ b/ui/pageview.cpp @@ -1,5753 +1,5755 @@ /*************************************************************************** * Copyright (C) 2004-2005 by Enrico Ros * * Copyright (C) 2004-2006 by Albert Astals Cid * * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * * company, info@kdab.com. Work sponsored by the * * LiMux project of the city of Munich * * * * With portions of code from kpdf/kpdf_pagewidget.cc by: * * Copyright (C) 2002 by Wilco Greven * * Copyright (C) 2003 by Christophe Devriese * * * * Copyright (C) 2003 by Laurent Montel * * Copyright (C) 2003 by Dirk Mueller * * Copyright (C) 2004 by James Ots * * Copyright (C) 2011 by Jiri Baum - NICTA * * * * 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. * ***************************************************************************/ #include "pageview.h" // qt/kde includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // system includes #include #include // local includes #include "debug_ui.h" #include "formwidgets.h" #include "pageviewutils.h" #include "pagepainter.h" #include "core/annotations.h" #include "annotwindow.h" #include "guiutils.h" #include "annotationpopup.h" #include "pageviewannotator.h" #include "pageviewmouseannotation.h" #include "priorities.h" #include "toggleactionmenu.h" #include "okmenutitle.h" #ifdef HAVE_SPEECH #include "tts.h" #endif #include "videowidget.h" #include "core/action.h" #include "core/area.h" #include "core/document_p.h" #include "core/form.h" #include "core/page.h" #include "core/page_p.h" #include "core/misc.h" #include "core/generator.h" #include "core/movie.h" #include "core/audioplayer.h" #include "core/sourcereference.h" #include "core/tile.h" #include "settings.h" #include "settings_core.h" #include "url_utils.h" #include "magnifierview.h" static const int pageflags = PagePainter::Accessibility | PagePainter::EnhanceLinks | PagePainter::EnhanceImages | PagePainter::Highlights | PagePainter::TextSelection | PagePainter::Annotations; static const float kZoomValues[] = { 0.12, 0.25, 0.33, 0.50, 0.66, 0.75, 1.00, 1.25, 1.50, 2.00, 4.00, 8.00, 16.00 }; // This is the length of the text that will be shown when the user is searching for a specific piece of text. static const int searchTextPreviewLength = 21; // When following a link, only a preview of this length will be used to set the text of the action. static const int linkTextPreviewLength = 30; static inline double normClamp( double value, double def ) { return ( value < 0.0 || value > 1.0 ) ? def : value; } struct TableSelectionPart { PageViewItem * item; Okular::NormalizedRect rectInItem; Okular::NormalizedRect rectInSelection; TableSelectionPart(PageViewItem * item_p, const Okular::NormalizedRect &rectInItem_p, const Okular::NormalizedRect &rectInSelection_p); }; TableSelectionPart::TableSelectionPart( PageViewItem * item_p, const Okular::NormalizedRect &rectInItem_p, const Okular::NormalizedRect &rectInSelection_p) : item ( item_p ), rectInItem (rectInItem_p), rectInSelection (rectInSelection_p) { } // structure used internally by PageView for data storage class PageViewPrivate { public: PageViewPrivate( PageView *qq ); FormWidgetsController* formWidgetsController(); #ifdef HAVE_SPEECH OkularTTS* tts(); #endif QString selectedText() const; // the document, pageviewItems and the 'visible cache' PageView *q; Okular::Document * document; QVector< PageViewItem * > items; QLinkedList< PageViewItem * > visibleItems; MagnifierView *magnifierView; // view layout (columns and continuous in Settings), zoom and mouse PageView::ZoomMode zoomMode; float zoomFactor; QPoint mouseGrabOffset; QPoint mousePressPos; QPoint mouseSelectPos; QPoint previousMouseMovePos; int mouseMidLastY; bool mouseSelecting; QRect mouseSelectionRect; QColor mouseSelectionColor; bool mouseTextSelecting; QSet< int > pagesWithTextSelection; bool mouseOnRect; int mouseMode; MouseAnnotation * mouseAnnotation; // table selection QList tableSelectionCols; QList tableSelectionRows; QList tableSelectionParts; bool tableDividersGuessed; int lastSourceLocationViewportPageNumber; double lastSourceLocationViewportNormalizedX; double lastSourceLocationViewportNormalizedY; int controlWheelAccumulatedDelta; // auto scroll int scrollIncrement; QTimer * autoScrollTimer; // annotations PageViewAnnotator * annotator; //text annotation dialogs list QSet< AnnotWindow * > m_annowindows; // other stuff QTimer * delayResizeEventTimer; bool dirtyLayout; bool blockViewport; // prevents changes to viewport bool blockPixmapsRequest; // prevent pixmap requests PageViewMessage * messageWindow; // in pageviewutils.h bool m_formsVisible; FormWidgetsController *formsWidgetController; #ifdef HAVE_SPEECH OkularTTS * m_tts; #endif QTimer * refreshTimer; QSet refreshPages; // bbox state for Trim to Selection mode Okular::NormalizedRect trimBoundingBox; // infinite resizing loop prevention bool verticalScrollBarVisible; bool horizontalScrollBarVisible; // drag scroll QPoint dragScrollVector; QTimer dragScrollTimer; // left click depress QTimer leftClickTimer; // actions QAction * aRotateClockwise; QAction * aRotateCounterClockwise; QAction * aRotateOriginal; KSelectAction * aPageSizes; KActionMenu * aTrimMode; KToggleAction * aTrimMargins; QAction * aMouseNormal; QAction * aMouseSelect; QAction * aMouseTextSelect; QAction * aMouseTableSelect; QAction * aMouseMagnifier; KToggleAction * aTrimToSelection; KToggleAction * aToggleAnnotator; KSelectAction * aZoom; QAction * aZoomIn; QAction * aZoomOut; QAction * aZoomActual; KToggleAction * aZoomFitWidth; KToggleAction * aZoomFitPage; KToggleAction * aZoomAutoFit; KActionMenu * aViewMode; KToggleAction * aViewContinuous; QAction * aPrevAction; QAction * aToggleForms; QAction * aSpeakDoc; QAction * aSpeakPage; QAction * aSpeakStop; QAction * aSpeakPauseResume; KActionCollection * actionCollection; QActionGroup * mouseModeActionGroup; ToggleActionMenu * aMouseModeMenu; QAction * aFitWindowToPage; int setting_viewCols; bool rtl_Mode; // Keep track of whether tablet pen is currently pressed down bool penDown; // Keep track of mouse over link object const Okular::ObjectRect * mouseOverLinkObject; QScroller * scroller; }; PageViewPrivate::PageViewPrivate( PageView *qq ) : q( qq ) #ifdef HAVE_SPEECH , m_tts( nullptr ) #endif { } FormWidgetsController* PageViewPrivate::formWidgetsController() { if ( !formsWidgetController ) { formsWidgetController = new FormWidgetsController( document ); QObject::connect( formsWidgetController, &FormWidgetsController::changed, q, &PageView::slotFormChanged ); QObject::connect( formsWidgetController, &FormWidgetsController::action, q, &PageView::slotAction ); QObject::connect( formsWidgetController, &FormWidgetsController::formatAction, q, [this] (const Okular::Action *action, Okular::FormFieldText *fft ) { document->processFormatAction( action, fft ); } ); QObject::connect( formsWidgetController, &FormWidgetsController::keystrokeAction, q, [this] (const Okular::Action *action, Okular::FormFieldText *fft, bool &ok ) { document->processKeystrokeAction( action, fft, ok ); } ); QObject::connect( formsWidgetController, &FormWidgetsController::focusAction, q, [this] (const Okular::Action *action, Okular::FormFieldText *fft ) { document->processFocusAction( action, fft ); } ); QObject::connect( formsWidgetController, &FormWidgetsController::validateAction, q, [this] (const Okular::Action *action, Okular::FormFieldText *fft, bool &ok ) { document->processValidateAction( action, fft, ok ); } ); } return formsWidgetController; } #ifdef HAVE_SPEECH OkularTTS* PageViewPrivate::tts() { if ( !m_tts ) { m_tts = new OkularTTS( q ); if ( aSpeakStop ) { QObject::connect( m_tts, &OkularTTS::canPauseOrResume, aSpeakStop, &QAction::setEnabled ); } if ( aSpeakPauseResume ) { QObject::connect( m_tts, &OkularTTS::canPauseOrResume, aSpeakPauseResume, &QAction::setEnabled ); } } return m_tts; } #endif /* PageView. What's in this file? -> quick overview. * Code weight (in rows) and meaning: * 160 - constructor and creating actions plus their connected slots (empty stuff) * 70 - DocumentObserver inherited methodes (important) * 550 - events: mouse, keyboard, drag * 170 - slotRelayoutPages: set contents of the scrollview on continuous/single modes * 100 - zoom: zooming pages in different ways, keeping update the toolbar actions, etc.. * other misc functions: only slotRequestVisiblePixmaps and pickItemOnPoint noticeable, * and many insignificant stuff like this comment :-) */ PageView::PageView( QWidget *parent, Okular::Document *document ) : QAbstractScrollArea( parent ) , Okular::View( QStringLiteral( "PageView" ) ) { // create and initialize private storage structure d = new PageViewPrivate( this ); d->document = document; d->aRotateClockwise = nullptr; d->aRotateCounterClockwise = nullptr; d->aRotateOriginal = nullptr; d->aViewMode = nullptr; d->zoomMode = PageView::ZoomFitWidth; d->zoomFactor = 1.0; d->mouseSelecting = false; d->mouseTextSelecting = false; d->mouseOnRect = false; d->mouseMode = Okular::Settings::mouseMode(); d->mouseAnnotation = new MouseAnnotation( this, document ); d->tableDividersGuessed = false; d->lastSourceLocationViewportPageNumber = -1; d->lastSourceLocationViewportNormalizedX = 0.0; d->lastSourceLocationViewportNormalizedY = 0.0; d->controlWheelAccumulatedDelta = 0; d->scrollIncrement = 0; d->autoScrollTimer = nullptr; d->annotator = nullptr; d->dirtyLayout = false; d->blockViewport = false; d->blockPixmapsRequest = false; d->messageWindow = new PageViewMessage(this); d->m_formsVisible = false; d->formsWidgetController = nullptr; #ifdef HAVE_SPEECH d->m_tts = nullptr; #endif d->refreshTimer = nullptr; d->aRotateClockwise = nullptr; d->aRotateCounterClockwise = nullptr; d->aRotateOriginal = nullptr; d->aPageSizes = nullptr; d->aTrimMode = nullptr; d->aTrimMargins = nullptr; d->aTrimToSelection = nullptr; d->aMouseNormal = nullptr; d->aMouseSelect = nullptr; d->aMouseTextSelect = nullptr; d->aToggleAnnotator = nullptr; d->aZoomFitWidth = nullptr; d->aZoomFitPage = nullptr; d->aZoomAutoFit = nullptr; d->aViewMode = nullptr; d->aViewContinuous = nullptr; d->aPrevAction = nullptr; d->aToggleForms = nullptr; d->aSpeakDoc = nullptr; d->aSpeakPage = nullptr; d->aSpeakStop = nullptr; d->aSpeakPauseResume = nullptr; d->actionCollection = nullptr; d->aPageSizes=nullptr; d->setting_viewCols = Okular::Settings::viewColumns(); d->rtl_Mode = Okular::Settings::rtlReadingDirection(); d->mouseModeActionGroup = nullptr; d->aMouseModeMenu = nullptr; d->penDown = false; d->aMouseMagnifier = nullptr; d->aFitWindowToPage = nullptr; d->trimBoundingBox = Okular::NormalizedRect(); // Null box switch( Okular::Settings::zoomMode() ) { case 0: { d->zoomFactor = 1; d->zoomMode = PageView::ZoomFixed; break; } case 1: { d->zoomMode = PageView::ZoomFitWidth; break; } case 2: { d->zoomMode = PageView::ZoomFitPage; break; } case 3: { d->zoomMode = PageView::ZoomFitAuto; break; } } d->delayResizeEventTimer = new QTimer( this ); d->delayResizeEventTimer->setSingleShot( true ); d->delayResizeEventTimer->setObjectName("delayResizeEventTimer"); connect( d->delayResizeEventTimer, &QTimer::timeout, this, &PageView::delayedResizeEvent ); setFrameStyle(QFrame::NoFrame); setAttribute( Qt::WA_StaticContents ); setObjectName( QStringLiteral( "okular::pageView" ) ); // viewport setup: setup focus, and track mouse viewport()->setFocusProxy( this ); viewport()->setFocusPolicy( Qt::StrongFocus ); viewport()->setAttribute( Qt::WA_OpaquePaintEvent ); viewport()->setAttribute( Qt::WA_NoSystemBackground ); viewport()->setMouseTracking( true ); viewport()->setAutoFillBackground( false ); d->scroller = QScroller::scroller(viewport()); QScrollerProperties prop; prop.setScrollMetric(QScrollerProperties::DecelerationFactor, 0.3); prop.setScrollMetric(QScrollerProperties::MaximumVelocity, 1); prop.setScrollMetric(QScrollerProperties::OvershootScrollDistanceFactor, 0.05); prop.setScrollMetric(QScrollerProperties::OvershootDragDistanceFactor, 0.05); d->scroller->setScrollerProperties(prop); connect(d->scroller, &QScroller::stateChanged, this, &PageView::slotRequestVisiblePixmaps); // the apparently "magic" value of 20 is the same used internally in QScrollArea verticalScrollBar()->setCursor( Qt::ArrowCursor ); verticalScrollBar()->setSingleStep( 20 ); horizontalScrollBar()->setCursor( Qt::ArrowCursor ); horizontalScrollBar()->setSingleStep( 20 ); // connect the padding of the viewport to pixmaps requests connect(horizontalScrollBar(), &QAbstractSlider::valueChanged, this, &PageView::slotRequestVisiblePixmaps); connect(verticalScrollBar(), &QAbstractSlider::valueChanged, this, &PageView::slotRequestVisiblePixmaps); auto update_scroller = [=](){ d->scroller->scrollTo(QPoint(horizontalScrollBar()->value(), verticalScrollBar()->value()), 0); //sync scroller with scrollbar }; connect(verticalScrollBar(), &QAbstractSlider::sliderReleased, this, update_scroller); connect(horizontalScrollBar(), &QAbstractSlider::sliderReleased, this, update_scroller); connect(verticalScrollBar(), &QAbstractSlider::sliderMoved, this, update_scroller); connect(horizontalScrollBar(), &QAbstractSlider::sliderMoved, this, update_scroller); connect( &d->dragScrollTimer, &QTimer::timeout, this, &PageView::slotDragScroll ); d->leftClickTimer.setSingleShot( true ); connect( &d->leftClickTimer, &QTimer::timeout, this, &PageView::slotShowSizeAllCursor ); // set a corner button to resize the view to the page size // QPushButton * resizeButton = new QPushButton( viewport() ); // resizeButton->setPixmap( SmallIcon("crop") ); // setCornerWidget( resizeButton ); // resizeButton->setEnabled( false ); // connect(...); setAttribute( Qt::WA_InputMethodEnabled, true ); // Grab pinch gestures to zoom and rotate the view grabGesture(Qt::PinchGesture); d->magnifierView = new MagnifierView(document, this); d->magnifierView->hide(); d->magnifierView->setGeometry(0, 0, 351, 201); // TODO: more dynamic? connect(document, &Okular::Document::processMovieAction, this, &PageView::slotProcessMovieAction); connect(document, &Okular::Document::processRenditionAction, this, &PageView::slotProcessRenditionAction); // schedule the welcome message QMetaObject::invokeMethod(this, "slotShowWelcome", Qt::QueuedConnection); } PageView::~PageView() { #ifdef HAVE_SPEECH if ( d->m_tts ) d->m_tts->stopAllSpeechs(); #endif delete d->mouseAnnotation; // delete the local storage structure // We need to assign it to a different list otherwise slotAnnotationWindowDestroyed // will bite us and clear d->m_annowindows QSet< AnnotWindow * > annowindows = d->m_annowindows; d->m_annowindows.clear(); qDeleteAll( annowindows ); // delete all widgets qDeleteAll( d->items ); delete d->formsWidgetController; d->document->removeObserver( this ); delete d; } void PageView::setupBaseActions( KActionCollection * ac ) { d->actionCollection = ac; // Zoom actions ( higher scales takes lots of memory! ) d->aZoom = new KSelectAction(QIcon::fromTheme( QStringLiteral("page-zoom") ), i18n("Zoom"), this); ac->addAction(QStringLiteral("zoom_to"), d->aZoom ); d->aZoom->setEditable( true ); d->aZoom->setMaxComboViewCount( 14 ); connect( d->aZoom, QOverload::of(&KSelectAction::triggered), this, &PageView::slotZoom ); updateZoomText(); d->aZoomIn = KStandardAction::zoomIn( this, SLOT(slotZoomIn()), ac ); d->aZoomOut = KStandardAction::zoomOut( this, SLOT(slotZoomOut()), ac ); d->aZoomActual = KStandardAction::actualSize( this, &PageView::slotZoomActual, ac ); d->aZoomActual->setText(i18n("Zoom to 100%")); } void PageView::setupViewerActions( KActionCollection * ac ) { d->actionCollection = ac; ac->setDefaultShortcut(d->aZoomIn, QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_Plus)); ac->setDefaultShortcut(d->aZoomOut, QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_Minus)); // orientation menu actions d->aRotateClockwise = new QAction( QIcon::fromTheme( QStringLiteral("object-rotate-right") ), i18n( "Rotate &Right" ), this ); d->aRotateClockwise->setIconText( i18nc( "Rotate right", "Right" ) ); ac->addAction( QStringLiteral("view_orientation_rotate_cw"), d->aRotateClockwise ); d->aRotateClockwise->setEnabled( false ); connect( d->aRotateClockwise, &QAction::triggered, this, &PageView::slotRotateClockwise ); d->aRotateCounterClockwise = new QAction( QIcon::fromTheme( QStringLiteral("object-rotate-left") ), i18n( "Rotate &Left" ), this ); d->aRotateCounterClockwise->setIconText( i18nc( "Rotate left", "Left" ) ); ac->addAction( QStringLiteral("view_orientation_rotate_ccw"), d->aRotateCounterClockwise ); d->aRotateCounterClockwise->setEnabled( false ); connect( d->aRotateCounterClockwise, &QAction::triggered, this, &PageView::slotRotateCounterClockwise ); d->aRotateOriginal = new QAction( i18n( "Original Orientation" ), this ); ac->addAction( QStringLiteral("view_orientation_original"), d->aRotateOriginal ); d->aRotateOriginal->setEnabled( false ); connect( d->aRotateOriginal, &QAction::triggered, this, &PageView::slotRotateOriginal ); d->aPageSizes = new KSelectAction(i18n("&Page Size"), this); ac->addAction(QStringLiteral("view_pagesizes"), d->aPageSizes); d->aPageSizes->setEnabled( false ); connect( d->aPageSizes , QOverload::of(&KSelectAction::triggered), this, &PageView::slotPageSizes ); // Trim View actions d->aTrimMode = new KActionMenu(i18n( "&Trim View" ), this ); d->aTrimMode->setDelayed( false ); ac->addAction(QStringLiteral("view_trim_mode"), d->aTrimMode ); d->aTrimMargins = new KToggleAction(QIcon::fromTheme( QStringLiteral("trim-margins") ), i18n( "&Trim Margins" ), d->aTrimMode->menu() ); d->aTrimMode->addAction( d->aTrimMargins ); ac->addAction( QStringLiteral("view_trim_margins"), d->aTrimMargins ); d->aTrimMargins->setData( QVariant::fromValue( (int)Okular::Settings::EnumTrimMode::Margins ) ); connect( d->aTrimMargins, &QAction::toggled, this, &PageView::slotTrimMarginsToggled ); d->aTrimMargins->setChecked( Okular::Settings::trimMargins() ); d->aTrimToSelection = new KToggleAction(QIcon::fromTheme( QStringLiteral("trim-to-selection") ), i18n( "Trim To &Selection" ), d->aTrimMode->menu() ); d->aTrimMode->addAction( d->aTrimToSelection); ac->addAction( QStringLiteral("view_trim_selection"), d->aTrimToSelection); d->aTrimToSelection->setData( QVariant::fromValue( (int)Okular::Settings::EnumTrimMode::Selection ) ); connect( d->aTrimToSelection, &QAction::toggled, this, &PageView::slotTrimToSelectionToggled ); d->aZoomFitWidth = new KToggleAction(QIcon::fromTheme( QStringLiteral("zoom-fit-width") ), i18n("Fit &Width"), this); ac->addAction(QStringLiteral("view_fit_to_width"), d->aZoomFitWidth ); connect( d->aZoomFitWidth, &QAction::toggled, this, &PageView::slotFitToWidthToggled ); d->aZoomFitPage = new KToggleAction(QIcon::fromTheme( QStringLiteral("zoom-fit-best") ), i18n("Fit &Page"), this); ac->addAction(QStringLiteral("view_fit_to_page"), d->aZoomFitPage ); connect( d->aZoomFitPage, &QAction::toggled, this, &PageView::slotFitToPageToggled ); d->aZoomAutoFit = new KToggleAction(QIcon::fromTheme( QStringLiteral("zoom-fit-best") ), i18n("&Auto Fit"), this); ac->addAction(QStringLiteral("view_auto_fit"), d->aZoomAutoFit ); connect( d->aZoomAutoFit, &QAction::toggled, this, &PageView::slotAutoFitToggled ); d->aFitWindowToPage = new QAction(QIcon::fromTheme( QStringLiteral("zoom-fit-width") ), i18n("Fit Wi&ndow to Page"), this); d->aFitWindowToPage->setEnabled( Okular::Settings::viewMode() == (int)Okular::Settings::EnumViewMode::Single ); ac->setDefaultShortcut(d->aFitWindowToPage, QKeySequence(Qt::CTRL + Qt::Key_J) ); ac->addAction( QStringLiteral("fit_window_to_page"), d->aFitWindowToPage ); connect( d->aFitWindowToPage, &QAction::triggered, this, &PageView::slotFitWindowToPage ); // View-Layout actions d->aViewMode = new KActionMenu( QIcon::fromTheme( QStringLiteral("view-split-left-right") ), i18n( "&View Mode" ), this ); d->aViewMode->setDelayed( false ); #define ADD_VIEWMODE_ACTION( text, name, id ) \ do { \ QAction *vm = new QAction( text, this ); \ vm->setCheckable( true ); \ vm->setData( QVariant::fromValue( id ) ); \ d->aViewMode->addAction( vm ); \ ac->addAction( QStringLiteral(name), vm ); \ vmGroup->addAction( vm ); \ } while( 0 ) ac->addAction(QStringLiteral("view_render_mode"), d->aViewMode ); QActionGroup *vmGroup = new QActionGroup( this ); //d->aViewMode->menu() ); ADD_VIEWMODE_ACTION( i18n( "Single Page" ), "view_render_mode_single", (int)Okular::Settings::EnumViewMode::Single ); ADD_VIEWMODE_ACTION( i18n( "Facing Pages" ), "view_render_mode_facing", (int)Okular::Settings::EnumViewMode::Facing ); ADD_VIEWMODE_ACTION( i18n( "Facing Pages (Center First Page)" ), "view_render_mode_facing_center_first", (int)Okular::Settings::EnumViewMode::FacingFirstCentered ); ADD_VIEWMODE_ACTION( i18n( "Overview" ), "view_render_mode_overview", (int)Okular::Settings::EnumViewMode::Summary ); const QList viewModeActions = d->aViewMode->menu()->actions(); for (QAction *viewModeAction : viewModeActions) { if (viewModeAction->data().toInt() == Okular::Settings::viewMode()) { viewModeAction->setChecked( true ); } } connect( vmGroup, &QActionGroup::triggered, this, &PageView::slotViewMode ); #undef ADD_VIEWMODE_ACTION d->aViewContinuous = new KToggleAction(QIcon::fromTheme( QStringLiteral("view-list-text") ), i18n("&Continuous"), this); ac->addAction(QStringLiteral("view_continuous"), d->aViewContinuous ); connect( d->aViewContinuous, &QAction::toggled, this, &PageView::slotContinuousToggled ); d->aViewContinuous->setChecked( Okular::Settings::viewContinuous() ); // Mouse mode actions for viewer mode d->mouseModeActionGroup = new QActionGroup( this ); d->mouseModeActionGroup->setExclusive( true ); d->aMouseNormal = new QAction( QIcon::fromTheme( QStringLiteral("transform-browse") ), i18n( "&Browse" ), this ); ac->addAction(QStringLiteral("mouse_drag"), d->aMouseNormal ); connect( d->aMouseNormal, &QAction::triggered, this, &PageView::slotSetMouseNormal ); d->aMouseNormal->setCheckable( true ); ac->setDefaultShortcut(d->aMouseNormal, QKeySequence(Qt::CTRL + Qt::Key_1)); d->aMouseNormal->setActionGroup( d->mouseModeActionGroup ); d->aMouseNormal->setChecked( Okular::Settings::mouseMode() == Okular::Settings::EnumMouseMode::Browse ); QAction * mz = new QAction(QIcon::fromTheme( QStringLiteral("page-zoom") ), i18n("&Zoom"), this); ac->addAction(QStringLiteral("mouse_zoom"), mz ); connect( mz, &QAction::triggered, this, &PageView::slotSetMouseZoom ); mz->setCheckable( true ); ac->setDefaultShortcut(mz, QKeySequence(Qt::CTRL + Qt::Key_2)); mz->setActionGroup( d->mouseModeActionGroup ); mz->setChecked( Okular::Settings::mouseMode() == Okular::Settings::EnumMouseMode::Zoom ); QAction * aToggleChangeColors = new QAction(i18n("&Toggle Change Colors"), this); ac->addAction(QStringLiteral("toggle_change_colors"), aToggleChangeColors ); connect( aToggleChangeColors, &QAction::triggered, this, &PageView::slotToggleChangeColors ); } // WARNING: 'setupViewerActions' must have been called before this method void PageView::setupActions( KActionCollection * ac ) { d->actionCollection = ac; ac->setDefaultShortcuts(d->aZoomIn, KStandardShortcut::zoomIn()); ac->setDefaultShortcuts(d->aZoomOut, KStandardShortcut::zoomOut()); // Mouse-Mode actions d->aMouseSelect = new QAction(QIcon::fromTheme( QStringLiteral("select-rectangular") ), i18n("Area &Selection"), this); ac->addAction(QStringLiteral("mouse_select"), d->aMouseSelect ); connect( d->aMouseSelect, &QAction::triggered, this, &PageView::slotSetMouseSelect ); d->aMouseSelect->setCheckable( true ); ac->setDefaultShortcut(d->aMouseSelect, Qt::CTRL + Qt::Key_3); d->aMouseSelect->setActionGroup( d->mouseModeActionGroup ); d->aMouseSelect->setChecked( Okular::Settings::mouseMode() == Okular::Settings::EnumMouseMode::RectSelect ); d->aMouseTextSelect = new QAction(QIcon::fromTheme( QStringLiteral("edit-select-text") ), i18n("&Text Selection"), this); ac->addAction(QStringLiteral("mouse_textselect"), d->aMouseTextSelect ); connect( d->aMouseTextSelect, &QAction::triggered, this, &PageView::slotSetMouseTextSelect ); d->aMouseTextSelect->setCheckable( true ); ac->setDefaultShortcut(d->aMouseTextSelect, Qt::CTRL + Qt::Key_4); d->aMouseTextSelect->setActionGroup( d->mouseModeActionGroup ); d->aMouseTextSelect->setChecked( Okular::Settings::mouseMode() == Okular::Settings::EnumMouseMode::TextSelect ); d->aMouseTableSelect = new QAction(QIcon::fromTheme( QStringLiteral("table") ), i18n("T&able Selection"), this); ac->addAction(QStringLiteral("mouse_tableselect"), d->aMouseTableSelect ); connect( d->aMouseTableSelect, &QAction::triggered, this, &PageView::slotSetMouseTableSelect ); d->aMouseTableSelect->setCheckable( true ); ac->setDefaultShortcut(d->aMouseTableSelect, Qt::CTRL + Qt::Key_5); d->aMouseTableSelect->setActionGroup( d->mouseModeActionGroup ); d->aMouseTableSelect->setChecked( Okular::Settings::mouseMode() == Okular::Settings::EnumMouseMode::TableSelect ); d->aMouseMagnifier = new QAction(QIcon::fromTheme( QStringLiteral("document-preview") ), i18n("&Magnifier"), this); ac->addAction(QStringLiteral("mouse_magnifier"), d->aMouseMagnifier ); connect( d->aMouseMagnifier, &QAction::triggered, this, &PageView::slotSetMouseMagnifier ); d->aMouseMagnifier->setCheckable( true ); ac->setDefaultShortcut(d->aMouseMagnifier, Qt::CTRL + Qt::Key_6); d->aMouseMagnifier->setActionGroup( d->mouseModeActionGroup ); d->aMouseMagnifier->setChecked( Okular::Settings::mouseMode() == Okular::Settings::EnumMouseMode::Magnifier ); // Mouse-Mode action menu d->aMouseModeMenu = new ToggleActionMenu( QIcon(),QString(), this, ToggleActionMenu::MenuButtonPopup, ToggleActionMenu::ImplicitDefaultAction ); d->aMouseModeMenu->addAction( d->aMouseSelect ); d->aMouseModeMenu->addAction( d->aMouseTextSelect ); d->aMouseModeMenu->addAction( d->aMouseTableSelect ); d->aMouseModeMenu->suggestDefaultAction( d->aMouseTextSelect ); d->aMouseModeMenu->setText( i18nc( "@action", "Selection Tools" ) ); ac->addAction( QStringLiteral( "mouse_selecttools" ), d->aMouseModeMenu ); d->aToggleAnnotator = new KToggleAction(QIcon::fromTheme( QStringLiteral("draw-freehand") ), i18n("&Review"), this); ac->addAction(QStringLiteral("mouse_toggle_annotate"), d->aToggleAnnotator ); d->aToggleAnnotator->setCheckable( true ); connect( d->aToggleAnnotator, &QAction::toggled, this, &PageView::slotToggleAnnotator ); ac->setDefaultShortcut(d->aToggleAnnotator, Qt::Key_F6); // speak actions #ifdef HAVE_SPEECH d->aSpeakDoc = new QAction( QIcon::fromTheme( QStringLiteral("text-speak") ), i18n( "Speak Whole Document" ), this ); ac->addAction( QStringLiteral("speak_document"), d->aSpeakDoc ); d->aSpeakDoc->setEnabled( false ); connect( d->aSpeakDoc, &QAction::triggered, this, &PageView::slotSpeakDocument ); d->aSpeakPage = new QAction( QIcon::fromTheme( QStringLiteral("text-speak") ), i18n( "Speak Current Page" ), this ); ac->addAction( QStringLiteral("speak_current_page"), d->aSpeakPage ); d->aSpeakPage->setEnabled( false ); connect( d->aSpeakPage, &QAction::triggered, this, &PageView::slotSpeakCurrentPage ); d->aSpeakStop = new QAction( QIcon::fromTheme( QStringLiteral("media-playback-stop") ), i18n( "Stop Speaking" ), this ); ac->addAction( QStringLiteral("speak_stop_all"), d->aSpeakStop ); d->aSpeakStop->setEnabled( false ); connect( d->aSpeakStop, &QAction::triggered, this, &PageView::slotStopSpeaks ); d->aSpeakPauseResume = new QAction( QIcon::fromTheme( QStringLiteral("media-playback-pause") ), i18n( "Pause/Resume Speaking" ), this ); ac->addAction( QStringLiteral("speak_pause_resume"), d->aSpeakPauseResume ); d->aSpeakPauseResume->setEnabled( false ); connect( d->aSpeakPauseResume, &QAction::triggered, this, &PageView::slotPauseResumeSpeech ); #else d->aSpeakDoc = nullptr; d->aSpeakPage = nullptr; d->aSpeakStop = nullptr; d->aSpeakPauseResume = nullptr; #endif // Other actions QAction * su = new QAction(i18n("Scroll Up"), this); ac->addAction(QStringLiteral("view_scroll_up"), su ); connect( su, &QAction::triggered, this, &PageView::slotAutoScrollUp ); ac->setDefaultShortcut(su, QKeySequence(Qt::SHIFT + Qt::Key_Up)); addAction(su); QAction * sd = new QAction(i18n("Scroll Down"), this); ac->addAction(QStringLiteral("view_scroll_down"), sd ); connect( sd, &QAction::triggered, this, &PageView::slotAutoScrollDown ); ac->setDefaultShortcut(sd, QKeySequence(Qt::SHIFT + Qt::Key_Down)); addAction(sd); QAction * spu = new QAction(i18n("Scroll Page Up"), this); ac->addAction( QStringLiteral("view_scroll_page_up"), spu ); connect( spu, &QAction::triggered, this, &PageView::slotScrollUp ); ac->setDefaultShortcut(spu, QKeySequence(Qt::SHIFT + Qt::Key_Space)); addAction( spu ); QAction * spd = new QAction(i18n("Scroll Page Down"), this); ac->addAction( QStringLiteral("view_scroll_page_down"), spd ); connect( spd, &QAction::triggered, this, &PageView::slotScrollDown ); ac->setDefaultShortcut(spd, QKeySequence(Qt::Key_Space)); addAction( spd ); d->aToggleForms = new QAction( this ); ac->addAction( QStringLiteral("view_toggle_forms"), d->aToggleForms ); connect( d->aToggleForms, &QAction::triggered, this, &PageView::slotToggleForms ); d->aToggleForms->setEnabled( false ); toggleFormWidgets( false ); // Setup undo and redo actions QAction *kundo = KStandardAction::create( KStandardAction::Undo, d->document, SLOT(undo()), ac ); QAction *kredo = KStandardAction::create( KStandardAction::Redo, d->document, SLOT(redo()), ac ); connect(d->document, &Okular::Document::canUndoChanged, kundo, &QAction::setEnabled); connect(d->document, &Okular::Document::canRedoChanged, kredo, &QAction::setEnabled); kundo->setEnabled(false); kredo->setEnabled(false); } bool PageView::canFitPageWidth() const { return Okular::Settings::viewMode() != Okular::Settings::EnumViewMode::Single || d->zoomMode != ZoomFitWidth; } void PageView::fitPageWidth( int page ) { // zoom: Fit Width, columns: 1. setActions + relayout + setPage + update d->zoomMode = ZoomFitWidth; Okular::Settings::setViewMode( 0 ); d->aZoomFitWidth->setChecked( true ); d->aZoomFitPage->setChecked( false ); d->aZoomAutoFit->setChecked( false ); d->aViewMode->menu()->actions().at( 0 )->setChecked( true ); viewport()->setUpdatesEnabled( false ); slotRelayoutPages(); viewport()->setUpdatesEnabled( true ); d->document->setViewportPage( page ); updateZoomText(); setFocus(); } void PageView::openAnnotationWindow( Okular::Annotation * annotation, int pageNumber ) { if ( !annotation ) return; // find the annot window AnnotWindow* existWindow = nullptr; for (AnnotWindow *aw : qAsConst(d->m_annowindows)) { if ( aw->annotation() == annotation ) { existWindow = aw; break; } } if ( existWindow == nullptr ) { existWindow = new AnnotWindow( this, annotation, d->document, pageNumber ); connect(existWindow, &QObject::destroyed, this, &PageView::slotAnnotationWindowDestroyed); d->m_annowindows << existWindow; } else { existWindow->raise(); existWindow->findChild()->setFocus(); } existWindow->show(); } void PageView::slotAnnotationWindowDestroyed( QObject * window ) { d->m_annowindows.remove( static_cast( window ) ); } void PageView::displayMessage( const QString & message, const QString & details, PageViewMessage::Icon icon, int duration ) { if ( !Okular::Settings::showOSD() ) { if (icon == PageViewMessage::Error) { if ( !details.isEmpty() ) KMessageBox::detailedError( this, message, details ); else KMessageBox::error( this, message ); } return; } // hide messageWindow if string is empty - if ( message.isEmpty() ) - return d->messageWindow->hide(); + if ( message.isEmpty() ) { + d->messageWindow->hide(); + return; + } // display message (duration is length dependent) if (duration==-1) { duration = 500 + 100 * message.length(); if ( !details.isEmpty() ) duration += 500 + 100 * details.length(); } d->messageWindow->display( message, details, icon, duration ); } void PageView::reparseConfig() { // set the scroll bars policies Qt::ScrollBarPolicy scrollBarMode = Okular::Settings::showScrollBars() ? Qt::ScrollBarAsNeeded : Qt::ScrollBarAlwaysOff; if ( horizontalScrollBarPolicy() != scrollBarMode ) { setHorizontalScrollBarPolicy( scrollBarMode ); setVerticalScrollBarPolicy( scrollBarMode ); } if ( Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Summary && ( (int)Okular::Settings::viewColumns() != d->setting_viewCols ) ) { d->setting_viewCols = Okular::Settings::viewColumns(); slotRelayoutPages(); } if (Okular::Settings::rtlReadingDirection() != d->rtl_Mode ) { d->rtl_Mode = Okular::Settings::rtlReadingDirection(); slotRelayoutPages(); } updatePageStep(); if ( d->annotator ) { d->annotator->setEnabled( false ); d->annotator->reparseConfig(); if ( d->aToggleAnnotator->isChecked() ) slotToggleAnnotator( true ); } // Something like invert colors may have changed // As we don't have a way to find out the old value // We just update the viewport, this shouldn't be that bad // since it's just a repaint of pixmaps we already have viewport()->update(); } KActionCollection *PageView::actionCollection() const { return d->actionCollection; } QAction *PageView::toggleFormsAction() const { return d->aToggleForms; } int PageView::contentAreaWidth() const { return horizontalScrollBar()->maximum() + viewport()->width(); } int PageView::contentAreaHeight() const { return verticalScrollBar()->maximum() + viewport()->height(); } QPoint PageView::contentAreaPosition() const { return QPoint( horizontalScrollBar()->value(), verticalScrollBar()->value() ); } -QPoint PageView::contentAreaPoint( const QPoint & pos ) const +QPoint PageView::contentAreaPoint( const QPoint pos ) const { return pos + contentAreaPosition(); } -QPointF PageView::contentAreaPoint( const QPointF & pos ) const +QPointF PageView::contentAreaPoint( const QPointF pos ) const { return pos + contentAreaPosition(); } QString PageViewPrivate::selectedText() const { if ( pagesWithTextSelection.isEmpty() ) return QString(); QString text; QList< int > selpages = pagesWithTextSelection.values(); std::sort(selpages.begin(), selpages.end()); const Okular::Page * pg = nullptr; if ( selpages.count() == 1 ) { pg = document->page( selpages.first() ); text.append( pg->text( pg->textSelection(), Okular::TextPage::CentralPixelTextAreaInclusionBehaviour ) ); } else { pg = document->page( selpages.first() ); text.append( pg->text( pg->textSelection(), Okular::TextPage::CentralPixelTextAreaInclusionBehaviour ) ); int end = selpages.count() - 1; for( int i = 1; i < end; ++i ) { pg = document->page( selpages.at( i ) ); text.append( pg->text( nullptr, Okular::TextPage::CentralPixelTextAreaInclusionBehaviour ) ); } pg = document->page( selpages.last() ); text.append( pg->text( pg->textSelection(), Okular::TextPage::CentralPixelTextAreaInclusionBehaviour ) ); } return text; } void PageView::copyTextSelection() const { const QString text = d->selectedText(); if ( !text.isEmpty() ) { QClipboard *cb = QApplication::clipboard(); cb->setText( text, QClipboard::Clipboard ); } } void PageView::selectAll() { for ( const PageViewItem * item : qAsConst( d->items ) ) { Okular::RegularAreaRect * area = textSelectionForItem( item ); d->pagesWithTextSelection.insert( item->pageNumber() ); d->document->setPageTextSelection( item->pageNumber(), area, palette().color( QPalette::Active, QPalette::Highlight ) ); } } void PageView::createAnnotationsVideoWidgets(PageViewItem *item, const QLinkedList< Okular::Annotation * > &annotations) { qDeleteAll( item->videoWidgets() ); item->videoWidgets().clear(); for ( Okular::Annotation * a : annotations ) { if ( a->subType() == Okular::Annotation::AMovie ) { Okular::MovieAnnotation * movieAnn = static_cast< Okular::MovieAnnotation * >( a ); VideoWidget * vw = new VideoWidget( movieAnn, movieAnn->movie(), d->document, viewport() ); item->videoWidgets().insert( movieAnn->movie(), vw ); vw->pageInitialized(); } else if ( a->subType() == Okular::Annotation::ARichMedia ) { Okular::RichMediaAnnotation * richMediaAnn = static_cast< Okular::RichMediaAnnotation * >( a ); VideoWidget * vw = new VideoWidget( richMediaAnn, richMediaAnn->movie(), d->document, viewport() ); item->videoWidgets().insert( richMediaAnn->movie(), vw ); vw->pageInitialized(); } else if ( a->subType() == Okular::Annotation::AScreen ) { const Okular::ScreenAnnotation * screenAnn = static_cast< Okular::ScreenAnnotation * >( a ); Okular::Movie *movie = GuiUtils::renditionMovieFromScreenAnnotation( screenAnn ); if ( movie ) { VideoWidget * vw = new VideoWidget( screenAnn, movie, d->document, viewport() ); item->videoWidgets().insert( movie, vw ); vw->pageInitialized(); } } } } //BEGIN DocumentObserver inherited methods void PageView::notifySetup( const QVector< Okular::Page * > & pageSet, int setupFlags ) { bool documentChanged = setupFlags & Okular::DocumentObserver::DocumentChanged; const bool allownotes = d->document->isAllowed( Okular::AllowNotes ); const bool allowfillforms = d->document->isAllowed( Okular::AllowFillForms ); // allownotes may have changed if ( d->aToggleAnnotator ) d->aToggleAnnotator->setEnabled( allownotes ); // reuse current pages if nothing new if ( ( pageSet.count() == d->items.count() ) && !documentChanged && !( setupFlags & Okular::DocumentObserver::NewLayoutForPages ) ) { int count = pageSet.count(); for ( int i = 0; (i < count) && !documentChanged; i++ ) { if ( (int)pageSet[i]->number() != d->items[i]->pageNumber() ) { documentChanged = true; } else { // even if the document has not changed, allowfillforms may have // changed, so update all fields' "canBeFilled" flag const QSet formWidgetsList = d->items[i]->formWidgets(); for ( FormWidgetIface *w : formWidgetsList ) w->setCanBeFilled( allowfillforms ); } } if ( !documentChanged ) { if ( setupFlags & Okular::DocumentObserver::UrlChanged ) { // Here with UrlChanged and no document changed it means we // need to update all the Annotation* and Form* otherwise // they still point to the old document ones, luckily the old ones are still // around so we can look for the new ones using unique ids, etc d->mouseAnnotation->updateAnnotationPointers(); for (AnnotWindow *aw : qAsConst(d->m_annowindows)) { Okular::Annotation *newA = d->document->page( aw->pageNumber() )->annotation( aw->annotation()->uniqueName() ); aw->updateAnnotation( newA ); } const QRect viewportRect( horizontalScrollBar()->value(), verticalScrollBar()->value(), viewport()->width(), viewport()->height() ); for ( int i = 0; i < count; i++ ) { PageViewItem *item = d->items[i]; const QSet fws = item->formWidgets(); for ( FormWidgetIface *w : fws ) { Okular::FormField *f = Okular::PagePrivate::findEquivalentForm( d->document->page( i ), w->formField() ); if (f) { w->setFormField( f ); } else { qWarning() << "Lost form field on document save, something is wrong"; item->formWidgets().remove(w); delete w; } } // For the video widgets we don't really care about reusing them since they don't contain much info so just // create them again createAnnotationsVideoWidgets( item, pageSet[i]->annotations() ); const QHash videoWidgets = item->videoWidgets(); for ( VideoWidget *vw : videoWidgets ) { const Okular::NormalizedRect r = vw->normGeometry(); vw->setGeometry( qRound( item->uncroppedGeometry().left() + item->uncroppedWidth() * r.left ) + 1 - viewportRect.left(), qRound( item->uncroppedGeometry().top() + item->uncroppedHeight() * r.top ) + 1 - viewportRect.top(), qRound( fabs( r.right - r.left ) * item->uncroppedGeometry().width() ), qRound( fabs( r.bottom - r.top ) * item->uncroppedGeometry().height() ) ); // Workaround, otherwise the size somehow gets lost vw->show(); vw->hide(); } } } return; } } // mouseAnnotation must not access our PageViewItem widgets any longer d->mouseAnnotation->reset(); // delete all widgets (one for each page in pageSet) qDeleteAll( d->items ); d->items.clear(); d->visibleItems.clear(); d->pagesWithTextSelection.clear(); toggleFormWidgets( false ); if ( d->formsWidgetController ) d->formsWidgetController->dropRadioButtons(); bool haspages = !pageSet.isEmpty(); bool hasformwidgets = false; // create children widgets for ( const Okular::Page * page : pageSet ) { PageViewItem * item = new PageViewItem( page ); d->items.push_back( item ); #ifdef PAGEVIEW_DEBUG qCDebug(OkularUiDebug).nospace() << "cropped geom for " << d->items.last()->pageNumber() << " is " << d->items.last()->croppedGeometry(); #endif const QLinkedList< Okular::FormField * > pageFields = page->formFields(); for ( Okular::FormField * ff : pageFields ) { FormWidgetIface * w = FormWidgetFactory::createWidget( ff, viewport() ); if ( w ) { w->setPageItem( item ); w->setFormWidgetsController( d->formWidgetsController() ); w->setVisibility( false ); w->setCanBeFilled( allowfillforms ); item->formWidgets().insert( w ); hasformwidgets = true; } } createAnnotationsVideoWidgets( item, page->annotations() ); } // invalidate layout so relayout/repaint will happen on next viewport change if ( haspages ) { // We do a delayed call to slotRelayoutPages but also set the dirtyLayout // because we might end up in notifyViewportChanged while slotRelayoutPages // has not been done and we don't want that to happen d->dirtyLayout = true; QMetaObject::invokeMethod(this, "slotRelayoutPages", Qt::QueuedConnection); } else { // update the mouse cursor when closing because we may have close through a link and // want the cursor to come back to the normal cursor updateCursor(); // then, make the message window and scrollbars disappear, and trigger a repaint d->messageWindow->hide(); resizeContentArea( QSize( 0,0 ) ); viewport()->update(); // when there is no change to the scrollbars, no repaint would // be done and the old document would still be shown } // OSD to display pages if ( documentChanged && pageSet.count() > 0 && Okular::Settings::showOSD() ) d->messageWindow->display( i18np(" Loaded a one-page document.", " Loaded a %1-page document.", pageSet.count() ), QString(), PageViewMessage::Info, 4000 ); updateActionState( haspages, documentChanged, hasformwidgets ); // We need to assign it to a different list otherwise slotAnnotationWindowDestroyed // will bite us and clear d->m_annowindows QSet< AnnotWindow * > annowindows = d->m_annowindows; d->m_annowindows.clear(); qDeleteAll( annowindows ); selectionClear(); } void PageView::updateActionState( bool haspages, bool documentChanged, bool hasformwidgets ) { if ( d->aPageSizes ) { // may be null if dummy mode is on bool pageSizes = d->document->supportsPageSizes(); d->aPageSizes->setEnabled( pageSizes ); // set the new page sizes: // - if the generator supports them // - if the document changed if ( pageSizes && documentChanged ) { QStringList items; const QList sizes = d->document->pageSizes(); for ( const Okular::PageSize &p : sizes ) items.append( p.name() ); d->aPageSizes->setItems( items ); } } if ( d->aTrimMargins ) d->aTrimMargins->setEnabled( haspages ); if ( d->aTrimToSelection ) d->aTrimToSelection->setEnabled( haspages ); if ( d->aViewMode ) d->aViewMode->setEnabled( haspages ); if ( d->aViewContinuous ) d->aViewContinuous->setEnabled( haspages ); if ( d->aZoomFitWidth ) d->aZoomFitWidth->setEnabled( haspages ); if ( d->aZoomFitPage ) d->aZoomFitPage->setEnabled( haspages ); if ( d->aZoomAutoFit ) d->aZoomAutoFit->setEnabled( haspages ); if ( d->aZoom ) { d->aZoom->selectableActionGroup()->setEnabled( haspages ); d->aZoom->setEnabled( haspages ); } if ( d->aZoomIn ) d->aZoomIn->setEnabled( haspages ); if ( d->aZoomOut ) d->aZoomOut->setEnabled( haspages ); if ( d->aZoomActual ) d->aZoomActual->setEnabled( haspages && d->zoomFactor != 1.0 ); if ( d->mouseModeActionGroup ) d->mouseModeActionGroup->setEnabled( haspages ); if ( d->aMouseModeMenu ) d->aMouseModeMenu->setEnabled( haspages ); if ( d->aRotateClockwise ) d->aRotateClockwise->setEnabled( haspages ); if ( d->aRotateCounterClockwise ) d->aRotateCounterClockwise->setEnabled( haspages ); if ( d->aRotateOriginal ) d->aRotateOriginal->setEnabled( haspages ); if ( d->aToggleForms ) { // may be null if dummy mode is on d->aToggleForms->setEnabled( haspages && hasformwidgets ); } bool allowAnnotations = d->document->isAllowed( Okular::AllowNotes ); if ( d->annotator ) { bool allowTools = haspages && allowAnnotations; d->annotator->setToolsEnabled( allowTools ); d->annotator->setTextToolsEnabled( allowTools && d->document->supportsSearching() ); } if ( d->aToggleAnnotator ) { if ( !allowAnnotations && d->aToggleAnnotator->isChecked() ) { d->aToggleAnnotator->trigger(); } d->aToggleAnnotator->setEnabled( allowAnnotations ); } #ifdef HAVE_SPEECH if ( d->aSpeakDoc ) { const bool enablettsactions = haspages ? Okular::Settings::useTTS() : false; d->aSpeakDoc->setEnabled( enablettsactions ); d->aSpeakPage->setEnabled( enablettsactions ); } #endif if (d->aMouseMagnifier) d->aMouseMagnifier->setEnabled(d->document->supportsTiles()); if ( d->aFitWindowToPage ) d->aFitWindowToPage->setEnabled( haspages && !Okular::Settings::viewContinuous() ); } bool PageView::areSourceLocationsShownGraphically() const { return Okular::Settings::showSourceLocationsGraphically(); } void PageView::setShowSourceLocationsGraphically(bool show) { if( show == Okular::Settings::showSourceLocationsGraphically() ) { return; } Okular::Settings::setShowSourceLocationsGraphically( show ); viewport()->update(); } void PageView::setLastSourceLocationViewport( const Okular::DocumentViewport& vp ) { if( vp.rePos.enabled ) { d->lastSourceLocationViewportNormalizedX = normClamp( vp.rePos.normalizedX, 0.5 ); d->lastSourceLocationViewportNormalizedY = normClamp( vp.rePos.normalizedY, 0.0 ); } else { d->lastSourceLocationViewportNormalizedX = 0.5; d->lastSourceLocationViewportNormalizedY = 0.0; } d->lastSourceLocationViewportPageNumber = vp.pageNumber; viewport()->update(); } void PageView::clearLastSourceLocationViewport() { d->lastSourceLocationViewportPageNumber = -1; d->lastSourceLocationViewportNormalizedX = 0.0; d->lastSourceLocationViewportNormalizedY = 0.0; viewport()->update(); } void PageView::notifyViewportChanged( bool smoothMove ) { QMetaObject::invokeMethod(this, "slotRealNotifyViewportChanged", Qt::QueuedConnection, Q_ARG( bool, smoothMove )); } void PageView::slotRealNotifyViewportChanged( bool smoothMove ) { // if we are the one changing viewport, skip this notify if ( d->blockViewport ) return; // block setViewport outgoing calls d->blockViewport = true; // find PageViewItem matching the viewport description const Okular::DocumentViewport & vp = d->document->viewport(); const PageViewItem * item = nullptr; for ( const PageViewItem * tmpItem : qAsConst( d->items ) ) if ( tmpItem->pageNumber() == vp.pageNumber ) { item = tmpItem; break; } if ( !item ) { qCWarning(OkularUiDebug) << "viewport for page" << vp.pageNumber << "has no matching item!"; d->blockViewport = false; return; } #ifdef PAGEVIEW_DEBUG qCDebug(OkularUiDebug) << "document viewport changed"; #endif // relayout in "Single Pages" mode or if a relayout is pending d->blockPixmapsRequest = true; if ( !Okular::Settings::viewContinuous() || d->dirtyLayout ) slotRelayoutPages(); // restore viewport center or use default {x-center,v-top} alignment const QPoint centerCoord = viewportToContentArea( vp ); // if smooth movement requested, setup parameters and start it center( centerCoord.x(), centerCoord.y(), smoothMove ); d->blockPixmapsRequest = false; // request visible pixmaps in the current viewport and recompute it slotRequestVisiblePixmaps(); // enable setViewport calls d->blockViewport = false; if( viewport() ) { viewport()->update(); } // since the page has moved below cursor, update it updateCursor(); } void PageView::notifyPageChanged( int pageNumber, int changedFlags ) { // only handle pixmap / highlight changes notifies if ( changedFlags & DocumentObserver::Bookmark ) return; if ( changedFlags & DocumentObserver::Annotations ) { const QLinkedList< Okular::Annotation * > annots = d->document->page( pageNumber )->annotations(); const QLinkedList< Okular::Annotation * >::ConstIterator annItEnd = annots.end(); QSet< AnnotWindow * >::Iterator it = d->m_annowindows.begin(); for ( ; it != d->m_annowindows.end(); ) { QLinkedList< Okular::Annotation * >::ConstIterator annIt = std::find( annots.begin(), annots.end(), (*it)->annotation() ); if ( annIt != annItEnd ) { (*it)->reloadInfo(); ++it; } else { AnnotWindow *w = *it; it = d->m_annowindows.erase( it ); // Need to delete after removing from the list // otherwise deleting will call slotAnnotationWindowDestroyed which will mess // the list and the iterators delete w; } } d->mouseAnnotation->notifyAnnotationChanged( pageNumber ); } if ( changedFlags & DocumentObserver::BoundingBox ) { #ifdef PAGEVIEW_DEBUG qCDebug(OkularUiDebug) << "BoundingBox change on page" << pageNumber; #endif slotRelayoutPages(); slotRequestVisiblePixmaps(); // TODO: slotRelayoutPages() may have done this already! // Repaint the whole widget since layout may have changed viewport()->update(); return; } // iterate over visible items: if page(pageNumber) is one of them, repaint it for ( const PageViewItem * visibleItem : qAsConst( d->visibleItems ) ) if ( visibleItem->pageNumber() == pageNumber && visibleItem->isVisible() ) { // update item's rectangle plus the little outline QRect expandedRect = visibleItem->croppedGeometry(); // a PageViewItem is placed in the global page layout, // while we need to map its position in the viewport coordinates // (to get the correct area to repaint) expandedRect.translate( -contentAreaPosition() ); expandedRect.adjust( -1, -1, 3, 3 ); viewport()->update( expandedRect ); // if we were "zoom-dragging" do not overwrite the "zoom-drag" cursor if ( cursor().shape() != Qt::SizeVerCursor ) { // since the page has been regenerated below cursor, update it updateCursor(); } break; } } void PageView::notifyContentsCleared( int changedFlags ) { // if pixmaps were cleared, re-ask them if ( changedFlags & DocumentObserver::Pixmap ) QMetaObject::invokeMethod(this, "slotRequestVisiblePixmaps", Qt::QueuedConnection); } void PageView::notifyZoom( int factor ) { if ( factor > 0 ) updateZoom( ZoomIn ); else updateZoom( ZoomOut ); } bool PageView::canUnloadPixmap( int pageNumber ) const { if ( Okular::SettingsCore::memoryLevel() == Okular::SettingsCore::EnumMemoryLevel::Low || Okular::SettingsCore::memoryLevel() == Okular::SettingsCore::EnumMemoryLevel::Normal ) { // if the item is visible, forbid unloading for ( const PageViewItem * visibleItem : qAsConst( d->visibleItems ) ) if ( visibleItem->pageNumber() == pageNumber ) return false; } else { // forbid unloading of the visible items, and of the previous and next for ( const PageViewItem * visibleItem : qAsConst( d->visibleItems ) ) if ( abs( visibleItem->pageNumber() - pageNumber ) <= 1 ) return false; } // if hidden premit unloading return true; } void PageView::notifyCurrentPageChanged( int previous, int current ) { if ( previous != -1 ) { PageViewItem * item = d->items.at( previous ); if ( item ) { const QHash videoWidgetsList = item->videoWidgets(); for ( VideoWidget *videoWidget : videoWidgetsList ) videoWidget->pageLeft(); } // On close, run the widget scripts, needed for running animated PDF const Okular::Page *page = d->document->page( previous ); const QLinkedList annotations = page->annotations(); for ( Okular::Annotation *annotation : annotations ) { if ( annotation->subType() == Okular::Annotation::AWidget ) { Okular::WidgetAnnotation *widgetAnnotation = static_cast( annotation ); d->document->processAction( widgetAnnotation->additionalAction( Okular::Annotation::PageClosing ) ); } } } if ( current != -1 ) { PageViewItem * item = d->items.at( current ); if ( item ) { const QHash videoWidgetsList = item->videoWidgets(); for ( VideoWidget *videoWidget : videoWidgetsList ) videoWidget->pageEntered(); } // update zoom text and factor if in a ZoomFit/* zoom mode if ( d->zoomMode != ZoomFixed ) updateZoomText(); // Opening any widget scripts, needed for running animated PDF const Okular::Page *page = d->document->page( current ); const QLinkedList annotations = page->annotations(); for ( Okular::Annotation *annotation : annotations ) { if ( annotation->subType() == Okular::Annotation::AWidget ) { Okular::WidgetAnnotation *widgetAnnotation = static_cast( annotation ); d->document->processAction( widgetAnnotation->additionalAction( Okular::Annotation::PageOpening ) ); } } } } //END DocumentObserver inherited methods //BEGIN View inherited methods bool PageView::supportsCapability( ViewCapability capability ) const { switch ( capability ) { case Zoom: case ZoomModality: case Continuous: case ViewModeModality: case TrimMargins: return true; } return false; } Okular::View::CapabilityFlags PageView::capabilityFlags( ViewCapability capability ) const { switch ( capability ) { case Zoom: case ZoomModality: case Continuous: case ViewModeModality: case TrimMargins: return CapabilityRead | CapabilityWrite | CapabilitySerializable; } return NoFlag; } QVariant PageView::capability( ViewCapability capability ) const { switch ( capability ) { case Zoom: return d->zoomFactor; case ZoomModality: return d->zoomMode; case Continuous: return d->aViewContinuous ? d->aViewContinuous->isChecked() : true; case ViewModeModality: { const int nActions = d->aViewMode ? d->aViewMode->menu()->actions().size() : 0; for (int i=0; i < nActions; ++i) { const QAction* action = d->aViewMode->menu()->actions().at(i); if ( action->isChecked() ) return action->data(); } return QVariant(); } case TrimMargins: return d->aTrimMargins ? d->aTrimMargins->isChecked() : false; } return QVariant(); } void PageView::setCapability( ViewCapability capability, const QVariant &option ) { switch ( capability ) { case Zoom: { bool ok = true; double factor = option.toDouble( &ok ); if ( ok && factor > 0.0 ) { d->zoomFactor = static_cast< float >( factor ); updateZoom( ZoomRefreshCurrent ); } break; } case ZoomModality: { bool ok = true; int mode = option.toInt( &ok ); if ( ok ) { if ( mode >= 0 && mode < 3 ) updateZoom( (ZoomMode)mode ); } break; } case ViewModeModality: { bool ok = true; int mode = option.toInt( &ok ); if ( ok ) { if ( mode >= 0 && mode < Okular::Settings::EnumViewMode::COUNT) updateViewMode(mode); } break; } case Continuous: { bool mode = option.toBool( ); d->aViewContinuous->setChecked(mode); slotContinuousToggled(mode); break; } case TrimMargins: { bool value = option.toBool( ); d->aTrimMargins->setChecked(value); slotTrimMarginsToggled(value); break; } } } //END View inherited methods //BEGIN widget events bool PageView::event( QEvent * event ) { if ( event->type() == QEvent::Gesture ) { return gestureEvent(static_cast( event )); } // do not stop the event return QAbstractScrollArea::event( event ); } bool PageView::gestureEvent( QGestureEvent * event ) { QPinchGesture *pinch = static_cast(event->gesture(Qt::PinchGesture)); if (pinch) { // Viewport zoom level at the moment where the pinch gesture starts. // The viewport zoom level _during_ the gesture will be this value // times the relative zoom reported by QGestureEvent. static qreal vanillaZoom = d->zoomFactor; if (pinch->state() == Qt::GestureStarted) { vanillaZoom = d->zoomFactor; } const QPinchGesture::ChangeFlags changeFlags = pinch->changeFlags(); // Zoom if (pinch->changeFlags() & QPinchGesture::ScaleFactorChanged) { d->zoomFactor = vanillaZoom * pinch->totalScaleFactor(); d->blockPixmapsRequest = true; updateZoom( ZoomRefreshCurrent ); d->blockPixmapsRequest = false; viewport()->update(); } // Count the number of 90-degree rotations we did since the start of the pinch gesture. // Otherwise a pinch turned to 90 degrees and held there will rotate the page again and again. static int rotations = 0; if (changeFlags & QPinchGesture::RotationAngleChanged) { // Rotation angle relative to the accumulated page rotations triggered by the current pinch // We actually turn at 80 degrees rather than at 90 degrees. That's less strain on the hands. const qreal relativeAngle = pinch->rotationAngle() - rotations*90; if (relativeAngle > 80) { slotRotateClockwise(); rotations++; } if (relativeAngle < -80) { slotRotateCounterClockwise(); rotations--; } } if (pinch->state() == Qt::GestureFinished) { rotations = 0; } return true; } return false; } void PageView::paintEvent(QPaintEvent *pe) { const QPoint areaPos = contentAreaPosition(); // create the rect into contents from the clipped screen rect QRect viewportRect = viewport()->rect(); viewportRect.translate( areaPos ); QRect contentsRect = pe->rect().translated( areaPos ).intersected( viewportRect ); if ( !contentsRect.isValid() ) return; #ifdef PAGEVIEW_DEBUG qCDebug(OkularUiDebug) << "paintevent" << contentsRect; #endif // create the screen painter. a pixel painted at contentsX,contentsY // appears to the top-left corner of the scrollview. QPainter screenPainter( viewport() ); // translate to simulate the scrolled content widget screenPainter.translate( -areaPos ); // selectionRect is the normalized mouse selection rect QRect selectionRect = d->mouseSelectionRect; if ( !selectionRect.isNull() ) selectionRect = selectionRect.normalized(); // selectionRectInternal without the border QRect selectionRectInternal = selectionRect; selectionRectInternal.adjust( 1, 1, -1, -1 ); // color for blending QColor selBlendColor = (selectionRect.width() > 8 || selectionRect.height() > 8) ? d->mouseSelectionColor : Qt::red; // subdivide region into rects QRegion rgn = pe->region(); // preprocess rects area to see if it worths or not using subdivision uint summedArea = 0; for ( const QRect & r : rgn ) { summedArea += r.width() * r.height(); } // very elementary check: SUMj(Region[j].area) is less than boundingRect.area const bool useSubdivision = summedArea < (0.6 * contentsRect.width() * contentsRect.height()); if ( !useSubdivision ) { rgn = contentsRect; } // iterate over the rects (only one loop if not using subdivision) for ( const QRect & r : rgn ) { if ( useSubdivision ) { // set 'contentsRect' to a part of the sub-divided region contentsRect = r.translated( areaPos ).intersected( viewportRect ); if ( !contentsRect.isValid() ) continue; } #ifdef PAGEVIEW_DEBUG qCDebug(OkularUiDebug) << contentsRect; #endif // note: this check will take care of all things requiring alpha blending (not only selection) bool wantCompositing = !selectionRect.isNull() && contentsRect.intersects( selectionRect ); // also alpha-blend when there is a table selection... wantCompositing |= !d->tableSelectionParts.isEmpty(); if ( wantCompositing && Okular::Settings::enableCompositing() ) { // create pixmap and open a painter over it (contents{left,top} becomes pixmap {0,0}) QPixmap doubleBuffer( contentsRect.size() * devicePixelRatioF() ); doubleBuffer.setDevicePixelRatio(devicePixelRatioF()); QPainter pixmapPainter( &doubleBuffer ); pixmapPainter.translate( -contentsRect.left(), -contentsRect.top() ); // 1) Layer 0: paint items and clear bg on unpainted rects drawDocumentOnPainter( contentsRect, &pixmapPainter ); // 2a) Layer 1a: paint (blend) transparent selection (rectangle) if ( !selectionRect.isNull() && selectionRect.intersects( contentsRect ) && !selectionRectInternal.contains( contentsRect ) ) { QRect blendRect = selectionRectInternal.intersected( contentsRect ); // skip rectangles covered by the selection's border if ( blendRect.isValid() ) { // grab current pixmap into a new one to colorize contents QPixmap blendedPixmap( blendRect.width() * devicePixelRatioF(), blendRect.height() * devicePixelRatioF() ); blendedPixmap.setDevicePixelRatio(devicePixelRatioF()); QPainter p( &blendedPixmap ); p.drawPixmap( 0, 0, doubleBuffer, (blendRect.left() - contentsRect.left()) * devicePixelRatioF(), (blendRect.top() - contentsRect.top()) * devicePixelRatioF(), blendRect.width() * devicePixelRatioF(), blendRect.height() * devicePixelRatioF() ); QColor blCol = selBlendColor.darker( 140 ); blCol.setAlphaF( 0.2 ); p.fillRect( blendedPixmap.rect(), blCol ); p.end(); // copy the blended pixmap back to its place pixmapPainter.drawPixmap( blendRect.left(), blendRect.top(), blendedPixmap ); } // draw border (red if the selection is too small) pixmapPainter.setPen( selBlendColor ); pixmapPainter.drawRect( selectionRect.adjusted( 0, 0, -1, -1 ) ); } // 2b) Layer 1b: paint (blend) transparent selection (table) for (const TableSelectionPart &tsp : qAsConst(d->tableSelectionParts)) { QRect selectionPartRect = tsp.rectInItem.geometry(tsp.item->uncroppedWidth(), tsp.item->uncroppedHeight()); selectionPartRect.translate( tsp.item->uncroppedGeometry().topLeft () ); QRect selectionPartRectInternal = selectionPartRect; selectionPartRectInternal.adjust( 1, 1, -1, -1 ); if ( !selectionPartRect.isNull() && selectionPartRect.intersects( contentsRect ) && !selectionPartRectInternal.contains( contentsRect ) ) { QRect blendRect = selectionPartRectInternal.intersected( contentsRect ); // skip rectangles covered by the selection's border if ( blendRect.isValid() ) { // grab current pixmap into a new one to colorize contents QPixmap blendedPixmap( blendRect.width() * devicePixelRatioF(), blendRect.height() * devicePixelRatioF() ); blendedPixmap.setDevicePixelRatio(devicePixelRatioF()); QPainter p( &blendedPixmap ); p.drawPixmap( 0, 0, doubleBuffer, (blendRect.left() - contentsRect.left()) * devicePixelRatioF(), (blendRect.top() - contentsRect.top()) * devicePixelRatioF(), blendRect.width() * devicePixelRatioF(), blendRect.height() * devicePixelRatioF() ); QColor blCol = d->mouseSelectionColor.darker( 140 ); blCol.setAlphaF( 0.2 ); p.fillRect( blendedPixmap.rect(), blCol ); p.end(); // copy the blended pixmap back to its place pixmapPainter.drawPixmap( blendRect.left(), blendRect.top(), blendedPixmap ); } // draw border (red if the selection is too small) pixmapPainter.setPen( d->mouseSelectionColor ); pixmapPainter.drawRect( selectionPartRect.adjusted( 0, 0, -1, -1 ) ); } } drawTableDividers( &pixmapPainter ); // 3a) Layer 1: give annotator painting control if ( d->annotator && d->annotator->routePaints( contentsRect ) ) d->annotator->routePaint( &pixmapPainter, contentsRect ); // 3b) Layer 1: give mouseAnnotation painting control d->mouseAnnotation->routePaint( &pixmapPainter, contentsRect ); // 4) Layer 2: overlays if ( Okular::Settings::debugDrawBoundaries() ) { pixmapPainter.setPen( Qt::blue ); pixmapPainter.drawRect( contentsRect ); } // finish painting and draw contents pixmapPainter.end(); screenPainter.drawPixmap( contentsRect.left(), contentsRect.top(), doubleBuffer ); } else { // 1) Layer 0: paint items and clear bg on unpainted rects drawDocumentOnPainter( contentsRect, &screenPainter ); // 2a) Layer 1a: paint opaque selection (rectangle) if ( !selectionRect.isNull() && selectionRect.intersects( contentsRect ) && !selectionRectInternal.contains( contentsRect ) ) { screenPainter.setPen( palette().color( QPalette::Active, QPalette::Highlight ).darker(110) ); screenPainter.drawRect( selectionRect ); } // 2b) Layer 1b: paint opaque selection (table) for (const TableSelectionPart &tsp : qAsConst(d->tableSelectionParts)) { QRect selectionPartRect = tsp.rectInItem.geometry(tsp.item->uncroppedWidth(), tsp.item->uncroppedHeight()); selectionPartRect.translate( tsp.item->uncroppedGeometry().topLeft () ); QRect selectionPartRectInternal = selectionPartRect; selectionPartRectInternal.adjust( 1, 1, -1, -1 ); if ( !selectionPartRect.isNull() && selectionPartRect.intersects( contentsRect ) && !selectionPartRectInternal.contains( contentsRect ) ) { screenPainter.setPen( palette().color( QPalette::Active, QPalette::Highlight ).darker(110) ); screenPainter.drawRect( selectionPartRect ); } } drawTableDividers( &screenPainter ); // 3a) Layer 1: give annotator painting control if ( d->annotator && d->annotator->routePaints( contentsRect ) ) d->annotator->routePaint( &screenPainter, contentsRect ); // 3b) Layer 1: give mouseAnnotation painting control d->mouseAnnotation->routePaint( &screenPainter, contentsRect ); // 4) Layer 2: overlays if ( Okular::Settings::debugDrawBoundaries() ) { screenPainter.setPen( Qt::red ); screenPainter.drawRect( contentsRect ); } } } } void PageView::drawTableDividers(QPainter * screenPainter) { if (!d->tableSelectionParts.isEmpty()) { screenPainter->setPen( d->mouseSelectionColor.darker() ); if (d->tableDividersGuessed) { QPen p = screenPainter->pen(); p.setStyle( Qt::DashLine ); screenPainter->setPen( p ); } for (const TableSelectionPart &tsp : qAsConst(d->tableSelectionParts)) { QRect selectionPartRect = tsp.rectInItem.geometry(tsp.item->uncroppedWidth(), tsp.item->uncroppedHeight()); selectionPartRect.translate( tsp.item->uncroppedGeometry().topLeft () ); QRect selectionPartRectInternal = selectionPartRect; selectionPartRectInternal.adjust( 1, 1, -1, -1 ); for (double col : qAsConst(d->tableSelectionCols)) { if (col >= tsp.rectInSelection.left && col <= tsp.rectInSelection.right) { col = (col - tsp.rectInSelection.left) / (tsp.rectInSelection.right - tsp.rectInSelection.left); const int x = selectionPartRect.left() + col * selectionPartRect.width() + 0.5; screenPainter->drawLine( x, selectionPartRectInternal.top(), x, selectionPartRectInternal.top() + selectionPartRectInternal.height() ); } } for (double row : qAsConst(d->tableSelectionRows)) { if (row >= tsp.rectInSelection.top && row <= tsp.rectInSelection.bottom) { row = (row - tsp.rectInSelection.top) / (tsp.rectInSelection.bottom - tsp.rectInSelection.top); const int y = selectionPartRect.top() + row * selectionPartRect.height() + 0.5; screenPainter->drawLine( selectionPartRectInternal.left(), y, selectionPartRectInternal.left() + selectionPartRectInternal.width(), y ); } } } } } void PageView::resizeEvent( QResizeEvent *e ) { if ( d->items.isEmpty() ) { resizeContentArea( e->size() ); return; } if ( ( d->zoomMode == ZoomFitWidth || d->zoomMode == ZoomFitAuto ) && !verticalScrollBar()->isVisible() && qAbs(e->oldSize().height() - e->size().height()) < verticalScrollBar()->width() && d->verticalScrollBarVisible ) { // this saves us from infinite resizing loop because of scrollbars appearing and disappearing // see bug 160628 for more info // TODO looks are still a bit ugly because things are left uncentered // but better a bit ugly than unusable d->verticalScrollBarVisible = false; resizeContentArea( e->size() ); return; } else if ( d->zoomMode == ZoomFitAuto && !horizontalScrollBar()->isVisible() && qAbs(e->oldSize().width() - e->size().width()) < horizontalScrollBar()->height() && d->horizontalScrollBarVisible ) { // this saves us from infinite resizing loop because of scrollbars appearing and disappearing // TODO looks are still a bit ugly because things are left uncentered // but better a bit ugly than unusable d->horizontalScrollBarVisible = false; resizeContentArea( e->size() ); return; } // start a timer that will refresh the pixmap after 0.2s d->delayResizeEventTimer->start( 200 ); d->verticalScrollBarVisible = verticalScrollBar()->isVisible(); d->horizontalScrollBarVisible = horizontalScrollBar()->isVisible(); } void PageView::keyPressEvent( QKeyEvent * e ) { e->accept(); // if performing a selection or dyn zooming, disable keys handling if ( ( d->mouseSelecting && e->key() != Qt::Key_Escape ) || ( QApplication::mouseButtons () & Qt::MidButton ) ) return; // move/scroll page by using keys switch ( e->key() ) { case Qt::Key_J: case Qt::Key_Down: slotScrollDown( 1 /* go down 1 step */ ); break; case Qt::Key_PageDown: slotScrollDown(); break; case Qt::Key_K: case Qt::Key_Up: slotScrollUp( 1 /* go up 1 step */ ); break; case Qt::Key_PageUp: case Qt::Key_Backspace: slotScrollUp(); break; case Qt::Key_Left: case Qt::Key_H: if ( horizontalScrollBar()->maximum() == 0 ) { //if we cannot scroll we go to the previous page vertically int next_page = d->document->currentPage() - viewColumns(); d->document->setViewportPage(next_page); } else{ d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(-horizontalScrollBar()->singleStep(), 0), 100); } break; case Qt::Key_Right: case Qt::Key_L: if ( horizontalScrollBar()->maximum() == 0 ) { //if we cannot scroll we advance the page vertically int next_page = d->document->currentPage() + viewColumns(); d->document->setViewportPage(next_page); } else{ d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(horizontalScrollBar()->singleStep(), 0), 100); } break; case Qt::Key_Escape: emit escPressed(); selectionClear( d->tableDividersGuessed ? ClearOnlyDividers : ClearAllSelection ); d->mousePressPos = QPoint(); if ( d->aPrevAction ) { d->aPrevAction->trigger(); d->aPrevAction = nullptr; } d->mouseAnnotation->routeKeyPressEvent( e ); break; case Qt::Key_Delete: d->mouseAnnotation->routeKeyPressEvent( e ); break; case Qt::Key_Shift: case Qt::Key_Control: if ( d->autoScrollTimer ) { if ( d->autoScrollTimer->isActive() ) d->autoScrollTimer->stop(); else slotAutoScroll(); return; } // fallthrough default: e->ignore(); return; } // if a known key has been pressed, stop scrolling the page if ( d->autoScrollTimer ) { d->scrollIncrement = 0; d->autoScrollTimer->stop(); } } void PageView::keyReleaseEvent( QKeyEvent * e ) { e->accept(); if ( d->annotator && d->annotator->active() ) { if ( d->annotator->routeKeyEvent( e ) ) return; } if ( e->key() == Qt::Key_Escape && d->autoScrollTimer ) { d->scrollIncrement = 0; d->autoScrollTimer->stop(); } } void PageView::inputMethodEvent( QInputMethodEvent * e ) { Q_UNUSED(e) } void PageView::tabletEvent( QTabletEvent * e ) { // Ignore tablet events that we don't care about if ( !( e->type() == QEvent::TabletPress || e->type() == QEvent::TabletRelease || e->type() == QEvent::TabletMove ) ) { e->ignore(); return; } // Determine pen state bool penReleased = false; if ( e->type() == QEvent::TabletPress ) { d->penDown = true; } if ( e->type() == QEvent::TabletRelease ) { d->penDown = false; penReleased = true; } // If we're editing an annotation and the tablet pen is either down or just released // then dispatch event to annotator if ( d->annotator && d->annotator->active() && ( d->penDown || penReleased ) ) { const QPoint eventPos = contentAreaPoint( e->pos() ); PageViewItem * pageItem = pickItemOnPoint( eventPos.x(), eventPos.y() ); const QPoint localOriginInGlobal = mapToGlobal( QPoint(0,0) ); // routeTabletEvent will accept or ignore event as appropriate d->annotator->routeTabletEvent( e, pageItem, localOriginInGlobal ); } else { e->ignore(); } } void PageView::mouseMoveEvent( QMouseEvent * e ) { // For some reason in Qt 5.11.2 (no idea when this started) all wheel // events are followed by mouse move events (without changing position), // so we only actually reset the controlWheelAccumulatedDelta if there is a mouse movement if ( e->globalPos() != d->previousMouseMovePos ) { d->controlWheelAccumulatedDelta = 0; } d->previousMouseMovePos = e->globalPos(); // don't perform any mouse action when no document is shown if ( d->items.isEmpty() ) return; // if holding mouse mid button, perform zoom if ( e->buttons() & Qt::MidButton ) { int mouseY = e->globalPos().y(); int deltaY = d->mouseMidLastY - mouseY; // wrap mouse from top to bottom const QRect mouseContainer = QApplication::desktop()->screenGeometry( this ); const int absDeltaY = abs(deltaY); if ( absDeltaY > mouseContainer.height() / 2 ) { deltaY = mouseContainer.height() - absDeltaY; } const float upperZoomLimit = d->document->supportsTiles() ? 15.99 : 3.99; if ( mouseY <= mouseContainer.top() + 4 && d->zoomFactor < upperZoomLimit ) { mouseY = mouseContainer.bottom() - 5; QCursor::setPos( e->globalPos().x(), mouseY ); } // wrap mouse from bottom to top else if ( mouseY >= mouseContainer.bottom() - 4 && d->zoomFactor > 0.101 ) { mouseY = mouseContainer.top() + 5; QCursor::setPos( e->globalPos().x(), mouseY ); } // remember last position d->mouseMidLastY = mouseY; // update zoom level, perform zoom and redraw if ( deltaY ) { d->zoomFactor *= ( 1.0 + ( (double)deltaY / 500.0 ) ); d->blockPixmapsRequest = true; updateZoom( ZoomRefreshCurrent ); d->blockPixmapsRequest = false; viewport()->update(); } return; } const QPoint eventPos = contentAreaPoint( e->pos() ); // if we're editing an annotation, dispatch event to it if ( d->annotator && d->annotator->active() ) { PageViewItem * pageItem = pickItemOnPoint( eventPos.x(), eventPos.y() ); updateCursor( eventPos ); d->annotator->routeMouseEvent( e, pageItem ); return; } bool leftButton = (e->buttons() == Qt::LeftButton); bool rightButton = (e->buttons() == Qt::RightButton); switch ( d->mouseMode ) { case Okular::Settings::EnumMouseMode::Browse: { PageViewItem * pageItem = pickItemOnPoint( eventPos.x(), eventPos.y() ); if ( leftButton ) { d->leftClickTimer.stop(); if ( pageItem && d->mouseAnnotation->isActive() ) { // if left button pressed and annotation is focused, forward move event d->mouseAnnotation->routeMouseMoveEvent( pageItem, eventPos, leftButton ); } // drag page else { if(d->scroller->state() == QScroller::Inactive || d->scroller->state() == QScroller::Scrolling) { d->mouseGrabOffset = QPoint(0,0); d->scroller->handleInput(QScroller::InputPress, e->pos(), e->timestamp()-1); } setCursor( Qt::ClosedHandCursor ); QPoint mousePos = e->globalPos(); const QRect mouseContainer = QApplication::desktop()->screenGeometry( this ); // wrap mouse from top to bottom if ( mousePos.y() <= mouseContainer.top() + 4 && verticalScrollBar()->value() < verticalScrollBar()->maximum() - 10 ) { mousePos.setY( mouseContainer.bottom() - 5 ); QCursor::setPos( mousePos ); d->mouseGrabOffset -= QPoint(0, mouseContainer.height()); } // wrap mouse from bottom to top else if ( mousePos.y() >= mouseContainer.bottom() - 4 && verticalScrollBar()->value() > 10 ) { mousePos.setY( mouseContainer.top() + 5 ); d->mouseGrabOffset += QPoint(0, mouseContainer.height()); QCursor::setPos( mousePos ); } d->scroller->handleInput(QScroller::InputMove, e->pos() + d->mouseGrabOffset, e->timestamp()); } } else if ( rightButton && !d->mousePressPos.isNull() && d->aMouseSelect ) { // if mouse moves 5 px away from the press point, switch to 'selection' int deltaX = d->mousePressPos.x() - e->globalPos().x(), deltaY = d->mousePressPos.y() - e->globalPos().y(); if ( deltaX > 5 || deltaX < -5 || deltaY > 5 || deltaY < -5 ) { d->aPrevAction = d->aMouseNormal; d->aMouseSelect->trigger(); QPoint newPos = eventPos + QPoint( deltaX, deltaY ); selectionStart( newPos, palette().color( QPalette::Active, QPalette::Highlight ).lighter( 120 ), false ); updateSelection( eventPos ); break; } } else { /* Forward move events which are still not yet consumed by "mouse grab" or aMouseSelect */ d->mouseAnnotation->routeMouseMoveEvent( pageItem, eventPos, leftButton ); updateCursor(); } } break; case Okular::Settings::EnumMouseMode::Zoom: case Okular::Settings::EnumMouseMode::RectSelect: case Okular::Settings::EnumMouseMode::TableSelect: case Okular::Settings::EnumMouseMode::TrimSelect: // set second corner of selection if ( d->mouseSelecting ) { updateSelection( eventPos ); d->mouseOverLinkObject = nullptr; } updateCursor(); break; case Okular::Settings::EnumMouseMode::Magnifier: if ( e->buttons() ) // if any button is pressed at all { moveMagnifier( e->pos() ); updateMagnifier( eventPos ); } break; case Okular::Settings::EnumMouseMode::TextSelect: // if mouse moves 5 px away from the press point and the document supports text extraction, do 'textselection' if ( !d->mouseTextSelecting && !d->mousePressPos.isNull() && d->document->supportsSearching() && ( ( eventPos - d->mouseSelectPos ).manhattanLength() > 5 ) ) { d->mouseTextSelecting = true; } updateSelection( eventPos ); updateCursor(); break; } } void PageView::mousePressEvent( QMouseEvent * e ) { d->controlWheelAccumulatedDelta = 0; // don't perform any mouse action when no document is shown if ( d->items.isEmpty() ) return; // if performing a selection or dyn zooming, disable mouse press if ( d->mouseSelecting || ( e->button() != Qt::MidButton && ( e->buttons() & Qt::MidButton) )) return; // if the page is scrolling, stop it if ( d->autoScrollTimer ) { d->scrollIncrement = 0; d->autoScrollTimer->stop(); } // if pressing mid mouse button while not doing other things, begin 'continuous zoom' mode if ( e->button() == Qt::MidButton ) { d->mouseMidLastY = e->globalPos().y(); setCursor( Qt::SizeVerCursor ); return; } const QPoint eventPos = contentAreaPoint( e->pos() ); // if we're editing an annotation, dispatch event to it if ( d->annotator && d->annotator->active() ) { d->scroller->stop(); PageViewItem * pageItem = pickItemOnPoint( eventPos.x(), eventPos.y() ); d->annotator->routeMouseEvent( e, pageItem ); return; } // trigger history navigation for additional mouse buttons if ( e->button() == Qt::XButton1 ) { emit mouseBackButtonClick(); return; } if ( e->button() == Qt::XButton2 ) { emit mouseForwardButtonClick(); return; } // update press / 'start drag' mouse position d->mousePressPos = e->globalPos(); // handle mode dependent mouse press actions bool leftButton = e->button() == Qt::LeftButton, rightButton = e->button() == Qt::RightButton; // Not sure we should erase the selection when clicking with left. if ( d->mouseMode != Okular::Settings::EnumMouseMode::TextSelect ) textSelectionClear(); switch ( d->mouseMode ) { case Okular::Settings::EnumMouseMode::Browse: // drag start / click / link following { PageViewItem * pageItem = pickItemOnPoint( eventPos.x(), eventPos.y() ); if ( leftButton ) { if ( pageItem ) { d->mouseAnnotation->routeMousePressEvent( pageItem, eventPos ); } if( !d->mouseOnRect ){ d->mouseGrabOffset = QPoint(0,0); d->scroller->handleInput(QScroller::InputPress, e->pos(), e->timestamp()); d->leftClickTimer.start( QApplication::doubleClickInterval() + 10 ); } } else if ( rightButton ) { if ( pageItem ) { // find out normalized mouse coords inside current item const QRect & itemRect = pageItem->uncroppedGeometry(); double nX = pageItem->absToPageX(eventPos.x()); double nY = pageItem->absToPageY(eventPos.y()); const QLinkedList< const Okular::ObjectRect *> orects = pageItem->page()->objectRects( Okular::ObjectRect::OAnnotation, nX, nY, itemRect.width(), itemRect.height() ); if ( !orects.isEmpty() ) { AnnotationPopup popup( d->document, AnnotationPopup::MultiAnnotationMode, this ); for ( const Okular::ObjectRect * orect : orects ) { Okular::Annotation * ann = ( (Okular::AnnotationObjectRect *)orect )->annotation(); if ( ann && (ann->subType() != Okular::Annotation::AWidget) ) popup.addAnnotation( ann, pageItem->pageNumber() ); } connect( &popup, &AnnotationPopup::openAnnotationWindow, this, &PageView::openAnnotationWindow ); popup.exec( e->globalPos() ); // Since ↑ spins its own event loop we won't get the mouse release event // so reset mousePressPos here d->mousePressPos = QPoint(); } } } } break; case Okular::Settings::EnumMouseMode::Zoom: // set first corner of the zoom rect if ( leftButton ) selectionStart( eventPos, palette().color( QPalette::Active, QPalette::Highlight ), false ); else if ( rightButton ) updateZoom( ZoomOut ); break; case Okular::Settings::EnumMouseMode::Magnifier: moveMagnifier( e->pos() ); d->magnifierView->show(); updateMagnifier( eventPos ); break; case Okular::Settings::EnumMouseMode::RectSelect: // set first corner of the selection rect case Okular::Settings::EnumMouseMode::TrimSelect: if ( leftButton ) { selectionStart( eventPos, palette().color( QPalette::Active, QPalette::Highlight ).lighter( 120 ), false ); } break; case Okular::Settings::EnumMouseMode::TableSelect: if ( leftButton ) { if (d->tableSelectionParts.isEmpty()) { selectionStart( eventPos, palette().color( QPalette::Active, QPalette::Highlight ).lighter( 120 ), false ); } else { QRect updatedRect; for (const TableSelectionPart &tsp : qAsConst(d->tableSelectionParts)) { QRect selectionPartRect = tsp.rectInItem.geometry(tsp.item->uncroppedWidth(), tsp.item->uncroppedHeight()); selectionPartRect.translate( tsp.item->uncroppedGeometry().topLeft () ); // This will update the whole table rather than just the added/removed divider // (which can span more than one part). updatedRect = updatedRect.united(selectionPartRect); if (!selectionPartRect.contains(eventPos)) continue; // At this point it's clear we're either adding or removing a divider manually, so obviously the user is happy with the guess (if any). d->tableDividersGuessed = false; // There's probably a neat trick to finding which edge it's closest to, // but this way has the advantage of simplicity. const int fromLeft = abs(selectionPartRect.left() - eventPos.x()); const int fromRight = abs(selectionPartRect.left() + selectionPartRect.width() - eventPos.x()); const int fromTop = abs(selectionPartRect.top() - eventPos.y()); const int fromBottom = abs(selectionPartRect.top() + selectionPartRect.height() - eventPos.y()); const int colScore = fromToptableSelectionCols.length(); i++) { const double col = (d->tableSelectionCols[i] - tsp.rectInSelection.left) / (tsp.rectInSelection.right - tsp.rectInSelection.left); const int colX = selectionPartRect.left() + col * selectionPartRect.width() + 0.5; if (abs(colX - eventPos.x())<=3) { d->tableSelectionCols.removeAt(i); deleted=true; break; } } if (!deleted) { double col = eventPos.x() - selectionPartRect.left(); col /= selectionPartRect.width(); // at this point, it's normalised within the part col *= (tsp.rectInSelection.right - tsp.rectInSelection.left); col += tsp.rectInSelection.left; // at this point, it's normalised within the whole table d->tableSelectionCols.append(col); std::sort(d->tableSelectionCols.begin(), d->tableSelectionCols.end()); } } else { bool deleted=false; for(int i=0; itableSelectionRows.length(); i++) { const double row = (d->tableSelectionRows[i] - tsp.rectInSelection.top) / (tsp.rectInSelection.bottom - tsp.rectInSelection.top); const int rowY = selectionPartRect.top() + row * selectionPartRect.height() + 0.5; if (abs(rowY - eventPos.y())<=3) { d->tableSelectionRows.removeAt(i); deleted=true; break; } } if (!deleted) { double row = eventPos.y() - selectionPartRect.top(); row /= selectionPartRect.height(); // at this point, it's normalised within the part row *= (tsp.rectInSelection.bottom - tsp.rectInSelection.top); row += tsp.rectInSelection.top; // at this point, it's normalised within the whole table d->tableSelectionRows.append(row); std::sort(d->tableSelectionRows.begin(), d->tableSelectionRows.end()); } } } updatedRect.translate( -contentAreaPosition() ); viewport()->update( updatedRect ); } } break; case Okular::Settings::EnumMouseMode::TextSelect: d->mouseSelectPos = eventPos; if ( !rightButton ) { textSelectionClear(); } break; } } void PageView::mouseReleaseEvent( QMouseEvent * e ) { d->controlWheelAccumulatedDelta = 0; // stop the drag scrolling d->dragScrollTimer.stop(); d->leftClickTimer.stop(); const bool leftButton = e->button() == Qt::LeftButton; const bool rightButton = e->button() == Qt::RightButton; if ( d->mouseAnnotation->isActive() && leftButton ) { // Just finished to move the annotation d->mouseAnnotation->routeMouseReleaseEvent(); } // don't perform any mouse action when no document is shown.. if ( d->items.isEmpty() ) { // ..except for right Clicks (emitted even it viewport is empty) if ( e->button() == Qt::RightButton ) emit rightClick( nullptr, e->globalPos() ); return; } const QPoint eventPos = contentAreaPoint( e->pos() ); // handle mode independent mid bottom zoom if ( e->button() == Qt::MidButton ) { // request pixmaps since it was disabled during drag slotRequestVisiblePixmaps(); // the cursor may now be over a link.. update it updateCursor( eventPos ); return; } // if we're editing an annotation, dispatch event to it if ( d->annotator && d->annotator->active() ) { PageViewItem * pageItem = pickItemOnPoint( eventPos.x(), eventPos.y() ); d->annotator->routeMouseEvent( e, pageItem ); return; } switch ( d->mouseMode ) { case Okular::Settings::EnumMouseMode::Browse:{ d->scroller->handleInput(QScroller::InputRelease, e->pos() + d->mouseGrabOffset, e->timestamp()); //disable flick if the cursor has wrapped around if(d->mouseGrabOffset != QPoint(0,0)) d->scroller->stop(); // return the cursor to its normal state after dragging if ( cursor().shape() == Qt::ClosedHandCursor ) updateCursor( eventPos ); PageViewItem * pageItem = pickItemOnPoint( eventPos.x(), eventPos.y() ); const QPoint pressPos = contentAreaPoint( mapFromGlobal( d->mousePressPos ) ); const PageViewItem * pageItemPressPos = pickItemOnPoint( pressPos.x(), pressPos.y() ); // if the mouse has not moved since the press, that's a -click- if ( leftButton && pageItem && pageItem == pageItemPressPos && ( (d->mousePressPos - e->globalPos()).manhattanLength() < QApplication::startDragDistance() ) ) { if ( !mouseReleaseOverLink( d->mouseOverLinkObject ) && ( e->modifiers() == Qt::ShiftModifier ) ) { const double nX = pageItem->absToPageX(eventPos.x()); const double nY = pageItem->absToPageY(eventPos.y()); const Okular::ObjectRect * rect; // TODO: find a better way to activate the source reference "links" // for the moment they are activated with Shift + left click // Search the nearest source reference. rect = pageItem->page()->objectRect( Okular::ObjectRect::SourceRef, nX, nY, pageItem->uncroppedWidth(), pageItem->uncroppedHeight() ); if ( !rect ) { static const double s_minDistance = 0.025; // FIXME?: empirical value? double distance = 0.0; rect = pageItem->page()->nearestObjectRect( Okular::ObjectRect::SourceRef, nX, nY, pageItem->uncroppedWidth(), pageItem->uncroppedHeight(), &distance ); // distance is distanceSqr, adapt it to a normalized value distance = distance / (pow( pageItem->uncroppedWidth(), 2 ) + pow( pageItem->uncroppedHeight(), 2 )); if ( rect && ( distance > s_minDistance ) ) rect = nullptr; } if ( rect ) { const Okular::SourceReference * ref = static_cast< const Okular::SourceReference * >( rect->object() ); d->document->processSourceReference( ref ); } else { const Okular::SourceReference * ref = d->document->dynamicSourceReference( pageItem-> pageNumber(), nX * pageItem->page()->width(), nY * pageItem->page()->height() ); if ( ref ) { d->document->processSourceReference( ref ); delete ref; } } } #if 0 else { // a link can move us to another page or even to another document, there's no point in trying to // process the click on the image once we have processes the click on the link rect = pageItem->page()->objectRect( Okular::ObjectRect::Image, nX, nY, pageItem->width(), pageItem->height() ); if ( rect ) { // handle click over a image } /* Enrico and me have decided this is not worth the trouble it generates else { // if not on a rect, the click selects the page // if ( pageItem->pageNumber() != (int)d->document->currentPage() ) d->document->setViewportPage( pageItem->pageNumber(), this ); }*/ } #endif } else if ( rightButton && !d->mouseAnnotation->isModified() ) { if ( pageItem && pageItem == pageItemPressPos && ( (d->mousePressPos - e->globalPos()).manhattanLength() < QApplication::startDragDistance() ) ) { QMenu * menu = createProcessLinkMenu(pageItem, eventPos ); if ( menu ) { menu->exec( e->globalPos() ); menu->deleteLater(); } else { const double nX = pageItem->absToPageX(eventPos.x()); const double nY = pageItem->absToPageY(eventPos.y()); // a link can move us to another page or even to another document, there's no point in trying to // process the click on the image once we have processes the click on the link const Okular::ObjectRect * rect = pageItem->page()->objectRect( Okular::ObjectRect::Image, nX, nY, pageItem->uncroppedWidth(), pageItem->uncroppedHeight() ); if ( rect ) { // handle right click over a image } else { // right click (if not within 5 px of the press point, the mode // had been already changed to 'Selection' instead of 'Normal') emit rightClick( pageItem->page(), e->globalPos() ); } } } else { // right click (if not within 5 px of the press point, the mode // had been already changed to 'Selection' instead of 'Normal') emit rightClick( pageItem ? pageItem->page() : nullptr, e->globalPos() ); } } }break; case Okular::Settings::EnumMouseMode::Zoom: // if a selection rect has been defined, zoom into it if ( leftButton && d->mouseSelecting ) { QRect selRect = d->mouseSelectionRect.normalized(); if ( selRect.width() <= 8 && selRect.height() <= 8 ) { selectionClear(); break; } // find out new zoom ratio and normalized view center (relative to the contentsRect) double zoom = qMin( (double)viewport()->width() / (double)selRect.width(), (double)viewport()->height() / (double)selRect.height() ); double nX = (double)(selRect.left() + selRect.right()) / (2.0 * (double)contentAreaWidth()); double nY = (double)(selRect.top() + selRect.bottom()) / (2.0 * (double)contentAreaHeight()); const float upperZoomLimit = d->document->supportsTiles() ? 16.0 : 4.0; if ( d->zoomFactor <= upperZoomLimit || zoom <= 1.0 ) { d->zoomFactor *= zoom; viewport()->setUpdatesEnabled( false ); updateZoom( ZoomRefreshCurrent ); viewport()->setUpdatesEnabled( true ); } // recenter view and update the viewport center( (int)(nX * contentAreaWidth()), (int)(nY * contentAreaHeight()) ); viewport()->update(); // hide message box and delete overlay window selectionClear(); } break; case Okular::Settings::EnumMouseMode::Magnifier: d->magnifierView->hide(); break; case Okular::Settings::EnumMouseMode::TrimSelect: { // if it is a left release checks if is over a previous link press if ( leftButton && mouseReleaseOverLink ( d->mouseOverLinkObject ) ) { selectionClear(); break; } // if mouse is released and selection is null this is a rightClick if ( rightButton && !d->mouseSelecting ) { break; } PageViewItem * pageItem = pickItemOnPoint(eventPos.x(), eventPos.y()); // ensure end point rests within a page, or ignore if (!pageItem) { break; } QRect selectionRect = d->mouseSelectionRect.normalized(); double nLeft = pageItem->absToPageX(selectionRect.left()); double nRight = pageItem->absToPageX(selectionRect.right()); double nTop = pageItem->absToPageY(selectionRect.top()); double nBottom = pageItem->absToPageY(selectionRect.bottom()); if ( nLeft < 0 ) nLeft = 0; if ( nTop < 0 ) nTop = 0; if ( nRight > 1 ) nRight = 1; if ( nBottom > 1 ) nBottom = 1; d->trimBoundingBox = Okular::NormalizedRect(nLeft, nTop, nRight, nBottom); // Trim Selection successfully done, hide prompt d->messageWindow->hide(); // clear widget selection and invalidate rect selectionClear(); // When Trim selection bbox interaction is over, we should switch to another mousemode. if ( d->aPrevAction ) { d->aPrevAction->trigger(); d->aPrevAction = nullptr; } else { d->aMouseNormal->trigger(); } // with d->trimBoundingBox defined, redraw for trim to take visual effect if ( d->document->pages() > 0 ) { slotRelayoutPages(); slotRequestVisiblePixmaps(); // TODO: slotRelayoutPages() may have done this already! } break; } case Okular::Settings::EnumMouseMode::RectSelect: { // if it is a left release checks if is over a previous link press if ( leftButton && mouseReleaseOverLink ( d->mouseOverLinkObject ) ) { selectionClear(); break; } // if mouse is released and selection is null this is a rightClick if ( rightButton && !d->mouseSelecting ) { PageViewItem * pageItem = pickItemOnPoint( eventPos.x(), eventPos.y() ); emit rightClick( pageItem ? pageItem->page() : nullptr, e->globalPos() ); break; } // if a selection is defined, display a popup if ( (!leftButton && !d->aPrevAction) || (!rightButton && d->aPrevAction) || !d->mouseSelecting ) break; QRect selectionRect = d->mouseSelectionRect.normalized(); if ( selectionRect.width() <= 8 && selectionRect.height() <= 8 ) { selectionClear(); if ( d->aPrevAction ) { d->aPrevAction->trigger(); d->aPrevAction = nullptr; } break; } // if we support text generation QString selectedText; if (d->document->supportsSearching()) { // grab text in selection by extracting it from all intersected pages const Okular::Page * okularPage=nullptr; for ( const PageViewItem * item : qAsConst( d->items ) ) { if ( !item->isVisible() ) continue; const QRect & itemRect = item->croppedGeometry(); if ( selectionRect.intersects( itemRect ) ) { // request the textpage if there isn't one okularPage= item->page(); qCDebug(OkularUiDebug) << "checking if page" << item->pageNumber() << "has text:" << okularPage->hasTextPage(); if ( !okularPage->hasTextPage() ) d->document->requestTextPage( okularPage->number() ); // grab text in the rect that intersects itemRect QRect relativeRect = selectionRect.intersected( itemRect ); relativeRect.translate( -item->uncroppedGeometry().topLeft() ); Okular::RegularAreaRect rects; rects.append( Okular::NormalizedRect( relativeRect, item->uncroppedWidth(), item->uncroppedHeight() ) ); selectedText += okularPage->text( &rects ); } } } // popup that ask to copy:text and copy/save:image QMenu menu( this ); menu.setObjectName(QStringLiteral("PopupMenu")); QAction *textToClipboard = nullptr; #ifdef HAVE_SPEECH QAction *speakText = nullptr; #endif QAction *imageToClipboard = nullptr; QAction *imageToFile = nullptr; if ( d->document->supportsSearching() && !selectedText.isEmpty() ) { menu.addAction( new OKMenuTitle( &menu, i18np( "Text (1 character)", "Text (%1 characters)", selectedText.length() ) ) ); textToClipboard = menu.addAction( QIcon::fromTheme(QStringLiteral("edit-copy")), i18n( "Copy to Clipboard" ) ); textToClipboard->setObjectName(QStringLiteral("CopyTextToClipboard")); bool copyAllowed = d->document->isAllowed( Okular::AllowCopy ); if ( !copyAllowed ) { textToClipboard->setEnabled( false ); textToClipboard->setText( i18n("Copy forbidden by DRM") ); } #ifdef HAVE_SPEECH if ( Okular::Settings::useTTS() ) speakText = menu.addAction( QIcon::fromTheme(QStringLiteral("text-speak")), i18n( "Speak Text" ) ); #endif if ( copyAllowed ) { addSearchWithinDocumentAction( &menu, selectedText ); addWebShortcutsMenu( &menu, selectedText ); } } menu.addAction( new OKMenuTitle( &menu, i18n( "Image (%1 by %2 pixels)", selectionRect.width(), selectionRect.height() ) ) ); imageToClipboard = menu.addAction( QIcon::fromTheme(QStringLiteral("image-x-generic")), i18n( "Copy to Clipboard" ) ); imageToFile = menu.addAction( QIcon::fromTheme(QStringLiteral("document-save")), i18n( "Save to File..." ) ); QAction *choice = menu.exec( e->globalPos() ); // check if the user really selected an action if ( choice ) { // IMAGE operation chosen if ( choice == imageToClipboard || choice == imageToFile ) { // renders page into a pixmap QPixmap copyPix( selectionRect.width(), selectionRect.height() ); QPainter copyPainter( ©Pix ); copyPainter.translate( -selectionRect.left(), -selectionRect.top() ); drawDocumentOnPainter( selectionRect, ©Painter ); copyPainter.end(); if ( choice == imageToClipboard ) { // [2] copy pixmap to clipboard QClipboard *cb = QApplication::clipboard(); cb->setPixmap( copyPix, QClipboard::Clipboard ); if ( cb->supportsSelection() ) cb->setPixmap( copyPix, QClipboard::Selection ); d->messageWindow->display( i18n( "Image [%1x%2] copied to clipboard.", copyPix.width(), copyPix.height() ) ); } else if ( choice == imageToFile ) { // [3] save pixmap to file QString fileName = QFileDialog::getSaveFileName(this, i18n("Save file"), QString(), i18n("Images (*.png *.jpeg)")); if ( fileName.isEmpty() ) d->messageWindow->display( i18n( "File not saved." ), QString(), PageViewMessage::Warning ); else { QMimeDatabase db; QMimeType mime = db.mimeTypeForUrl( QUrl::fromLocalFile(fileName) ); QString type; if ( !mime.isDefault() ) type = QStringLiteral("PNG"); else type = mime.name().section( QLatin1Char('/'), -1 ).toUpper(); copyPix.save( fileName, qPrintable( type ) ); d->messageWindow->display( i18n( "Image [%1x%2] saved to %3 file.", copyPix.width(), copyPix.height(), type ) ); } } } // TEXT operation chosen else { if ( choice == textToClipboard ) { // [1] copy text to clipboard QClipboard *cb = QApplication::clipboard(); cb->setText( selectedText, QClipboard::Clipboard ); if ( cb->supportsSelection() ) cb->setText( selectedText, QClipboard::Selection ); } #ifdef HAVE_SPEECH else if ( choice == speakText ) { // [2] speech selection using TTS d->tts()->say( selectedText ); } #endif } } // clear widget selection and invalidate rect selectionClear(); // restore previous action if came from it using right button if ( d->aPrevAction ) { d->aPrevAction->trigger(); d->aPrevAction = nullptr; } }break; case Okular::Settings::EnumMouseMode::TableSelect: { // if it is a left release checks if is over a previous link press if ( leftButton && mouseReleaseOverLink ( d->mouseOverLinkObject ) ) { selectionClear(); break; } // if mouse is released and selection is null this is a rightClick if ( rightButton && !d->mouseSelecting ) { PageViewItem * pageItem = pickItemOnPoint( eventPos.x(), eventPos.y() ); emit rightClick( pageItem ? pageItem->page() : nullptr, e->globalPos() ); break; } QRect selectionRect = d->mouseSelectionRect.normalized(); if ( selectionRect.width() <= 8 && selectionRect.height() <= 8 && d->tableSelectionParts.isEmpty() ) { selectionClear(); if ( d->aPrevAction ) { d->aPrevAction->trigger(); d->aPrevAction = nullptr; } break; } if (d->mouseSelecting) { // break up the selection into page-relative pieces d->tableSelectionParts.clear(); const Okular::Page * okularPage=nullptr; for ( PageViewItem * item : qAsConst( d->items ) ) { if ( !item->isVisible() ) continue; const QRect & itemRect = item->croppedGeometry(); if ( selectionRect.intersects( itemRect ) ) { // request the textpage if there isn't one okularPage= item->page(); qCDebug(OkularUiDebug) << "checking if page" << item->pageNumber() << "has text:" << okularPage->hasTextPage(); if ( !okularPage->hasTextPage() ) d->document->requestTextPage( okularPage->number() ); // grab text in the rect that intersects itemRect QRect rectInItem = selectionRect.intersected( itemRect ); rectInItem.translate( -item->uncroppedGeometry().topLeft() ); QRect rectInSelection = selectionRect.intersected( itemRect ); rectInSelection.translate( -selectionRect.topLeft() ); d->tableSelectionParts.append( TableSelectionPart( item, Okular::NormalizedRect( rectInItem, item->uncroppedWidth(), item->uncroppedHeight() ), Okular::NormalizedRect( rectInSelection, selectionRect.width(), selectionRect.height() ) ) ); } } QRect updatedRect = d->mouseSelectionRect.normalized().adjusted( 0, 0, 1, 1 ); updatedRect.translate( -contentAreaPosition() ); d->mouseSelecting = false; d->mouseSelectionRect.setCoords( 0, 0, 0, 0 ); d->tableSelectionCols.clear(); d->tableSelectionRows.clear(); guessTableDividers(); viewport()->update( updatedRect ); } if ( !d->document->isAllowed( Okular::AllowCopy ) ) { d->messageWindow->display( i18n("Copy forbidden by DRM"), QString(), PageViewMessage::Info, -1 ); break; } QString selText; QString selHtml; QList xs = d->tableSelectionCols; QList ys = d->tableSelectionRows; xs.prepend(0.0); xs.append(1.0); ys.prepend(0.0); ys.append(1.0); selHtml = QString::fromLatin1("" "" ""); for (int r=0; r+1"); for (int c=0; c+1tableSelectionParts)) { // first, crop the cell to this part if (!tsp.rectInSelection.intersects(cell)) continue; Okular::NormalizedRect cellPart = tsp.rectInSelection & cell; // intersection // second, convert it from table coordinates to part coordinates cellPart.left -= tsp.rectInSelection.left; cellPart.left /= (tsp.rectInSelection.right - tsp.rectInSelection.left); cellPart.right -= tsp.rectInSelection.left; cellPart.right /= (tsp.rectInSelection.right - tsp.rectInSelection.left); cellPart.top -= tsp.rectInSelection.top; cellPart.top /= (tsp.rectInSelection.bottom - tsp.rectInSelection.top); cellPart.bottom -= tsp.rectInSelection.top; cellPart.bottom /= (tsp.rectInSelection.bottom - tsp.rectInSelection.top); // third, convert from part coordinates to item coordinates cellPart.left *= (tsp.rectInItem.right - tsp.rectInItem.left); cellPart.left += tsp.rectInItem.left; cellPart.right *= (tsp.rectInItem.right - tsp.rectInItem.left); cellPart.right += tsp.rectInItem.left; cellPart.top *= (tsp.rectInItem.bottom - tsp.rectInItem.top); cellPart.top += tsp.rectInItem.top; cellPart.bottom *= (tsp.rectInItem.bottom - tsp.rectInItem.top); cellPart.bottom += tsp.rectInItem.top; // now get the text Okular::RegularAreaRect rects; rects.append( cellPart ); txt += tsp.item->page()->text( &rects, Okular::TextPage::CentralPixelTextAreaInclusionBehaviour ); } QString html = txt; selText += txt.replace(QLatin1Char('\n'), QLatin1Char(' ')); html.replace(QLatin1Char('&'), QLatin1String("&")).replace(QLatin1Char('<'), QLatin1String("<")).replace(QLatin1Char('>'), QLatin1String(">")); // Remove newlines, do not turn them into
, because // Excel interprets
within cell as new cell... html.replace(QLatin1Char('\n'), QLatin1String(" ")); selHtml += QStringLiteral("
"); } selText += QLatin1Char('\n'); selHtml += QLatin1String("\n"); } selHtml += QLatin1String("
") + html + QStringLiteral("
\n"); QClipboard *cb = QApplication::clipboard(); QMimeData *md = new QMimeData(); md->setText(selText); md->setHtml(selHtml); cb->setMimeData( md, QClipboard::Clipboard ); if ( cb->supportsSelection() ) cb->setMimeData( md, QClipboard::Selection ); }break; case Okular::Settings::EnumMouseMode::TextSelect: // if it is a left release checks if is over a previous link press if ( leftButton && mouseReleaseOverLink ( d->mouseOverLinkObject ) ) { selectionClear(); break; } if ( d->mouseTextSelecting ) { d->mouseTextSelecting = false; // textSelectionClear(); if ( d->document->isAllowed( Okular::AllowCopy ) ) { const QString text = d->selectedText(); if ( !text.isEmpty() ) { QClipboard *cb = QApplication::clipboard(); if ( cb->supportsSelection() ) cb->setText( text, QClipboard::Selection ); } } } else if ( !d->mousePressPos.isNull() && rightButton ) { PageViewItem* item = pickItemOnPoint(eventPos.x(),eventPos.y()); const Okular::Page *page; //if there is text selected in the page if (item) { QAction * httpLink = nullptr; QAction * textToClipboard = nullptr; QString url; QMenu * menu = createProcessLinkMenu( item, eventPos ); const bool mouseClickOverLink = (menu != nullptr); #ifdef HAVE_SPEECH QAction *speakText = nullptr; #endif if ( (page = item->page())->textSelection() ) { if ( !menu ) { menu = new QMenu(this); } textToClipboard = menu->addAction( QIcon::fromTheme( QStringLiteral("edit-copy") ), i18n( "Copy Text" ) ); #ifdef HAVE_SPEECH if ( Okular::Settings::useTTS() ) speakText = menu->addAction( QIcon::fromTheme( QStringLiteral("text-speak") ), i18n( "Speak Text" ) ); #endif if ( !d->document->isAllowed( Okular::AllowCopy ) ) { textToClipboard->setEnabled( false ); textToClipboard->setText( i18n("Copy forbidden by DRM") ); } else { addSearchWithinDocumentAction(menu, d->selectedText()); addWebShortcutsMenu( menu, d->selectedText() ); } // if the right-click was over a link add "Follow This link" instead of "Go to" if (!mouseClickOverLink) { url = UrlUtils::getUrl( d->selectedText() ); if ( !url.isEmpty() ) { const QString squeezedText = KStringHandler::rsqueeze( url, linkTextPreviewLength ); httpLink = menu->addAction( i18n( "Go to '%1'", squeezedText ) ); httpLink->setObjectName(QStringLiteral("GoToAction")); } } } if ( menu ) { menu->setObjectName(QStringLiteral("PopupMenu")); QAction *choice = menu->exec( e->globalPos() ); // check if the user really selected an action if ( choice ) { if ( choice == textToClipboard ) copyTextSelection(); #ifdef HAVE_SPEECH else if ( choice == speakText ) { const QString text = d->selectedText(); d->tts()->say( text ); } #endif else if ( choice == httpLink ) { new KRun( QUrl( url ), this ); } } menu->deleteLater(); } } } break; } // reset mouse press / 'drag start' position d->mousePressPos = QPoint(); } void PageView::guessTableDividers() { QList< QPair > colTicks, rowTicks, colSelectionTicks, rowSelectionTicks; for ( const TableSelectionPart& tsp : qAsConst(d->tableSelectionParts) ) { // add ticks for the edges of this area... colSelectionTicks.append( qMakePair( tsp.rectInSelection.left, +1 ) ); colSelectionTicks.append( qMakePair( tsp.rectInSelection.right, -1 ) ); rowSelectionTicks.append( qMakePair( tsp.rectInSelection.top, +1 ) ); rowSelectionTicks.append( qMakePair( tsp.rectInSelection.bottom, -1 ) ); // get the words in this part Okular::RegularAreaRect rects; rects.append( tsp.rectInItem ); const Okular::TextEntity::List words = tsp.item->page()->words( &rects, Okular::TextPage::CentralPixelTextAreaInclusionBehaviour ); for (const Okular::TextEntity *te : words) { if (te->text().isEmpty()) { delete te; continue; } Okular::NormalizedRect wordArea = *te->area(); // convert it from item coordinates to part coordinates wordArea.left -= tsp.rectInItem.left; wordArea.left /= (tsp.rectInItem.right - tsp.rectInItem.left); wordArea.right -= tsp.rectInItem.left; wordArea.right /= (tsp.rectInItem.right - tsp.rectInItem.left); wordArea.top -= tsp.rectInItem.top; wordArea.top /= (tsp.rectInItem.bottom - tsp.rectInItem.top); wordArea.bottom -= tsp.rectInItem.top; wordArea.bottom /= (tsp.rectInItem.bottom - tsp.rectInItem.top); // convert from part coordinates to table coordinates wordArea.left *= (tsp.rectInSelection.right - tsp.rectInSelection.left); wordArea.left += tsp.rectInSelection.left; wordArea.right *= (tsp.rectInSelection.right - tsp.rectInSelection.left); wordArea.right += tsp.rectInSelection.left; wordArea.top *= (tsp.rectInSelection.bottom - tsp.rectInSelection.top); wordArea.top += tsp.rectInSelection.top; wordArea.bottom *= (tsp.rectInSelection.bottom - tsp.rectInSelection.top); wordArea.bottom += tsp.rectInSelection.top; // add to the ticks arrays... colTicks.append( qMakePair( wordArea.left, +1) ); colTicks.append( qMakePair( wordArea.right, -1) ); rowTicks.append( qMakePair( wordArea.top, +1) ); rowTicks.append( qMakePair( wordArea.bottom, -1) ); delete te; } } int tally = 0; std::sort(colSelectionTicks.begin(), colSelectionTicks.end()); std::sort(rowSelectionTicks.begin(), rowSelectionTicks.end()); for (int i = 0; i < colSelectionTicks.length(); ++i) { tally += colSelectionTicks[i].second; if ( tally == 0 && i + 1 < colSelectionTicks.length() && colSelectionTicks[i+1].first != colSelectionTicks[i].first) { colTicks.append( qMakePair( colSelectionTicks[i].first, +1 ) ); colTicks.append( qMakePair( colSelectionTicks[i+1].first, -1 ) ); } } Q_ASSERT( tally == 0 ); for (int i = 0; i < rowSelectionTicks.length(); ++i) { tally += rowSelectionTicks[i].second; if ( tally == 0 && i + 1 < rowSelectionTicks.length() && rowSelectionTicks[i+1].first != rowSelectionTicks[i].first) { rowTicks.append( qMakePair( rowSelectionTicks[i].first, +1 ) ); rowTicks.append( qMakePair( rowSelectionTicks[i+1].first, -1 ) ); } } Q_ASSERT( tally == 0 ); std::sort(colTicks.begin(), colTicks.end()); std::sort(rowTicks.begin(), rowTicks.end()); for (int i = 0; i < colTicks.length(); ++i) { tally += colTicks[i].second; if ( tally == 0 && i + 1 < colTicks.length() && colTicks[i+1].first != colTicks[i].first) { d->tableSelectionCols.append( (colTicks[i].first+colTicks[i+1].first) / 2 ); d->tableDividersGuessed = true; } } Q_ASSERT( tally == 0 ); for (int i = 0; i < rowTicks.length(); ++i) { tally += rowTicks[i].second; if ( tally == 0 && i + 1 < rowTicks.length() && rowTicks[i+1].first != rowTicks[i].first) { d->tableSelectionRows.append( (rowTicks[i].first+rowTicks[i+1].first) / 2 ); d->tableDividersGuessed = true; } } Q_ASSERT( tally == 0 ); } void PageView::mouseDoubleClickEvent( QMouseEvent * e ) { d->controlWheelAccumulatedDelta = 0; if ( e->button() == Qt::LeftButton ) { const QPoint eventPos = contentAreaPoint( e->pos() ); PageViewItem * pageItem = pickItemOnPoint( eventPos.x(), eventPos.y() ); if ( pageItem ) { // find out normalized mouse coords inside current item double nX = pageItem->absToPageX(eventPos.x()); double nY = pageItem->absToPageY(eventPos.y()); if ( d->mouseMode == Okular::Settings::EnumMouseMode::TextSelect ) { textSelectionClear(); Okular::RegularAreaRect *wordRect = pageItem->page()->wordAt( Okular::NormalizedPoint( nX, nY ) ); if ( wordRect ) { // TODO words with hyphens across pages d->document->setPageTextSelection( pageItem->pageNumber(), wordRect, palette().color( QPalette::Active, QPalette::Highlight ) ); d->pagesWithTextSelection << pageItem->pageNumber(); if ( d->document->isAllowed( Okular::AllowCopy ) ) { const QString text = d->selectedText(); if ( !text.isEmpty() ) { QClipboard *cb = QApplication::clipboard(); if ( cb->supportsSelection() ) cb->setText( text, QClipboard::Selection ); } } return; } } const QRect & itemRect = pageItem->uncroppedGeometry(); Okular::Annotation * ann = nullptr; const Okular::ObjectRect * orect = pageItem->page()->objectRect( Okular::ObjectRect::OAnnotation, nX, nY, itemRect.width(), itemRect.height() ); if ( orect ) ann = ( (Okular::AnnotationObjectRect *)orect )->annotation(); if ( ann && ann->subType() != Okular::Annotation::AWidget ) { openAnnotationWindow( ann, pageItem->pageNumber() ); } } } } void PageView::wheelEvent( QWheelEvent *e ) { if ( !d->document->isOpened() ) { QAbstractScrollArea::wheelEvent( e ); return; } int delta = e->angleDelta().y(), vScroll = verticalScrollBar()->value(); e->accept(); if ( (e->modifiers() & Qt::ControlModifier) == Qt::ControlModifier ) { d->controlWheelAccumulatedDelta += delta; if ( d->controlWheelAccumulatedDelta <= -QWheelEvent::DefaultDeltasPerStep ) { slotZoomOut(); d->controlWheelAccumulatedDelta = 0; } else if ( d->controlWheelAccumulatedDelta >= QWheelEvent::DefaultDeltasPerStep ) { slotZoomIn(); d->controlWheelAccumulatedDelta = 0; } } else { d->controlWheelAccumulatedDelta = 0; if ( delta <= -QWheelEvent::DefaultDeltasPerStep && !Okular::Settings::viewContinuous() && vScroll == verticalScrollBar()->maximum() ) { // go to next page if ( (int)d->document->currentPage() < d->items.count() - 1 ) { // more optimized than document->setNextPage and then move view to top Okular::DocumentViewport newViewport = d->document->viewport(); newViewport.pageNumber += viewColumns(); if ( newViewport.pageNumber >= (int)d->items.count() ) newViewport.pageNumber = d->items.count() - 1; newViewport.rePos.enabled = true; newViewport.rePos.normalizedY = 0.0; d->document->setViewport( newViewport ); d->scroller->scrollTo(QPoint(horizontalScrollBar()->value(), verticalScrollBar()->value()), 0); //sync scroller with scrollbar } } else if ( delta >= QWheelEvent::DefaultDeltasPerStep && !Okular::Settings::viewContinuous() && vScroll == verticalScrollBar()->minimum() ) { // go to prev page if ( d->document->currentPage() > 0 ) { // more optimized than document->setPrevPage and then move view to bottom Okular::DocumentViewport newViewport = d->document->viewport(); newViewport.pageNumber -= viewColumns(); if ( newViewport.pageNumber < 0 ) newViewport.pageNumber = 0; newViewport.rePos.enabled = true; newViewport.rePos.normalizedY = 1.0; d->document->setViewport( newViewport ); d->scroller->scrollTo(QPoint(horizontalScrollBar()->value(), verticalScrollBar()->value()), 0); //sync scroller with scrollbar } } else{ if(delta != 0 && delta % QWheelEvent::DefaultDeltasPerStep == 0 ){ //number of scroll wheel steps Qt gives to us at the same time int count = abs(delta / QWheelEvent::DefaultDeltasPerStep); if(delta<0){ slotScrollDown(count); }else{ slotScrollUp(count); } } else{ d->scroller->scrollTo(d->scroller->finalPosition() - e->angleDelta()/4.0 , 0 ); } } } updateCursor(); } bool PageView::viewportEvent( QEvent * e ) { if ( e->type() == QEvent::ToolTip // Show tool tips only for those modes that change the cursor // to a hand when hovering over the link. && ( d->mouseMode == Okular::Settings::EnumMouseMode::Browse || d->mouseMode == Okular::Settings::EnumMouseMode::RectSelect || d->mouseMode == Okular::Settings::EnumMouseMode::TextSelect || d->mouseMode == Okular::Settings::EnumMouseMode::TrimSelect ) ) { QHelpEvent * he = static_cast< QHelpEvent* >( e ); if ( d->mouseAnnotation->isMouseOver() ) { d->mouseAnnotation->routeTooltipEvent( he ); } else { const QPoint eventPos = contentAreaPoint( he->pos() ); PageViewItem * pageItem = pickItemOnPoint( eventPos.x(), eventPos.y() ); const Okular::ObjectRect * rect = nullptr; const Okular::Action * link = nullptr; if ( pageItem ) { double nX = pageItem->absToPageX( eventPos.x() ); double nY = pageItem->absToPageY( eventPos.y() ); rect = pageItem->page()->objectRect( Okular::ObjectRect::Action, nX, nY, pageItem->uncroppedWidth(), pageItem->uncroppedHeight() ); if ( rect ) link = static_cast< const Okular::Action * >( rect->object() ); } if ( link ) { QRect r = rect->boundingRect( pageItem->uncroppedWidth(), pageItem->uncroppedHeight() ); r.translate( pageItem->uncroppedGeometry().topLeft() ); r.translate( -contentAreaPosition() ); QString tip = link->actionTip(); if ( !tip.isEmpty() ) QToolTip::showText( he->globalPos(), tip, viewport(), r ); } } e->accept(); return true; } else // do not stop the event return QAbstractScrollArea::viewportEvent( e ); } void PageView::scrollContentsBy( int dx, int dy ) { const QRect r = viewport()->rect(); viewport()->scroll( dx, dy, r ); // HACK manually repaint the damaged regions, as it seems some updates are missed // thus leaving artifacts around QRegion rgn( r ); rgn -= rgn & r.translated( dx, dy ); for ( const QRect &rect : rgn ) viewport()->update( rect ); } //END widget events -QList< Okular::RegularAreaRect * > PageView::textSelections( const QPoint& start, const QPoint& end, int& firstpage ) +QList< Okular::RegularAreaRect * > PageView::textSelections( const QPoint start, const QPoint end, int& firstpage ) { firstpage = -1; QList< Okular::RegularAreaRect * > ret; QSet< int > affectedItemsSet; QRect selectionRect = QRect( start, end ).normalized(); for ( const PageViewItem *item : qAsConst(d->items) ) { if ( item->isVisible() && selectionRect.intersects( item->croppedGeometry() ) ) affectedItemsSet.insert( item->pageNumber() ); } #ifdef PAGEVIEW_DEBUG qCDebug(OkularUiDebug) << ">>>> item selected by mouse:" << affectedItemsSet.count(); #endif if ( !affectedItemsSet.isEmpty() ) { // is the mouse drag line the ne-sw diagonal of the selection rect? bool direction_ne_sw = start == selectionRect.topRight() || start == selectionRect.bottomLeft(); int tmpmin = d->document->pages(); int tmpmax = 0; for ( const int p : qAsConst(affectedItemsSet) ) { if ( p < tmpmin ) tmpmin = p; if ( p > tmpmax ) tmpmax = p; } PageViewItem * a = pickItemOnPoint( (int)( direction_ne_sw ? selectionRect.right() : selectionRect.left() ), (int)selectionRect.top() ); int min = a && ( a->pageNumber() != tmpmax ) ? a->pageNumber() : tmpmin; PageViewItem * b = pickItemOnPoint( (int)( direction_ne_sw ? selectionRect.left() : selectionRect.right() ), (int)selectionRect.bottom() ); int max = b && ( b->pageNumber() != tmpmin ) ? b->pageNumber() : tmpmax; QList< int > affectedItemsIds; for ( int i = min; i <= max; ++i ) affectedItemsIds.append( i ); #ifdef PAGEVIEW_DEBUG qCDebug(OkularUiDebug) << ">>>> pages:" << affectedItemsIds; #endif firstpage = affectedItemsIds.first(); if ( affectedItemsIds.count() == 1 ) { PageViewItem * item = d->items[ affectedItemsIds.first() ]; selectionRect.translate( -item->uncroppedGeometry().topLeft() ); ret.append( textSelectionForItem( item, direction_ne_sw ? selectionRect.topRight() : selectionRect.topLeft(), direction_ne_sw ? selectionRect.bottomLeft() : selectionRect.bottomRight() ) ); } else if ( affectedItemsIds.count() > 1 ) { // first item PageViewItem * first = d->items[ affectedItemsIds.first() ]; QRect geom = first->croppedGeometry().intersected( selectionRect ).translated( -first->uncroppedGeometry().topLeft() ); ret.append( textSelectionForItem( first, selectionRect.bottom() > geom.height() ? ( direction_ne_sw ? geom.topRight() : geom.topLeft() ) : ( direction_ne_sw ? geom.bottomRight() : geom.bottomLeft() ), QPoint() ) ); // last item PageViewItem * last = d->items[ affectedItemsIds.last() ]; geom = last->croppedGeometry().intersected( selectionRect ).translated( -last->uncroppedGeometry().topLeft() ); // the last item needs to appended at last... Okular::RegularAreaRect * lastArea = textSelectionForItem( last, QPoint(), selectionRect.bottom() > geom.height() ? ( direction_ne_sw ? geom.bottomLeft() : geom.bottomRight() ) : ( direction_ne_sw ? geom.topLeft() : geom.topRight() ) ); affectedItemsIds.removeFirst(); affectedItemsIds.removeLast(); // item between the two above for ( const int page : qAsConst(affectedItemsIds) ) { ret.append( textSelectionForItem( d->items[ page ] ) ); } ret.append( lastArea ); } } return ret; } -void PageView::drawDocumentOnPainter( const QRect & contentsRect, QPainter * p ) +void PageView::drawDocumentOnPainter( const QRect contentsRect, QPainter * p ) { QColor backColor; if ( Okular::Settings::useCustomBackgroundColor() ) backColor = Okular::Settings::backgroundColor(); else backColor = viewport()->palette().color( QPalette::Dark ); // create a region from which we'll subtract painted rects QRegion remainingArea( contentsRect ); // This loop draws the actual pages // iterate over all items painting the ones intersecting contentsRect for ( const PageViewItem * item : qAsConst( d->items ) ) { // check if a piece of the page intersects the contents rect if ( !item->isVisible() || !item->croppedGeometry().intersects( contentsRect ) ) continue; // get item and item's outline geometries QRect itemGeometry = item->croppedGeometry(); // move the painter to the top-left corner of the real page p->save(); p->translate( itemGeometry.left(), itemGeometry.top() ); // draw the page using the PagePainter with all flags active if ( contentsRect.intersects( itemGeometry ) ) { Okular::NormalizedPoint *viewPortPoint = nullptr; Okular::NormalizedPoint point( d->lastSourceLocationViewportNormalizedX, d->lastSourceLocationViewportNormalizedY ); if( Okular::Settings::showSourceLocationsGraphically() && item->pageNumber() == d->lastSourceLocationViewportPageNumber ) { viewPortPoint = &point; } QRect pixmapRect = contentsRect.intersected( itemGeometry ); pixmapRect.translate( -item->croppedGeometry().topLeft() ); PagePainter::paintCroppedPageOnPainter( p, item->page(), this, pageflags, item->uncroppedWidth(), item->uncroppedHeight(), pixmapRect, item->crop(), viewPortPoint ); } // remove painted area from 'remainingArea' and restore painter remainingArea -= itemGeometry; p->restore(); } // fill the visible area around the page with the background color for (const QRect& backRect : remainingArea ) p->fillRect( backRect, backColor ); // take outline and shadow into account when testing whether a repaint is necessary auto dpr = devicePixelRatioF(); QRect checkRect = contentsRect; checkRect.adjust( -3, -3, 1, 1 ); // Method to linearly interpolate between black (=(0,0,0), omitted) and the background color auto interpolateColor = [&backColor]( double t ) { return QColor( t*backColor.red(), t*backColor.green(), t*backColor.blue() ); }; // width of the shadow in device pixels static const int shadowWidth = 2*dpr; // iterate over all items painting a black outline and a simple bottom/right gradient for ( const PageViewItem * item : qAsConst( d->items ) ) { // check if a piece of the page intersects the contents rect if ( !item->isVisible() || !item->croppedGeometry().intersects( checkRect ) ) continue; // get item and item's outline geometries QRect itemGeometry = item->croppedGeometry(); // move the painter to the top-left corner of the real page p->save(); p->translate( itemGeometry.left(), itemGeometry.top() ); // draw the page outline (black border and bottom-right shadow) if ( !itemGeometry.contains( contentsRect ) ) { int itemWidth = itemGeometry.width(); int itemHeight = itemGeometry.height(); // draw simple outline QPen pen( Qt::black ); pen.setWidth(0); p->setPen( pen ); QRectF outline( -1.0/dpr, -1.0/dpr, itemWidth + 1.0/dpr, itemHeight + 1.0/dpr ); p->drawRect( outline ); // draw bottom/right gradient for ( int i = 1; i <= shadowWidth; i++ ) { pen.setColor( interpolateColor( double(i)/( shadowWidth+1 ) ) ); p->setPen( pen ); QPointF left( (i-1)/dpr, itemHeight + i/dpr ); QPointF up( itemWidth + i/dpr, (i-1)/dpr ); QPointF corner( itemWidth + i/dpr, itemHeight + i/dpr); p->drawLine( left, corner ); p->drawLine( up, corner ); } } p->restore(); } } void PageView::updateItemSize( PageViewItem * item, int colWidth, int rowHeight ) { const Okular::Page * okularPage = item->page(); double width = okularPage->width(), height = okularPage->height(), zoom = d->zoomFactor; Okular::NormalizedRect crop( 0., 0., 1., 1. ); // Handle cropping, due to either "Trim Margin" or "Trim to Selection" cases if (( Okular::Settings::trimMargins() && okularPage->isBoundingBoxKnown() && !okularPage->boundingBox().isNull() ) || ( d->aTrimToSelection && d->aTrimToSelection->isChecked() && !d->trimBoundingBox.isNull())) { crop = Okular::Settings::trimMargins() ? okularPage->boundingBox() : d->trimBoundingBox; // Rotate the bounding box for ( int i = okularPage->rotation(); i > 0; --i ) { Okular::NormalizedRect rot = crop; crop.left = 1 - rot.bottom; crop.top = rot.left; crop.right = 1 - rot.top; crop.bottom = rot.right; } // Expand the crop slightly beyond the bounding box (for Trim Margins only) if (Okular::Settings::trimMargins()) { static const double cropExpandRatio = 0.04; const double cropExpand = cropExpandRatio * ( (crop.right-crop.left) + (crop.bottom-crop.top) ) / 2; crop = Okular::NormalizedRect( crop.left - cropExpand, crop.top - cropExpand, crop.right + cropExpand, crop.bottom + cropExpand ) & Okular::NormalizedRect( 0, 0, 1, 1 ); } // We currently generate a larger image and then crop it, so if the // crop rect is very small the generated image is huge. Hence, we shouldn't // let the crop rect become too small. static double minCropRatio; if (Okular::Settings::trimMargins()) { // Make sure we crop by at most 50% in either dimension: minCropRatio = 0.5; } else { // Looser Constraint for "Trim Selection" minCropRatio = 0.20; } if ( ( crop.right - crop.left ) < minCropRatio ) { const double newLeft = ( crop.left + crop.right ) / 2 - minCropRatio/2; crop.left = qMax( 0.0, qMin( 1.0 - minCropRatio, newLeft ) ); crop.right = crop.left + minCropRatio; } if ( ( crop.bottom - crop.top ) < minCropRatio ) { const double newTop = ( crop.top + crop.bottom ) / 2 - minCropRatio/2; crop.top = qMax( 0.0, qMin( 1.0 - minCropRatio, newTop ) ); crop.bottom = crop.top + minCropRatio; } width *= ( crop.right - crop.left ); height *= ( crop.bottom - crop.top ); #ifdef PAGEVIEW_DEBUG qCDebug(OkularUiDebug) << "Cropped page" << okularPage->number() << "to" << crop << "width" << width << "height" << height << "by bbox" << okularPage->boundingBox(); #endif } if ( d->zoomMode == ZoomFixed ) { width *= zoom; height *= zoom; item->setWHZC( (int)width, (int)height, d->zoomFactor, crop ); } else if ( d->zoomMode == ZoomFitWidth ) { height = ( height / width ) * colWidth; zoom = (double)colWidth / width; item->setWHZC( colWidth, (int)height, zoom, crop ); if ((uint)item->pageNumber() == d->document->currentPage()) d->zoomFactor = zoom; } else if ( d->zoomMode == ZoomFitPage ) { const double scaleW = (double)colWidth / (double)width; const double scaleH = (double)rowHeight / (double)height; zoom = qMin( scaleW, scaleH ); item->setWHZC( (int)(zoom * width), (int)(zoom * height), zoom, crop ); if ((uint)item->pageNumber() == d->document->currentPage()) d->zoomFactor = zoom; } else if ( d->zoomMode == ZoomFitAuto ) { const double aspectRatioRelation = 1.25; // relation between aspect ratios for "auto fit" const double uiAspect = (double)rowHeight / (double)colWidth; const double pageAspect = (double)height / (double)width; const double rel = uiAspect / pageAspect; const bool isContinuous = Okular::Settings::viewContinuous(); if ( !isContinuous && rel > aspectRatioRelation ) { // UI space is relatively much higher than the page zoom = (double)rowHeight / (double)height; } else if ( rel < 1.0 / aspectRatioRelation ) { // UI space is relatively much wider than the page in relation zoom = (double)colWidth / (double)width; } else { // aspect ratios of page and UI space are very similar const double scaleW = (double)colWidth / (double)width; const double scaleH = (double)rowHeight / (double)height; zoom = qMin( scaleW, scaleH ); } item->setWHZC( (int)(zoom * width), (int)(zoom * height), zoom, crop ); if ((uint)item->pageNumber() == d->document->currentPage()) d->zoomFactor = zoom; } #ifndef NDEBUG else qCDebug(OkularUiDebug) << "calling updateItemSize with unrecognized d->zoomMode!"; #endif } PageViewItem * PageView::pickItemOnPoint( int x, int y ) { PageViewItem * item = nullptr; for ( PageViewItem * i : qAsConst( d->visibleItems ) ) { const QRect & r = i->croppedGeometry(); if ( x < r.right() && x > r.left() && y < r.bottom() ) { if ( y > r.top() ) item = i; break; } } return item; } void PageView::textSelectionClear() { // something to clear if ( !d->pagesWithTextSelection.isEmpty() ) { for ( const int page : qAsConst( d->pagesWithTextSelection ) ) d->document->setPageTextSelection( page, nullptr, QColor() ); d->pagesWithTextSelection.clear(); } } -void PageView::selectionStart( const QPoint & pos, const QColor & color, bool /*aboveAll*/ ) +void PageView::selectionStart( const QPoint pos, const QColor & color, bool /*aboveAll*/ ) { selectionClear(); d->mouseSelecting = true; d->mouseSelectionRect.setRect( pos.x(), pos.y(), 1, 1 ); d->mouseSelectionColor = color; // ensures page doesn't scroll if ( d->autoScrollTimer ) { d->scrollIncrement = 0; d->autoScrollTimer->stop(); } } -void PageView::scrollPosIntoView( const QPoint & pos ) +void PageView::scrollPosIntoView( const QPoint pos ) { // this number slows the speed of the page by its value, chosen not to be too fast or too slow, the actual speed is determined from the mouse position, not critical const int damping=6; if (pos.x() < horizontalScrollBar()->value()) d->dragScrollVector.setX((pos.x() - horizontalScrollBar()->value())/damping); else if (horizontalScrollBar()->value() + viewport()->width() < pos.x()) d->dragScrollVector.setX((pos.x() - horizontalScrollBar()->value() - viewport()->width())/damping); else d->dragScrollVector.setX(0); if (pos.y() < verticalScrollBar()->value()) d->dragScrollVector.setY((pos.y() - verticalScrollBar()->value())/damping); else if (verticalScrollBar()->value() + viewport()->height() < pos.y()) d->dragScrollVector.setY((pos.y() - verticalScrollBar()->value() - viewport()->height())/damping); else d->dragScrollVector.setY(0); if (d->dragScrollVector != QPoint(0, 0)) { if (!d->dragScrollTimer.isActive()) d->dragScrollTimer.start(1000/60); //60 fps } else d->dragScrollTimer.stop(); } QPoint PageView::viewportToContentArea( const Okular::DocumentViewport & vp ) const { Q_ASSERT( vp.pageNumber >= 0 ); const QRect & r = d->items[ vp.pageNumber ]->croppedGeometry(); QPoint c { r.left(), r.top() }; if ( vp.rePos.enabled ) { if ( vp.rePos.pos == Okular::DocumentViewport::Center ) { c.rx() += qRound( normClamp( vp.rePos.normalizedX, 0.5 ) * (double)r.width() ); c.ry() += qRound( normClamp( vp.rePos.normalizedY, 0.0 ) * (double)r.height() ); } else { // TopLeft c.rx() += qRound( normClamp( vp.rePos.normalizedX, 0.0 ) * (double)r.width() + viewport()->width() / 2.0 ); c.ry() += qRound( normClamp( vp.rePos.normalizedY, 0.0 ) * (double)r.height() + viewport()->height() / 2.0 ); } } else { // exact repositioning disabled, align page top margin with viewport top border by default c.rx() += r.width() / 2; c.ry() += viewport()->height() / 2 - 10; } return c; } -void PageView::updateSelection( const QPoint & pos ) +void PageView::updateSelection( const QPoint pos ) { if ( d->mouseSelecting ) { scrollPosIntoView( pos ); // update the selection rect QRect updateRect = d->mouseSelectionRect; d->mouseSelectionRect.setBottomLeft( pos ); updateRect |= d->mouseSelectionRect; updateRect.translate( -contentAreaPosition() ); viewport()->update( updateRect.adjusted( -1, -2, 2, 1 ) ); } else if ( d->mouseTextSelecting) { scrollPosIntoView( pos ); int first = -1; const QList< Okular::RegularAreaRect * > selections = textSelections( pos, d->mouseSelectPos, first ); QSet< int > pagesWithSelectionSet; for ( int i = 0; i < selections.count(); ++i ) pagesWithSelectionSet.insert( i + first ); const QSet< int > noMoreSelectedPages = d->pagesWithTextSelection - pagesWithSelectionSet; // clear the selection from pages not selected anymore for ( int p : noMoreSelectedPages ) { d->document->setPageTextSelection( p, nullptr, QColor() ); } // set the new selection for the selected pages for ( int p : qAsConst(pagesWithSelectionSet) ) { d->document->setPageTextSelection( p, selections[ p - first ], palette().color( QPalette::Active, QPalette::Highlight ) ); } d->pagesWithTextSelection = pagesWithSelectionSet; } } -static Okular::NormalizedPoint rotateInNormRect( const QPoint &rotated, const QRect &rect, Okular::Rotation rotation ) +static Okular::NormalizedPoint rotateInNormRect( const QPoint rotated, const QRect rect, Okular::Rotation rotation ) { Okular::NormalizedPoint ret; switch ( rotation ) { case Okular::Rotation0: ret = Okular::NormalizedPoint( rotated.x(), rotated.y(), rect.width(), rect.height() ); break; case Okular::Rotation90: ret = Okular::NormalizedPoint( rotated.y(), rect.width() - rotated.x(), rect.height(), rect.width() ); break; case Okular::Rotation180: ret = Okular::NormalizedPoint( rect.width() - rotated.x(), rect.height() - rotated.y(), rect.width(), rect.height() ); break; case Okular::Rotation270: ret = Okular::NormalizedPoint( rect.height() - rotated.y(), rotated.x(), rect.height(), rect.width() ); break; } return ret; } -Okular::RegularAreaRect * PageView::textSelectionForItem( const PageViewItem * item, const QPoint & startPoint, const QPoint & endPoint ) +Okular::RegularAreaRect * PageView::textSelectionForItem( const PageViewItem * item, const QPoint startPoint, const QPoint endPoint ) { const QRect & geometry = item->uncroppedGeometry(); Okular::NormalizedPoint startCursor( 0.0, 0.0 ); if ( !startPoint.isNull() ) { startCursor = rotateInNormRect( startPoint, geometry, item->page()->rotation() ); } Okular::NormalizedPoint endCursor( 1.0, 1.0 ); if ( !endPoint.isNull() ) { endCursor = rotateInNormRect( endPoint, geometry, item->page()->rotation() ); } Okular::TextSelection mouseTextSelectionInfo( startCursor, endCursor ); const Okular::Page * okularPage = item->page(); if ( !okularPage->hasTextPage() ) d->document->requestTextPage( okularPage->number() ); Okular::RegularAreaRect * selectionArea = okularPage->textArea( &mouseTextSelectionInfo ); #ifdef PAGEVIEW_DEBUG qCDebug(OkularUiDebug).nospace() << "text areas (" << okularPage->number() << "): " << ( selectionArea ? QString::number( selectionArea->count() ) : "(none)" ); #endif return selectionArea; } void PageView::selectionClear(const ClearMode mode) { QRect updatedRect = d->mouseSelectionRect.normalized().adjusted( -2, -2, 2, 2 ); d->mouseSelecting = false; d->mouseSelectionRect.setCoords( 0, 0, 0, 0 ); d->tableSelectionCols.clear(); d->tableSelectionRows.clear(); d->tableDividersGuessed = false; for (const TableSelectionPart &tsp : qAsConst(d->tableSelectionParts)) { QRect selectionPartRect = tsp.rectInItem.geometry(tsp.item->uncroppedWidth(), tsp.item->uncroppedHeight()); selectionPartRect.translate( tsp.item->uncroppedGeometry().topLeft () ); // should check whether this is on-screen here? updatedRect = updatedRect.united(selectionPartRect); } if ( mode != ClearOnlyDividers ) { d->tableSelectionParts.clear(); } d->tableSelectionParts.clear(); updatedRect.translate( -contentAreaPosition() ); viewport()->update( updatedRect ); } // const to be used for both zoomFactorFitMode function and slotRelayoutPages. static const int kcolWidthMargin = 6; static const int krowHeightMargin = 12; double PageView::zoomFactorFitMode( ZoomMode mode ) { const int pageCount = d->items.count(); if ( pageCount == 0 ) return 0; const bool facingCentered = Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::FacingFirstCentered || (Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Facing && pageCount == 1); const bool overrideCentering = facingCentered && pageCount < 3; const int nCols = overrideCentering ? 1 : viewColumns(); const double colWidth = viewport()->width() / static_cast(nCols) - kcolWidthMargin; const double rowHeight = viewport()->height() - krowHeightMargin; const PageViewItem * currentItem = d->items[ qMax( 0, (int)d->document->currentPage()) ]; // prevent segmentation fault when opening a new document; if ( !currentItem ) return 0; const Okular::Page * okularPage = currentItem->page(); const double width = okularPage->width(), height = okularPage->height(); if ( mode == ZoomFitWidth ) return (double) colWidth / width; if ( mode == ZoomFitPage ) { const double scaleW = (double) colWidth / (double)width; const double scaleH = (double) rowHeight / (double)height; return qMin(scaleW, scaleH); } return 0; } void PageView::updateZoom( ZoomMode newZoomMode ) { if ( newZoomMode == ZoomFixed ) { if ( d->aZoom->currentItem() == 0 ) newZoomMode = ZoomFitWidth; else if ( d->aZoom->currentItem() == 1 ) newZoomMode = ZoomFitPage; else if ( d->aZoom->currentItem() == 2 ) newZoomMode = ZoomFitAuto; } float newFactor = d->zoomFactor; QAction * checkedZoomAction = nullptr; switch ( newZoomMode ) { case ZoomFixed:{ //ZoomFixed case QString z = d->aZoom->currentText(); // kdelibs4 sometimes adds accelerators to actions' text directly :( z.remove (QLatin1Char('&')); z.remove (QLatin1Char('%')); newFactor = QLocale().toDouble( z ) / 100.0; }break; case ZoomIn: case ZoomOut:{ const float zoomFactorFitWidth = zoomFactorFitMode(ZoomFitWidth); const float zoomFactorFitPage = zoomFactorFitMode(ZoomFitPage); QVector zoomValue(15); std::copy(kZoomValues, kZoomValues + 13, zoomValue.begin()); zoomValue[13] = zoomFactorFitWidth; zoomValue[14] = zoomFactorFitPage; std::sort(zoomValue.begin(), zoomValue.end()); QVector::iterator i; if ( newZoomMode == ZoomOut ) { if (newFactor <= zoomValue.first()) return; i = std::lower_bound(zoomValue.begin(), zoomValue.end(), newFactor) - 1; } else { if (newFactor >= zoomValue.last()) return; i = std::upper_bound(zoomValue.begin(), zoomValue.end(), newFactor); } const float tmpFactor = *i; if ( tmpFactor == zoomFactorFitWidth ) { newZoomMode = ZoomFitWidth; checkedZoomAction = d->aZoomFitWidth; } else if ( tmpFactor == zoomFactorFitPage ) { newZoomMode = ZoomFitPage; checkedZoomAction = d->aZoomFitPage; } else { newFactor = tmpFactor; newZoomMode = ZoomFixed; } } break; case ZoomActual: newZoomMode = ZoomFixed; newFactor = 1.0; break; case ZoomFitWidth: checkedZoomAction = d->aZoomFitWidth; break; case ZoomFitPage: checkedZoomAction = d->aZoomFitPage; break; case ZoomFitAuto: checkedZoomAction = d->aZoomAutoFit; break; case ZoomRefreshCurrent: newZoomMode = ZoomFixed; d->zoomFactor = -1; break; } const float upperZoomLimit = d->document->supportsTiles() ? 16.0 : 4.0; if ( newFactor > upperZoomLimit ) newFactor = upperZoomLimit; if ( newFactor < 0.1 ) newFactor = 0.1; if ( newZoomMode != d->zoomMode || (newZoomMode == ZoomFixed && newFactor != d->zoomFactor ) ) { // rebuild layout and update the whole viewport d->zoomMode = newZoomMode; d->zoomFactor = newFactor; // be sure to block updates to document's viewport bool prevState = d->blockViewport; d->blockViewport = true; slotRelayoutPages(); d->blockViewport = prevState; // request pixmaps slotRequestVisiblePixmaps(); // update zoom text updateZoomText(); // update actions checked state if ( d->aZoomFitWidth ) { d->aZoomFitWidth->setChecked( checkedZoomAction == d->aZoomFitWidth ); d->aZoomFitPage->setChecked( checkedZoomAction == d->aZoomFitPage ); d->aZoomAutoFit->setChecked( checkedZoomAction == d->aZoomAutoFit ); } } else if ( newZoomMode == ZoomFixed && newFactor == d->zoomFactor ) updateZoomText(); d->aZoomIn->setEnabled( d->zoomFactor < upperZoomLimit-0.001 ); d->aZoomOut->setEnabled( d->zoomFactor > 0.101 ); d->aZoomActual->setEnabled( d->zoomFactor != 1.0 ); } void PageView::updateZoomText() { // use current page zoom as zoomFactor if in ZoomFit/* mode if ( d->zoomMode != ZoomFixed && d->items.count() > 0 ) d->zoomFactor = d->items[ qMax( 0, (int)d->document->currentPage() ) ]->zoomFactor(); float newFactor = d->zoomFactor; d->aZoom->removeAllActions(); // add items that describe fit actions QStringList translated; translated << i18n("Fit Width") << i18n("Fit Page") << i18n("Auto Fit"); // add percent items int idx = 0, selIdx = 3; bool inserted = false; //use: "d->zoomMode != ZoomFixed" to hide Fit/* zoom ratio int zoomValueCount = 11; if ( d->document->supportsTiles() ) zoomValueCount = 13; while ( idx < zoomValueCount || !inserted ) { float value = idx < zoomValueCount ? kZoomValues[ idx ] : newFactor; if ( !inserted && newFactor < (value - 0.0001) ) value = newFactor; else idx ++; if ( value > (newFactor - 0.0001) && value < (newFactor + 0.0001) ) inserted = true; if ( !inserted ) selIdx++; // we do not need to display 2-digit precision QString localValue( QLocale().toString( value * 100.0, 'f', 1 ) ); localValue.remove( QLocale().decimalPoint() + QLatin1Char('0') ); // remove a trailing zero in numbers like 66.70 if ( localValue.right( 1 ) == QLatin1String( "0" ) && localValue.indexOf( QLocale().decimalPoint() ) > -1 ) localValue.chop( 1 ); translated << QStringLiteral( "%1%" ).arg( localValue ); } d->aZoom->setItems( translated ); // select current item in list if ( d->zoomMode == ZoomFitWidth ) selIdx = 0; else if ( d->zoomMode == ZoomFitPage ) selIdx = 1; else if ( d->zoomMode == ZoomFitAuto ) selIdx = 2; // we have to temporarily enable the actions as otherwise we can't set a new current item d->aZoom->setEnabled( true ); d->aZoom->selectableActionGroup()->setEnabled( true ); d->aZoom->setCurrentItem( selIdx ); d->aZoom->setEnabled( d->items.size() > 0 ); d->aZoom->selectableActionGroup()->setEnabled( d->items.size() > 0 ); } void PageView::updateViewMode(const int nr) { const QList actions = d->aViewMode->menu()->actions(); for ( QAction* action : actions ) { QVariant mode_id = action->data(); if (mode_id.toInt() == nr) { action->trigger(); } } } void PageView::updateCursor() { const QPoint p = contentAreaPosition() + viewport()->mapFromGlobal( QCursor::pos() ); updateCursor( p ); } -void PageView::updateCursor( const QPoint &p ) +void PageView::updateCursor( const QPoint p ) { // reset mouse over link it will be re-set if that still valid d->mouseOverLinkObject = nullptr; // detect the underlaying page (if present) PageViewItem * pageItem = pickItemOnPoint( p.x(), p.y() ); if ( d->annotator && d->annotator->active() ) { if ( pageItem || d->annotator->annotating() ) setCursor( d->annotator->cursor() ); else setCursor( Qt::ForbiddenCursor ); } else if ( pageItem ) { double nX = pageItem->absToPageX(p.x()); double nY = pageItem->absToPageY(p.y()); Qt::CursorShape cursorShapeFallback; // if over a ObjectRect (of type Link) change cursor to hand switch ( d->mouseMode ) { case Okular::Settings::EnumMouseMode::TextSelect: if (d->mouseTextSelecting) { setCursor( Qt::IBeamCursor ); return; } cursorShapeFallback = Qt::IBeamCursor; break; case Okular::Settings::EnumMouseMode::Magnifier: setCursor( Qt::CrossCursor ); return; case Okular::Settings::EnumMouseMode::RectSelect: case Okular::Settings::EnumMouseMode::TrimSelect: if (d->mouseSelecting) { setCursor( Qt::CrossCursor ); return; } cursorShapeFallback = Qt::CrossCursor; break; case Okular::Settings::EnumMouseMode::Browse: d->mouseOnRect = false; if ( d->mouseAnnotation->isMouseOver() ) { d->mouseOnRect = true; setCursor( d->mouseAnnotation->cursor() ); return; } else { cursorShapeFallback = Qt::OpenHandCursor; } break; default: setCursor( Qt::ArrowCursor ); return; } const Okular::ObjectRect * linkobj = pageItem->page()->objectRect( Okular::ObjectRect::Action, nX, nY, pageItem->uncroppedWidth(), pageItem->uncroppedHeight() ); if ( linkobj ) { d->mouseOverLinkObject = linkobj; d->mouseOnRect = true; setCursor( Qt::PointingHandCursor ); } else { setCursor(cursorShapeFallback); } } else { // if there's no page over the cursor and we were showing the pointingHandCursor // go back to the normal one d->mouseOnRect = false; setCursor( Qt::ArrowCursor ); } } void PageView::reloadForms() { if( d->m_formsVisible ) { for ( PageViewItem * item : qAsConst( d->visibleItems ) ) { item->reloadFormWidgetsState(); } } } -void PageView::moveMagnifier( const QPoint& p ) // non scaled point +void PageView::moveMagnifier( const QPoint p ) // non scaled point { const int w = d->magnifierView->width() * 0.5; const int h = d->magnifierView->height() * 0.5; int x = p.x() - w; int y = p.y() - h; const int max_x = viewport()->width(); const int max_y = viewport()->height(); QPoint scroll(0,0); if (x < 0) { if (horizontalScrollBar()->value() > 0) scroll.setX(x - w); x = 0; } if (y < 0) { if (verticalScrollBar()->value() > 0) scroll.setY(y - h); y = 0; } if (p.x() + w > max_x) { if (horizontalScrollBar()->value() < horizontalScrollBar()->maximum()) scroll.setX(p.x() + 2 * w - max_x); x = max_x - d->magnifierView->width() - 1; } if (p.y() + h > max_y) { if (verticalScrollBar()->value() < verticalScrollBar()->maximum()) scroll.setY(p.y() + 2 * h - max_y); y = max_y - d->magnifierView->height() - 1; } if (!scroll.isNull()) scrollPosIntoView(contentAreaPoint(p + scroll)); d->magnifierView->move(x, y); } -void PageView::updateMagnifier( const QPoint& p ) // scaled point +void PageView::updateMagnifier( const QPoint p ) // scaled point { /* translate mouse coordinates to page coordinates and inform the magnifier of the situation */ PageViewItem *item = pickItemOnPoint(p.x(), p.y()); if (item) { Okular::NormalizedPoint np(item->absToPageX(p.x()), item->absToPageY(p.y())); d->magnifierView->updateView( np, item->page() ); } } int PageView::viewColumns() const { int vm = Okular::Settings::viewMode(); if (vm == Okular::Settings::EnumViewMode::Single) return 1; else if (vm == Okular::Settings::EnumViewMode::Facing || vm == Okular::Settings::EnumViewMode::FacingFirstCentered) return 2; else if (vm == Okular::Settings::EnumViewMode::Summary && d->document->pages() < Okular::Settings::viewColumns() ) return d->document->pages(); else return Okular::Settings::viewColumns(); } void PageView::center(int cx, int cy, bool smoothMove) { scrollTo( cx - viewport()->width() / 2, cy - viewport()->height() / 2, smoothMove ); } void PageView::scrollTo( int x, int y, bool smoothMove ) { bool prevState = d->blockPixmapsRequest; int newValue = -1; if ( x != horizontalScrollBar()->value() || y != verticalScrollBar()->value() ) newValue = 1; // Pretend this call is the result of a scrollbar event d->blockPixmapsRequest = true; if(smoothMove) d->scroller->scrollTo(QPoint(x, y)); else d->scroller->scrollTo(QPoint(x, y), 0); d->blockPixmapsRequest = prevState; slotRequestVisiblePixmaps( newValue ); } void PageView::toggleFormWidgets( bool on ) { bool somehadfocus = false; for ( PageViewItem * item : qAsConst( d->items ) ) { const bool hadfocus = item->setFormWidgetsVisible( on ); somehadfocus = somehadfocus || hadfocus; } if ( somehadfocus ) setFocus(); d->m_formsVisible = on; if ( d->aToggleForms ) // it may not exist if we are on dummy mode { if ( d->m_formsVisible ) { d->aToggleForms->setText( i18n( "Hide Forms" ) ); } else { d->aToggleForms->setText( i18n( "Show Forms" ) ); } } } -void PageView::resizeContentArea( const QSize & newSize ) +void PageView::resizeContentArea( const QSize newSize ) { const QSize vs = viewport()->size(); int hRange = newSize.width() - vs.width(); int vRange = newSize.height() - vs.height(); if ( horizontalScrollBar()->isVisible() && hRange == verticalScrollBar()->width() && verticalScrollBar()->isVisible() && vRange == horizontalScrollBar()->height() && Okular::Settings::showScrollBars() ) { hRange = 0; vRange = 0; } horizontalScrollBar()->setRange( 0, hRange ); verticalScrollBar()->setRange( 0, vRange ); updatePageStep(); } void PageView::updatePageStep() { const QSize vs = viewport()->size(); horizontalScrollBar()->setPageStep( vs.width() ); verticalScrollBar()->setPageStep( vs.height() * (100 - Okular::Settings::scrollOverlap()) / 100 ); } void PageView::addWebShortcutsMenu( QMenu * menu, const QString & text ) { if ( text.isEmpty() ) { return; } QString searchText = text; searchText = searchText.replace( QLatin1Char('\n'), QLatin1Char(' ') ).replace(QLatin1Char( '\r'), QLatin1Char(' ') ).simplified(); if ( searchText.isEmpty() ) { return; } KUriFilterData filterData( searchText ); filterData.setSearchFilteringOptions( KUriFilterData::RetrievePreferredSearchProvidersOnly ); if ( KUriFilter::self()->filterSearchUri( filterData, KUriFilter::NormalTextFilter ) ) { const QStringList searchProviders = filterData.preferredSearchProviders(); if ( !searchProviders.isEmpty() ) { QMenu *webShortcutsMenu = new QMenu( menu ); webShortcutsMenu->setIcon( QIcon::fromTheme( QStringLiteral("preferences-web-browser-shortcuts") ) ); const QString squeezedText = KStringHandler::rsqueeze( searchText, searchTextPreviewLength ); webShortcutsMenu->setTitle( i18n( "Search for '%1' with", squeezedText ) ); QAction *action = nullptr; for ( const QString &searchProvider : searchProviders ) { action = new QAction( searchProvider, webShortcutsMenu ); action->setIcon( QIcon::fromTheme( filterData.iconNameForPreferredSearchProvider( searchProvider ) ) ); action->setData( filterData.queryForPreferredSearchProvider( searchProvider ) ); connect( action, &QAction::triggered, this, &PageView::slotHandleWebShortcutAction ); webShortcutsMenu->addAction( action ); } webShortcutsMenu->addSeparator(); action = new QAction( i18n( "Configure Web Shortcuts..." ), webShortcutsMenu ); action->setIcon( QIcon::fromTheme( QStringLiteral("configure") ) ); connect( action, &QAction::triggered, this, &PageView::slotConfigureWebShortcuts ); webShortcutsMenu->addAction( action ); menu->addMenu(webShortcutsMenu); } } } -QMenu* PageView::createProcessLinkMenu(PageViewItem *item, const QPoint &eventPos) +QMenu* PageView::createProcessLinkMenu(PageViewItem *item, const QPoint eventPos) { // check if the right-click was over a link const double nX = item->absToPageX(eventPos.x()); const double nY = item->absToPageY(eventPos.y()); const Okular::ObjectRect * rect = item->page()->objectRect( Okular::ObjectRect::Action, nX, nY, item->uncroppedWidth(), item->uncroppedHeight() ); if ( rect ) { const Okular::Action * link = static_cast< const Okular::Action * >( rect->object() ); if (!link) return nullptr; QMenu *menu = new QMenu(this); // creating the menu and its actions QAction * processLink = menu->addAction( i18n( "Follow This Link" ) ); processLink->setObjectName(QStringLiteral("ProcessLinkAction")); if ( link->actionType() == Okular::Action::Sound ) { processLink->setText( i18n( "Play this Sound" ) ); if ( Okular::AudioPlayer::instance()->state() == Okular::AudioPlayer::PlayingState ) { QAction * actStopSound = menu->addAction( i18n( "Stop Sound" ) ); connect( actStopSound, &QAction::triggered, []() { Okular::AudioPlayer::instance()->stopPlaybacks(); }); } } if ( dynamic_cast< const Okular::BrowseAction * >( link ) ) { QAction * actCopyLinkLocation = menu->addAction( QIcon::fromTheme( QStringLiteral("edit-copy") ), i18n( "Copy Link Address" ) ); actCopyLinkLocation->setObjectName(QStringLiteral("CopyLinkLocationAction")); connect( actCopyLinkLocation, &QAction::triggered, menu, [ link ]() { const Okular::BrowseAction * browseLink = static_cast< const Okular::BrowseAction * >( link ); QClipboard *cb = QApplication::clipboard(); cb->setText( browseLink->url().toDisplayString(), QClipboard::Clipboard ); if ( cb->supportsSelection() ) cb->setText( browseLink->url().toDisplayString(), QClipboard::Selection ); } ); } connect( processLink, &QAction::triggered, this, [this, link]() { d->document->processAction( link ); }); return menu; } return nullptr; } void PageView::addSearchWithinDocumentAction(QMenu *menu, const QString &searchText) { const QString squeezedText = KStringHandler::rsqueeze( searchText, searchTextPreviewLength ); QAction *action = new QAction(i18n("Search for '%1' in this document", squeezedText), menu); action->setIcon( QIcon::fromTheme( QStringLiteral("document-preview") ) ); connect(action, &QAction::triggered, this, [this, searchText]{Q_EMIT triggerSearch(searchText);}); menu->addAction( action ); } //BEGIN private SLOTS void PageView::slotRelayoutPages() // called by: notifySetup, viewportResizeEvent, slotViewMode, slotContinuousToggled, updateZoom { // set an empty container if we have no pages const int pageCount = d->items.count(); if ( pageCount < 1 ) { return; } int viewportWidth = viewport()->width(), viewportHeight = viewport()->height(), fullWidth = 0, fullHeight = 0; // handle the 'center first page in row' stuff const bool facing = Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Facing && pageCount > 1; const bool facingCentered = Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::FacingFirstCentered || (Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Facing && pageCount == 1); const bool overrideCentering = facingCentered && pageCount < 3; const bool centerFirstPage = facingCentered && !overrideCentering; const bool facingPages = facing || centerFirstPage; const bool centerLastPage = centerFirstPage && pageCount % 2 == 0; const bool continuousView = Okular::Settings::viewContinuous(); const int nCols = overrideCentering ? 1 : viewColumns(); const bool singlePageViewMode = Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Single; if ( d->aFitWindowToPage ) d->aFitWindowToPage->setEnabled( !continuousView && singlePageViewMode ); // set all items geometry and resize contents. handle 'continuous' and 'single' modes separately PageViewItem * currentItem = d->items[ qMax( 0, (int)d->document->currentPage() ) ]; // Here we find out column's width and row's height to compute a table // so we can place widgets 'centered in virtual cells'. const int nRows = (int)ceil( (float)(centerFirstPage ? (pageCount + nCols - 1) : pageCount) / (float)nCols ); int * colWidth = new int[ nCols ], * rowHeight = new int[ nRows ], cIdx = 0, rIdx = 0; for ( int i = 0; i < nCols; i++ ) colWidth[ i ] = viewportWidth / nCols; for ( int i = 0; i < nRows; i++ ) rowHeight[ i ] = 0; // handle the 'centering on first row' stuff if ( centerFirstPage ) cIdx += nCols - 1; // 1) find the maximum columns width and rows height for a grid in // which each page must well-fit inside a cell for ( PageViewItem * item : qAsConst( d->items ) ) { // update internal page size (leaving a little margin in case of Fit* modes) updateItemSize( item, colWidth[ cIdx ] - kcolWidthMargin, viewportHeight - krowHeightMargin ); // find row's maximum height and column's max width if ( item->croppedWidth() + kcolWidthMargin > colWidth[ cIdx ] ) colWidth[ cIdx ] = item->croppedWidth() + kcolWidthMargin; if ( item->croppedHeight() + krowHeightMargin > rowHeight[ rIdx ] ) rowHeight[ rIdx ] = item->croppedHeight() + krowHeightMargin; // handle the 'centering on first row' stuff // update col/row indices if ( ++cIdx == nCols ) { cIdx = 0; rIdx++; } } const int pageRowIdx = ( ( centerFirstPage ? nCols - 1 : 0 ) + currentItem->pageNumber() ) / nCols; // 2) compute full size for ( int i = 0; i < nCols; i++ ) fullWidth += colWidth[ i ]; if ( continuousView ) { for ( int i = 0; i < nRows; i++ ) fullHeight += rowHeight[ i ]; } else fullHeight = rowHeight[ pageRowIdx ]; // 3) arrange widgets inside cells (and refine fullHeight if needed) int insertX = 0, insertY = fullHeight < viewportHeight ? ( viewportHeight - fullHeight ) / 2 : 0; const int origInsertY = insertY; cIdx = 0; rIdx = 0; if ( centerFirstPage ) { cIdx += nCols - 1; for ( int i = 0; i < cIdx; ++i ) insertX += colWidth[ i ]; } for ( PageViewItem * item : qAsConst( d->items ) ) { int cWidth = colWidth[ cIdx ], rHeight = rowHeight[ rIdx ]; if ( continuousView || rIdx == pageRowIdx ) { const bool reallyDoCenterFirst = item->pageNumber() == 0 && centerFirstPage; const bool reallyDoCenterLast = item->pageNumber() == pageCount - 1 && centerLastPage; int actualX = 0; if ( reallyDoCenterFirst || reallyDoCenterLast ) { // page is centered across entire viewport actualX = (fullWidth - item->croppedWidth()) / 2; } else if ( facingPages ) { if (Okular::Settings::rtlReadingDirection()){ // RTL reading mode actualX = ( (centerFirstPage && item->pageNumber() % 2 == 0) || (!centerFirstPage && item->pageNumber() % 2 == 1) ) ? (fullWidth / 2) - item->croppedWidth() - 1 : (fullWidth / 2) + 1; } else { // page edges 'touch' the center of the viewport actualX = ( (centerFirstPage && item->pageNumber() % 2 == 1) || (!centerFirstPage && item->pageNumber() % 2 == 0) ) ? (fullWidth / 2) - item->croppedWidth() - 1 : (fullWidth / 2) + 1; } } else { // page is centered within its virtual column //actualX = insertX + (cWidth - item->croppedWidth()) / 2; if (Okular::Settings::rtlReadingDirection()){ actualX = fullWidth - insertX - cWidth +( (cWidth - item->croppedWidth()) / 2); } else { actualX = insertX + (cWidth - item->croppedWidth()) / 2; } } item->moveTo( actualX, (continuousView ? insertY : origInsertY) + (rHeight - item->croppedHeight()) / 2 ); item->setVisible( true ); } else { item->moveTo( 0, 0 ); item->setVisible( false ); } item->setFormWidgetsVisible( d->m_formsVisible ); // advance col/row index insertX += cWidth; if ( ++cIdx == nCols ) { cIdx = 0; rIdx++; insertX = 0; insertY += rHeight; } #ifdef PAGEVIEW_DEBUG kWarning() << "updating size for pageno" << item->pageNumber() << "cropped" << item->croppedGeometry() << "uncropped" << item->uncroppedGeometry(); #endif } delete [] colWidth; delete [] rowHeight; // 3) reset dirty state d->dirtyLayout = false; // 4) update scrollview's contents size and recenter view bool wasUpdatesEnabled = viewport()->updatesEnabled(); if ( fullWidth != contentAreaWidth() || fullHeight != contentAreaHeight() ) { const Okular::DocumentViewport vp = d->document->viewport(); // disable updates and resize the viewportContents if ( wasUpdatesEnabled ) viewport()->setUpdatesEnabled( false ); resizeContentArea( QSize( fullWidth, fullHeight ) ); // restore previous viewport if defined and updates enabled if ( wasUpdatesEnabled ) { if ( vp.pageNumber >= 0 ) { int prevX = horizontalScrollBar()->value(), prevY = verticalScrollBar()->value(); const QPoint centerPos = viewportToContentArea( vp ); center( centerPos.x(), centerPos.y() ); // center() usually moves the viewport, that requests pixmaps too. // if that doesn't happen we have to request them by hand if ( prevX == horizontalScrollBar()->value() && prevY == verticalScrollBar()->value() ) slotRequestVisiblePixmaps(); } // or else go to center page else center( fullWidth / 2, 0 ); viewport()->setUpdatesEnabled( true ); } } // 5) update the whole viewport if updated enabled if ( wasUpdatesEnabled ) viewport()->update(); } void PageView::delayedResizeEvent() { // If we already got here we don't need to execute the timer slot again d->delayResizeEventTimer->stop(); slotRelayoutPages(); slotRequestVisiblePixmaps(); } -static void slotRequestPreloadPixmap( Okular::DocumentObserver * observer, const PageViewItem * i, const QRect &expandedViewportRect, QLinkedList< Okular::PixmapRequest * > *requestedPixmaps ) +static void slotRequestPreloadPixmap( Okular::DocumentObserver * observer, const PageViewItem * i, const QRect expandedViewportRect, QLinkedList< Okular::PixmapRequest * > *requestedPixmaps ) { Okular::NormalizedRect preRenderRegion; const QRect intersectionRect = expandedViewportRect.intersected( i->croppedGeometry() ); if ( !intersectionRect.isEmpty() ) preRenderRegion = Okular::NormalizedRect( intersectionRect.translated( -i->uncroppedGeometry().topLeft() ), i->uncroppedWidth(), i->uncroppedHeight() ); // request the pixmap if not already present if ( !i->page()->hasPixmap( observer, i->uncroppedWidth(), i->uncroppedHeight(), preRenderRegion ) && i->uncroppedWidth() > 0 ) { Okular::PixmapRequest::PixmapRequestFeatures requestFeatures = Okular::PixmapRequest::Preload; requestFeatures |= Okular::PixmapRequest::Asynchronous; const bool pageHasTilesManager = i->page()->hasTilesManager( observer ); if ( pageHasTilesManager && !preRenderRegion.isNull() ) { Okular::PixmapRequest * p = new Okular::PixmapRequest( observer, i->pageNumber(), i->uncroppedWidth(), i->uncroppedHeight(), PAGEVIEW_PRELOAD_PRIO, requestFeatures ); requestedPixmaps->push_back( p ); p->setNormalizedRect( preRenderRegion ); p->setTile( true ); } else if ( !pageHasTilesManager ) { Okular::PixmapRequest * p = new Okular::PixmapRequest( observer, i->pageNumber(), i->uncroppedWidth(), i->uncroppedHeight(), PAGEVIEW_PRELOAD_PRIO, requestFeatures ); requestedPixmaps->push_back( p ); p->setNormalizedRect( preRenderRegion ); } } } void PageView::slotRequestVisiblePixmaps( int newValue ) { // if requests are blocked (because raised by an unwanted event), exit if ( d->blockPixmapsRequest || d->scroller->state() == QScroller::Scrolling) return; // precalc view limits for intersecting with page coords inside the loop const bool isEvent = newValue != -1 && !d->blockViewport; const QRect viewportRect( horizontalScrollBar()->value(), verticalScrollBar()->value(), viewport()->width(), viewport()->height() ); const QRect viewportRectAtZeroZero( 0, 0, viewport()->width(), viewport()->height() ); // some variables used to determine the viewport int nearPageNumber = -1; const double viewportCenterX = (viewportRect.left() + viewportRect.right()) / 2.0; const double viewportCenterY = (viewportRect.top() + viewportRect.bottom()) / 2.0; double focusedX = 0.5, focusedY = 0.0, minDistance = -1.0; // Margin (in pixels) around the viewport to preload const int pixelsToExpand = 512; // iterate over all items d->visibleItems.clear(); QLinkedList< Okular::PixmapRequest * > requestedPixmaps; QVector< Okular::VisiblePageRect * > visibleRects; for ( PageViewItem * i : qAsConst( d->items ) ) { const QSet formWidgetsList = i->formWidgets(); for ( FormWidgetIface *fwi : formWidgetsList) { Okular::NormalizedRect r = fwi->rect(); fwi->moveTo( qRound( i->uncroppedGeometry().left() + i->uncroppedWidth() * r.left ) + 1 - viewportRect.left(), qRound( i->uncroppedGeometry().top() + i->uncroppedHeight() * r.top ) + 1 - viewportRect.top() ); } const QHash videoWidgets = i->videoWidgets(); for ( VideoWidget *vw : videoWidgets ) { const Okular::NormalizedRect r = vw->normGeometry(); vw->move( qRound( i->uncroppedGeometry().left() + i->uncroppedWidth() * r.left ) + 1 - viewportRect.left(), qRound( i->uncroppedGeometry().top() + i->uncroppedHeight() * r.top ) + 1 - viewportRect.top() ); if ( vw->isPlaying() && viewportRectAtZeroZero.intersected( vw->geometry() ).isEmpty() ) { vw->stop(); vw->pageLeft(); } } if ( !i->isVisible() ) continue; #ifdef PAGEVIEW_DEBUG kWarning() << "checking page" << i->pageNumber(); kWarning().nospace() << "viewportRect is " << viewportRect << ", page item is " << i->croppedGeometry() << " intersect : " << viewportRect.intersects( i->croppedGeometry() ); #endif // if the item doesn't intersect the viewport, skip it QRect intersectionRect = viewportRect.intersected( i->croppedGeometry() ); if ( intersectionRect.isEmpty() ) { continue; } // add the item to the 'visible list' d->visibleItems.push_back( i ); Okular::VisiblePageRect * vItem = new Okular::VisiblePageRect( i->pageNumber(), Okular::NormalizedRect( intersectionRect.translated( -i->uncroppedGeometry().topLeft() ), i->uncroppedWidth(), i->uncroppedHeight() ) ); visibleRects.push_back( vItem ); #ifdef PAGEVIEW_DEBUG kWarning() << "checking for pixmap for page" << i->pageNumber() << "=" << i->page()->hasPixmap( this, i->uncroppedWidth(), i->uncroppedHeight() ); kWarning() << "checking for text for page" << i->pageNumber() << "=" << i->page()->hasTextPage(); #endif Okular::NormalizedRect expandedVisibleRect = vItem->rect; if ( i->page()->hasTilesManager( this ) && Okular::Settings::memoryLevel() != Okular::Settings::EnumMemoryLevel::Low ) { double rectMargin = pixelsToExpand/(double)i->uncroppedHeight(); expandedVisibleRect.left = qMax( 0.0, vItem->rect.left - rectMargin ); expandedVisibleRect.top = qMax( 0.0, vItem->rect.top - rectMargin ); expandedVisibleRect.right = qMin( 1.0, vItem->rect.right + rectMargin ); expandedVisibleRect.bottom = qMin( 1.0, vItem->rect.bottom + rectMargin ); } // if the item has not the right pixmap, add a request for it if ( !i->page()->hasPixmap( this, i->uncroppedWidth(), i->uncroppedHeight(), expandedVisibleRect ) ) { #ifdef PAGEVIEW_DEBUG kWarning() << "rerequesting visible pixmaps for page" << i->pageNumber() << "!"; #endif Okular::PixmapRequest * p = new Okular::PixmapRequest( this, i->pageNumber(), i->uncroppedWidth(), i->uncroppedHeight(), PAGEVIEW_PRIO, Okular::PixmapRequest::Asynchronous ); requestedPixmaps.push_back( p ); if ( i->page()->hasTilesManager( this ) ) { p->setNormalizedRect( expandedVisibleRect ); p->setTile( true ); } else p->setNormalizedRect( vItem->rect ); } // look for the item closest to viewport center and the relative // position between the item and the viewport center if ( isEvent ) { const QRect & geometry = i->croppedGeometry(); // compute distance between item center and viewport center (slightly moved left) const double distance = hypot( (geometry.left() + geometry.right()) / 2.0 - (viewportCenterX - 4), (geometry.top() + geometry.bottom()) / 2.0 - viewportCenterY ); if ( distance >= minDistance && nearPageNumber != -1 ) continue; nearPageNumber = i->pageNumber(); minDistance = distance; if ( geometry.height() > 0 && geometry.width() > 0 ) { focusedX = ( viewportCenterX - (double)geometry.left() ) / (double)geometry.width(); focusedY = ( viewportCenterY - (double)geometry.top() ) / (double)geometry.height(); } } } // if preloading is enabled, add the pages before and after in preloading if ( !d->visibleItems.isEmpty() && Okular::SettingsCore::memoryLevel() != Okular::SettingsCore::EnumMemoryLevel::Low ) { // as the requests are done in the order as they appear in the list, // request first the next page and then the previous int pagesToPreload = viewColumns(); // if the greedy option is set, preload all pages if (Okular::SettingsCore::memoryLevel() == Okular::SettingsCore::EnumMemoryLevel::Greedy) pagesToPreload = d->items.count(); const QRect expandedViewportRect = viewportRect.adjusted( 0, -pixelsToExpand, 0, pixelsToExpand ); for( int j = 1; j <= pagesToPreload; j++ ) { // add the page after the 'visible series' in preload const int tailRequest = d->visibleItems.last()->pageNumber() + j; if ( tailRequest < (int)d->items.count() ) { slotRequestPreloadPixmap( this, d->items[ tailRequest ], expandedViewportRect, &requestedPixmaps ); } // add the page before the 'visible series' in preload const int headRequest = d->visibleItems.first()->pageNumber() - j; if ( headRequest >= 0 ) { slotRequestPreloadPixmap( this, d->items[ headRequest ], expandedViewportRect, &requestedPixmaps ); } // stop if we've already reached both ends of the document if ( headRequest < 0 && tailRequest >= (int)d->items.count() ) break; } } // send requests to the document if ( !requestedPixmaps.isEmpty() ) { d->document->requestPixmaps( requestedPixmaps ); } // if this functions was invoked by viewport events, send update to document if ( isEvent && nearPageNumber != -1 ) { // determine the document viewport Okular::DocumentViewport newViewport( nearPageNumber ); newViewport.rePos.enabled = true; newViewport.rePos.normalizedX = focusedX; newViewport.rePos.normalizedY = focusedY; // set the viewport to other observers d->document->setViewport( newViewport , this ); } d->document->setVisiblePageRects( visibleRects, this ); } void PageView::slotAutoScroll() { // the first time create the timer if ( !d->autoScrollTimer ) { d->autoScrollTimer = new QTimer( this ); d->autoScrollTimer->setSingleShot( true ); connect( d->autoScrollTimer, &QTimer::timeout, this, &PageView::slotAutoScroll ); } // if scrollIncrement is zero, stop the timer if ( !d->scrollIncrement ) { d->autoScrollTimer->stop(); return; } // compute delay between timer ticks and scroll amount per tick int index = abs( d->scrollIncrement ) - 1; // 0..9 const int scrollDelay[10] = { 200, 100, 50, 30, 20, 30, 25, 20, 30, 20 }; const int scrollOffset[10] = { 1, 1, 1, 1, 1, 2, 2, 2, 4, 4 }; d->autoScrollTimer->start( scrollDelay[ index ] ); int delta = d->scrollIncrement > 0 ? scrollOffset[ index ] : -scrollOffset[ index ]; d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(0, delta), scrollDelay[ index ]); } void PageView::slotDragScroll() { scrollTo( horizontalScrollBar()->value() + d->dragScrollVector.x(), verticalScrollBar()->value() + d->dragScrollVector.y() ); QPoint p = contentAreaPosition() + viewport()->mapFromGlobal( QCursor::pos() ); updateSelection( p ); } void PageView::slotShowWelcome() { // show initial welcome text d->messageWindow->display( i18n( "Welcome" ), QString(), PageViewMessage::Info, 2000 ); } void PageView::slotShowSizeAllCursor() { setCursor( Qt::SizeAllCursor ); } void PageView::slotHandleWebShortcutAction() { QAction *action = qobject_cast( sender() ); if (action) { KUriFilterData filterData( action->data().toString() ); if ( KUriFilter::self()->filterSearchUri( filterData, KUriFilter::WebShortcutFilter ) ) { QDesktopServices::openUrl( filterData.uri() ); } } } void PageView::slotConfigureWebShortcuts() { KToolInvocation::kdeinitExec( QStringLiteral("kcmshell5"), QStringList() << QStringLiteral("webshortcuts") ); } void PageView::slotZoom() { if ( !d->aZoom->selectableActionGroup()->isEnabled() ) return; setFocus(); updateZoom( ZoomFixed ); } void PageView::slotZoomIn() { updateZoom( ZoomIn ); } void PageView::slotZoomOut() { updateZoom( ZoomOut ); } void PageView::slotZoomActual() { updateZoom( ZoomActual ); } void PageView::slotFitToWidthToggled( bool on ) { if ( on ) updateZoom( ZoomFitWidth ); } void PageView::slotFitToPageToggled( bool on ) { if ( on ) updateZoom( ZoomFitPage ); } void PageView::slotAutoFitToggled( bool on ) { if ( on ) updateZoom( ZoomFitAuto ); } void PageView::slotViewMode( QAction *action ) { const int nr = action->data().toInt(); if ( (int)Okular::Settings::viewMode() != nr ) { Okular::Settings::setViewMode( nr ); Okular::Settings::self()->save(); if ( d->document->pages() > 0 ) slotRelayoutPages(); } } void PageView::slotContinuousToggled( bool on ) { if ( Okular::Settings::viewContinuous() != on ) { Okular::Settings::setViewContinuous( on ); Okular::Settings::self()->save(); if ( d->document->pages() > 0 ) slotRelayoutPages(); } } void PageView::slotSetMouseNormal() { d->mouseMode = Okular::Settings::EnumMouseMode::Browse; Okular::Settings::setMouseMode( d->mouseMode ); // hide the messageWindow d->messageWindow->hide(); // reshow the annotator toolbar if hiding was forced (and if it is not already visible) if ( d->annotator && d->annotator->hidingWasForced() && d->aToggleAnnotator && !d->aToggleAnnotator->isChecked() ) d->aToggleAnnotator->trigger(); // force an update of the cursor updateCursor(); Okular::Settings::self()->save(); } void PageView::slotSetMouseZoom() { d->mouseMode = Okular::Settings::EnumMouseMode::Zoom; Okular::Settings::setMouseMode( d->mouseMode ); // change the text in messageWindow (and show it if hidden) d->messageWindow->display( i18n( "Select zooming area. Right-click to zoom out." ), QString(), PageViewMessage::Info, -1 ); // force hiding of annotator toolbar if ( d->aToggleAnnotator && d->aToggleAnnotator->isChecked() ) { d->aToggleAnnotator->trigger(); d->annotator->setHidingForced( true ); } // force an update of the cursor updateCursor(); Okular::Settings::self()->save(); } void PageView::slotSetMouseMagnifier() { d->mouseMode = Okular::Settings::EnumMouseMode::Magnifier; Okular::Settings::setMouseMode( d->mouseMode ); d->messageWindow->display( i18n( "Click to see the magnified view." ), QString() ); // force an update of the cursor updateCursor(); Okular::Settings::self()->save(); } void PageView::slotSetMouseSelect() { d->mouseMode = Okular::Settings::EnumMouseMode::RectSelect; Okular::Settings::setMouseMode( d->mouseMode ); // change the text in messageWindow (and show it if hidden) d->messageWindow->display( i18n( "Draw a rectangle around the text/graphics to copy." ), QString(), PageViewMessage::Info, -1 ); // force hiding of annotator toolbar if ( d->aToggleAnnotator && d->aToggleAnnotator->isChecked() ) { d->aToggleAnnotator->trigger(); d->annotator->setHidingForced( true ); } // force an update of the cursor updateCursor(); Okular::Settings::self()->save(); } void PageView::slotSetMouseTextSelect() { d->mouseMode = Okular::Settings::EnumMouseMode::TextSelect; Okular::Settings::setMouseMode( d->mouseMode ); // change the text in messageWindow (and show it if hidden) d->messageWindow->display( i18n( "Select text" ), QString(), PageViewMessage::Info, -1 ); // force hiding of annotator toolbar if ( d->aToggleAnnotator && d->aToggleAnnotator->isChecked() ) { d->aToggleAnnotator->trigger(); d->annotator->setHidingForced( true ); } // force an update of the cursor updateCursor(); Okular::Settings::self()->save(); } void PageView::slotSetMouseTableSelect() { d->mouseMode = Okular::Settings::EnumMouseMode::TableSelect; Okular::Settings::setMouseMode( d->mouseMode ); // change the text in messageWindow (and show it if hidden) d->messageWindow->display( i18n( "Draw a rectangle around the table, then click near edges to divide up; press Esc to clear." ), QString(), PageViewMessage::Info, -1 ); // force hiding of annotator toolbar if ( d->aToggleAnnotator && d->aToggleAnnotator->isChecked() ) { d->aToggleAnnotator->trigger(); d->annotator->setHidingForced( true ); } // force an update of the cursor updateCursor(); Okular::Settings::self()->save(); } void PageView::slotToggleAnnotator( bool on ) { // the 'inHere' trick is needed as the slotSetMouseZoom() calls this static bool inHere = false; if ( inHere ) return; inHere = true; // the annotator can be used in normal mouse mode only, so if asked for it, // switch to normal mode if ( on && d->mouseMode != Okular::Settings::EnumMouseMode::Browse ) d->aMouseNormal->trigger(); // ask for Author's name if not already set if ( Okular::Settings::identityAuthor().isEmpty() ) { // get default username from the kdelibs/kdecore/KUser KUser currentUser; QString userName = currentUser.property( KUser::FullName ).toString(); // ask the user for confirmation/change if ( userName.isEmpty() ) { bool ok = false; userName = QInputDialog::getText(nullptr, i18n( "Annotations author" ), i18n( "Please insert your name or initials:" ), QLineEdit::Normal, QString(), &ok ); if ( !ok ) { d->aToggleAnnotator->trigger(); inHere = false; return; } } // save the name Okular::Settings::setIdentityAuthor( userName ); Okular::Settings::self()->save(); } // create the annotator object if not present if ( !d->annotator ) { d->annotator = new PageViewAnnotator( this, d->document ); bool allowTools = d->document->pages() > 0 && d->document->isAllowed( Okular::AllowNotes ); d->annotator->setToolsEnabled( allowTools ); d->annotator->setTextToolsEnabled( allowTools && d->document->supportsSearching() ); } // initialize/reset annotator (and show/hide toolbar) d->annotator->setEnabled( on ); d->annotator->setHidingForced( false ); inHere = false; } void PageView::slotAutoScrollUp() { if ( d->scrollIncrement < -9 ) return; d->scrollIncrement--; slotAutoScroll(); setFocus(); } void PageView::slotAutoScrollDown() { if ( d->scrollIncrement > 9 ) return; d->scrollIncrement++; slotAutoScroll(); setFocus(); } void PageView::slotScrollUp( int nSteps ) { //if we are too far behind the animation, do nothing and let it catch up auto limit_value = nSteps ? 200 : verticalScrollBar()->rect().height(); if(d->scroller->state() == QScroller::Scrolling && abs(d->scroller->finalPosition().y() - verticalScrollBar()->value()) > limit_value){ return; } // if in single page mode and at the top of the screen, go to \ page if ( Okular::Settings::viewContinuous() || verticalScrollBar()->value() > verticalScrollBar()->minimum() ) { if ( nSteps ){ d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(0,-100*nSteps), 100); }else{ if(d->scroller->finalPosition().y() > verticalScrollBar()->minimum()) d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(0, -verticalScrollBar()->rect().height() )); } } else if ( d->document->currentPage() > 0 ) { // more optimized than document->setPrevPage and then move view to bottom Okular::DocumentViewport newViewport = d->document->viewport(); newViewport.pageNumber -= viewColumns(); if ( newViewport.pageNumber < 0 ) newViewport.pageNumber = 0; newViewport.rePos.enabled = true; newViewport.rePos.normalizedY = 1.0; d->document->setViewport( newViewport ); } } void PageView::slotScrollDown( int nSteps ) { //if we are too far behind the animation, do nothing and let it catch up auto limit_value = nSteps ? 200 : verticalScrollBar()->rect().height(); if(d->scroller->state() == QScroller::Scrolling && abs(d->scroller->finalPosition().y() - verticalScrollBar()->value()) > limit_value){ return; } // if in single page mode and at the bottom of the screen, go to next page if ( Okular::Settings::viewContinuous() || verticalScrollBar()->value() < verticalScrollBar()->maximum() ) { if ( nSteps ){ d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(0,100*nSteps), 100); }else{ if(d->scroller->finalPosition().y() < verticalScrollBar()->maximum()) d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(0, verticalScrollBar()->rect().height() )); } } else if ( (int)d->document->currentPage() < d->items.count() - 1 ) { // more optimized than document->setNextPage and then move view to top Okular::DocumentViewport newViewport = d->document->viewport(); newViewport.pageNumber += viewColumns(); if ( newViewport.pageNumber >= (int)d->items.count() ) newViewport.pageNumber = d->items.count() - 1; newViewport.rePos.enabled = true; newViewport.rePos.normalizedY = 0.0; d->document->setViewport( newViewport ); } } void PageView::slotRotateClockwise() { int id = ( (int)d->document->rotation() + 1 ) % 4; d->document->setRotation( id ); } void PageView::slotRotateCounterClockwise() { int id = ( (int)d->document->rotation() + 3 ) % 4; d->document->setRotation( id ); } void PageView::slotRotateOriginal() { d->document->setRotation( 0 ); } void PageView::slotPageSizes( int newsize ) { if ( newsize < 0 || newsize >= d->document->pageSizes().count() ) return; d->document->setPageSize( d->document->pageSizes().at( newsize ) ); } // Enforce mutual-exclusion between trim modes // Each mode is uniquely identified by a single value // From Okular::Settings::EnumTrimMode void PageView::updateTrimMode( int except_id ) { const QList trimModeActions = d->aTrimMode->menu()->actions(); for (QAction *trimModeAction : trimModeActions) { if (trimModeAction->data().toInt() != except_id) trimModeAction->setChecked( false ); } } bool PageView::mouseReleaseOverLink( const Okular::ObjectRect * rect ) const { if ( rect ) { // handle click over a link const Okular::Action * action = static_cast< const Okular::Action * >( rect->object() ); d->document->processAction( action ); return true; } return false; } void PageView::slotTrimMarginsToggled( bool on ) { if (on) { // Turn off any other Trim modes updateTrimMode(d->aTrimMargins->data().toInt()); } if ( Okular::Settings::trimMargins() != on ) { Okular::Settings::setTrimMargins( on ); Okular::Settings::self()->save(); if ( d->document->pages() > 0 ) { slotRelayoutPages(); slotRequestVisiblePixmaps(); // TODO: slotRelayoutPages() may have done this already! } } } void PageView::slotTrimToSelectionToggled( bool on ) { if ( on ) { // Turn off any other Trim modes updateTrimMode(d->aTrimToSelection->data().toInt()); d->mouseMode = Okular::Settings::EnumMouseMode::TrimSelect; // change the text in messageWindow (and show it if hidden) d->messageWindow->display( i18n( "Draw a rectangle around the page area you wish to keep visible" ), QString(), PageViewMessage::Info, -1 ); // force hiding of annotator toolbar if ( d->aToggleAnnotator && d->aToggleAnnotator->isChecked() ) { d->aToggleAnnotator->trigger(); d->annotator->setHidingForced( true ); } // force an update of the cursor updateCursor(); } else { // toggled off while making selection if ( Okular::Settings::EnumMouseMode::TrimSelect == d->mouseMode ) { // clear widget selection and invalidate rect selectionClear(); // When Trim selection bbox interaction is over, we should switch to another mousemode. if ( d->aPrevAction ) { d->aPrevAction->trigger(); d->aPrevAction = nullptr; } else { d->aMouseNormal->trigger(); } } d->trimBoundingBox = Okular::NormalizedRect(); // invalidate box if ( d->document->pages() > 0 ) { slotRelayoutPages(); slotRequestVisiblePixmaps(); // TODO: slotRelayoutPages() may have done this already! } } } void PageView::slotToggleForms() { toggleFormWidgets( !d->m_formsVisible ); } void PageView::slotFormChanged( int pageNumber ) { if ( !d->refreshTimer ) { d->refreshTimer = new QTimer( this ); d->refreshTimer->setSingleShot( true ); connect( d->refreshTimer, &QTimer::timeout, this, &PageView::slotRefreshPage ); } d->refreshPages << pageNumber; int delay = 0; if ( d->m_formsVisible ) { delay = 1000; } d->refreshTimer->start( delay ); } void PageView::slotRefreshPage() { for (int req : qAsConst(d->refreshPages)) { QTimer::singleShot(0, this, [this, req] { d->document->refreshPixmaps(req); }); } d->refreshPages.clear(); } #ifdef HAVE_SPEECH void PageView::slotSpeakDocument() { QString text; for ( const PageViewItem * item : qAsConst( d->items ) ) { Okular::RegularAreaRect * area = textSelectionForItem( item ); text.append( item->page()->text( area ) ); text.append( '\n' ); delete area; } d->tts()->say( text ); } void PageView::slotSpeakCurrentPage() { const int currentPage = d->document->viewport().pageNumber; PageViewItem *item = d->items.at( currentPage ); Okular::RegularAreaRect * area = textSelectionForItem( item ); const QString text = item->page()->text( area ); delete area; d->tts()->say( text ); } void PageView::slotStopSpeaks() { if ( !d->m_tts ) return; d->m_tts->stopAllSpeechs(); } void PageView::slotPauseResumeSpeech() { if ( !d->m_tts ) return; d->m_tts->pauseResumeSpeech(); } #endif void PageView::slotAction( Okular::Action *action ) { d->document->processAction( action ); } void PageView::externalKeyPressEvent( QKeyEvent *e ) { keyPressEvent( e ); } void PageView::slotProcessMovieAction( const Okular::MovieAction *action ) { const Okular::MovieAnnotation *movieAnnotation = action->annotation(); if ( !movieAnnotation ) return; Okular::Movie *movie = movieAnnotation->movie(); if ( !movie ) return; const int currentPage = d->document->viewport().pageNumber; PageViewItem *item = d->items.at( currentPage ); if ( !item ) return; VideoWidget *vw = item->videoWidgets().value( movie ); if ( !vw ) return; vw->show(); switch ( action->operation() ) { case Okular::MovieAction::Play: vw->stop(); vw->play(); break; case Okular::MovieAction::Stop: vw->stop(); break; case Okular::MovieAction::Pause: vw->pause(); break; case Okular::MovieAction::Resume: vw->play(); break; }; } void PageView::slotProcessRenditionAction( const Okular::RenditionAction *action ) { Okular::Movie *movie = action->movie(); if ( !movie ) return; const int currentPage = d->document->viewport().pageNumber; PageViewItem *item = d->items.at( currentPage ); if ( !item ) return; VideoWidget *vw = item->videoWidgets().value( movie ); if ( !vw ) return; if ( action->operation() == Okular::RenditionAction::None ) return; vw->show(); switch ( action->operation() ) { case Okular::RenditionAction::Play: vw->stop(); vw->play(); break; case Okular::RenditionAction::Stop: vw->stop(); break; case Okular::RenditionAction::Pause: vw->pause(); break; case Okular::RenditionAction::Resume: vw->play(); break; default: return; }; } void PageView::slotSetChangeColors(bool active) { Okular::SettingsCore::setChangeColors(active); Okular::Settings::self()->save(); viewport()->update(); } void PageView::slotToggleChangeColors() { slotSetChangeColors( !Okular::SettingsCore::changeColors() ); } void PageView::slotFitWindowToPage() { const PageViewItem *currentPageItem = nullptr; QSize viewportSize = viewport()->size(); for ( const PageViewItem *pageItem : qAsConst(d->items) ) { if ( pageItem->isVisible() ) { currentPageItem = pageItem; break; } } if ( !currentPageItem ) return; const QSize pageSize = QSize( currentPageItem->uncroppedWidth() + kcolWidthMargin, currentPageItem->uncroppedHeight() + krowHeightMargin ); if ( verticalScrollBar()->isVisible() ) viewportSize.setWidth( viewportSize.width() + verticalScrollBar()->width() ); if ( horizontalScrollBar()->isVisible() ) viewportSize.setHeight( viewportSize.height() + horizontalScrollBar()->height() ); emit fitWindowToPage( viewportSize, pageSize ); } void PageView::slotSelectPage() { textSelectionClear(); const int currentPage = d->document->viewport().pageNumber; PageViewItem *item = d->items.at( currentPage ); if ( item ) { Okular::RegularAreaRect * area = textSelectionForItem( item ); d->pagesWithTextSelection.insert( currentPage ); d->document->setPageTextSelection( currentPage, area, palette().color( QPalette::Active, QPalette::Highlight ) ); } } void PageView::highlightSignatureFormWidget( const Okular::FormFieldSignature *form ) { QVector< PageViewItem * >::const_iterator dIt = d->items.constBegin(), dEnd = d->items.constEnd(); for ( ; dIt != dEnd; ++dIt ) { const QSet fwi = (*dIt)->formWidgets(); for ( FormWidgetIface *fw : fwi ) { if ( fw->formField() == form ) { SignatureEdit *widget = static_cast< SignatureEdit * >( fw ); widget->setDummyMode( true ); QTimer::singleShot( 250, this, [=]{ widget->setDummyMode( false ); }); return; } } } } //END private SLOTS /* kate: replace-tabs on; indent-width 4; */ diff --git a/ui/pageview.h b/ui/pageview.h index 61b9d20e2..614735140 100644 --- a/ui/pageview.h +++ b/ui/pageview.h @@ -1,289 +1,289 @@ /*************************************************************************** * Copyright (C) 2004 by Enrico Ros * * Copyright (C) 2004 by Albert Astals Cid * * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * * company, info@kdab.com. Work sponsored by the * * LiMux project of the city of Munich * * * * With portions of code from kpdf/kpdf_pagewidget.h by: * * Copyright (C) 2002 by Wilco Greven * * Copyright (C) 2003 by Christophe Devriese * * * * Copyright (C) 2003 by Laurent Montel * * Copyright (C) 2003 by Kurt Pfeifle * * * * 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 file follows coding style described in kdebase/kicker/HACKING #ifndef _OKULAR_PAGEVIEW_H_ #define _OKULAR_PAGEVIEW_H_ #include #include #include #include "ui/pageviewutils.h" #include "core/area.h" #include "core/observer.h" #include "core/view.h" class KActionCollection; namespace Okular { class Action; class Document; class DocumentViewport; class FormFieldSignature; class Annotation; class MovieAction; class RenditionAction; } class PageViewPrivate; class QGestureEvent; /** * @short The main view. Handles zoom and continuous mode.. oh, and page * @short display of course :-) * ... */ class PageView : public QAbstractScrollArea, public Okular::DocumentObserver, public Okular::View { Q_OBJECT public: PageView( QWidget *parent, Okular::Document *document ); ~PageView() override; // Zoom mode ( last 4 are internally used only! ) enum ZoomMode { ZoomFixed = 0, ZoomFitWidth = 1, ZoomFitPage = 2, ZoomFitAuto = 3, ZoomIn, ZoomOut, ZoomRefreshCurrent, ZoomActual }; enum ClearMode { ClearAllSelection, ClearOnlyDividers }; // create actions that interact with this widget void setupBaseActions( KActionCollection * ac ); void setupViewerActions( KActionCollection * ac ); void setupActions( KActionCollection * ac ); void updateActionState( bool docHasPages, bool documentChanged, bool docHasFormWidgets ); // misc methods (from RMB menu/children) bool canFitPageWidth() const; void fitPageWidth( int page ); // keep in sync with pageviewutils void displayMessage( const QString & message, const QString & details = QString(), PageViewMessage::Icon icon=PageViewMessage::Info, int duration=-1 ); // inherited from DocumentObserver void notifySetup( const QVector< Okular::Page * > & pages, int setupFlags ) override; void notifyViewportChanged( bool smoothMove ) override; void notifyPageChanged( int pageNumber, int changedFlags ) override; void notifyContentsCleared( int changedFlags ) override; void notifyZoom(int factor) override; bool canUnloadPixmap( int pageNum ) const override; void notifyCurrentPageChanged( int previous, int current ) override; // inherited from View bool supportsCapability( ViewCapability capability ) const override; CapabilityFlags capabilityFlags( ViewCapability capability ) const override; QVariant capability( ViewCapability capability ) const override; void setCapability( ViewCapability capability, const QVariant &option ) override; - QList< Okular::RegularAreaRect * > textSelections( const QPoint& start, const QPoint& end, int& firstpage ); - Okular::RegularAreaRect * textSelectionForItem( const PageViewItem * item, const QPoint & startPoint = QPoint(), const QPoint & endPoint = QPoint() ); + QList< Okular::RegularAreaRect * > textSelections( const QPoint start, const QPoint end, int& firstpage ); + Okular::RegularAreaRect * textSelectionForItem( const PageViewItem * item, const QPoint startPoint = QPoint(), const QPoint endPoint = QPoint() ); void reparseConfig(); KActionCollection *actionCollection() const; QAction *toggleFormsAction() const; int contentAreaWidth() const; int contentAreaHeight() const; QPoint contentAreaPosition() const; - QPoint contentAreaPoint( const QPoint & pos ) const; - QPointF contentAreaPoint( const QPointF & pos ) const; + QPoint contentAreaPoint( const QPoint pos ) const; + QPointF contentAreaPoint( const QPointF pos ) const; bool areSourceLocationsShownGraphically() const; void setShowSourceLocationsGraphically(bool show); void setLastSourceLocationViewport( const Okular::DocumentViewport& vp ); void clearLastSourceLocationViewport(); void updateCursor(); void highlightSignatureFormWidget( const Okular::FormFieldSignature *form ); public Q_SLOTS: void copyTextSelection() const; void selectAll(); void openAnnotationWindow( Okular::Annotation *annotation, int pageNumber ); void reloadForms(); void slotToggleChangeColors(); void slotSetChangeColors(bool active); void slotSelectPage(); void slotAction( Okular::Action *action ); void slotFormChanged( int pageNumber ); void externalKeyPressEvent( QKeyEvent *e ); Q_SIGNALS: - void rightClick( const Okular::Page *, const QPoint & ); + void rightClick( const Okular::Page *, const QPoint ); void mouseBackButtonClick(); void mouseForwardButtonClick(); void escPressed(); - void fitWindowToPage( const QSize& pageViewPortSize, const QSize& pageSize ); + void fitWindowToPage( const QSize pageViewPortSize, const QSize pageSize ); void triggerSearch( const QString& text ); protected: bool event( QEvent * event ) override; void resizeEvent( QResizeEvent* ) override; bool gestureEvent( QGestureEvent * e ); // mouse / keyboard events void keyPressEvent( QKeyEvent* ) override; void keyReleaseEvent( QKeyEvent* ) override; void inputMethodEvent( QInputMethodEvent * ) override; void wheelEvent( QWheelEvent* ) override; void paintEvent( QPaintEvent *e ) override; void tabletEvent (QTabletEvent *e ) override; void mouseMoveEvent( QMouseEvent *e ) override; void mousePressEvent( QMouseEvent *e ) override; void mouseReleaseEvent( QMouseEvent *e ) override; void mouseDoubleClickEvent( QMouseEvent *e ) override; bool viewportEvent( QEvent *e ) override; void scrollContentsBy( int dx, int dy ) override; private: // draw background and items on the opened qpainter - void drawDocumentOnPainter( const QRect & contentsRect, QPainter * p ); + void drawDocumentOnPainter( const QRect contentsRect, QPainter * p ); // update item width and height using current zoom parameters void updateItemSize( PageViewItem * item, int colWidth, int rowHeight ); // return the widget placed on a certain point or 0 if clicking on empty space PageViewItem * pickItemOnPoint( int x, int y ); // start / modify / clear selection rectangle - void selectionStart( const QPoint & pos, const QColor & color, bool aboveAll = false ); + void selectionStart( const QPoint pos, const QColor & color, bool aboveAll = false ); void selectionClear( const ClearMode mode = ClearAllSelection ); void drawTableDividers(QPainter * screenPainter); void guessTableDividers(); // update either text or rectangle selection - void updateSelection( const QPoint & pos ); + void updateSelection( const QPoint pos ); // compute the zoom factor value for FitWidth and FitPage mode double zoomFactorFitMode( ZoomMode mode ); // update internal zoom values and end in a slotRelayoutPages(); void updateZoom( ZoomMode newZoomMode ); // update the text on the label using global zoom value or current page's one void updateZoomText(); // update view mode (single, facing...) void updateViewMode ( const int nr ); void textSelectionClear(); // updates cursor - void updateCursor( const QPoint &p ); + void updateCursor( const QPoint p ); - void moveMagnifier( const QPoint &p ); - void updateMagnifier( const QPoint &p ); + void moveMagnifier( const QPoint p ); + void updateMagnifier( const QPoint p ); int viewColumns() const; void center(int cx, int cy, bool smoothMove = false); void scrollTo( int x, int y, bool smoothMove = false); void toggleFormWidgets( bool on ); - void resizeContentArea( const QSize & newSize ); + void resizeContentArea( const QSize newSize ); void updatePageStep(); void addSearchWithinDocumentAction(QMenu * menu, const QString & searchText ); void addWebShortcutsMenu( QMenu * menu, const QString & text ); - QMenu* createProcessLinkMenu( PageViewItem *item, const QPoint & eventPos ); + QMenu* createProcessLinkMenu( PageViewItem *item, const QPoint eventPos ); // used when selecting stuff, makes the view scroll as necessary to keep the mouse inside the view - void scrollPosIntoView( const QPoint & pos ); + void scrollPosIntoView( const QPoint pos ); QPoint viewportToContentArea( const Okular::DocumentViewport & vp ) const; // called from slots to turn off trim modes mutually exclusive to id void updateTrimMode( int except_id ); // handle link clicked bool mouseReleaseOverLink( const Okular::ObjectRect * rect ) const; void createAnnotationsVideoWidgets(PageViewItem *item, const QLinkedList< Okular::Annotation * > &annotations); // don't want to expose classes in here class PageViewPrivate * d; private Q_SLOTS: // used to decouple the notifyViewportChanged calle void slotRealNotifyViewportChanged(bool smoothMove); // activated either directly or via queued connection on notifySetup void slotRelayoutPages(); // activated by the resize event delay timer void delayedResizeEvent(); // activated either directly or via the contentsMoving(int,int) signal void slotRequestVisiblePixmaps( int newValue = -1 ); // activated by the autoscroll timer (Shift+Up/Down keys) void slotAutoScroll(); // activated by the dragScroll timer void slotDragScroll(); // show the welcome message void slotShowWelcome(); // activated by left click timer void slotShowSizeAllCursor(); void slotHandleWebShortcutAction(); void slotConfigureWebShortcuts(); // connected to local actions (toolbar, menu, ..) void slotZoom(); void slotZoomIn(); void slotZoomOut(); void slotZoomActual(); void slotFitToWidthToggled( bool ); void slotFitToPageToggled( bool ); void slotAutoFitToggled( bool ); void slotViewMode( QAction *action ); void slotContinuousToggled( bool ); void slotSetMouseNormal(); void slotSetMouseZoom(); void slotSetMouseMagnifier(); void slotSetMouseSelect(); void slotSetMouseTextSelect(); void slotSetMouseTableSelect(); void slotToggleAnnotator( bool ); void slotAutoScrollUp(); void slotAutoScrollDown(); void slotScrollUp( int nSteps = 0 ); void slotScrollDown(int nSteps = 0 ); void slotRotateClockwise(); void slotRotateCounterClockwise(); void slotRotateOriginal(); void slotPageSizes( int ); void slotTrimMarginsToggled( bool ); void slotTrimToSelectionToggled( bool ); void slotToggleForms(); void slotRefreshPage(); #ifdef HAVE_SPEECH void slotSpeakDocument(); void slotSpeakCurrentPage(); void slotStopSpeaks(); void slotPauseResumeSpeech(); #endif void slotAnnotationWindowDestroyed( QObject *window ); void slotProcessMovieAction( const Okular::MovieAction *action ); void slotProcessRenditionAction( const Okular::RenditionAction *action ); void slotFitWindowToPage(); }; #endif /* kate: replace-tabs on; indent-width 4; */ diff --git a/ui/pageviewannotator.cpp b/ui/pageviewannotator.cpp index c730f6c52..acbf111b4 100644 --- a/ui/pageviewannotator.cpp +++ b/ui/pageviewannotator.cpp @@ -1,1290 +1,1290 @@ /*************************************************************************** * Copyright (C) 2005 by Enrico Ros * * * * 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. * ***************************************************************************/ #include "pageviewannotator.h" // qt / kde includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // system includes #include #include #include // local includes #include "core/area.h" #include "core/document.h" #include "core/page.h" #include "core/annotations.h" #include "settings.h" #include "annotationtools.h" #include "guiutils.h" #include "pageview.h" #include "debug_ui.h" /** @short PickPointEngine */ class PickPointEngine : public AnnotatorEngine { public: PickPointEngine( const QDomElement & engineElement ) : AnnotatorEngine( engineElement ), clicked( false ), xscale( 1.0 ), yscale( 1.0 ) { // parse engine specific attributes hoverIconName = engineElement.attribute( QStringLiteral("hoverIcon") ); iconName = m_annotElement.attribute( QStringLiteral("icon") ); if ( m_annotElement.attribute( QStringLiteral("type") ) == QLatin1String("Stamp") && !iconName.simplified().isEmpty() ) hoverIconName = iconName; center = QVariant( engineElement.attribute( QStringLiteral("center") ) ).toBool(); bool ok = true; size = engineElement.attribute( QStringLiteral("size"), QStringLiteral("32") ).toInt( &ok ); if ( !ok ) size = 32; m_block = QVariant( engineElement.attribute( QStringLiteral("block") ) ).toBool(); // create engine objects if ( !hoverIconName.simplified().isEmpty() ) pixmap = GuiUtils::loadStamp( hoverIconName, size ); } QRect event( EventType type, Button button, double nX, double nY, double xScale, double yScale, const Okular::Page * page ) override { xscale=xScale; yscale=yScale; pagewidth = page->width(); pageheight = page->height(); // only proceed if pressing left button if ( button != Left ) return QRect(); // start operation on click if ( type == Press && clicked == false ) { clicked = true; startpoint.x=nX; startpoint.y=nY; } // repaint if moving while pressing else if ( type == Move && clicked == true ) { } // operation finished on release else if ( type == Release && clicked == true ) { m_creationCompleted = true; } else return QRect(); // update variables and extents (zoom invariant rect) point.x = nX; point.y = nY; if ( center ) { rect.left = nX - ( size / ( xScale * 2.0 ) ); rect.top = nY - ( size / ( yScale * 2.0 ) ); } else { rect.left = nX; rect.top = nY; } rect.right = rect.left + size; rect.bottom = rect.top + size; QRect boundrect = rect.geometry( (int)xScale, (int)yScale ).adjusted( 0, 0, 1, 1 ); if ( m_block ) { const Okular::NormalizedRect tmprect( qMin( startpoint.x, point.x ), qMin( startpoint.y, point.y ), qMax( startpoint.x, point.x ), qMax( startpoint.y, point.y ) ); boundrect |= tmprect.geometry( (int)xScale, (int)yScale ).adjusted( 0, 0, 1, 1 ); } return boundrect; } void paint( QPainter * painter, double xScale, double yScale, const QRect & /*clipRect*/ ) override { if ( clicked ) { if ( m_block ) { const QPen origpen = painter->pen(); QPen pen = painter->pen(); pen.setStyle( Qt::DashLine ); painter->setPen( pen ); const Okular::NormalizedRect tmprect( qMin( startpoint.x, point.x ), qMin( startpoint.y, point.y ), qMax( startpoint.x, point.x ), qMax( startpoint.y, point.y ) ); const QRect realrect = tmprect.geometry( (int)xScale, (int)yScale ); painter->drawRect( realrect ); painter->setPen( origpen ); } if ( !pixmap.isNull() ) painter->drawPixmap( QPointF( rect.left * xScale, rect.top * yScale ), pixmap ); } } void addInPlaceTextAnnotation( Okular::Annotation * &ann, const QString &summary, const QString &content, Okular::TextAnnotation::InplaceIntent inplaceIntent ) { Okular::TextAnnotation * ta = new Okular::TextAnnotation(); ann = ta; ta->setFlags( ta->flags() | Okular::Annotation::FixedRotation ); ta->setContents( content ); ta->setTextType( Okular::TextAnnotation::InPlace ); ta->setInplaceIntent( inplaceIntent ); //set alignment if ( m_annotElement.hasAttribute( QStringLiteral("align") ) ) ta->setInplaceAlignment( m_annotElement.attribute( QStringLiteral("align") ).toInt() ); //set font if ( m_annotElement.hasAttribute( QStringLiteral("font") ) ) { QFont f; f.fromString( m_annotElement.attribute( QStringLiteral("font") ) ); ta->setTextFont( f ); } // set font color if ( m_annotElement.hasAttribute( QStringLiteral("textColor") ) ) { if ( inplaceIntent == Okular::TextAnnotation::TypeWriter ) ta->setTextColor( m_annotElement.attribute( QStringLiteral("textColor") ) ); else ta->setTextColor( Qt::black ); } //set width if ( m_annotElement.hasAttribute( QStringLiteral ( "width" ) ) ) { ta->style().setWidth( m_annotElement.attribute( QStringLiteral ( "width" ) ).toDouble() ); } //set boundary rect.left = qMin(startpoint.x,point.x); rect.top = qMin(startpoint.y,point.y); rect.right = qMax(startpoint.x,point.x); rect.bottom = qMax(startpoint.y,point.y); qCDebug(OkularUiDebug).nospace() << "xyScale=" << xscale << "," << yscale; static const int padding = 2; const QFontMetricsF mf(ta->textFont()); const QRectF rcf = mf.boundingRect( Okular::NormalizedRect( rect.left, rect.top, 1.0, 1.0 ).geometry( (int)pagewidth, (int)pageheight ).adjusted( padding, padding, -padding, -padding ), Qt::AlignTop | Qt::AlignLeft | Qt::TextWordWrap, ta->contents() ); rect.right = qMax(rect.right, rect.left+(rcf.width()+padding*2)/pagewidth); rect.bottom = qMax(rect.bottom, rect.top+(rcf.height()+padding*2)/pageheight); ta->window().setSummary( summary ); } QList< Okular::Annotation* > end() override { // find out annotation's description node if ( m_annotElement.isNull() ) { m_creationCompleted = false; clicked = false; return QList< Okular::Annotation* >(); } // find out annotation's type Okular::Annotation * ann = nullptr; const QString typeString = m_annotElement.attribute( QStringLiteral("type") ); // create InPlace TextAnnotation from path if ( typeString == QLatin1String("FreeText") ) { bool resok; const QString content = QInputDialog::getMultiLineText(nullptr, i18n( "New Text Note" ), i18n( "Text of the new note:" ), QString(), &resok); if( resok ) addInPlaceTextAnnotation( ann, i18n("Inline Note"), content, Okular::TextAnnotation::Unknown ); } else if ( typeString == QLatin1String("Typewriter") ) { bool resok; const QString content = QInputDialog::getMultiLineText(nullptr, i18n( "New Text Note" ), i18n( "Text of the new note:" ), QString(), &resok); if( resok ) addInPlaceTextAnnotation( ann, i18n("Typewriter"), content, Okular::TextAnnotation::TypeWriter ); } else if ( typeString == QLatin1String("Text") ) { Okular::TextAnnotation * ta = new Okular::TextAnnotation(); ann = ta; ta->setTextType( Okular::TextAnnotation::Linked ); ta->setTextIcon( iconName ); //ta->window.flags &= ~(Okular::Annotation::Hidden); const double iconhei=0.03; rect.left = point.x; rect.top = point.y; rect.right=rect.left+iconhei; rect.bottom=rect.top+iconhei*xscale/yscale; ta->window().setSummary( i18n( "Pop-up Note" ) ); } // create StampAnnotation from path else if ( typeString == QLatin1String("Stamp") ) { Okular::StampAnnotation * sa = new Okular::StampAnnotation(); ann = sa; sa->setStampIconName( iconName ); // set boundary rect.left = qMin( startpoint.x, point.x ); rect.top = qMin( startpoint.y, point.y ); rect.right = qMax( startpoint.x, point.x ); rect.bottom = qMax( startpoint.y, point.y ); const QRectF rcf = rect.geometry( (int)xscale, (int)yscale ); const int ml = ( rcf.bottomRight() - rcf.topLeft() ).toPoint().manhattanLength(); if ( ml <= QApplication::startDragDistance() ) { const double stampxscale = pixmap.width() / xscale; const double stampyscale = pixmap.height() / yscale; if ( center ) { rect.left = point.x - stampxscale / 2; rect.top = point.y - stampyscale / 2; } else { rect.left = point.x; rect.top = point.y; } rect.right = rect.left + stampxscale; rect.bottom = rect.top + stampyscale; } } // create GeomAnnotation else if ( typeString == QLatin1String("GeomSquare") || typeString == QLatin1String("GeomCircle") ) { Okular::GeomAnnotation * ga = new Okular::GeomAnnotation(); ann = ga; // set the type if ( typeString == QLatin1String("GeomSquare") ) ga->setGeometricalType( Okular::GeomAnnotation::InscribedSquare ); else ga->setGeometricalType( Okular::GeomAnnotation::InscribedCircle ); if ( m_annotElement.hasAttribute( QStringLiteral("width") ) ) ann->style().setWidth( m_annotElement.attribute( QStringLiteral("width") ).toDouble() ); if ( m_annotElement.hasAttribute( QStringLiteral("innerColor") ) ) ga->setGeometricalInnerColor( QColor( m_annotElement.attribute( QStringLiteral("innerColor") ) ) ); //set boundary rect.left = qMin( startpoint.x, point.x ); rect.top = qMin( startpoint.y, point.y ); rect.right = qMax( startpoint.x, point.x ); rect.bottom = qMax( startpoint.y, point.y ); } m_creationCompleted = false; clicked = false; // safety check if ( !ann ) return QList< Okular::Annotation* >(); // set common attributes ann->style().setColor( m_annotElement.hasAttribute( QStringLiteral("color") ) ? m_annotElement.attribute( QStringLiteral("color") ) : m_engineColor ); if ( m_annotElement.hasAttribute( QStringLiteral("opacity") ) ) ann->style().setOpacity( m_annotElement.attribute( QStringLiteral("opacity"), QStringLiteral("1.0") ).toDouble() ); // set the bounding rectangle, and make sure that the newly created // annotation lies within the page by translating it if necessary if ( rect.right > 1 ) { rect.left -= rect.right - 1; rect.right = 1; } if ( rect.bottom > 1 ) { rect.top -= rect.bottom - 1; rect.bottom = 1; } ann->setBoundingRectangle( rect ); // return annotation return QList< Okular::Annotation* >() << ann; } private: bool clicked; Okular::NormalizedRect rect; Okular::NormalizedPoint startpoint; Okular::NormalizedPoint point; QPixmap pixmap; QString hoverIconName, iconName; int size; double xscale,yscale; double pagewidth, pageheight; bool center; bool m_block; }; /** @short PolyLineEngine */ class PolyLineEngine : public AnnotatorEngine { public: PolyLineEngine( const QDomElement & engineElement ) : AnnotatorEngine( engineElement ), last( false ) { // parse engine specific attributes m_block = engineElement.attribute( QStringLiteral("block") ) == QLatin1String("true"); bool ok = true; // numofpoints represents the max number of points for the current // polygon/polyline, with a pair of exceptions: // -1 means: the polyline must close on the first point (polygon) // 0 means: construct as many points as you want, right-click // to construct the last point numofpoints = engineElement.attribute( QStringLiteral("points") ).toInt( &ok ); if ( !ok ) numofpoints = -1; } QRect event( EventType type, Button button, double nX, double nY, double xScale, double yScale, const Okular::Page * /*page*/ ) override { // only proceed if pressing left button // if ( button != Left ) // return rect; // start operation if ( type == Press ) { newPoint.x = nX; newPoint.y = nY; if ( button == Right ) last = true; } // move the second point else if ( type == Move ) { movingpoint.x = nX; movingpoint.y = nY; const QRect oldmovingrect = movingrect; movingrect = rect | QRect( (int)( movingpoint.x * xScale ), (int)( movingpoint.y * yScale ), 1, 1 ); return oldmovingrect | movingrect; } else if ( type == Release ) { const Okular::NormalizedPoint tmppoint(nX, nY); if ( fabs( tmppoint.x - newPoint.x ) + fabs( tmppoint.y - newPoint.y ) > 1e-2 ) return rect; if ( numofpoints == -1 && points.count() > 1 && ( fabs( points[0].x - newPoint.x ) + fabs( points[0].y - newPoint.y ) < 1e-2 ) ) { last = true; } else { points.append( newPoint ); rect |= QRect( (int)( newPoint.x * xScale ), (int)( newPoint.y * yScale ), 1, 1 ); } // end creation if we have constructed the last point of enough points if ( last || points.count() == numofpoints ) { m_creationCompleted = true; last = false; normRect = Okular::NormalizedRect( rect, xScale, yScale ); } } return rect; } void paint( QPainter * painter, double xScale, double yScale, const QRect & /*clipRect*/ ) override { if ( points.count() < 1 ) return; if ( m_block && points.count() == 2 ) { const Okular::NormalizedPoint first = points[0]; const Okular::NormalizedPoint second = points[1]; // draw a semitransparent block around the 2 points painter->setPen( m_engineColor ); painter->setBrush( QBrush( m_engineColor.lighter(), Qt::Dense4Pattern ) ); painter->drawRect( (int)(first.x * (double)xScale), (int)(first.y * (double)yScale), (int)((second.x - first.x) * (double)xScale), (int)((second.y - first.y) * (double)yScale) ); } else { // draw a polyline that connects the constructed points painter->setPen( QPen( m_engineColor, 2 ) ); for ( int i = 1; i < points.count(); ++i ) painter->drawLine( (int)(points[i - 1].x * (double)xScale), (int)(points[i - 1].y * (double)yScale), (int)(points[i].x * (double)xScale), (int)(points[i].y * (double)yScale) ); painter->drawLine( (int)(points.last().x * (double)xScale), (int)(points.last().y * (double)yScale), (int)(movingpoint.x * (double)xScale), (int)(movingpoint.y * (double)yScale) ); } } QList< Okular::Annotation* > end() override { m_creationCompleted = false; // find out annotation's description node if ( m_annotElement.isNull() ) return QList< Okular::Annotation* >(); // find out annotation's type Okular::Annotation * ann = nullptr; const QString typeString = m_annotElement.attribute( QStringLiteral("type") ); // create LineAnnotation from path if ( typeString == QLatin1String("Line") || typeString == QLatin1String("Polyline") || typeString == QLatin1String("Polygon") ) { if ( points.count() < 2 ) return QList< Okular::Annotation* >(); //add note Okular::LineAnnotation * la = new Okular::LineAnnotation(); ann = la; QLinkedList list; for ( int i = 0; i < points.count(); ++i ) list.append( points[ i ] ); la->setLinePoints( list ); if ( numofpoints == -1 ) { la->setLineClosed( true ); if ( m_annotElement.hasAttribute( QStringLiteral("innerColor") ) ) la->setLineInnerColor( QColor( m_annotElement.attribute( QStringLiteral("innerColor") ) ) ); } else if ( numofpoints == 2 ) { if ( m_annotElement.hasAttribute( QStringLiteral("leadFwd") ) ) la->setLineLeadingForwardPoint( m_annotElement.attribute( QStringLiteral("leadFwd") ).toDouble() ); if ( m_annotElement.hasAttribute( QStringLiteral("leadBack") ) ) la->setLineLeadingBackwardPoint( m_annotElement.attribute( QStringLiteral("leadBack") ).toDouble() ); } if ( m_annotElement.hasAttribute( QStringLiteral("startStyle") ) ) la->setLineStartStyle( (Okular::LineAnnotation::TermStyle)m_annotElement.attribute( QStringLiteral("startStyle") ).toInt() ); if ( m_annotElement.hasAttribute( QStringLiteral("endStyle") ) ) la->setLineEndStyle( (Okular::LineAnnotation::TermStyle)m_annotElement.attribute( QStringLiteral("endStyle") ).toInt() ); la->setBoundingRectangle( normRect ); } // safety check if ( !ann ) return QList< Okular::Annotation* >(); if ( m_annotElement.hasAttribute( QStringLiteral("width") ) ) ann->style().setWidth( m_annotElement.attribute( QStringLiteral("width") ).toDouble() ); // set common attributes ann->style().setColor( m_annotElement.hasAttribute( QStringLiteral("color") ) ? m_annotElement.attribute( QStringLiteral("color") ) : m_engineColor ); if ( m_annotElement.hasAttribute( QStringLiteral("opacity") ) ) ann->style().setOpacity( m_annotElement.attribute( QStringLiteral("opacity"), QStringLiteral("1.0") ).toDouble() ); // return annotation return QList< Okular::Annotation* >() << ann; } private: QList points; Okular::NormalizedPoint newPoint; Okular::NormalizedPoint movingpoint; QRect rect; QRect movingrect; Okular::NormalizedRect normRect; bool m_block; bool last; int numofpoints; }; /** @short TextSelectorEngine */ class TextSelectorEngine : public AnnotatorEngine { public: TextSelectorEngine( const QDomElement & engineElement, PageView * pageView ) : AnnotatorEngine( engineElement ), m_pageView( pageView ) { // parse engine specific attributes } QRect event( EventType type, Button button, double nX, double nY, double xScale, double yScale, const Okular::Page * /*page*/ ) override { // only proceed if pressing left button if ( button != Left ) return QRect(); if ( type == Press ) { lastPoint.x = nX; lastPoint.y = nY; const QRect oldrect = rect; rect = QRect(); return oldrect; } else if ( type == Move ) { if ( item() ) { const QPoint start( (int)( lastPoint.x * item()->uncroppedWidth() ), (int)( lastPoint.y * item()->uncroppedHeight() ) ); const QPoint end( (int)( nX * item()->uncroppedWidth() ), (int)( nY * item()->uncroppedHeight() ) ); selection.reset(); std::unique_ptr newselection( m_pageView->textSelectionForItem( item(), start, end ) ); if ( newselection && !newselection->isEmpty() ) { const QList geom = newselection->geometry( (int)xScale, (int)yScale ); QRect newrect; for ( const QRect &r : geom ) { if ( newrect.isNull() ) newrect = r; else newrect |= r; } rect |= newrect; selection = std::move(newselection); } } } else if ( type == Release && selection ) { m_creationCompleted = true; } return rect; } void paint( QPainter * painter, double xScale, double yScale, const QRect & /*clipRect*/ ) override { if ( selection ) { painter->setPen( Qt::NoPen ); QColor col = m_engineColor; col.setAlphaF( 0.5 ); painter->setBrush( col ); for ( const Okular::NormalizedRect &r : qAsConst(*selection) ) { painter->drawRect( r.geometry( (int)xScale, (int)yScale ) ); } } } QList< Okular::Annotation* > end() override { m_creationCompleted = false; // safety checks if ( m_annotElement.isNull() || !selection ) return QList< Okular::Annotation* >(); // find out annotation's type Okular::Annotation * ann = nullptr; const QString typeString = m_annotElement.attribute( QStringLiteral("type") ); Okular::HighlightAnnotation::HighlightType type = Okular::HighlightAnnotation::Highlight; bool typevalid = false; // create HighlightAnnotation's from the selected area if ( typeString == QLatin1String("Highlight") ) { type = Okular::HighlightAnnotation::Highlight; typevalid = true; } else if ( typeString == QLatin1String("Squiggly") ) { type = Okular::HighlightAnnotation::Squiggly; typevalid = true; } else if ( typeString == QLatin1String("Underline") ) { type = Okular::HighlightAnnotation::Underline; typevalid = true; } else if ( typeString == QLatin1String("StrikeOut") ) { type = Okular::HighlightAnnotation::StrikeOut; typevalid = true; } if ( typevalid ) { Okular::HighlightAnnotation * ha = new Okular::HighlightAnnotation(); ha->setHighlightType( type ); ha->setBoundingRectangle( Okular::NormalizedRect( rect, item()->uncroppedWidth(), item()->uncroppedHeight() ) ); for ( const Okular::NormalizedRect &r : qAsConst(*selection) ) { Okular::HighlightAnnotation::Quad q; q.setCapStart( false ); q.setCapEnd( false ); q.setFeather( 1.0 ); q.setPoint( Okular::NormalizedPoint( r.left, r.bottom ), 0 ); q.setPoint( Okular::NormalizedPoint( r.right, r.bottom ), 1 ); q.setPoint( Okular::NormalizedPoint( r.right, r.top ), 2 ); q.setPoint( Okular::NormalizedPoint( r.left, r.top ), 3 ); ha->highlightQuads().append( q ); } ann = ha; } selection.reset(); // safety check if ( !ann ) return QList< Okular::Annotation* >(); // set common attributes ann->style().setColor( m_annotElement.hasAttribute( QStringLiteral("color") ) ? m_annotElement.attribute( QStringLiteral("color") ) : m_engineColor ); if ( m_annotElement.hasAttribute( QStringLiteral("opacity") ) ) ann->style().setOpacity( m_annotElement.attribute( QStringLiteral("opacity"), QStringLiteral("1.0") ).toDouble() ); // return annotations return QList< Okular::Annotation* >() << ann; } QCursor cursor() const override { return Qt::IBeamCursor; } private: // data PageView * m_pageView; // TODO: support more pages std::unique_ptr selection; Okular::NormalizedPoint lastPoint; QRect rect; }; /** PageViewAnnotator **/ PageViewAnnotator::PageViewAnnotator( PageView * parent, Okular::Document * storage ) : QObject( parent ), m_document( storage ), m_pageView( parent ), m_toolBar( nullptr ), m_engine( nullptr ), m_textToolsEnabled( false ), m_toolsEnabled( false ), m_continuousMode( false ), m_hidingWasForced( false ), m_lastToolID( -1 ), m_lockedItem( nullptr ) { reparseConfig(); } void PageViewAnnotator::reparseConfig() { m_items.clear(); // Read tool list from configuration. It's a list of XML elements const QStringList userTools = Okular::Settings::annotationTools(); // Populate m_toolsDefinition QDomDocument doc; m_toolsDefinition = doc.createElement( QStringLiteral("annotatingTools") ); for ( const QString &toolXml : userTools ) { QDomDocument entryParser; if ( entryParser.setContent( toolXml ) ) m_toolsDefinition.appendChild( doc.importNode( entryParser.documentElement(), true ) ); else qCWarning(OkularUiDebug) << "Skipping malformed tool XML in AnnotationTools setting"; } // Create the AnnotationToolItems from the XML dom tree QDomNode toolDescription = m_toolsDefinition.firstChild(); while ( toolDescription.isElement() ) { QDomElement toolElement = toolDescription.toElement(); if ( toolElement.tagName() == QLatin1String("tool") ) { AnnotationToolItem item; item.id = toolElement.attribute(QStringLiteral("id")).toInt(); if ( toolElement.hasAttribute( QStringLiteral("name") ) ) item.text = toolElement.attribute( QStringLiteral("name") ); else item.text = defaultToolName( toolElement ); item.pixmap = makeToolPixmap( toolElement ); QDomNode shortcutNode = toolElement.elementsByTagName( QStringLiteral("shortcut") ).item( 0 ); if ( shortcutNode.isElement() ) item.shortcut = shortcutNode.toElement().text(); QDomNodeList engineNodeList = toolElement.elementsByTagName( QStringLiteral("engine") ); if ( engineNodeList.size() > 0 ) { QDomElement engineEl = engineNodeList.item( 0 ).toElement(); if ( !engineEl.isNull() && engineEl.hasAttribute( QStringLiteral("type") ) ) item.isText = engineEl.attribute( QStringLiteral("type") ) == QLatin1String( "TextSelector" ); } m_items.push_back( item ); } toolDescription = toolDescription.nextSibling(); } } PageViewAnnotator::~PageViewAnnotator() { delete m_engine; } void PageViewAnnotator::setEnabled( bool enabled ) { if ( !enabled ) { // remove toolBar if ( m_toolBar ) m_toolBar->hideAndDestroy(); m_toolBar = nullptr; // deactivate the active tool, if any slotToolSelected( -1 ); return; } // if no tools are defined, don't show the toolbar if ( !m_toolsDefinition.hasChildNodes() ) return; // create toolBar if ( !m_toolBar ) { m_toolBar = new PageViewToolBar( m_pageView, m_pageView->viewport() ); m_toolBar->setSide( (PageViewToolBar::Side)Okular::Settings::editToolBarPlacement() ); m_toolBar->setItems( m_items ); m_toolBar->setToolsEnabled( m_toolsEnabled ); m_toolBar->setTextToolsEnabled( m_textToolsEnabled ); connect(m_toolBar, &PageViewToolBar::toolSelected, this, &PageViewAnnotator::slotToolSelected); connect(m_toolBar, &PageViewToolBar::orientationChanged, this, &PageViewAnnotator::slotSaveToolbarOrientation); connect(m_toolBar, &PageViewToolBar::buttonDoubleClicked, this, &PageViewAnnotator::slotToolDoubleClicked); m_toolBar->setCursor(Qt::ArrowCursor); } // show the toolBar m_toolBar->showAndAnimate(); } void PageViewAnnotator::setTextToolsEnabled( bool enabled ) { m_textToolsEnabled = enabled; if ( m_toolBar ) m_toolBar->setTextToolsEnabled( m_textToolsEnabled ); } void PageViewAnnotator::setToolsEnabled( bool enabled ) { m_toolsEnabled = enabled; if ( m_toolBar ) m_toolBar->setToolsEnabled( m_toolsEnabled ); } void PageViewAnnotator::setHidingForced( bool forced ) { m_hidingWasForced = forced; } bool PageViewAnnotator::hidingWasForced() const { return m_hidingWasForced; } bool PageViewAnnotator::active() const { return m_engine && m_toolBar; } bool PageViewAnnotator::annotating() const { return active() && m_lockedItem; } QCursor PageViewAnnotator::cursor() const { return m_engine->cursor(); } -QRect PageViewAnnotator::performRouteMouseOrTabletEvent(const AnnotatorEngine::EventType & eventType, const AnnotatorEngine::Button & button, - const QPointF & pos, PageViewItem * item ) +QRect PageViewAnnotator::performRouteMouseOrTabletEvent(const AnnotatorEngine::EventType eventType, const AnnotatorEngine::Button button, + const QPointF pos, PageViewItem * item ) { // creationCompleted is intended to be set by event(), handled subsequently by end(), and cleared within end(). // If it's set here, we recursed for some reason (e.g., stacked event loop). // Just bail out, all we want to do is already on stack. if ( m_engine->creationCompleted() ) { return QRect(); } // if the right mouse button was pressed, we simply do nothing. In this way, we are still editing the annotation // and so this function will receive and process the right mouse button release event too. If we detach now the annotation tool, // the release event will be processed by the PageView class which would create the annotation property widget, and we do not want this. if ( button == AnnotatorEngine::Right && eventType == AnnotatorEngine::Press ) return QRect(); else if ( button == AnnotatorEngine::Right && eventType == AnnotatorEngine::Release ) { detachAnnotation(); return QRect(); } // 1. lock engine to current item if ( !m_lockedItem && eventType == AnnotatorEngine::Press ) { m_lockedItem = item; m_engine->setItem( m_lockedItem ); } if ( !m_lockedItem ) { return QRect(); } // find out normalized mouse coords inside current item const QRect & itemRect = m_lockedItem->uncroppedGeometry(); const QPointF eventPos = m_pageView->contentAreaPoint( pos ); const double nX = qBound( 0.0, m_lockedItem->absToPageX( eventPos.x() ), 1.0 ); const double nY = qBound( 0.0, m_lockedItem->absToPageY( eventPos.y() ), 1.0 ); QRect modifiedRect; // 2. use engine to perform operations const QRect paintRect = m_engine->event( eventType, button, nX, nY, itemRect.width(), itemRect.height(), m_lockedItem->page() ); // 3. update absolute extents rect and send paint event(s) if ( paintRect.isValid() ) { // 3.1. unite old and new painting regions QRegion compoundRegion( m_lastDrawnRect ); m_lastDrawnRect = paintRect; m_lastDrawnRect.translate( itemRect.left(), itemRect.top() ); // 3.2. decompose paint region in rects and send paint events const QRegion rgn = compoundRegion.united( m_lastDrawnRect ); const QPoint areaPos = m_pageView->contentAreaPosition(); for ( const QRect &r : rgn ) m_pageView->viewport()->update( r.translated( -areaPos ) ); modifiedRect = compoundRegion.boundingRect() | m_lastDrawnRect; } // 4. if engine has finished, apply Annotation to the page if ( m_engine->creationCompleted() ) { // apply engine data to the Annotation's and reset engine const QList< Okular::Annotation* > annotations = m_engine->end(); // attach the newly filled annotations to the page for ( Okular::Annotation *annotation : annotations ) { if ( !annotation ) continue; annotation->setCreationDate( QDateTime::currentDateTime() ); annotation->setModificationDate( QDateTime::currentDateTime() ); annotation->setAuthor( Okular::Settings::identityAuthor() ); m_document->addPageAnnotation( m_lockedItem->pageNumber(), annotation ); if ( annotation->openDialogAfterCreation() ) m_pageView->openAnnotationWindow( annotation, m_lockedItem->pageNumber() ); } if ( m_continuousMode ) slotToolSelected( m_lastToolID ); else detachAnnotation(); } return modifiedRect; } QRect PageViewAnnotator::routeMouseEvent( QMouseEvent * e, PageViewItem * item ) { AnnotatorEngine::EventType eventType; AnnotatorEngine::Button button; // figure out the event type and button AnnotatorEngine::decodeEvent( e, &eventType, &button ); return performRouteMouseOrTabletEvent( eventType, button, e->localPos(), item ); } -QRect PageViewAnnotator::routeTabletEvent( QTabletEvent * e, PageViewItem * item, const QPoint & localOriginInGlobal ) +QRect PageViewAnnotator::routeTabletEvent( QTabletEvent * e, PageViewItem * item, const QPoint localOriginInGlobal ) { // Unlike routeMouseEvent, routeTabletEvent must explicitly ignore events it doesn't care about so that // the corresponding mouse event will later be delivered. if ( !item ) { e->ignore(); return QRect(); } // We set all tablet events that take place over the annotations toolbar to ignore so that corresponding mouse // events will be delivered to the toolbar. However, we still allow the annotations code to handle // TabletMove and TabletRelease events in case the user is drawing an annotation onto the toolbar. const QPoint toolBarPos = m_toolBar->mapFromGlobal( e->globalPos() ); const QRect toolBarRect = m_toolBar->rect(); if ( toolBarRect.contains( toolBarPos ) ) { e->ignore(); if (e->type() == QEvent::TabletPress) return QRect(); } AnnotatorEngine::EventType eventType; AnnotatorEngine::Button button; // figure out the event type and button AnnotatorEngine::decodeEvent( e, &eventType, &button ); const QPointF globalPosF = e->globalPosF(); const QPointF localPosF = globalPosF - localOriginInGlobal; return performRouteMouseOrTabletEvent( eventType, button, localPosF, item ); } bool PageViewAnnotator::routeKeyEvent( QKeyEvent * event ) { if ( event->key() == Qt::Key_Escape ) { detachAnnotation(); return true; } return false; } -bool PageViewAnnotator::routePaints( const QRect & wantedRect ) const +bool PageViewAnnotator::routePaints( const QRect wantedRect ) const { return m_engine && m_toolBar && wantedRect.intersects( m_lastDrawnRect ) && m_lockedItem; } -void PageViewAnnotator::routePaint( QPainter * painter, const QRect & paintRect ) +void PageViewAnnotator::routePaint( QPainter * painter, const QRect paintRect ) { // if there's no locked item, then there's no decided place to draw on if ( !m_lockedItem ) return; #ifndef NDEBUG // [DEBUG] draw the paint region if enabled if ( Okular::Settings::debugDrawAnnotationRect() ) painter->drawRect( paintRect ); #endif // move painter to current itemGeometry rect const QRect & itemRect = m_lockedItem->uncroppedGeometry(); painter->save(); painter->translate( itemRect.topLeft() ); // TODO: Clip annotation painting to cropped page. // transform cliprect from absolute to item relative coords QRect annotRect = paintRect.intersected( m_lastDrawnRect ); annotRect.translate( -itemRect.topLeft() ); // use current engine for painting (in virtual page coordinates) m_engine->paint( painter, m_lockedItem->uncroppedWidth(), m_lockedItem->uncroppedHeight(), annotRect ); painter->restore(); } void PageViewAnnotator::slotToolSelected( int toolID ) { // terminate any previous operation if ( m_engine ) { delete m_engine; m_engine = nullptr; } m_lockedItem = nullptr; if ( m_lastDrawnRect.isValid() ) { m_pageView->viewport()->update( m_lastDrawnRect.translated( -m_pageView->contentAreaPosition() ) ); m_lastDrawnRect = QRect(); } if ( toolID != m_lastToolID ) m_continuousMode = false; // store current tool for later usage m_lastToolID = toolID; // handle tool deselection if ( toolID == -1 ) { m_pageView->displayMessage( QString() ); m_pageView->updateCursor(); return; } // for the selected tool create the Engine QDomNode toolNode = m_toolsDefinition.firstChild(); while ( toolNode.isElement() ) { QDomElement toolElement = toolNode.toElement(); toolNode = toolNode.nextSibling(); // only find out the element describing selected tool if ( toolElement.tagName() != QLatin1String("tool") || toolElement.attribute(QStringLiteral("id")).toInt() != toolID ) continue; // parse tool properties QDomNode toolSubNode = toolElement.firstChild(); while ( toolSubNode.isElement() ) { QDomElement toolSubElement = toolSubNode.toElement(); toolSubNode = toolSubNode.nextSibling(); // create the AnnotatorEngine if ( toolSubElement.tagName() == QLatin1String("engine") ) { QString type = toolSubElement.attribute( QStringLiteral("type") ); if ( type == QLatin1String("SmoothLine") ) m_engine = new SmoothPathEngine( toolSubElement ); else if ( type == QLatin1String("PickPoint") ) m_engine = new PickPointEngine( toolSubElement ); else if ( type == QLatin1String("PolyLine") ) m_engine = new PolyLineEngine( toolSubElement ); else if ( type == QLatin1String("TextSelector") ) m_engine = new TextSelectorEngine( toolSubElement, m_pageView ); else qCWarning(OkularUiDebug).nospace() << "tools.xml: engine type:'" << type << "' is not defined!"; } // display the tooltip const QString annotType = toolElement.attribute( QStringLiteral("type") ); QString tip; if ( annotType == QLatin1String("ellipse") ) tip = i18nc( "Annotation tool", "Draw an ellipse (drag to select a zone)" ); else if ( annotType == QLatin1String("highlight") ) tip = i18nc( "Annotation tool", "Highlight text" ); else if ( annotType == QLatin1String("ink") ) tip = i18nc( "Annotation tool", "Draw a freehand line" ); else if ( annotType == QLatin1String("note-inline") ) tip = i18nc( "Annotation tool", "Inline Text Annotation (drag to select a zone)" ); else if ( annotType == QLatin1String("note-linked") ) tip = i18nc( "Annotation tool", "Put a pop-up note" ); else if ( annotType == QLatin1String("polygon") ) tip = i18nc( "Annotation tool", "Draw a polygon (click on the first point to close it)" ); else if ( annotType == QLatin1String("rectangle") ) tip = i18nc( "Annotation tool", "Draw a rectangle" ); else if ( annotType == QLatin1String("squiggly") ) tip = i18nc( "Annotation tool", "Squiggle text" ); else if ( annotType == QLatin1String("stamp") ) tip = i18nc( "Annotation tool", "Put a stamp symbol" ); else if ( annotType == QLatin1String("straight-line") ) tip = i18nc( "Annotation tool", "Draw a straight line" ); else if ( annotType == QLatin1String("strikeout") ) tip = i18nc( "Annotation tool", "Strike out text" ); else if ( annotType == QLatin1String("underline") ) tip = i18nc( "Annotation tool", "Underline text" ); else if ( annotType == QLatin1String("typewriter") ) tip = i18nc( "Annotation tool", "Typewriter Annotation (drag to select a zone)" ); if ( !tip.isEmpty() && !m_continuousMode ) m_pageView->displayMessage( tip, QString(), PageViewMessage::Annotation ); } // consistency warning if ( !m_engine ) { qCWarning(OkularUiDebug) << "tools.xml: couldn't find good engine description. check xml."; } m_pageView->updateCursor(); // stop after parsing selected tool's node break; } } void PageViewAnnotator::slotSaveToolbarOrientation( int side ) { Okular::Settings::setEditToolBarPlacement( (int)side ); Okular::Settings::self()->save(); } void PageViewAnnotator::slotToolDoubleClicked( int /*toolID*/ ) { m_continuousMode = true; } void PageViewAnnotator::detachAnnotation() { m_toolBar->selectButton( -1 ); } QString PageViewAnnotator::defaultToolName( const QDomElement &toolElement ) { const QString annotType = toolElement.attribute( QStringLiteral("type") ); if ( annotType == QLatin1String("ellipse") ) return i18n( "Ellipse" ); else if ( annotType == QLatin1String("highlight") ) return i18n( "Highlighter" ); else if ( annotType == QLatin1String("ink") ) return i18n( "Freehand Line" ); else if ( annotType == QLatin1String("note-inline") ) return i18n( "Inline Note" ); else if ( annotType == QLatin1String("note-linked") ) return i18n( "Pop-up Note" ); else if ( annotType == QLatin1String("polygon") ) return i18n( "Polygon" ); else if ( annotType == QLatin1String("rectangle") ) return i18n( "Rectangle" ); else if ( annotType == QLatin1String("squiggly") ) return i18n( "Squiggle" ); else if ( annotType == QLatin1String("stamp") ) return i18n( "Stamp" ); else if ( annotType == QLatin1String("straight-line") ) return i18n( "Straight Line" ); else if ( annotType == QLatin1String("strikeout") ) return i18n( "Strike out" ); else if ( annotType == QLatin1String("underline") ) return i18n( "Underline" ); else if ( annotType == QLatin1String("typewriter") ) return i18n( "Typewriter" ); else return QString(); } QPixmap PageViewAnnotator::makeToolPixmap( const QDomElement &toolElement ) { QPixmap pixmap( 32 * qApp->devicePixelRatio(), 32 * qApp->devicePixelRatio() ); pixmap.setDevicePixelRatio( qApp->devicePixelRatio() ); const QString annotType = toolElement.attribute( QStringLiteral("type") ); // Load HiDPI variant on HiDPI screen QString imageVariant; if ( qApp->devicePixelRatio() > 1.05 ) { imageVariant = QStringLiteral("@2x"); } // Load base pixmap. We'll draw on top of it pixmap.load( QStandardPaths::locate(QStandardPaths::GenericDataLocation, QString("okular/pics/tool-base-okular" + imageVariant + ".png") ) ); /* Parse color, innerColor and icon (if present) */ QColor engineColor, innerColor, textColor, annotColor; QString icon; QDomNodeList engineNodeList = toolElement.elementsByTagName( QStringLiteral("engine") ); if ( engineNodeList.size() > 0 ) { QDomElement engineEl = engineNodeList.item( 0 ).toElement(); if ( !engineEl.isNull() && engineEl.hasAttribute( QStringLiteral("color") ) ) engineColor = QColor( engineEl.attribute( QStringLiteral("color") ) ); } QDomNodeList annotationNodeList = toolElement.elementsByTagName( QStringLiteral("annotation") ); if ( annotationNodeList.size() > 0 ) { QDomElement annotationEl = annotationNodeList.item( 0 ).toElement(); if ( !annotationEl.isNull() ) { if ( annotationEl.hasAttribute( QStringLiteral("color") ) ) annotColor = annotationEl.attribute( QStringLiteral("color") ); if ( annotationEl.hasAttribute( QStringLiteral("innerColor") ) ) innerColor = QColor( annotationEl.attribute( QStringLiteral("innerColor") ) ); if ( annotationEl.hasAttribute( QStringLiteral("textColor") ) ) textColor = QColor( annotationEl.attribute( QStringLiteral("textColor") ) ); if ( annotationEl.hasAttribute( QStringLiteral("icon") ) ) icon = annotationEl.attribute( QStringLiteral("icon") ); } } QPainter p( &pixmap ); if ( annotType == QLatin1String("ellipse") ) { p.setRenderHint( QPainter::Antialiasing ); if ( innerColor.isValid() ) p.setBrush( innerColor ); p.setPen( QPen( engineColor, 2 ) ); p.drawEllipse( 2, 7, 21, 14 ); } else if ( annotType == QLatin1String("highlight") ) { QImage overlay( QStandardPaths::locate(QStandardPaths::GenericDataLocation, QString("okular/pics/tool-highlighter-okular-colorizable" + imageVariant + ".png") ) ); QImage colorizedOverlay = overlay; GuiUtils::colorizeImage( colorizedOverlay, engineColor ); p.drawImage( QPoint(0,0), colorizedOverlay ); // Trail p.drawImage( QPoint(0,-32), overlay ); // Text + Shadow (uncolorized) p.drawImage( QPoint(0,-64), colorizedOverlay ); // Pen } else if ( annotType == QLatin1String("ink") ) { QImage overlay( QStandardPaths::locate(QStandardPaths::GenericDataLocation, QString("okular/pics/tool-ink-okular-colorizable" + imageVariant + ".png") ) ); QImage colorizedOverlay = overlay; GuiUtils::colorizeImage( colorizedOverlay, engineColor ); p.drawImage( QPoint(0,0), colorizedOverlay ); // Trail p.drawImage( QPoint(0,-32), overlay ); // Shadow (uncolorized) p.drawImage( QPoint(0,-64), colorizedOverlay ); // Pen } else if ( annotType == QLatin1String("note-inline") ) { QImage overlay( QStandardPaths::locate(QStandardPaths::GenericDataLocation, QString("okular/pics/tool-note-inline-okular-colorizable" + imageVariant + ".png") ) ); GuiUtils::colorizeImage( overlay, engineColor ); p.drawImage( QPoint(0,0), overlay ); } else if ( annotType == QLatin1String("note-linked") ) { QImage overlay( QStandardPaths::locate(QStandardPaths::GenericDataLocation, QString("okular/pics/tool-note-okular-colorizable" + imageVariant + ".png") ) ); GuiUtils::colorizeImage( overlay, engineColor ); p.drawImage( QPoint(0,0), overlay ); } else if ( annotType == QLatin1String("polygon") ) { QPainterPath path; path.moveTo( 0, 7 ); path.lineTo( 19, 7 ); path.lineTo( 19, 14 ); path.lineTo( 23, 14 ); path.lineTo( 23, 20 ); path.lineTo( 0, 20 ); if ( innerColor.isValid() ) p.setBrush( innerColor ); p.setPen( QPen( engineColor, 1 ) ); p.drawPath( path ); } else if ( annotType == QLatin1String("rectangle") ) { p.setRenderHint( QPainter::Antialiasing ); if ( innerColor.isValid() ) p.setBrush( innerColor ); p.setPen( QPen( engineColor, 2 ) ); p.drawRect( 2, 7, 21, 14 ); } else if ( annotType == QLatin1String("squiggly") ) { QPen pen( engineColor, 1 ); pen.setDashPattern( QVector() << 1 << 1 ); p.setPen( pen ); p.drawLine( 1, 13, 16, 13 ); p.drawLine( 2, 14, 15, 14 ); p.drawLine( 0, 20, 19, 20 ); p.drawLine( 1, 21, 18, 21 ); } else if ( annotType == QLatin1String("stamp") ) { QPixmap stamp = GuiUtils::loadStamp( icon, 16, false /* keepAspectRatio */ ); p.setRenderHint( QPainter::Antialiasing ); p.drawPixmap( 16, 14, stamp ); } else if ( annotType == QLatin1String("straight-line") ) { QPainterPath path; path.moveTo( 1, 8 ); path.lineTo( 20, 8 ); path.lineTo( 1, 27 ); path.lineTo( 20, 27 ); p.setRenderHint( QPainter::Antialiasing ); p.setPen( QPen( engineColor, 1 ) ); p.drawPath( path ); // TODO To be discussed: This is not a straight line! } else if ( annotType == QLatin1String("strikeout") ) { p.setPen( QPen( engineColor, 1 ) ); p.drawLine( 1, 10, 16, 10 ); p.drawLine( 0, 17, 19, 17 ); } else if ( annotType == QLatin1String("underline") ) { p.setPen( QPen( engineColor, 1 ) ); p.drawLine( 1, 13, 16, 13 ); p.drawLine( 0, 20, 19, 20 ); } else if ( annotType == QLatin1String("typewriter") ) { QImage overlay( QStandardPaths::locate(QStandardPaths::GenericDataLocation, QString("okular/pics/tool-typewriter-okular-colorizable" + imageVariant + ".png") ) ); GuiUtils::colorizeImage( overlay, textColor ); p.drawImage( QPoint(-2,2), overlay ); } else { /* Unrecognized annotation type -- It shouldn't happen */ p.setPen( QPen( engineColor ) ); p.drawText( QPoint(20, 31), QStringLiteral("?") ); } return pixmap; } #include "moc_pageviewannotator.cpp" /* kate: replace-tabs on; indent-width 4; */ diff --git a/ui/pageviewannotator.h b/ui/pageviewannotator.h index 01f70b406..fc1bfe902 100644 --- a/ui/pageviewannotator.h +++ b/ui/pageviewannotator.h @@ -1,121 +1,121 @@ /*************************************************************************** * Copyright (C) 2005 by Enrico Ros * * * * 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. * ***************************************************************************/ #ifndef _OKULAR_PAGEVIEWANNOTATOR_H_ #define _OKULAR_PAGEVIEWANNOTATOR_H_ #include #include #include #include "pageviewutils.h" #include "annotationtools.h" class QKeyEvent; class QMouseEvent; class QPainter; namespace Okular { class Document; } // engines are defined and implemented in the cpp class AnnotatorEngine; class PageView; /** * @short PageView object devoted to annotation creation/handling. * * PageViewAnnotator is the okular class used for visually creating annotations. * It uses internal 'engines' for interacting with user events and attaches * the newly created annotation to the document when the creation is complete. * In the meanwhile all PageView events (actually mouse/paint ones) are routed * to this class that performs a rough visual representation of what the * annotation will become when finished. * * m_toolsDefinition is a DOM object that contains Annotations/Engine association * for the items placed in the toolbar. The XML is parsed (1) when populating * the toolbar and (2)after selecting a toolbar item, in which case an Ann is * initialized with the values in the XML and an engine is created to handle * that annotation. m_toolsDefinition is created in reparseConfig according to * user configuration. */ class PageViewAnnotator : public QObject { Q_OBJECT public: PageViewAnnotator( PageView * parent, Okular::Document * storage ); ~PageViewAnnotator() override; // called to show/hide the editing toolbar void setEnabled( bool enabled ); // called to toggle the usage of text annotating tools void setTextToolsEnabled( bool enabled ); void setToolsEnabled( bool enabled ); void setHidingForced( bool forced ); bool hidingWasForced() const; // methods used when creating the annotation // @return Is a tool currently selected? bool active() const; // @return Are we currently annotating (using the selected tool)? bool annotating() const; // returns the preferred cursor for the current tool. call this only // if active() == true QCursor cursor() const; QRect routeMouseEvent( QMouseEvent * event, PageViewItem * item ); - QRect routeTabletEvent( QTabletEvent * event, PageViewItem * item, const QPoint & localOriginInGlobal ); - QRect performRouteMouseOrTabletEvent( const AnnotatorEngine::EventType & eventType, const AnnotatorEngine::Button & button, - const QPointF & pos, PageViewItem * item ); + QRect routeTabletEvent( QTabletEvent * event, PageViewItem * item, const QPoint localOriginInGlobal ); + QRect performRouteMouseOrTabletEvent( const AnnotatorEngine::EventType eventType, const AnnotatorEngine::Button button, + const QPointF pos, PageViewItem * item ); bool routeKeyEvent( QKeyEvent * event ); - bool routePaints( const QRect & wantedRect ) const; - void routePaint( QPainter * painter, const QRect & paintRect ); + bool routePaints( const QRect wantedRect ) const; + void routePaint( QPainter * painter, const QRect paintRect ); void reparseConfig(); static QString defaultToolName( const QDomElement &toolElement ); static QPixmap makeToolPixmap( const QDomElement &toolElement ); private Q_SLOTS: void slotToolSelected( int toolID ); void slotSaveToolbarOrientation( int side ); void slotToolDoubleClicked( int toolID ); private: void detachAnnotation(); // global class pointers Okular::Document * m_document; PageView * m_pageView; PageViewToolBar * m_toolBar; AnnotatorEngine * m_engine; QDomElement m_toolsDefinition; QLinkedList m_items; bool m_textToolsEnabled; bool m_toolsEnabled; bool m_continuousMode; bool m_hidingWasForced; // creation related variables int m_lastToolID; QRect m_lastDrawnRect; PageViewItem * m_lockedItem; //selected annotation name //QString m_selectedAnnotationName; }; #endif /* kate: replace-tabs on; indent-width 4; */ diff --git a/ui/pageviewmouseannotation.cpp b/ui/pageviewmouseannotation.cpp index c837f58ac..d42c7e0c5 100644 --- a/ui/pageviewmouseannotation.cpp +++ b/ui/pageviewmouseannotation.cpp @@ -1,770 +1,770 @@ /*************************************************************************** * Copyright (C) 2017 by Tobias Deiminger * * Copyright (C) 2004-2005 by Enrico Ros * * Copyright (C) 2004-2006 by Albert Astals Cid * * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * * company, info@kdab.com. Work sponsored by the * * LiMux project of the city of Munich * * * * With portions of code from kpdf/kpdf_pagewidget.cc by: * * Copyright (C) 2002 by Wilco Greven * * Copyright (C) 2003 by Christophe Devriese * * * * Copyright (C) 2003 by Laurent Montel * * Copyright (C) 2003 by Dirk Mueller * * Copyright (C) 2004 by James Ots * * Copyright (C) 2011 by Jiri Baum - NICTA * * * * 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. * ***************************************************************************/ #include "pageviewmouseannotation.h" #include #include #include #include "core/annotations.h" #include "core/document.h" #include "core/page.h" #include "ui/guiutils.h" #include "ui/pageview.h" #include "ui/videowidget.h" static const int handleSize = 10; static const int handleSizeHalf = handleSize / 2; bool AnnotationDescription::isValid() const { return ( annotation != nullptr ); } bool AnnotationDescription::isContainedInPage( const Okular::Document * document, int pageNumber ) const { if ( AnnotationDescription::pageNumber == pageNumber ) { /* Don't access page via pageViewItem here. pageViewItem might have been deleted. */ const Okular::Page * page = document->page( pageNumber ); if ( page != nullptr ) { if ( page->annotations().contains( annotation ) ) { return true; } } } return false; } void AnnotationDescription::invalidate() { annotation = nullptr; pageViewItem = nullptr; pageNumber = -1; } -AnnotationDescription::AnnotationDescription( PageViewItem * newPageViewItem, const QPoint& eventPos ) +AnnotationDescription::AnnotationDescription( PageViewItem * newPageViewItem, const QPoint eventPos ) { const Okular::AnnotationObjectRect * annObjRect = nullptr; if ( newPageViewItem ) { const QRect & uncroppedPage = newPageViewItem->uncroppedGeometry(); /* find out normalized mouse coords inside current item (nX and nY will be in the range of 0..1). */ const double nX = newPageViewItem->absToPageX( eventPos.x() ); const double nY = newPageViewItem->absToPageY( eventPos.y() ); annObjRect = (Okular::AnnotationObjectRect *) newPageViewItem->page()->objectRect( Okular::ObjectRect::OAnnotation, nX, nY, uncroppedPage.width(), uncroppedPage.height() ); } if ( annObjRect ) { annotation = annObjRect->annotation(); pageViewItem = newPageViewItem; pageNumber = pageViewItem->pageNumber(); } else { invalidate(); } } MouseAnnotation::MouseAnnotation( PageView * parent, Okular::Document * document) : QObject( parent ), m_document( document ), m_pageView( parent ), m_state( StateInactive ), m_handle( RH_None ) { m_resizeHandleList << RH_Left << RH_Right << RH_Top << RH_Bottom << RH_TopLeft << RH_TopRight << RH_BottomLeft << RH_BottomRight; } MouseAnnotation::~MouseAnnotation() { } -void MouseAnnotation::routeMousePressEvent( PageViewItem * pageViewItem, const QPoint & eventPos ) +void MouseAnnotation::routeMousePressEvent( PageViewItem * pageViewItem, const QPoint eventPos ) { /* Is there a selected annotation? */ if ( m_focusedAnnotation.isValid() ) { m_mousePosition = eventPos - pageViewItem->uncroppedGeometry().topLeft(); m_handle = getHandleAt( m_mousePosition, m_focusedAnnotation ); if ( m_handle != RH_None ) { /* Returning here means, the selection-rectangle gets control, unconditionally. * Even if it overlaps with another annotation. */ return; } } AnnotationDescription ad( pageViewItem, eventPos ); /* qDebug() << "routeMousePressEvent: eventPos = " << eventPos; */ if ( ad.isValid() ) { if ( ad.annotation->subType() == Okular::Annotation::AMovie || ad.annotation->subType() == Okular::Annotation::AScreen || ad.annotation->subType() == Okular::Annotation::AFileAttachment ) { /* qDebug() << "routeMousePressEvent: trigger action for AMovie/AScreen/AFileAttachment"; */ processAction( ad ); } else { /* qDebug() << "routeMousePressEvent: select for modification"; */ m_mousePosition = eventPos - pageViewItem->uncroppedGeometry().topLeft(); m_handle = getHandleAt( m_mousePosition, ad ); if ( m_handle != RH_None ) { setState( StateFocused, ad ); } } } else { /* qDebug() << "routeMousePressEvent: no annotation under mouse, enter StateInactive"; */ setState( StateInactive, ad ); } } void MouseAnnotation::routeMouseReleaseEvent() { if ( isModified() ) { /* qDebug() << "routeMouseReleaseEvent: finish command"; */ finishCommand(); setState( StateFocused, m_focusedAnnotation ); } /* else { qDebug() << "routeMouseReleaseEvent: ignore"; } */ } -void MouseAnnotation::routeMouseMoveEvent( PageViewItem * pageViewItem, const QPoint & eventPos, bool leftButtonPressed ) +void MouseAnnotation::routeMouseMoveEvent( PageViewItem * pageViewItem, const QPoint eventPos, bool leftButtonPressed ) { if ( !pageViewItem ) { /* qDebug() << "routeMouseMoveEvent: no pageViewItem provided, ignore"; */ return; } if ( leftButtonPressed ) { if ( isFocused() ) { /* On first move event after annotation is selected, enter modification state */ if ( m_handle == RH_Content ) { /* qDebug() << "routeMouseMoveEvent: handle " << m_handle << ", enter StateMoving"; */ setState( StateMoving, m_focusedAnnotation ); } else if ( m_handle != RH_None ) { /* qDebug() << "routeMouseMoveEvent: handle " << m_handle << ", enter StateResizing"; */ setState( StateResizing, m_focusedAnnotation ); } } if ( isModified() ) { /* qDebug() << "routeMouseMoveEvent: perform command, delta " << eventPos - m_mousePosition; */ updateViewport( m_focusedAnnotation ); performCommand( eventPos ); m_mousePosition = eventPos - pageViewItem->uncroppedGeometry().topLeft(); updateViewport( m_focusedAnnotation ); } } else { if ( isFocused() ) { /* qDebug() << "routeMouseMoveEvent: update cursor for focused annotation, new eventPos " << eventPos; */ m_mousePosition = eventPos - pageViewItem->uncroppedGeometry().topLeft(); m_handle = getHandleAt( m_mousePosition, m_focusedAnnotation ); m_pageView->updateCursor(); } /* We get here quite frequently. */ const AnnotationDescription ad( pageViewItem, eventPos ); m_mousePosition = eventPos - pageViewItem->uncroppedGeometry().topLeft(); if ( ad.isValid() ) { if ( !( m_mouseOverAnnotation == ad ) ) { /* qDebug() << "routeMouseMoveEvent: Annotation under mouse (subtype " << ad.annotation->subType() << ", flags " << ad.annotation->flags() << ")"; */ m_mouseOverAnnotation = ad; m_pageView->updateCursor(); } } else { if ( !( m_mouseOverAnnotation == ad ) ) { /* qDebug() << "routeMouseMoveEvent: Annotation disappeared under mouse."; */ m_mouseOverAnnotation.invalidate(); m_pageView->updateCursor(); } } } } void MouseAnnotation::routeKeyPressEvent( const QKeyEvent * e ) { switch ( e->key() ) { case Qt::Key_Escape: cancel(); break; case Qt::Key_Delete: if ( m_focusedAnnotation.isValid() ) { AnnotationDescription adToBeDeleted = m_focusedAnnotation; cancel(); m_document->removePageAnnotation( adToBeDeleted.pageNumber, adToBeDeleted.annotation ); } break; } } void MouseAnnotation::routeTooltipEvent( const QHelpEvent * helpEvent ) { /* qDebug() << "MouseAnnotation::routeTooltipEvent, event " << helpEvent; */ if ( m_mouseOverAnnotation.isValid() && m_mouseOverAnnotation.annotation->subType() != Okular::Annotation::AWidget ) { /* get boundingRect in uncropped page coordinates */ QRect boundingRect = Okular::AnnotationUtils::annotationGeometry( m_mouseOverAnnotation.annotation, m_mouseOverAnnotation.pageViewItem->uncroppedWidth(), m_mouseOverAnnotation.pageViewItem->uncroppedHeight() ); /* uncropped page to content area */ boundingRect.translate( m_mouseOverAnnotation.pageViewItem->uncroppedGeometry().topLeft() ); /* content area to viewport */ boundingRect.translate( -m_pageView->contentAreaPosition() ); const QString tip = GuiUtils::prettyToolTip( m_mouseOverAnnotation.annotation ); QToolTip::showText( helpEvent->globalPos(), tip, m_pageView->viewport(), boundingRect ); } } -void MouseAnnotation::routePaint( QPainter * painter, const QRect & paintRect ) +void MouseAnnotation::routePaint( QPainter * painter, const QRect paintRect ) { /* QPainter draws relative to the origin of uncropped viewport. */ static const QColor borderColor = QColor::fromHsvF( 0, 0, 1.0 ); static const QColor fillColor = QColor::fromHsvF( 0, 0, 0.75, 0.66 ); if ( !isFocused() ) return; /* * Get annotation bounding rectangle in uncropped page coordinates. * Distinction between AnnotationUtils::annotationGeometry() and AnnotationObjectRect::boundingRect() is, * that boundingRect would enlarge the QRect to a minimum size of 14 x 14. * This is useful for getting focus an a very small annotation, * but for drawing and modification we want the real size. */ const QRect boundingRect = Okular::AnnotationUtils::annotationGeometry( m_focusedAnnotation.annotation, m_focusedAnnotation.pageViewItem->uncroppedWidth(), m_focusedAnnotation.pageViewItem->uncroppedHeight() ); if ( !paintRect.intersects( boundingRect .translated( m_focusedAnnotation.pageViewItem->uncroppedGeometry().topLeft() ) .adjusted( -handleSizeHalf, -handleSizeHalf, handleSizeHalf, handleSizeHalf ) ) ) { /* Our selection rectangle is not in a region that needs to be (re-)drawn. */ return; } painter->save(); painter->translate( m_focusedAnnotation.pageViewItem->uncroppedGeometry().topLeft() ); painter->setPen( QPen( fillColor, 2, Qt::SolidLine, Qt::SquareCap, Qt::BevelJoin ) ); painter->drawRect( boundingRect ); if ( m_focusedAnnotation.annotation->canBeResized() ) { painter->setPen(borderColor); painter->setBrush(fillColor); for ( const ResizeHandle &handle : qAsConst(m_resizeHandleList) ) { QRect rect = getHandleRect( handle, m_focusedAnnotation ); painter->drawRect( rect ); } } painter->restore(); } Okular::Annotation * MouseAnnotation::annotation() const { if ( m_focusedAnnotation.isValid() ) { return m_focusedAnnotation.annotation; } return nullptr; } bool MouseAnnotation::isActive() const { return ( m_state != StateInactive ); } bool MouseAnnotation::isMouseOver() const { return ( m_mouseOverAnnotation.isValid() || m_handle != RH_None ); } bool MouseAnnotation::isFocused() const { return ( m_state == StateFocused ); } bool MouseAnnotation::isMoved() const { return ( m_state == StateMoving ); } bool MouseAnnotation::isResized() const { return ( m_state == StateResizing ); } bool MouseAnnotation::isModified() const { return ( m_state == StateMoving || m_state == StateResizing ); } Qt::CursorShape MouseAnnotation::cursor() const { if ( m_handle != RH_None ) { if ( isMoved() ) { return Qt::SizeAllCursor; } else if ( isFocused() || isResized() ) { switch ( m_handle ) { case RH_Top: return Qt::SizeVerCursor; case RH_TopRight: return Qt::SizeBDiagCursor; case RH_Right: return Qt::SizeHorCursor; case RH_BottomRight: return Qt::SizeFDiagCursor; case RH_Bottom: return Qt::SizeVerCursor; case RH_BottomLeft: return Qt::SizeBDiagCursor; case RH_Left: return Qt::SizeHorCursor; case RH_TopLeft: return Qt::SizeFDiagCursor; case RH_Content: return Qt::SizeAllCursor; default: return Qt::OpenHandCursor; } } } else if ( m_mouseOverAnnotation.isValid() ) { /* Mouse is over annotation, but the annotation is not yet selected. */ if ( m_mouseOverAnnotation.annotation->subType() == Okular::Annotation::AMovie ) { return Qt::PointingHandCursor; } else if ( m_mouseOverAnnotation.annotation->subType() == Okular::Annotation::ARichMedia ) { return Qt::PointingHandCursor; } else if ( m_mouseOverAnnotation.annotation->subType() == Okular::Annotation::AScreen ) { if ( GuiUtils::renditionMovieFromScreenAnnotation( static_cast< const Okular::ScreenAnnotation * >( m_mouseOverAnnotation.annotation ) ) != nullptr ) { return Qt::PointingHandCursor; } } else if ( m_mouseOverAnnotation.annotation->subType() == Okular::Annotation::AFileAttachment ) { return Qt::PointingHandCursor; } else { return Qt::ArrowCursor; } } /* There's no none cursor, so we still have to return something. */ return Qt::ArrowCursor; } void MouseAnnotation::notifyAnnotationChanged( int pageNumber ) { const AnnotationDescription emptyAd; if ( m_focusedAnnotation.isValid() && ! m_focusedAnnotation.isContainedInPage( m_document, pageNumber ) ) { setState( StateInactive, emptyAd ); } if ( m_mouseOverAnnotation.isValid() && ! m_mouseOverAnnotation.isContainedInPage( m_document, pageNumber ) ) { m_mouseOverAnnotation = emptyAd; m_pageView->updateCursor(); } } void MouseAnnotation::updateAnnotationPointers() { if (m_focusedAnnotation.annotation) { m_focusedAnnotation.annotation = m_document->page( m_focusedAnnotation.pageNumber )->annotation( m_focusedAnnotation.annotation->uniqueName() ); } if (m_mouseOverAnnotation.annotation) { m_mouseOverAnnotation.annotation = m_document->page( m_mouseOverAnnotation.pageNumber )->annotation( m_mouseOverAnnotation.annotation->uniqueName() ); } } void MouseAnnotation::cancel() { if ( isActive() ) { finishCommand(); setState( StateInactive, m_focusedAnnotation ); } } void MouseAnnotation::reset() { cancel(); m_focusedAnnotation.invalidate(); m_mouseOverAnnotation.invalidate(); } /* Handle state changes for the focused annotation. */ void MouseAnnotation::setState( MouseAnnotationState state, const AnnotationDescription & ad ) { /* qDebug() << "setState: requested " << state; */ if ( m_focusedAnnotation.isValid() ) { /* If there was a annotation before, request also repaint for the previous area. */ updateViewport( m_focusedAnnotation ); } if ( !ad.isValid() ) { /* qDebug() << "No annotation provided, forcing state inactive." << state; */ state = StateInactive; } else if ( ( state == StateMoving && !ad.annotation->canBeMoved() ) || ( state == StateResizing && !ad.annotation->canBeResized() ) ) { /* qDebug() << "Annotation does not support requested state, forcing state selected." << state; */ state = StateInactive; } switch( state ) { case StateMoving: m_focusedAnnotation = ad; m_focusedAnnotation.annotation->setFlags( m_focusedAnnotation.annotation->flags() | Okular::Annotation::BeingMoved ); updateViewport( m_focusedAnnotation ); break; case StateResizing: m_focusedAnnotation = ad; m_focusedAnnotation.annotation->setFlags( m_focusedAnnotation.annotation->flags() | Okular::Annotation::BeingResized ); updateViewport( m_focusedAnnotation ); break; case StateFocused: m_focusedAnnotation = ad; m_focusedAnnotation.annotation->setFlags( m_focusedAnnotation.annotation->flags() & ~(Okular::Annotation::BeingMoved | Okular::Annotation::BeingResized) ); updateViewport( m_focusedAnnotation ); break; case StateInactive: default: if ( m_focusedAnnotation.isValid() ) { m_focusedAnnotation.annotation->setFlags( m_focusedAnnotation.annotation->flags() & ~(Okular::Annotation::BeingMoved | Okular::Annotation::BeingResized) ); } m_focusedAnnotation.invalidate(); m_handle = RH_None; } /* qDebug() << "setState: enter " << state; */ m_state = state; m_pageView->updateCursor(); } /* Get the rectangular boundary of the given annotation, enlarged for space needed by resize handles. * Returns a QRect in page view item coordinates. */ QRect MouseAnnotation::getFullBoundingRect( const AnnotationDescription & ad ) const { QRect boundingRect; if ( ad.isValid() ) { boundingRect = Okular::AnnotationUtils::annotationGeometry( ad.annotation, ad.pageViewItem->uncroppedWidth(), ad.pageViewItem->uncroppedHeight() ); boundingRect = boundingRect.adjusted( -handleSizeHalf, -handleSizeHalf, handleSizeHalf, handleSizeHalf ); } return boundingRect; } /* Apply the command determined by m_state to the currently focused annotation. */ -void MouseAnnotation::performCommand( const QPoint & newPos ) +void MouseAnnotation::performCommand( const QPoint newPos ) { const QRect & pageViewItemRect = m_focusedAnnotation.pageViewItem->uncroppedGeometry(); QPointF mouseDelta( newPos - pageViewItemRect.topLeft() - m_mousePosition ); QPointF normalizedRotatedMouseDelta( rotateInRect( QPointF( mouseDelta.x() / pageViewItemRect.width(), mouseDelta.y() / pageViewItemRect.height() ), m_focusedAnnotation.pageViewItem->page()->rotation() ) ); if ( isMoved() ) { m_document->translatePageAnnotation( m_focusedAnnotation.pageNumber, m_focusedAnnotation.annotation, Okular::NormalizedPoint( normalizedRotatedMouseDelta.x(), normalizedRotatedMouseDelta.y() ) ); } else if ( isResized() ) { QPointF delta1, delta2; handleToAdjust( normalizedRotatedMouseDelta, delta1, delta2, m_handle, m_focusedAnnotation.pageViewItem->page()->rotation() ); m_document->adjustPageAnnotation( m_focusedAnnotation.pageNumber, m_focusedAnnotation.annotation, Okular::NormalizedPoint( delta1.x(), delta1.y() ), Okular::NormalizedPoint( delta2.x(), delta2.y() ) ); } } /* Finalize a command in progress for the currently focused annotation. */ void MouseAnnotation::finishCommand() { /* * Note: * Translate-/resizePageAnnotation causes PopplerAnnotationProxy::notifyModification, * where modify flag needs to be already cleared. So it is important to call * setFlags before translatePageAnnotation-/adjustPageAnnotation. */ if ( isMoved() ) { m_focusedAnnotation.annotation->setFlags( m_focusedAnnotation.annotation->flags() & ~Okular::Annotation::BeingMoved ); m_document->translatePageAnnotation( m_focusedAnnotation.pageNumber, m_focusedAnnotation.annotation, Okular::NormalizedPoint( 0.0, 0.0 ) ); } else if ( isResized() ) { m_focusedAnnotation.annotation->setFlags( m_focusedAnnotation.annotation->flags() & ~Okular::Annotation::BeingResized ); m_document->adjustPageAnnotation( m_focusedAnnotation.pageNumber, m_focusedAnnotation.annotation, Okular::NormalizedPoint( 0.0, 0.0 ), Okular::NormalizedPoint( 0.0, 0.0 ) ); } } /* Tell viewport widget that the rectangular of the given annotation needs to be repainted. */ void MouseAnnotation::updateViewport( const AnnotationDescription & ad ) const { const QRect & changedPageViewItemRect = getFullBoundingRect( ad ); if ( changedPageViewItemRect.isValid() ) { m_pageView->viewport()->update( changedPageViewItemRect .translated( ad.pageViewItem->uncroppedGeometry().topLeft() ) .translated( -m_pageView->contentAreaPosition() ) ); } } /* eventPos: Mouse position in uncropped page coordinates. ad: The annotation to get the handle for. */ -MouseAnnotation::ResizeHandle MouseAnnotation::getHandleAt( const QPoint & eventPos, const AnnotationDescription & ad ) const +MouseAnnotation::ResizeHandle MouseAnnotation::getHandleAt( const QPoint eventPos, const AnnotationDescription & ad ) const { ResizeHandle selected = RH_None; if ( ad.annotation->canBeResized() ) { for ( const ResizeHandle &handle : m_resizeHandleList ) { const QRect rect = getHandleRect( handle, ad ); if ( rect.contains( eventPos ) ) { selected |= handle; } } /* * Handles may overlap when selection is very small. * Then it can happen that cursor is over more than one handles, * and therefore maybe more than two flags are set. * Favor one handle in that case. */ if ( ( selected & RH_BottomRight ) == RH_BottomRight ) return RH_BottomRight; if ( ( selected & RH_TopRight ) == RH_TopRight ) return RH_TopRight; if ( ( selected & RH_TopLeft ) == RH_TopLeft ) return RH_TopLeft; if ( ( selected & RH_BottomLeft ) == RH_BottomLeft ) return RH_BottomLeft; } if ( selected == RH_None && ad.annotation->canBeMoved() ) { const QRect boundingRect = Okular::AnnotationUtils::annotationGeometry( ad.annotation, ad.pageViewItem->uncroppedWidth(), ad.pageViewItem->uncroppedHeight() ); if ( boundingRect.contains( eventPos ) ) { return RH_Content; } } return selected; } /* Get the rectangle for a specified resizie handle. */ QRect MouseAnnotation::getHandleRect( ResizeHandle handle, const AnnotationDescription & ad ) const { const QRect boundingRect = Okular::AnnotationUtils::annotationGeometry( ad.annotation, ad.pageViewItem->uncroppedWidth(), ad.pageViewItem->uncroppedHeight() ); int left, top; if ( handle & RH_Top ) { top = boundingRect.top() - handleSizeHalf; } else if ( handle & RH_Bottom ) { top = boundingRect.bottom() - handleSizeHalf; } else { top = boundingRect.top() + boundingRect.height() / 2 - handleSizeHalf; } if ( handle & RH_Left ) { left = boundingRect.left() - handleSizeHalf; } else if ( handle & RH_Right ) { left = boundingRect.right() - handleSizeHalf; } else { left = boundingRect.left() + boundingRect.width() / 2 - handleSizeHalf; } return QRect( left, top, handleSize, handleSize ); } /* Convert a resize handle delta into two adjust delta coordinates. */ -void MouseAnnotation::handleToAdjust( const QPointF & dIn, QPointF & dOut1, QPointF & dOut2, MouseAnnotation::ResizeHandle handle, Okular::Rotation rotation ) +void MouseAnnotation::handleToAdjust( const QPointF dIn, QPointF & dOut1, QPointF & dOut2, MouseAnnotation::ResizeHandle handle, Okular::Rotation rotation ) { const MouseAnnotation::ResizeHandle rotatedHandle = MouseAnnotation::rotateHandle( handle, rotation); dOut1.rx() = ( rotatedHandle & MouseAnnotation::RH_Left ) ? dIn.x() : 0; dOut1.ry() = ( rotatedHandle & MouseAnnotation::RH_Top ) ? dIn.y() : 0; dOut2.rx() = ( rotatedHandle & MouseAnnotation::RH_Right ) ? dIn.x() : 0; dOut2.ry() = ( rotatedHandle & MouseAnnotation::RH_Bottom ) ? dIn.y() : 0; } -QPointF MouseAnnotation::rotateInRect( const QPointF & rotated, Okular::Rotation rotation ) +QPointF MouseAnnotation::rotateInRect( const QPointF rotated, Okular::Rotation rotation ) { QPointF ret; switch ( rotation ) { case Okular::Rotation90: ret = QPointF( rotated.y(), -rotated.x() ); break; case Okular::Rotation180: ret = QPointF( -rotated.x(), -rotated.y() ); break; case Okular::Rotation270: ret = QPointF( -rotated.y(), rotated.x() ); break; case Okular::Rotation0: /* no modifications */ default: /* other cases */ ret = rotated; } return ret; } MouseAnnotation::ResizeHandle MouseAnnotation::rotateHandle( MouseAnnotation::ResizeHandle handle, Okular::Rotation rotation ) { unsigned int rotatedHandle = 0; switch( rotation ) { case Okular::Rotation90: /* bit rotation: #1 => #4, #2 => #1, #3 => #2, #4 => #3 */ rotatedHandle = (handle << 3 | handle >> (4-3)) & RH_AllHandles; break; case Okular::Rotation180: /* bit rotation: #1 => #3, #2 => #4, #3 => #1, #4 => #2 */ rotatedHandle = (handle << 2 | handle >> (4-2)) & RH_AllHandles; break; case Okular::Rotation270: /* bit rotation: #1 => #2, #2 => #3, #3 => #4, #4 => #1 */ rotatedHandle = (handle << 1 | handle >> (4-1)) & RH_AllHandles; break; case Okular::Rotation0: /* no modifications */ default: /* other cases */ rotatedHandle = handle; break; } return (MouseAnnotation::ResizeHandle) rotatedHandle; } /* Start according action for AMovie/ARichMedia/AScreen/AFileAttachment. * It was formerly (before mouse annotation refactoring) called on mouse release event. * Now it's called on mouse press. Should we keep the former behavior? */ void MouseAnnotation::processAction( const AnnotationDescription& ad ) { if ( ad.isValid() ) { Okular::Annotation *ann = ad.annotation; PageViewItem * pageItem = ad.pageViewItem; if ( ann->subType() == Okular::Annotation::AMovie ) { VideoWidget *vw = pageItem->videoWidgets().value( static_cast( ann )->movie() ); vw->show(); vw->play(); } else if ( ann->subType() == Okular::Annotation::ARichMedia ) { VideoWidget *vw = pageItem->videoWidgets().value( static_cast( ann )->movie() ); vw->show(); vw->play(); } else if ( ann->subType() == Okular::Annotation::AScreen ) { m_document->processAction( static_cast( ann )->action() ); } else if ( ann->subType() == Okular::Annotation::AFileAttachment ) { const Okular::FileAttachmentAnnotation * fileAttachAnnot = static_cast< Okular::FileAttachmentAnnotation * >( ann ); GuiUtils::saveEmbeddedFile( fileAttachAnnot->embeddedFile(), m_pageView ); } } } diff --git a/ui/pageviewmouseannotation.h b/ui/pageviewmouseannotation.h index ccaf41fa0..e0229d360 100644 --- a/ui/pageviewmouseannotation.h +++ b/ui/pageviewmouseannotation.h @@ -1,177 +1,177 @@ /*************************************************************************** * Copyright (C) 2017 by Tobias Deiminger * * Copyright (C) 2004-2005 by Enrico Ros * * Copyright (C) 2004-2006 by Albert Astals Cid * * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * * company, info@kdab.com. Work sponsored by the * * LiMux project of the city of Munich * * * * With portions of code from kpdf/kpdf_pagewidget.cc by: * * Copyright (C) 2002 by Wilco Greven * * Copyright (C) 2003 by Christophe Devriese * * * * Copyright (C) 2003 by Laurent Montel * * Copyright (C) 2003 by Dirk Mueller * * Copyright (C) 2004 by James Ots * * Copyright (C) 2011 by Jiri Baum - NICTA * * * * 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. * ***************************************************************************/ #ifndef _OKULAR_PAGEVIEWMOUSEANNOTATION_H_ #define _OKULAR_PAGEVIEWMOUSEANNOTATION_H_ #include #include "pageviewutils.h" #include "core/annotations.h" class QHelpEvent; class QPainter; class QPoint; class PageView; class PageViewItem; class AnnotationDescription; namespace Okular { class Document; } /* This class shall help to keep data for one annotation consistent. */ class AnnotationDescription { public: AnnotationDescription() : annotation( nullptr ), pageViewItem( nullptr ), pageNumber( -1 ) {} - AnnotationDescription( PageViewItem * newPageViewItem, const QPoint& eventPos ); + AnnotationDescription( PageViewItem * newPageViewItem, const QPoint eventPos ); bool isValid() const; bool isContainedInPage( const Okular::Document * document, int pageNumber ) const; void invalidate(); bool operator==( const AnnotationDescription & rhs ) const { return ( annotation == rhs.annotation ); } Okular::Annotation * annotation; PageViewItem * pageViewItem; int pageNumber; }; /** * @short Handle UI for annotation interactions, like moving, resizing and triggering actions. * * An object of this class tracks which annotation is currently under the mouse cursor. * Some annotation types can be focused in order to move or resize them. * State is determined from mouse and keyboard events, which are forwarded from the parent PageView object. * Move and resize actions are dispatched to the Document object. */ class MouseAnnotation : public QObject { Q_OBJECT public: MouseAnnotation( PageView * parent, Okular::Document * document ); ~MouseAnnotation() override; /* Process a mouse press event. eventPos: Mouse position in content area coordinates. */ - void routeMousePressEvent( PageViewItem * pageViewItem, const QPoint & eventPos ); + void routeMousePressEvent( PageViewItem * pageViewItem, const QPoint eventPos ); /* Process a mouse release event. */ void routeMouseReleaseEvent(); /* Process a mouse move event. eventPos: Mouse position in content area coordinates. */ - void routeMouseMoveEvent( PageViewItem * pageViewItem, const QPoint & eventPos, bool leftButtonPressed ); + void routeMouseMoveEvent( PageViewItem * pageViewItem, const QPoint eventPos, bool leftButtonPressed ); /* Process a key event. */ void routeKeyPressEvent( const QKeyEvent * e ); /* Process a tooltip event. eventPos: Mouse position in content area coordinates. */ void routeTooltipEvent( const QHelpEvent * helpEvent ); /* Process a paint event. */ - void routePaint( QPainter * painter, const QRect & paintRect ); + void routePaint( QPainter * painter, const QRect paintRect ); /* Cancel the current selection or action, if any. */ void cancel(); /* Reset to initial state. Cancel current action and relinquish references to PageViewItem widgets. */ void reset(); Okular::Annotation * annotation() const; /* Return true, if MouseAnnotation demands control for a mouse click on the current cursor position. */ bool isMouseOver() const; bool isActive() const; bool isFocused() const; bool isMoved() const; bool isResized() const; bool isModified() const; Qt::CursorShape cursor() const; /* Forward DocumentObserver::notifyPageChanged to this method. */ void notifyAnnotationChanged( int pageNumber ); /* Forward DocumentObserver::notifySetup to this method. */ void updateAnnotationPointers(); enum MouseAnnotationState { StateInactive, StateFocused, StateMoving, StateResizing }; enum ResizeHandleFlag { RH_None = 0, RH_Top = 1, RH_Right = 2, RH_Bottom = 4, RH_Left = 8, RH_TopLeft = RH_Top | RH_Left, RH_BottomLeft = RH_Bottom | RH_Left, RH_TopRight = RH_Top | RH_Right, RH_BottomRight = RH_Bottom | RH_Right, RH_Content = 16, RH_AllHandles = RH_Top | RH_Right | RH_Bottom | RH_Left }; Q_DECLARE_FLAGS( ResizeHandle, ResizeHandleFlag ) private: void setState( MouseAnnotationState state, const AnnotationDescription & ad ); QRect getFullBoundingRect( const AnnotationDescription & ad ) const; - void performCommand( const QPoint & newPos ); + void performCommand( const QPoint newPos ); void finishCommand(); void updateViewport( const AnnotationDescription & ad ) const; - ResizeHandle getHandleAt( const QPoint & eventPos, const AnnotationDescription & ad ) const; + ResizeHandle getHandleAt( const QPoint eventPos, const AnnotationDescription & ad ) const; QRect getHandleRect( ResizeHandle handle, const AnnotationDescription & ad ) const; - static void handleToAdjust( const QPointF & dIn, QPointF & dOut1, QPointF & dOut2, MouseAnnotation::ResizeHandle handle, Okular::Rotation rotation ); - static QPointF rotateInRect( const QPointF & rotated, Okular::Rotation rotation ); + static void handleToAdjust( const QPointF dIn, QPointF & dOut1, QPointF & dOut2, MouseAnnotation::ResizeHandle handle, Okular::Rotation rotation ); + static QPointF rotateInRect( const QPointF rotated, Okular::Rotation rotation ); static ResizeHandle rotateHandle( ResizeHandle handle, Okular::Rotation rotation ); void processAction( const AnnotationDescription& ad ); /* We often have to delegate to the document model and our parent widget. */ Okular::Document * m_document; PageView * m_pageView; /* Remember which annotation is currently focused/modified. */ MouseAnnotationState m_state; MouseAnnotation::ResizeHandle m_handle; AnnotationDescription m_focusedAnnotation; /* Mouse tracking, always kept up to date with the latest mouse position and annotation under mouse cursor. */ AnnotationDescription m_mouseOverAnnotation; QPoint m_mousePosition; // in page view item coordinates QList m_resizeHandleList; }; #endif diff --git a/ui/presentationwidget.cpp b/ui/presentationwidget.cpp index d0d0cbc0a..d6fc83e3d 100644 --- a/ui/presentationwidget.cpp +++ b/ui/presentationwidget.cpp @@ -1,2530 +1,2530 @@ /*************************************************************************** * Copyright (C) 2004 by Enrico Ros * * * * 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. * ***************************************************************************/ #include "presentationwidget.h" // qt/kde includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_LINUX #include #include // For ::close() for sleep inhibition #endif // system includes #include #include // local includes #include "annotationtools.h" #include "debug_ui.h" #include "drawingtoolactions.h" #include "guiutils.h" #include "pagepainter.h" #include "presentationsearchbar.h" #include "priorities.h" #include "videowidget.h" #include "core/action.h" #include "core/annotations.h" #include "core/audioplayer.h" #include "core/document.h" #include "core/generator.h" #include "core/movie.h" #include "core/page.h" #include "settings.h" #include "settings_core.h" // comment this to disable the top-right progress indicator #define ENABLE_PROGRESS_OVERLAY // a frame contains a pointer to the page object, its geometry and the // transition effect to the next frame struct PresentationFrame { PresentationFrame() = default; ~PresentationFrame() { qDeleteAll( videoWidgets ); } PresentationFrame(const PresentationFrame &) = delete; PresentationFrame &operator=(const PresentationFrame &) = delete; void recalcGeometry( int width, int height, float screenRatio ) { // calculate frame geometry keeping constant aspect ratio float pageRatio = page->ratio(); int pageWidth = width, pageHeight = height; if ( pageRatio > screenRatio ) pageWidth = (int)( (float)pageHeight / pageRatio ); else pageHeight = (int)( (float)pageWidth * pageRatio ); geometry.setRect( ( width - pageWidth ) / 2, ( height - pageHeight ) / 2, pageWidth, pageHeight ); for ( VideoWidget *vw : qAsConst(videoWidgets) ) { const Okular::NormalizedRect r = vw->normGeometry(); QRect vwgeom = r.geometry( geometry.width(), geometry.height() ); vw->resize( vwgeom.size() ); vw->move( geometry.topLeft() + vwgeom.topLeft() ); } } const Okular::Page * page; QRect geometry; QHash< Okular::Movie *, VideoWidget * > videoWidgets; QLinkedList< SmoothPath > drawings; }; // a custom QToolBar that basically does not propagate the event if the widget // background is not automatically filled class PresentationToolBar : public QToolBar { Q_OBJECT public: PresentationToolBar( QWidget * parent = Q_NULLPTR ) : QToolBar( parent ) {} protected: void mousePressEvent( QMouseEvent * e ) override { QToolBar::mousePressEvent( e ); e->accept(); } void mouseReleaseEvent( QMouseEvent * e ) override { QToolBar::mouseReleaseEvent( e ); e->accept(); } }; PresentationWidget::PresentationWidget( QWidget * parent, Okular::Document * doc, DrawingToolActions * drawingToolActions, KActionCollection * collection ) : QWidget( nullptr /* must be null, to have an independent widget */, Qt::FramelessWindowHint ), m_pressedLink( nullptr ), m_handCursor( false ), m_drawingEngine( nullptr ), m_screenInhibitCookie(0), m_sleepInhibitFd(-1), m_parentWidget( parent ), m_document( doc ), m_frameIndex( -1 ), m_topBar( nullptr ), m_pagesEdit( nullptr ), m_searchBar( nullptr ), m_ac( collection ), m_screenSelect( nullptr ), m_isSetup( false ), m_blockNotifications( false ), m_inBlackScreenMode( false ), m_showSummaryView( Okular::Settings::slidesShowSummary() ), m_advanceSlides( Okular::SettingsCore::slidesAdvance() ), m_goToPreviousPageOnRelease( false ), m_goToNextPageOnRelease( false ) { Q_UNUSED( parent ) setAttribute( Qt::WA_DeleteOnClose ); setAttribute( Qt::WA_OpaquePaintEvent ); setObjectName( QStringLiteral( "presentationWidget" ) ); QString caption = doc->metaData( QStringLiteral("DocumentTitle") ).toString(); if ( caption.trimmed().isEmpty() ) caption = doc->currentDocument().fileName(); caption = i18nc( "[document title/filename] – Presentation", "%1 – Presentation", caption ); setWindowTitle( caption ); m_width = -1; m_screen = -2; // create top toolbar m_topBar = new PresentationToolBar( this ); m_topBar->setObjectName( QStringLiteral( "presentationBar" ) ); m_topBar->setMovable( false ); m_topBar->layout()->setContentsMargins(0, 0, 0, 0); m_topBar->addAction( QIcon::fromTheme( layoutDirection() == Qt::RightToLeft ? QStringLiteral("go-next") : QStringLiteral("go-previous") ), i18n( "Previous Page" ), this, SLOT(slotPrevPage()) ); m_pagesEdit = new KLineEdit( m_topBar ); QSizePolicy sp = m_pagesEdit->sizePolicy(); sp.setHorizontalPolicy( QSizePolicy::Minimum ); m_pagesEdit->setSizePolicy( sp ); QFontMetrics fm( m_pagesEdit->font() ); QStyleOptionFrame option; option.initFrom( m_pagesEdit ); m_pagesEdit->setMaximumWidth( fm.width( QString::number( m_document->pages() ) ) + 2 * style()->pixelMetric( QStyle::PM_DefaultFrameWidth, &option, m_pagesEdit ) + 4 ); // the 4 comes from 2*horizontalMargin, horizontalMargin being a define in qlineedit.cpp QIntValidator *validator = new QIntValidator( 1, m_document->pages(), m_pagesEdit ); m_pagesEdit->setValidator( validator ); m_topBar->addWidget( m_pagesEdit ); QLabel *pagesLabel = new QLabel( m_topBar ); pagesLabel->setText( QLatin1String( " / " ) + QString::number( m_document->pages() ) + QLatin1String( " " ) ); m_topBar->addWidget( pagesLabel ); connect(m_pagesEdit, &QLineEdit::returnPressed, this, &PresentationWidget::slotPageChanged); m_topBar->addAction( QIcon::fromTheme( layoutDirection() == Qt::RightToLeft ? QStringLiteral("go-previous") : QStringLiteral("go-next") ), i18n( "Next Page" ), this, SLOT(slotNextPage()) ); m_topBar->addSeparator(); QAction *playPauseAct = collection->action( QStringLiteral("presentation_play_pause") ); playPauseAct->setEnabled( true ); connect(playPauseAct, &QAction::triggered, this, &PresentationWidget::slotTogglePlayPause); m_topBar->addAction( playPauseAct ); setPlayPauseIcon(); addAction( playPauseAct ); m_topBar->addSeparator(); const QList actionsList = drawingToolActions->actions(); for (QAction *action : actionsList) { action->setEnabled( true ); m_topBar->addAction( action ); addAction( action ); } connect( drawingToolActions, &DrawingToolActions::changeEngine, this, &PresentationWidget::slotChangeDrawingToolEngine ); connect( drawingToolActions, &DrawingToolActions::actionsRecreated, this, &PresentationWidget::slotAddDrawingToolActions ); QAction *eraseDrawingAct = collection->action( QStringLiteral("presentation_erase_drawings") ); eraseDrawingAct->setEnabled( true ); connect(eraseDrawingAct, &QAction::triggered, this, &PresentationWidget::clearDrawings); m_topBar->addAction( eraseDrawingAct ); addAction( eraseDrawingAct ); QDesktopWidget *desktop = QApplication::desktop(); if ( desktop->numScreens() > 1 ) { m_topBar->addSeparator(); m_screenSelect = new KSelectAction( QIcon::fromTheme( QStringLiteral("video-display") ), i18n( "Switch Screen" ), m_topBar ); m_screenSelect->setToolBarMode( KSelectAction::MenuMode ); m_screenSelect->setToolButtonPopupMode( QToolButton::InstantPopup ); m_topBar->addAction( m_screenSelect ); const int screenCount = desktop->numScreens(); for ( int i = 0; i < screenCount; ++i ) { QAction *act = m_screenSelect->addAction( i18nc( "%1 is the screen number (0, 1, ...)", "Screen %1", i ) ); act->setData( QVariant::fromValue( i ) ); } } QWidget *spacer = new QWidget( m_topBar ); spacer->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::MinimumExpanding ); m_topBar->addWidget( spacer ); m_topBar->addAction( QIcon::fromTheme( QStringLiteral("application-exit") ), i18n( "Exit Presentation Mode" ), this, SLOT(close()) ); m_topBar->setAutoFillBackground( true ); showTopBar( false ); // change topbar background color QPalette p = m_topBar->palette(); p.setColor( QPalette::Active, QPalette::Button, Qt::gray ); p.setColor( QPalette::Active, QPalette::Window, Qt::darkGray ); m_topBar->setPalette( p ); // Grab swipe gestures to change pages grabGesture(Qt::SwipeGesture); // misc stuff setMouseTracking( true ); setContextMenuPolicy( Qt::PreventContextMenu ); m_transitionTimer = new QTimer( this ); m_transitionTimer->setSingleShot( true ); connect(m_transitionTimer, &QTimer::timeout, this, &PresentationWidget::slotTransitionStep); m_overlayHideTimer = new QTimer( this ); m_overlayHideTimer->setSingleShot( true ); connect(m_overlayHideTimer, &QTimer::timeout, this, &PresentationWidget::slotHideOverlay); m_nextPageTimer = new QTimer( this ); m_nextPageTimer->setSingleShot( true ); connect(m_nextPageTimer, &QTimer::timeout, this, &PresentationWidget::slotNextPage); connect(m_document, &Okular::Document::processMovieAction, this, &PresentationWidget::slotProcessMovieAction); connect(m_document, &Okular::Document::processRenditionAction, this, &PresentationWidget::slotProcessRenditionAction); // handle cursor appearance as specified in configuration if ( Okular::Settings::slidesCursor() == Okular::Settings::EnumSlidesCursor::HiddenDelay ) { KCursor::setAutoHideCursor( this, true ); KCursor::setHideCursorDelay( 3000 ); } else if ( Okular::Settings::slidesCursor() == Okular::Settings::EnumSlidesCursor::Hidden ) { setCursor( QCursor( Qt::BlankCursor ) ); } setupActions(); // inhibit power management inhibitPowerManagement(); show(); QTimer::singleShot( 0, this, &PresentationWidget::slotDelayedEvents ); // setFocus() so KCursor::setAutoHideCursor() goes into effect if it's enabled setFocus( Qt::OtherFocusReason ); // Catch TabletEnterProximity and TabletLeaveProximity events from the QApplication qApp->installEventFilter( this ); } PresentationWidget::~PresentationWidget() { // allow power management saver again allowPowerManagement(); // stop the audio playbacks Okular::AudioPlayer::instance()->stopPlaybacks(); // remove our highlights if ( m_searchBar ) { m_document->resetSearch( PRESENTATION_SEARCH_ID ); } // remove this widget from document observer m_document->removeObserver( this ); const QList actionsList = actions(); for ( QAction *action : actionsList ) { action->setChecked( false ); action->setEnabled( false ); } delete m_drawingEngine; // delete frames qDeleteAll( m_frames ); qApp->removeEventFilter( this ); } void PresentationWidget::notifySetup( const QVector< Okular::Page * > & pageSet, int setupFlags ) { // same document, nothing to change - here we assume the document sets up // us with the whole document set as first notifySetup() if ( !( setupFlags & Okular::DocumentObserver::DocumentChanged ) ) return; // delete previous frames (if any (shouldn't be)) qDeleteAll( m_frames ); if ( !m_frames.isEmpty() ) qCWarning(OkularUiDebug) << "Frames setup changed while a Presentation is in progress."; m_frames.clear(); // create the new frames float screenRatio = (float)m_height / (float)m_width; for ( const Okular::Page * page : pageSet ) { PresentationFrame * frame = new PresentationFrame(); frame->page = page; const QLinkedList< Okular::Annotation * > annotations = page->annotations(); for ( Okular::Annotation * a : annotations ) { if ( a->subType() == Okular::Annotation::AMovie ) { Okular::MovieAnnotation * movieAnn = static_cast< Okular::MovieAnnotation * >( a ); VideoWidget * vw = new VideoWidget( movieAnn, movieAnn->movie(), m_document, this ); frame->videoWidgets.insert( movieAnn->movie(), vw ); vw->pageInitialized(); } else if ( a->subType() == Okular::Annotation::ARichMedia ) { Okular::RichMediaAnnotation * richMediaAnn = static_cast< Okular::RichMediaAnnotation * >( a ); if ( richMediaAnn->movie() ) { VideoWidget * vw = new VideoWidget( richMediaAnn, richMediaAnn->movie(), m_document, this ); frame->videoWidgets.insert( richMediaAnn->movie(), vw ); vw->pageInitialized(); } } else if ( a->subType() == Okular::Annotation::AScreen ) { const Okular::ScreenAnnotation * screenAnn = static_cast< Okular::ScreenAnnotation * >( a ); Okular::Movie *movie = GuiUtils::renditionMovieFromScreenAnnotation( screenAnn ); if ( movie ) { VideoWidget * vw = new VideoWidget( screenAnn, movie, m_document, this ); frame->videoWidgets.insert( movie, vw ); vw->pageInitialized(); } } } frame->recalcGeometry( m_width, m_height, screenRatio ); // add the frame to the vector m_frames.push_back( frame ); } // get metadata from the document m_metaStrings.clear(); const Okular::DocumentInfo info = m_document->documentInfo( QSet() << Okular::DocumentInfo::Title << Okular::DocumentInfo::Author ); if ( !info.get( Okular::DocumentInfo::Title ).isNull() ) m_metaStrings += i18n( "Title: %1", info.get( Okular::DocumentInfo::Title ) ); if ( !info.get( Okular::DocumentInfo::Author ).isNull() ) m_metaStrings += i18n( "Author: %1", info.get( Okular::DocumentInfo::Author ) ); m_metaStrings += i18n( "Pages: %1", m_document->pages() ); m_metaStrings += i18n( "Click to begin" ); m_isSetup = true; } void PresentationWidget::notifyViewportChanged( bool /*smoothMove*/ ) { // display the current page changePage( m_document->viewport().pageNumber ); // auto advance to the next page if set startAutoChangeTimer(); } void PresentationWidget::notifyPageChanged( int pageNumber, int changedFlags ) { // if we are blocking the notifications, do nothing if ( m_blockNotifications ) return; // check if it's the last requested pixmap. if so update the widget. if ( (changedFlags & ( DocumentObserver::Pixmap | DocumentObserver::Annotations | DocumentObserver::Highlights ) ) && pageNumber == m_frameIndex ) generatePage( changedFlags & ( DocumentObserver::Annotations | DocumentObserver::Highlights ) ); } void PresentationWidget::notifyCurrentPageChanged( int previousPage, int currentPage ) { if ( previousPage != -1 ) { // stop video playback for ( VideoWidget *vw : qAsConst(m_frames[ previousPage ]->videoWidgets) ) { vw->stop(); vw->pageLeft(); } // stop audio playback, if any Okular::AudioPlayer::instance()->stopPlaybacks(); // perform the page closing action, if any if ( m_document->page( previousPage )->pageAction( Okular::Page::Closing ) ) m_document->processAction( m_document->page( previousPage )->pageAction( Okular::Page::Closing ) ); // perform the additional actions of the page's annotations, if any const QLinkedList< Okular::Annotation* > annotationsList = m_document->page( previousPage )->annotations(); for ( const Okular::Annotation *annotation : annotationsList ) { Okular::Action *action = nullptr; if ( annotation->subType() == Okular::Annotation::AScreen ) action = static_cast( annotation )->additionalAction( Okular::Annotation::PageClosing ); else if ( annotation->subType() == Okular::Annotation::AWidget ) action = static_cast( annotation )->additionalAction( Okular::Annotation::PageClosing ); if ( action ) m_document->processAction( action ); } } if ( currentPage != -1 ) { m_frameIndex = currentPage; // check if pixmap exists or else request it PresentationFrame * frame = m_frames[ m_frameIndex ]; int pixW = frame->geometry.width(); int pixH = frame->geometry.height(); bool signalsBlocked = m_pagesEdit->signalsBlocked(); m_pagesEdit->blockSignals( true ); m_pagesEdit->setText( QString::number( m_frameIndex + 1 ) ); m_pagesEdit->blockSignals( signalsBlocked ); // if pixmap not inside the Okular::Page we request it and wait for // notifyPixmapChanged call or else we can proceed to pixmap generation if ( !frame->page->hasPixmap( this, ceil(pixW * qApp->devicePixelRatio()), ceil(pixH * qApp->devicePixelRatio()) ) ) { requestPixmaps(); } else { // make the background pixmap generatePage(); } // perform the page opening action, if any if ( m_document->page( m_frameIndex )->pageAction( Okular::Page::Opening ) ) m_document->processAction( m_document->page( m_frameIndex )->pageAction( Okular::Page::Opening ) ); // perform the additional actions of the page's annotations, if any const QLinkedList< Okular::Annotation* > annotationsList = m_document->page( m_frameIndex )->annotations(); for ( const Okular::Annotation *annotation : annotationsList ) { Okular::Action *action = nullptr; if ( annotation->subType() == Okular::Annotation::AScreen ) action = static_cast( annotation )->additionalAction( Okular::Annotation::PageOpening ); else if ( annotation->subType() == Okular::Annotation::AWidget ) action = static_cast( annotation )->additionalAction( Okular::Annotation::PageOpening ); if ( action ) m_document->processAction( action ); } // start autoplay video playback for ( VideoWidget *vw : qAsConst(m_frames[ m_frameIndex ]->videoWidgets) ) { vw->pageEntered(); } } } bool PresentationWidget::canUnloadPixmap( int pageNumber ) const { if ( Okular::SettingsCore::memoryLevel() == Okular::SettingsCore::EnumMemoryLevel::Low || Okular::SettingsCore::memoryLevel() == Okular::SettingsCore::EnumMemoryLevel::Normal ) { // can unload all pixmaps except for the currently visible one return pageNumber != m_frameIndex; } else { // can unload all pixmaps except for the currently visible one, previous and next return qAbs(pageNumber - m_frameIndex) <= 1; } } void PresentationWidget::setupActions() { addAction( m_ac->action( QStringLiteral("first_page") ) ); addAction( m_ac->action( QStringLiteral("last_page") ) ); addAction( m_ac->action( QString::fromLocal8Bit(KStandardAction::name( KStandardAction::Prior ) ) ) ); addAction( m_ac->action( QString::fromLocal8Bit(KStandardAction::name( KStandardAction::Next ) ) ) ); addAction( m_ac->action( QString::fromLocal8Bit(KStandardAction::name( KStandardAction::DocumentBack ) ) ) ); addAction( m_ac->action( QString::fromLocal8Bit(KStandardAction::name( KStandardAction::DocumentForward ) ) ) ); QAction *action = m_ac->action( QStringLiteral("switch_blackscreen_mode") ); connect(action, &QAction::toggled, this, &PresentationWidget::toggleBlackScreenMode); action->setEnabled( true ); addAction( action ); } void PresentationWidget::setPlayPauseIcon() { QAction *playPauseAction = m_ac->action( QStringLiteral("presentation_play_pause") ); if ( m_advanceSlides ) { playPauseAction->setIcon( QIcon::fromTheme( QStringLiteral("media-playback-pause") ) ); playPauseAction->setToolTip( i18nc( "For Presentation", "Pause" ) ); } else { playPauseAction->setIcon( QIcon::fromTheme( QStringLiteral("media-playback-start") ) ); playPauseAction->setToolTip( i18nc( "For Presentation", "Play" ) ); } } bool PresentationWidget::eventFilter (QObject *o, QEvent *e ) { if ( o == qApp ) { if ( e->type() == QTabletEvent::TabletEnterProximity ) { setCursor( QCursor( Qt::CrossCursor ) ); } else if ( e->type() == QTabletEvent::TabletLeaveProximity ) { setCursor( QCursor( Okular::Settings::slidesCursor() == Okular::Settings::EnumSlidesCursor::Hidden ? Qt::BlankCursor : Qt::ArrowCursor ) ); if ( Okular::Settings::slidesCursor() == Okular::Settings::EnumSlidesCursor::HiddenDelay) { // Trick KCursor to hide the cursor if needed by sending an "unknown" key press event // Send also the key release to make the world happy even it's probably not needed QKeyEvent kp( QEvent::KeyPress, 0, Qt::NoModifier ); qApp->sendEvent( this, &kp ); QKeyEvent kr( QEvent::KeyRelease, 0, Qt::NoModifier ); qApp->sendEvent( this, &kr ); } } } return false; } // bool PresentationWidget::event( QEvent * e ) { if ( e->type() == QEvent::Gesture ) return gestureEvent(static_cast(e)); if ( e->type() == QEvent::ToolTip ) { QHelpEvent * he = (QHelpEvent*)e; QRect r; const Okular::Action * link = getLink( he->x(), he->y(), &r ); if ( link ) { QString tip = link->actionTip(); if ( !tip.isEmpty() ) QToolTip::showText( he->globalPos(), tip, this, r ); } e->accept(); return true; } else // do not stop the event return QWidget::event( e ); } bool PresentationWidget::gestureEvent( QGestureEvent * event ) { // Swiping left or right on a touch screen will go to the previous or next slide, respectively. // The precise gesture is the standard Qt swipe: with three(!) fingers. if (QGesture *swipe = event->gesture(Qt::SwipeGesture)) { QSwipeGesture * swipeEvent = static_cast(swipe); if (swipeEvent->state() == Qt::GestureFinished) { if (swipeEvent->horizontalDirection() == QSwipeGesture::Left) { slotPrevPage(); event->accept(); return true; } if (swipeEvent->horizontalDirection() == QSwipeGesture::Right) { slotNextPage(); event->accept(); return true; } } } return false; } void PresentationWidget::keyPressEvent( QKeyEvent * e ) { if ( !m_isSetup ) return; switch ( e->key() ) { case Qt::Key_Left: case Qt::Key_Backspace: case Qt::Key_PageUp: case Qt::Key_Up: slotPrevPage(); break; case Qt::Key_Right: case Qt::Key_Space: case Qt::Key_PageDown: case Qt::Key_Down: slotNextPage(); break; case Qt::Key_Home: slotFirstPage(); break; case Qt::Key_End: slotLastPage(); break; case Qt::Key_Escape: if ( !m_topBar->isHidden() ) showTopBar( false ); else close(); break; } } void PresentationWidget::wheelEvent( QWheelEvent * e ) { if ( !m_isSetup ) return; // performance note: don't remove the clipping int div = e->angleDelta().y() / 120; if ( div > 0 ) { if ( div > 3 ) div = 3; while ( div-- ) slotPrevPage(); } else if ( div < 0 ) { if ( div < -3 ) div = -3; while ( div++ ) slotNextPage(); } } void PresentationWidget::mousePressEvent( QMouseEvent * e ) { if ( !m_isSetup ) return; if ( m_drawingEngine ) { QRect r = routeMouseDrawingEvent( e ); if ( r.isValid() ) { m_drawingRect |= r.translated( m_frames[ m_frameIndex ]->geometry.topLeft() ); update( m_drawingRect ); } return; } // pressing left button if ( e->button() == Qt::LeftButton ) { // if pressing on a link, skip other checks if ( ( m_pressedLink = getLink( e->x(), e->y() ) ) ) return; const Okular::Annotation *annotation = getAnnotation( e->x(), e->y() ); if ( annotation ) { if ( annotation->subType() == Okular::Annotation::AMovie ) { const Okular::MovieAnnotation *movieAnnotation = static_cast( annotation ); VideoWidget *vw = m_frames[ m_frameIndex ]->videoWidgets.value( movieAnnotation->movie() ); vw->show(); vw->play(); return; } else if ( annotation->subType() == Okular::Annotation::ARichMedia ) { const Okular::RichMediaAnnotation *richMediaAnnotation = static_cast( annotation ); VideoWidget *vw = m_frames[ m_frameIndex ]->videoWidgets.value( richMediaAnnotation->movie() ); vw->show(); vw->play(); return; } else if ( annotation->subType() == Okular::Annotation::AScreen ) { m_document->processAction( static_cast( annotation )->action() ); return; } } // handle clicking on top-right overlay if ( !( Okular::Settings::slidesCursor() == Okular::Settings::EnumSlidesCursor::Hidden ) && m_overlayGeometry.contains( e->pos() ) ) { overlayClick( e->pos() ); return; } // Actual mouse press events always lead to the next page page if ( e->source() == Qt::MouseEventNotSynthesized ) { m_goToNextPageOnRelease = true; } // Touch events may lead to the previous or next page else if ( Okular::Settings::slidesTapNavigation() != Okular::Settings::EnumSlidesTapNavigation::Disabled ) { switch ( Okular::Settings::slidesTapNavigation() ) { case Okular::Settings::EnumSlidesTapNavigation::ForwardBackward: { if ( e->x() < ( geometry().width()/2 ) ) { m_goToPreviousPageOnRelease = true; } else { m_goToNextPageOnRelease = true; } break; } case Okular::Settings::EnumSlidesTapNavigation::Forward: { m_goToNextPageOnRelease = true; break; } case Okular::Settings::EnumSlidesTapNavigation::Disabled: { // Do Nothing } } } } // pressing forward button else if ( e->button() == Qt::ForwardButton ) { m_goToNextPageOnRelease = true; } // pressing right or backward button else if ( e->button() == Qt::RightButton || e->button() == Qt::BackButton ) m_goToPreviousPageOnRelease = true; } void PresentationWidget::mouseReleaseEvent( QMouseEvent * e ) { if ( m_drawingEngine ) { routeMouseDrawingEvent( e ); return; } // if releasing on the same link we pressed over, execute it if ( m_pressedLink && e->button() == Qt::LeftButton ) { const Okular::Action * link = getLink( e->x(), e->y() ); if ( link == m_pressedLink ) m_document->processAction( link ); m_pressedLink = nullptr; } if ( m_goToPreviousPageOnRelease ) { slotPrevPage(); m_goToPreviousPageOnRelease = false; } if ( m_goToNextPageOnRelease ) { slotNextPage(); m_goToNextPageOnRelease = false; } } void PresentationWidget::mouseMoveEvent( QMouseEvent * e ) { // safety check if ( !m_isSetup ) return; // update cursor and tooltip if hovering a link if ( !m_drawingEngine && Okular::Settings::slidesCursor() != Okular::Settings::EnumSlidesCursor::Hidden ) testCursorOnLink( e->x(), e->y() ); if ( !m_topBar->isHidden() ) { // hide a shown bar when exiting the area if ( e->y() > ( m_topBar->height() + 1 ) ) { showTopBar( false ); setFocus( Qt::OtherFocusReason ); } } else { if ( m_drawingEngine && e->buttons() != Qt::NoButton ) { QRect r = routeMouseDrawingEvent( e ); if ( r.isValid() ) { m_drawingRect |= r.translated( m_frames[ m_frameIndex ]->geometry.topLeft() ); update( m_drawingRect ); } } else { // show the bar if reaching top 2 pixels if ( e->y() <= 1 ) showTopBar( true ); // handle "dragging the wheel" if clicking on its geometry else if ( ( QApplication::mouseButtons() & Qt::LeftButton ) && m_overlayGeometry.contains( e->pos() ) ) overlayClick( e->pos() ); } } } void PresentationWidget::paintEvent( QPaintEvent * pe ) { qreal dpr = devicePixelRatioF(); if ( m_inBlackScreenMode ) { QPainter painter( this ); painter.fillRect( pe->rect(), Qt::black ); return; } if ( !m_isSetup ) { m_width = width(); m_height = height(); connect(m_document, &Okular::Document::linkFind, this, &PresentationWidget::slotFind); // register this observer in document. events will come immediately m_document->addObserver( this ); // show summary if requested if ( Okular::Settings::slidesShowSummary() ) generatePage(); } // check painting rect consistency QRect r = pe->rect().intersected( QRect( QPoint( 0, 0 ), geometry().size() ) ); if ( r.isNull() ) return; if ( m_lastRenderedPixmap.isNull() ) { QPainter painter( this ); painter.fillRect( pe->rect(), Okular::Settings::slidesBackgroundColor() ); return; } // blit the pixmap to the screen QPainter painter( this ); for ( const QRect &r : pe->region() ) { if ( !r.isValid() ) continue; #ifdef ENABLE_PROGRESS_OVERLAY const QRect dR(QRectF(r.x() * dpr, r.y() * dpr, r.width() * dpr, r.height() * dpr).toAlignedRect()); if ( Okular::Settings::slidesShowProgress() && r.intersects( m_overlayGeometry ) ) { // backbuffer the overlay operation QPixmap backPixmap( dR.size() ); backPixmap.setDevicePixelRatio( dpr ); QPainter pixPainter( &backPixmap ); // first draw the background on the backbuffer pixPainter.drawPixmap( QPoint(0,0), m_lastRenderedPixmap, dR ); // then blend the overlay (a piece of) over the background QRect ovr = m_overlayGeometry.intersected( r ); pixPainter.drawPixmap( (ovr.left() - r.left()), (ovr.top() - r.top()), m_lastRenderedOverlay, (ovr.left() - m_overlayGeometry.left()) * dpr, (ovr.top() - m_overlayGeometry.top()) * dpr, ovr.width() * dpr, ovr.height() * dpr ); // finally blit the pixmap to the screen pixPainter.end(); const QRect backPixmapRect = backPixmap.rect(); const QRect dBackPixmapRect(QRectF(backPixmapRect.x() * dpr, backPixmapRect.y() * dpr, backPixmapRect.width() * dpr, backPixmapRect.height() * dpr).toAlignedRect()); painter.drawPixmap( r.topLeft(), backPixmap, dBackPixmapRect ); } else #endif // copy the rendered pixmap to the screen painter.drawPixmap( r.topLeft(), m_lastRenderedPixmap, dR ); } // paint drawings if ( m_frameIndex != -1 ) { painter.save(); const QRect & geom = m_frames[ m_frameIndex ]->geometry; const QSize pmSize( geom.width() * dpr, geom.height() * dpr ); QPixmap pm( pmSize ); pm.fill( Qt::transparent ); QPainter pmPainter( &pm ); pm.setDevicePixelRatio( dpr ); pmPainter.setRenderHints( QPainter::Antialiasing ); // Paint old paths for ( const SmoothPath &drawing : qAsConst(m_frames[ m_frameIndex ]->drawings) ) { drawing.paint( &pmPainter, pmSize.width(), pmSize.height() ); } // Paint the path that is currently being drawn by the user if ( m_drawingEngine && m_drawingRect.intersects( pe->rect() ) ) m_drawingEngine->paint( &pmPainter, pmSize.width(), pmSize.height(), m_drawingRect.intersected( pe->rect() ) ); painter.setRenderHints( QPainter::Antialiasing ); painter.drawPixmap( geom.topLeft() , pm ); painter.restore(); } painter.end(); } void PresentationWidget::resizeEvent( QResizeEvent *re ) { // qCDebug(OkularUiDebug) << re->oldSize() << "=>" << re->size(); if ( re->oldSize() == QSize( -1, -1 ) ) return; m_screen = QApplication::desktop()->screenNumber( this ); applyNewScreenSize( re->oldSize() ); } void PresentationWidget::leaveEvent( QEvent * e ) { Q_UNUSED( e ) if ( !m_topBar->isHidden() ) { showTopBar( false ); } } // const void * PresentationWidget::getObjectRect( Okular::ObjectRect::ObjectType type, int x, int y, QRect * geometry ) const { // no links on invalid pages if ( geometry && !geometry->isNull() ) geometry->setRect( 0, 0, 0, 0 ); if ( m_frameIndex < 0 || m_frameIndex >= (int)m_frames.size() ) return nullptr; // get frame, page and geometry const PresentationFrame * frame = m_frames[ m_frameIndex ]; const Okular::Page * page = frame->page; const QRect & frameGeometry = frame->geometry; // compute normalized x and y double nx = (double)(x - frameGeometry.left()) / (double)frameGeometry.width(); double ny = (double)(y - frameGeometry.top()) / (double)frameGeometry.height(); // no links outside the pages if ( nx < 0 || nx > 1 || ny < 0 || ny > 1 ) return nullptr; // check if 1) there is an object and 2) it's a link const QRect d = QApplication::desktop()->screenGeometry( m_screen ); const Okular::ObjectRect * object = page->objectRect( type, nx, ny, d.width(), d.height() ); if ( !object ) return nullptr; // compute link geometry if destination rect present if ( geometry ) { *geometry = object->boundingRect( frameGeometry.width(), frameGeometry.height() ); geometry->translate( frameGeometry.left(), frameGeometry.top() ); } // return the link pointer return object->object(); } const Okular::Action * PresentationWidget::getLink( int x, int y, QRect * geometry ) const { return reinterpret_cast( getObjectRect( Okular::ObjectRect::Action, x, y, geometry ) ); } const Okular::Annotation * PresentationWidget::getAnnotation( int x, int y, QRect * geometry ) const { return reinterpret_cast( getObjectRect( Okular::ObjectRect::OAnnotation, x, y, geometry ) ); } void PresentationWidget::testCursorOnLink( int x, int y ) { const Okular::Action * link = getLink( x, y, nullptr ); const Okular::Annotation *annotation = getAnnotation( x, y, nullptr ); const bool needsHandCursor = ( ( link != nullptr ) || ( ( annotation != nullptr ) && ( annotation->subType() == Okular::Annotation::AMovie ) ) || ( ( annotation != nullptr ) && ( annotation->subType() == Okular::Annotation::ARichMedia ) ) || ( ( annotation != nullptr ) && ( annotation->subType() == Okular::Annotation::AScreen ) && ( GuiUtils::renditionMovieFromScreenAnnotation( static_cast< const Okular::ScreenAnnotation * >( annotation ) ) != nullptr ) ) ); // only react on changes (in/out from a link) if ( ( needsHandCursor && !m_handCursor ) || ( !needsHandCursor && m_handCursor ) ) { // change cursor shape m_handCursor = needsHandCursor; setCursor( QCursor( m_handCursor ? Qt::PointingHandCursor : Qt::ArrowCursor ) ); } } -void PresentationWidget::overlayClick( const QPoint & position ) +void PresentationWidget::overlayClick( const QPoint position ) { // clicking the progress indicator int xPos = position.x() - m_overlayGeometry.x() - m_overlayGeometry.width() / 2, yPos = m_overlayGeometry.height() / 2 - position.y(); if ( !xPos && !yPos ) return; // compute angle relative to indicator (note coord transformation) float angle = 0.5 + 0.5 * atan2( (double)-xPos, (double)-yPos ) / M_PI; int pageIndex = (int)( angle * ( m_frames.count() - 1 ) + 0.5 ); // go to selected page changePage( pageIndex ); } void PresentationWidget::changePage( int newPage ) { if ( m_showSummaryView ) { m_showSummaryView = false; m_frameIndex = -1; return; } if ( m_frameIndex == newPage ) return; // switch to newPage m_document->setViewportPage( newPage, this ); if ( (Okular::Settings::slidesShowSummary() && !m_showSummaryView) || m_frameIndex == -1 ) notifyCurrentPageChanged( -1, newPage ); } void PresentationWidget::generatePage( bool disableTransition ) { if ( m_lastRenderedPixmap.isNull() ) { qreal dpr = qApp->devicePixelRatio(); m_lastRenderedPixmap = QPixmap( m_width * dpr, m_height * dpr ); m_lastRenderedPixmap.setDevicePixelRatio(dpr); m_previousPagePixmap = QPixmap(); } else { m_previousPagePixmap = m_lastRenderedPixmap; } // opens the painter over the pixmap QPainter pixmapPainter; pixmapPainter.begin( &m_lastRenderedPixmap ); // generate welcome page if ( m_frameIndex == -1 ) generateIntroPage( pixmapPainter ); // generate a normal pixmap with extended margin filling if ( m_frameIndex >= 0 && m_frameIndex < (int)m_document->pages() ) generateContentsPage( m_frameIndex, pixmapPainter ); pixmapPainter.end(); // generate the top-right corner overlay #ifdef ENABLE_PROGRESS_OVERLAY if ( Okular::Settings::slidesShowProgress() && m_frameIndex != -1 ) generateOverlay(); #endif // start transition on pages that have one if ( !disableTransition && Okular::Settings::slidesTransitionsEnabled() ) { const Okular::PageTransition * transition = m_frameIndex != -1 ? m_frames[ m_frameIndex ]->page->transition() : nullptr; if ( transition ) initTransition( transition ); else { Okular::PageTransition trans = defaultTransition(); initTransition( &trans ); } } else { Okular::PageTransition trans = defaultTransition( Okular::Settings::EnumSlidesTransition::Replace ); initTransition( &trans ); } // update cursor + tooltip if ( !m_drawingEngine && Okular::Settings::slidesCursor() != Okular::Settings::EnumSlidesCursor::Hidden ) { QPoint p = mapFromGlobal( QCursor::pos() ); testCursorOnLink( p.x(), p.y() ); } } void PresentationWidget::generateIntroPage( QPainter & p ) { qreal dpr = qApp->devicePixelRatio(); // use a vertical gray gradient background int blend1 = m_height / 10, blend2 = 9 * m_height / 10; int baseTint = QColor(Qt::gray).red(); for ( int i = 0; i < m_height; i++ ) { int k = baseTint; if ( i < blend1 ) k -= (int)( baseTint * (i-blend1)*(i-blend1) / (float)(blend1 * blend1) ); if ( i > blend2 ) k += (int)( (255-baseTint) * (i-blend2)*(i-blend2) / (float)(blend1 * blend1) ); p.fillRect( 0, i, m_width, 1, QColor( k, k, k ) ); } // draw okular logo in the four corners QPixmap logo = QIcon::fromTheme( QStringLiteral("okular") ).pixmap( 64 * dpr ); logo.setDevicePixelRatio( dpr ); if ( !logo.isNull() ) { p.drawPixmap( 5, 5, logo ); p.drawPixmap( m_width - 5 - logo.width(), 5, logo ); p.drawPixmap( m_width - 5 - logo.width(), m_height - 5 - logo.height(), logo ); p.drawPixmap( 5, m_height - 5 - logo.height(), logo ); } // draw metadata text (the last line is 'click to begin') int strNum = m_metaStrings.count(), strHeight = m_height / ( strNum + 4 ), fontHeight = 2 * strHeight / 3; QFont font( p.font() ); font.setPixelSize( fontHeight ); QFontMetrics metrics( font ); for ( int i = 0; i < strNum; i++ ) { // set a font to fit text width float wScale = (float)metrics.boundingRect( m_metaStrings[i] ).width() / (float)m_width; QFont f( font ); if ( wScale > 1.0 ) f.setPixelSize( (int)( (float)fontHeight / (float)wScale ) ); p.setFont( f ); // text shadow p.setPen( Qt::darkGray ); p.drawText( 2, m_height / 4 + strHeight * i + 2, m_width, strHeight, Qt::AlignHCenter | Qt::AlignVCenter, m_metaStrings[i] ); // text body p.setPen( 128 + (127 * i) / strNum ); p.drawText( 0, m_height / 4 + strHeight * i, m_width, strHeight, Qt::AlignHCenter | Qt::AlignVCenter, m_metaStrings[i] ); } } void PresentationWidget::generateContentsPage( int pageNum, QPainter & p ) { PresentationFrame * frame = m_frames[ pageNum ]; // translate painter and contents rect QRect geom( frame->geometry ); p.translate( geom.left(), geom.top() ); geom.translate( -geom.left(), -geom.top() ); // draw the page using the shared PagePainter class int flags = PagePainter::Accessibility | PagePainter::Highlights | PagePainter::Annotations; PagePainter::paintPageOnPainter( &p, frame->page, this, flags, geom.width(), geom.height(), geom ); // restore painter p.translate( -frame->geometry.left(), -frame->geometry.top() ); // fill unpainted areas with background color const QRegion unpainted( QRect( 0, 0, m_width, m_height ) ); const QRegion rgn = unpainted.subtracted( frame->geometry ); for ( const QRect & r : rgn ) { p.fillRect( r, Okular::Settings::slidesBackgroundColor() ); } } // from Arthur - Qt4 - (is defined elsewhere as 'qt_div_255' to not break final compilation) inline int qt_div255(int x) { return (x + (x>>8) + 0x80) >> 8; } void PresentationWidget::generateOverlay() { #ifdef ENABLE_PROGRESS_OVERLAY qreal dpr = qApp->devicePixelRatio(); // calculate overlay geometry and resize pixmap if needed double side = m_width / 16.0; m_overlayGeometry.setRect( m_width - side - 4, 4, side, side ); // note: to get a sort of antialiasing, we render the pixmap double sized // and the resulting image is smoothly scaled down. So here we open a // painter on the double sized pixmap. side *= 2; QPixmap doublePixmap( side * dpr, side * dpr ); doublePixmap.setDevicePixelRatio( dpr ); doublePixmap.fill( Qt::black ); QPainter pixmapPainter( &doublePixmap ); pixmapPainter.setRenderHints( QPainter::Antialiasing ); // draw PIE SLICES in blue levels (the levels will then be the alpha component) int pages = m_document->pages(); if ( pages > 28 ) { // draw continuous slices int degrees = (int)( 360 * (float)(m_frameIndex + 1) / (float)pages ); pixmapPainter.setPen( 0x05 ); pixmapPainter.setBrush( QColor( 0x40 ) ); pixmapPainter.drawPie( 2, 2, side - 4, side - 4, 90*16, (360-degrees)*16 ); pixmapPainter.setPen( 0x40 ); pixmapPainter.setBrush( QColor( 0xF0 ) ); pixmapPainter.drawPie( 2, 2, side - 4, side - 4, 90*16, -degrees*16 ); } else { // draw discrete slices float oldCoord = -90; for ( int i = 0; i < pages; i++ ) { float newCoord = -90 + 360 * (float)(i + 1) / (float)pages; pixmapPainter.setPen( i <= m_frameIndex ? 0x40 : 0x05 ); pixmapPainter.setBrush( QColor( i <= m_frameIndex ? 0xF0 : 0x40 ) ); pixmapPainter.drawPie( 2, 2, side - 4, side - 4, (int)( -16*(oldCoord + 1) ), (int)( -16*(newCoord - (oldCoord + 2)) ) ); oldCoord = newCoord; } } int circleOut = side / 4; pixmapPainter.setPen( Qt::black ); pixmapPainter.setBrush( Qt::black ); pixmapPainter.drawEllipse( circleOut, circleOut, side - 2*circleOut, side - 2*circleOut ); // draw TEXT using maximum opacity QFont f( pixmapPainter.font() ); f.setPixelSize( side / 4 ); pixmapPainter.setFont( f ); pixmapPainter.setPen( 0xFF ); // use a little offset to prettify output pixmapPainter.drawText( 2, 2, side, side, Qt::AlignCenter, QString::number( m_frameIndex + 1 ) ); // end drawing pixmap and halve image pixmapPainter.end(); QImage image( doublePixmap.toImage().scaled( (side / 2) * dpr, (side / 2) * dpr, Qt::IgnoreAspectRatio, Qt::SmoothTransformation ) ); image.setDevicePixelRatio( dpr ); image = image.convertToFormat( QImage::Format_ARGB32 ); image.setDevicePixelRatio( dpr ); // draw circular shadow using the same technique doublePixmap.fill( Qt::black ); pixmapPainter.begin( &doublePixmap ); pixmapPainter.setPen( 0x40 ); pixmapPainter.setBrush( QColor( 0x80 ) ); pixmapPainter.drawEllipse( 0, 0, side, side ); pixmapPainter.end(); QImage shadow( doublePixmap.toImage().scaled( (side / 2) * dpr, (side / 2) * dpr, Qt::IgnoreAspectRatio, Qt::SmoothTransformation ) ); shadow.setDevicePixelRatio( dpr ); // generate a 2 colors pixmap using mixing shadow (made with highlight color) // and image (made with highlightedText color) QPalette pal = palette(); QColor color = pal.color( QPalette::Active, QPalette::HighlightedText ); int red = color.red(), green = color.green(), blue = color.blue(); color = pal.color( QPalette::Active, QPalette::Highlight ); int sRed = color.red(), sGreen = color.green(), sBlue = color.blue(); // pointers unsigned int * data = reinterpret_cast(image.bits()), * shadowData = reinterpret_cast(shadow.bits()), pixels = image.width() * image.height(); // cache data (reduce computation time to 26%!) int c1 = -1, c2 = -1, cR = 0, cG = 0, cB = 0, cA = 0; // foreach pixel for( unsigned int i = 0; i < pixels; ++i ) { // alpha for shadow and image int shadowAlpha = shadowData[i] & 0xFF, srcAlpha = data[i] & 0xFF; // cache values if ( srcAlpha != c1 || shadowAlpha != c2 ) { c1 = srcAlpha; c2 = shadowAlpha; // fuse color components and alpha value of image over shadow data[i] = qRgba( cR = qt_div255( srcAlpha * red + (255 - srcAlpha) * sRed ), cG = qt_div255( srcAlpha * green + (255 - srcAlpha) * sGreen ), cB = qt_div255( srcAlpha * blue + (255 - srcAlpha) * sBlue ), cA = qt_div255( srcAlpha * srcAlpha + (255 - srcAlpha) * shadowAlpha ) ); } else data[i] = qRgba( cR, cG, cB, cA ); } m_lastRenderedOverlay = QPixmap::fromImage( image ); m_lastRenderedOverlay.setDevicePixelRatio( dpr ); // start the autohide timer //repaint( m_overlayGeometry ); // toggle with next line update( m_overlayGeometry ); m_overlayHideTimer->start( 2500 ); #endif } QRect PresentationWidget::routeMouseDrawingEvent( QMouseEvent * e ) { if ( m_frameIndex == -1 ) // Can't draw on the summary page return QRect(); const QRect & geom = m_frames[ m_frameIndex ]->geometry; const Okular::Page * page = m_frames[ m_frameIndex ]->page; AnnotatorEngine::EventType eventType; AnnotatorEngine::Button button; // figure out the event type and button AnnotatorEngine::decodeEvent( e, &eventType, &button ); static bool hasclicked = false; if ( eventType == AnnotatorEngine::Press ) hasclicked = true; QPointF mousePos = e->localPos(); double nX = ( mousePos.x() - (double)geom.left() ) / (double)geom.width(); double nY = ( mousePos.y() - (double)geom.top() ) / (double)geom.height(); QRect ret; bool isInside = nX >= 0 && nX < 1 && nY >= 0 && nY < 1; if ( hasclicked && !isInside ) { // Fake a move to the last border pos nX = qBound(0., nX, 1.); nY = qBound(0., nY, 1.); m_drawingEngine->event( AnnotatorEngine::Move, button, nX, nY, geom.width(), geom.height(), page ); // Fake a release in the following lines eventType = AnnotatorEngine::Release; isInside = true; } else if ( !hasclicked && isInside ) { // we're coming from the outside, pretend we started clicking at the closest border if ( nX < ( 1 - nX ) && nX < nY && nX < ( 1 - nY ) ) nX = 0; else if ( nY < ( 1 - nY ) && nY < nX && nY < ( 1 - nX ) ) nY = 0; else if ( ( 1 - nX ) < nX && ( 1 - nX ) < nY && ( 1 - nX ) < ( 1 - nY ) ) nX = 1; else nY = 1; hasclicked = true; eventType = AnnotatorEngine::Press; } if ( hasclicked && isInside ) { ret = m_drawingEngine->event( eventType, button, nX, nY, geom.width(), geom.height(), page ); } if ( eventType == AnnotatorEngine::Release ) { hasclicked = false; } if ( m_drawingEngine->creationCompleted() ) { // add drawing to current page m_frames[ m_frameIndex ]->drawings << m_drawingEngine->endSmoothPath(); // remove the actual drawer and create a new one just after // that - that gives continuous drawing delete m_drawingEngine; m_drawingRect = QRect(); m_drawingEngine = new SmoothPathEngine( m_currentDrawingToolElement ); // schedule repaint update(); } return ret; } void PresentationWidget::startAutoChangeTimer() { double pageDuration = m_frameIndex >= 0 && m_frameIndex < (int)m_frames.count() ? m_frames[ m_frameIndex ]->page->duration() : -1; if ( m_advanceSlides || pageDuration >= 0.0 ) { double secs; if ( pageDuration < 0.0 ) secs = Okular::SettingsCore::slidesAdvanceTime(); else if ( m_advanceSlides ) secs = qMin( pageDuration, Okular::SettingsCore::slidesAdvanceTime() ); else secs = pageDuration; m_nextPageTimer->start( (int)( secs * 1000 ) ); } } void PresentationWidget::recalcGeometry() { QDesktopWidget *desktop = QApplication::desktop(); const int preferenceScreen = Okular::Settings::slidesScreen(); int screen = 0; if ( preferenceScreen == -2 ) { screen = desktop->screenNumber( m_parentWidget ); } else if ( preferenceScreen == -1 ) { screen = desktop->primaryScreen(); } else if ( preferenceScreen >= 0 && preferenceScreen < desktop->numScreens() ) { screen = preferenceScreen; } else { screen = desktop->screenNumber( m_parentWidget ); Okular::Settings::setSlidesScreen( -2 ); } const QRect screenGeom = desktop->screenGeometry( screen ); // qCDebug(OkularUiDebug) << screen << "=>" << screenGeom; m_screen = screen; setGeometry( screenGeom ); } void PresentationWidget::repositionContent() { const QRect ourGeom = geometry(); // tool bar height in pixels, make it large enough to hold the text fields with the page numbers const int toolBarHeight = m_pagesEdit->height() * 1.5; m_topBar->setGeometry( 0, 0, ourGeom.width(), toolBarHeight ); m_topBar->setIconSize( QSize( toolBarHeight * 0.75, toolBarHeight * 0.75 ) ); } void PresentationWidget::requestPixmaps() { PresentationFrame * frame = m_frames[ m_frameIndex ]; int pixW = frame->geometry.width(); int pixH = frame->geometry.height(); // operation will take long: set busy cursor QApplication::setOverrideCursor( QCursor( Qt::BusyCursor ) ); // request the pixmap QLinkedList< Okular::PixmapRequest * > requests; requests.push_back( new Okular::PixmapRequest( this, m_frameIndex, pixW, pixH, PRESENTATION_PRIO, Okular::PixmapRequest::NoFeature ) ); // restore cursor QApplication::restoreOverrideCursor(); // ask for next and previous page if not in low memory usage setting if ( Okular::SettingsCore::memoryLevel() != Okular::SettingsCore::EnumMemoryLevel::Low ) { int pagesToPreload = 1; // If greedy, preload everything if (Okular::SettingsCore::memoryLevel() == Okular::SettingsCore::EnumMemoryLevel::Greedy) pagesToPreload = (int)m_document->pages(); Okular::PixmapRequest::PixmapRequestFeatures requestFeatures = Okular::PixmapRequest::Preload; requestFeatures |= Okular::PixmapRequest::Asynchronous; for( int j = 1; j <= pagesToPreload; j++ ) { int tailRequest = m_frameIndex + j; if ( tailRequest < (int)m_document->pages() ) { PresentationFrame *nextFrame = m_frames[ tailRequest ]; pixW = nextFrame->geometry.width(); pixH = nextFrame->geometry.height(); if ( !nextFrame->page->hasPixmap( this, pixW, pixH ) ) requests.push_back( new Okular::PixmapRequest( this, tailRequest, pixW, pixH, PRESENTATION_PRELOAD_PRIO, requestFeatures ) ); } int headRequest = m_frameIndex - j; if ( headRequest >= 0 ) { PresentationFrame *prevFrame = m_frames[ headRequest ]; pixW = prevFrame->geometry.width(); pixH = prevFrame->geometry.height(); if ( !prevFrame->page->hasPixmap( this, pixW, pixH ) ) requests.push_back( new Okular::PixmapRequest( this, headRequest, pixW, pixH, PRESENTATION_PRELOAD_PRIO, requestFeatures ) ); } // stop if we've already reached both ends of the document if ( headRequest < 0 && tailRequest >= (int)m_document->pages() ) break; } } m_document->requestPixmaps( requests ); } void PresentationWidget::slotNextPage() { int nextIndex = m_frameIndex + 1; // loop when configured if ( nextIndex == m_frames.count() && Okular::Settings::slidesLoop() ) nextIndex = 0; if ( nextIndex < m_frames.count() ) { // go to next page changePage( nextIndex ); // auto advance to the next page if set startAutoChangeTimer(); } else { #ifdef ENABLE_PROGRESS_OVERLAY if ( Okular::Settings::slidesShowProgress() ) generateOverlay(); #endif if ( m_transitionTimer->isActive() ) { m_transitionTimer->stop(); m_lastRenderedPixmap = m_currentPagePixmap; update(); } } // we need the setFocus() call here to let KCursor::autoHide() work correctly setFocus(); } void PresentationWidget::slotPrevPage() { if ( m_frameIndex > 0 ) { // go to previous page changePage( m_frameIndex - 1 ); // auto advance to the next page if set startAutoChangeTimer(); } else { #ifdef ENABLE_PROGRESS_OVERLAY if ( Okular::Settings::slidesShowProgress() ) generateOverlay(); #endif if ( m_transitionTimer->isActive() ) { m_transitionTimer->stop(); m_lastRenderedPixmap = m_currentPagePixmap; update(); } } } void PresentationWidget::slotFirstPage() { changePage( 0 ); } void PresentationWidget::slotLastPage() { changePage( (int)m_frames.count() - 1 ); } void PresentationWidget::slotHideOverlay() { QRect geom( m_overlayGeometry ); m_overlayGeometry.setCoords( 0, 0, -1, -1 ); update( geom ); } void PresentationWidget::slotTransitionStep() { switch( m_currentTransition.type() ) { case Okular::PageTransition::Fade: { QPainter pixmapPainter; m_currentPixmapOpacity += 1.0 / m_transitionSteps; m_lastRenderedPixmap = QPixmap( m_lastRenderedPixmap.size() ); m_lastRenderedPixmap.setDevicePixelRatio( qApp->devicePixelRatio() ); m_lastRenderedPixmap.fill( Qt::transparent ); pixmapPainter.begin( &m_lastRenderedPixmap ); pixmapPainter.setCompositionMode( QPainter::CompositionMode_Source ); pixmapPainter.setOpacity( 1 - m_currentPixmapOpacity ); pixmapPainter.drawPixmap( 0, 0, m_previousPagePixmap ); pixmapPainter.setOpacity( m_currentPixmapOpacity ); pixmapPainter.drawPixmap( 0, 0, m_currentPagePixmap ); update(); if( m_currentPixmapOpacity >= 1 ) return; } break; default: { if ( m_transitionRects.empty() ) { // it's better to fix the transition to cover the whole screen than // enabling the following line that wastes cpu for nothing //update(); return; } for ( int i = 0; i < m_transitionMul && !m_transitionRects.empty(); i++ ) { update( m_transitionRects.first() ); m_transitionRects.pop_front(); } } break; } m_transitionTimer->start( m_transitionDelay ); } void PresentationWidget::slotDelayedEvents() { recalcGeometry(); repositionContent(); if ( m_screenSelect ) { m_screenSelect->setCurrentItem( m_screen ); connect( m_screenSelect->selectableActionGroup(), &QActionGroup::triggered, this, &PresentationWidget::chooseScreen ); } // show widget and take control show(); setWindowState( windowState() | Qt::WindowFullScreen ); connect( QApplication::desktop(), &QDesktopWidget::resized, this, &PresentationWidget::screenResized ); // inform user on how to exit from presentation mode KMessageBox::information( this, i18n("There are two ways of exiting presentation mode, you can press either ESC key or click with the quit button that appears when placing the mouse in the top-right corner. Of course you can cycle windows (Alt+TAB by default)"), QString(), QStringLiteral("presentationInfo") ); } void PresentationWidget::slotPageChanged() { bool ok = true; int p = m_pagesEdit->text().toInt( &ok ); if ( !ok ) return; changePage( p - 1 ); } void PresentationWidget::slotChangeDrawingToolEngine( const QDomElement &element ) { if ( element.isNull() ) { delete m_drawingEngine; m_drawingEngine = nullptr; m_drawingRect = QRect(); setCursor( Qt::ArrowCursor ); } else { m_drawingEngine = new SmoothPathEngine( element ); setCursor( QCursor( QPixmap( QStringLiteral("pencil") ), Qt::ArrowCursor ) ); m_currentDrawingToolElement = element; } } void PresentationWidget::slotAddDrawingToolActions() { DrawingToolActions *drawingToolActions = qobject_cast(sender()); const QList actionsList = drawingToolActions->actions(); for (QAction *action : actionsList) { action->setEnabled( true ); m_topBar->addAction( action ); addAction( action ); } } void PresentationWidget::clearDrawings() { if ( m_frameIndex != -1 ) m_frames[ m_frameIndex ]->drawings.clear(); update(); } void PresentationWidget::screenResized( int screen ) { // we can ignore if a screen was resized in the case the screen is not // where we are on if ( screen != m_screen ) return; setScreen( screen ); } void PresentationWidget::chooseScreen( QAction *act ) { if ( !act || act->data().type() != QVariant::Int ) return; const int newScreen = act->data().toInt(); setScreen( newScreen ); } void PresentationWidget::toggleBlackScreenMode( bool ) { m_inBlackScreenMode = !m_inBlackScreenMode; update(); } void PresentationWidget::setScreen( int newScreen ) { const QRect screenGeom = QApplication::desktop()->screenGeometry( newScreen ); const QSize oldSize = size(); // qCDebug(OkularUiDebug) << newScreen << "=>" << screenGeom; m_screen = newScreen; setGeometry( screenGeom ); applyNewScreenSize( oldSize ); } -void PresentationWidget::applyNewScreenSize( const QSize & oldSize ) +void PresentationWidget::applyNewScreenSize( const QSize oldSize ) { repositionContent(); // if by chance the new screen has the same resolution of the previous, // do not invalidate pixmaps and such.. if ( size() == oldSize ) return; m_width = width(); m_height = height(); // update the frames const float screenRatio = (float)m_height / (float)m_width; for ( PresentationFrame * frame : qAsConst( m_frames ) ) { frame->recalcGeometry( m_width, m_height, screenRatio ); } if ( m_frameIndex != -1 ) { // ugliness alarm! const_cast< Okular::Page * >( m_frames[ m_frameIndex ]->page )->deletePixmap( this ); // force the regeneration of the pixmap m_lastRenderedPixmap = QPixmap(); m_blockNotifications = true; requestPixmaps(); m_blockNotifications = false; } if ( m_transitionTimer->isActive() ) { m_transitionTimer->stop(); } generatePage( true /* no transitions */ ); } void PresentationWidget::inhibitPowerManagement() { #ifdef Q_OS_LINUX QString reason = i18nc( "Reason for inhibiting the screensaver activation, when the presentation mode is active", "Giving a presentation" ); if (!m_screenInhibitCookie) { QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.ScreenSaver"), QStringLiteral("/ScreenSaver"), QStringLiteral("org.freedesktop.ScreenSaver"), QStringLiteral("Inhibit")); message << QCoreApplication::applicationName(); message << reason; QDBusPendingReply reply = QDBusConnection::sessionBus().asyncCall(message); reply.waitForFinished(); if (reply.isValid()) { m_screenInhibitCookie = reply.value(); qCDebug(OkularUiDebug) << "Screen inhibition cookie" << m_screenInhibitCookie; } else { qCWarning(OkularUiDebug) << "Unable to inhibit screensaver" << reply.error(); } } if (m_sleepInhibitFd != -1) { QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.login1"), QStringLiteral("/org/freedesktop/login1"), QStringLiteral("org.freedesktop.login1.Manager"), QStringLiteral("Inhibit") ); message << QStringLiteral("sleep"); message << QCoreApplication::applicationName(); message << reason; message << QStringLiteral("block"); QDBusPendingReply reply = QDBusConnection::systemBus().asyncCall(message); reply.waitForFinished(); if (reply.isValid()) { m_sleepInhibitFd = dup(reply.value().fileDescriptor()); } else { qCWarning(OkularUiDebug) << "Unable to inhibit sleep" << reply.error(); } } #endif } void PresentationWidget::allowPowerManagement() { #ifdef Q_OS_LINUX if (m_sleepInhibitFd != -1) { ::close(m_sleepInhibitFd); m_sleepInhibitFd = -1; } if (m_screenInhibitCookie) { QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.ScreenSaver"), QStringLiteral("/ScreenSaver"), QStringLiteral("org.freedesktop.ScreenSaver"), QStringLiteral("UnInhibit")); message << m_screenInhibitCookie; QDBusPendingReply reply = QDBusConnection::sessionBus().asyncCall(message); reply.waitForFinished(); m_screenInhibitCookie = 0; } #endif } void PresentationWidget::showTopBar( bool show ) { if ( show ) { m_topBar->show(); // Don't autohide the mouse cursor if it's over the toolbar if ( Okular::Settings::slidesCursor() == Okular::Settings::EnumSlidesCursor::HiddenDelay ) { KCursor::setAutoHideCursor( this, false ); } // Always show a cursor when topBar is visible if ( !m_drawingEngine ) { setCursor( QCursor( Qt::ArrowCursor ) ); } } else { m_topBar->hide(); // Reenable autohide if need be when leaving the toolbar if ( Okular::Settings::slidesCursor() == Okular::Settings::EnumSlidesCursor::HiddenDelay ) { KCursor::setAutoHideCursor( this, true ); } // Or hide the cursor again if hidden cursor is enabled else if ( Okular::Settings::slidesCursor() == Okular::Settings::EnumSlidesCursor::Hidden ) { // Don't hide the cursor if drawing mode is on if ( !m_drawingEngine ) { setCursor( QCursor( Qt::BlankCursor ) ); } } } // Make sure mouse tracking isn't off after the KCursor::setAutoHideCursor() calls setMouseTracking( true ); } void PresentationWidget::slotFind() { if ( !m_searchBar ) { m_searchBar = new PresentationSearchBar( m_document, this, this ); m_searchBar->forceSnap(); } m_searchBar->focusOnSearchEdit(); m_searchBar->show(); } const Okular::PageTransition PresentationWidget::defaultTransition() const { return defaultTransition( Okular::Settings::slidesTransition() ); } const Okular::PageTransition PresentationWidget::defaultTransition( int type ) const { switch ( type ) { case Okular::Settings::EnumSlidesTransition::BlindsHorizontal: { Okular::PageTransition transition( Okular::PageTransition::Blinds ); transition.setAlignment( Okular::PageTransition::Horizontal ); return transition; break; } case Okular::Settings::EnumSlidesTransition::BlindsVertical: { Okular::PageTransition transition( Okular::PageTransition::Blinds ); transition.setAlignment( Okular::PageTransition::Vertical ); return transition; break; } case Okular::Settings::EnumSlidesTransition::BoxIn: { Okular::PageTransition transition( Okular::PageTransition::Box ); transition.setDirection( Okular::PageTransition::Inward ); return transition; break; } case Okular::Settings::EnumSlidesTransition::BoxOut: { Okular::PageTransition transition( Okular::PageTransition::Box ); transition.setDirection( Okular::PageTransition::Outward ); return transition; break; } case Okular::Settings::EnumSlidesTransition::Dissolve: { return Okular::PageTransition( Okular::PageTransition::Dissolve ); break; } case Okular::Settings::EnumSlidesTransition::GlitterDown: { Okular::PageTransition transition( Okular::PageTransition::Glitter ); transition.setAngle( 270 ); return transition; break; } case Okular::Settings::EnumSlidesTransition::GlitterRight: { Okular::PageTransition transition( Okular::PageTransition::Glitter ); transition.setAngle( 0 ); return transition; break; } case Okular::Settings::EnumSlidesTransition::GlitterRightDown: { Okular::PageTransition transition( Okular::PageTransition::Glitter ); transition.setAngle( 315 ); return transition; break; } case Okular::Settings::EnumSlidesTransition::Random: { return defaultTransition( KRandom::random() % 18 ); break; } case Okular::Settings::EnumSlidesTransition::SplitHorizontalIn: { Okular::PageTransition transition( Okular::PageTransition::Split ); transition.setAlignment( Okular::PageTransition::Horizontal ); transition.setDirection( Okular::PageTransition::Inward ); return transition; break; } case Okular::Settings::EnumSlidesTransition::SplitHorizontalOut: { Okular::PageTransition transition( Okular::PageTransition::Split ); transition.setAlignment( Okular::PageTransition::Horizontal ); transition.setDirection( Okular::PageTransition::Outward ); return transition; break; } case Okular::Settings::EnumSlidesTransition::SplitVerticalIn: { Okular::PageTransition transition( Okular::PageTransition::Split ); transition.setAlignment( Okular::PageTransition::Vertical ); transition.setDirection( Okular::PageTransition::Inward ); return transition; break; } case Okular::Settings::EnumSlidesTransition::SplitVerticalOut: { Okular::PageTransition transition( Okular::PageTransition::Split ); transition.setAlignment( Okular::PageTransition::Vertical ); transition.setDirection( Okular::PageTransition::Outward ); return transition; break; } case Okular::Settings::EnumSlidesTransition::WipeDown: { Okular::PageTransition transition( Okular::PageTransition::Wipe ); transition.setAngle( 270 ); return transition; break; } case Okular::Settings::EnumSlidesTransition::WipeRight: { Okular::PageTransition transition( Okular::PageTransition::Wipe ); transition.setAngle( 0 ); return transition; break; } case Okular::Settings::EnumSlidesTransition::WipeLeft: { Okular::PageTransition transition( Okular::PageTransition::Wipe ); transition.setAngle( 180 ); return transition; break; } case Okular::Settings::EnumSlidesTransition::WipeUp: { Okular::PageTransition transition( Okular::PageTransition::Wipe ); transition.setAngle( 90 ); return transition; break; } case Okular::Settings::EnumSlidesTransition::Fade: { return Okular::PageTransition( Okular::PageTransition::Fade ); break; } case Okular::Settings::EnumSlidesTransition::Replace: default: return Okular::PageTransition( Okular::PageTransition::Replace ); break; } // should not happen, just make gcc happy return Okular::PageTransition(); } /** ONLY the TRANSITIONS GENERATION function from here on **/ void PresentationWidget::initTransition( const Okular::PageTransition *transition ) { // if it's just a 'replace' transition, repaint the screen if ( transition->type() == Okular::PageTransition::Replace ) { update(); return; } const bool isInward = transition->direction() == Okular::PageTransition::Inward; const bool isHorizontal = transition->alignment() == Okular::PageTransition::Horizontal; const float totalTime = transition->duration(); m_transitionRects.clear(); m_currentTransition = *transition; m_currentPagePixmap = m_lastRenderedPixmap; switch( transition->type() ) { // split: horizontal / vertical and inward / outward case Okular::PageTransition::Split: { const int steps = isHorizontal ? 100 : 75; if ( isHorizontal ) { if ( isInward ) { int xPosition = 0; for ( int i = 0; i < steps; i++ ) { int xNext = ((i + 1) * m_width) / (2 * steps); m_transitionRects.push_back( QRect( xPosition, 0, xNext - xPosition, m_height ) ); m_transitionRects.push_back( QRect( m_width - xNext, 0, xNext - xPosition, m_height ) ); xPosition = xNext; } } else { int xPosition = m_width / 2; for ( int i = 0; i < steps; i++ ) { int xNext = ((steps - (i + 1)) * m_width) / (2 * steps); m_transitionRects.push_back( QRect( xNext, 0, xPosition - xNext, m_height ) ); m_transitionRects.push_back( QRect( m_width - xPosition, 0, xPosition - xNext, m_height ) ); xPosition = xNext; } } } else { if ( isInward ) { int yPosition = 0; for ( int i = 0; i < steps; i++ ) { int yNext = ((i + 1) * m_height) / (2 * steps); m_transitionRects.push_back( QRect( 0, yPosition, m_width, yNext - yPosition ) ); m_transitionRects.push_back( QRect( 0, m_height - yNext, m_width, yNext - yPosition ) ); yPosition = yNext; } } else { int yPosition = m_height / 2; for ( int i = 0; i < steps; i++ ) { int yNext = ((steps - (i + 1)) * m_height) / (2 * steps); m_transitionRects.push_back( QRect( 0, yNext, m_width, yPosition - yNext ) ); m_transitionRects.push_back( QRect( 0, m_height - yPosition, m_width, yPosition - yNext ) ); yPosition = yNext; } } } m_transitionMul = 2; m_transitionDelay = (int)( (totalTime * 1000) / steps ); } break; // blinds: horizontal(l-to-r) / vertical(t-to-b) case Okular::PageTransition::Blinds: { const int blinds = isHorizontal ? 8 : 6; const int steps = m_width / (4 * blinds); if ( isHorizontal ) { int xPosition[ 8 ]; for ( int b = 0; b < blinds; b++ ) xPosition[ b ] = (b * m_width) / blinds; for ( int i = 0; i < steps; i++ ) { int stepOffset = (int)( ((float)i * (float)m_width) / ((float)blinds * (float)steps) ); for ( int b = 0; b < blinds; b++ ) { m_transitionRects.push_back( QRect( xPosition[ b ], 0, stepOffset, m_height ) ); xPosition[ b ] = stepOffset + (b * m_width) / blinds; } } } else { int yPosition[ 6 ]; for ( int b = 0; b < blinds; b++ ) yPosition[ b ] = (b * m_height) / blinds; for ( int i = 0; i < steps; i++ ) { int stepOffset = (int)( ((float)i * (float)m_height) / ((float)blinds * (float)steps) ); for ( int b = 0; b < blinds; b++ ) { m_transitionRects.push_back( QRect( 0, yPosition[ b ], m_width, stepOffset ) ); yPosition[ b ] = stepOffset + (b * m_height) / blinds; } } } m_transitionMul = blinds; m_transitionDelay = (int)( (totalTime * 1000) / steps ); } break; // box: inward / outward case Okular::PageTransition::Box: { const int steps = m_width / 10; if ( isInward ) { int L = 0, T = 0, R = m_width, B = m_height; for ( int i = 0; i < steps; i++ ) { // compute shrunk box coords int newL = ((i + 1) * m_width) / (2 * steps); int newT = ((i + 1) * m_height) / (2 * steps); int newR = m_width - newL; int newB = m_height - newT; // add left, right, topcenter, bottomcenter rects m_transitionRects.push_back( QRect( L, T, newL - L, B - T ) ); m_transitionRects.push_back( QRect( newR, T, R - newR, B - T ) ); m_transitionRects.push_back( QRect( newL, T, newR - newL, newT - T ) ); m_transitionRects.push_back( QRect( newL, newB, newR - newL, B - newB ) ); L = newL; T = newT; R = newR, B = newB; } } else { int L = m_width / 2, T = m_height / 2, R = L, B = T; for ( int i = 0; i < steps; i++ ) { // compute shrunk box coords int newL = ((steps - (i + 1)) * m_width) / (2 * steps); int newT = ((steps - (i + 1)) * m_height) / (2 * steps); int newR = m_width - newL; int newB = m_height - newT; // add left, right, topcenter, bottomcenter rects m_transitionRects.push_back( QRect( newL, newT, L - newL, newB - newT ) ); m_transitionRects.push_back( QRect( R, newT, newR - R, newB - newT ) ); m_transitionRects.push_back( QRect( L, newT, R - L, T - newT ) ); m_transitionRects.push_back( QRect( L, B, R - L, newB - B ) ); L = newL; T = newT; R = newR, B = newB; } } m_transitionMul = 4; m_transitionDelay = (int)( (totalTime * 1000) / steps ); } break; // wipe: implemented for 4 canonical angles case Okular::PageTransition::Wipe: { const int angle = transition->angle(); const int steps = (angle == 0) || (angle == 180) ? m_width / 8 : m_height / 8; if ( angle == 0 ) { int xPosition = 0; for ( int i = 0; i < steps; i++ ) { int xNext = ((i + 1) * m_width) / steps; m_transitionRects.push_back( QRect( xPosition, 0, xNext - xPosition, m_height ) ); xPosition = xNext; } } else if ( angle == 90 ) { int yPosition = m_height; for ( int i = 0; i < steps; i++ ) { int yNext = ((steps - (i + 1)) * m_height) / steps; m_transitionRects.push_back( QRect( 0, yNext, m_width, yPosition - yNext ) ); yPosition = yNext; } } else if ( angle == 180 ) { int xPosition = m_width; for ( int i = 0; i < steps; i++ ) { int xNext = ((steps - (i + 1)) * m_width) / steps; m_transitionRects.push_back( QRect( xNext, 0, xPosition - xNext, m_height ) ); xPosition = xNext; } } else if ( angle == 270 ) { int yPosition = 0; for ( int i = 0; i < steps; i++ ) { int yNext = ((i + 1) * m_height) / steps; m_transitionRects.push_back( QRect( 0, yPosition, m_width, yNext - yPosition ) ); yPosition = yNext; } } else { update(); return; } m_transitionMul = 1; m_transitionDelay = (int)( (totalTime * 1000) / steps ); } break; // dissolve: replace 'random' rects case Okular::PageTransition::Dissolve: { const int gridXsteps = 50; const int gridYsteps = 38; const int steps = gridXsteps * gridYsteps; int oldX = 0; int oldY = 0; // create a grid of gridXstep by gridYstep QRects for ( int y = 0; y < gridYsteps; y++ ) { int newY = (int)( m_height * ((float)(y+1) / (float)gridYsteps) ); for ( int x = 0; x < gridXsteps; x++ ) { int newX = (int)( m_width * ((float)(x+1) / (float)gridXsteps) ); m_transitionRects.push_back( QRect( oldX, oldY, newX - oldX, newY - oldY ) ); oldX = newX; } oldX = 0; oldY = newY; } // randomize the grid for ( int i = 0; i < steps; i++ ) { #ifndef Q_OS_WIN int n1 = (int)(steps * drand48()); int n2 = (int)(steps * drand48()); #else int n1 = (int)(steps * (std::rand() / RAND_MAX)); int n2 = (int)(steps * (std::rand() / RAND_MAX)); #endif // swap items if index differs if ( n1 != n2 ) { QRect r = m_transitionRects[ n2 ]; m_transitionRects[ n2 ] = m_transitionRects[ n1 ]; m_transitionRects[ n1 ] = r; } } // set global transition parameters m_transitionMul = 40; m_transitionDelay = (int)( (m_transitionMul * 1000 * totalTime) / steps ); } break; // glitter: similar to dissolve but has a direction case Okular::PageTransition::Glitter: { const int gridXsteps = 50; const int gridYsteps = 38; const int steps = gridXsteps * gridYsteps; const int angle = transition->angle(); // generate boxes using a given direction if ( angle == 90 ) { int yPosition = m_height; for ( int i = 0; i < gridYsteps; i++ ) { int yNext = ((gridYsteps - (i + 1)) * m_height) / gridYsteps; int xPosition = 0; for ( int j = 0; j < gridXsteps; j++ ) { int xNext = ((j + 1) * m_width) / gridXsteps; m_transitionRects.push_back( QRect( xPosition, yNext, xNext - xPosition, yPosition - yNext ) ); xPosition = xNext; } yPosition = yNext; } } else if ( angle == 180 ) { int xPosition = m_width; for ( int i = 0; i < gridXsteps; i++ ) { int xNext = ((gridXsteps - (i + 1)) * m_width) / gridXsteps; int yPosition = 0; for ( int j = 0; j < gridYsteps; j++ ) { int yNext = ((j + 1) * m_height) / gridYsteps; m_transitionRects.push_back( QRect( xNext, yPosition, xPosition - xNext, yNext - yPosition ) ); yPosition = yNext; } xPosition = xNext; } } else if ( angle == 270 ) { int yPosition = 0; for ( int i = 0; i < gridYsteps; i++ ) { int yNext = ((i + 1) * m_height) / gridYsteps; int xPosition = 0; for ( int j = 0; j < gridXsteps; j++ ) { int xNext = ((j + 1) * m_width) / gridXsteps; m_transitionRects.push_back( QRect( xPosition, yPosition, xNext - xPosition, yNext - yPosition ) ); xPosition = xNext; } yPosition = yNext; } } else // if angle is 0 or 315 { int xPosition = 0; for ( int i = 0; i < gridXsteps; i++ ) { int xNext = ((i + 1) * m_width) / gridXsteps; int yPosition = 0; for ( int j = 0; j < gridYsteps; j++ ) { int yNext = ((j + 1) * m_height) / gridYsteps; m_transitionRects.push_back( QRect( xPosition, yPosition, xNext - xPosition, yNext - yPosition ) ); yPosition = yNext; } xPosition = xNext; } } // add a 'glitter' (1 over 10 pieces is randomized) int randomSteps = steps / 20; for ( int i = 0; i < randomSteps; i++ ) { #ifndef Q_OS_WIN int n1 = (int)(steps * drand48()); int n2 = (int)(steps * drand48()); #else int n1 = (int)(steps * (std::rand() / RAND_MAX)); int n2 = (int)(steps * (std::rand() / RAND_MAX)); #endif // swap items if index differs if ( n1 != n2 ) { QRect r = m_transitionRects[ n2 ]; m_transitionRects[ n2 ] = m_transitionRects[ n1 ]; m_transitionRects[ n1 ] = r; } } // set global transition parameters m_transitionMul = (angle == 90) || (angle == 270) ? gridYsteps : gridXsteps; m_transitionMul /= 2; m_transitionDelay = (int)( (m_transitionMul * 1000 * totalTime) / steps ); } break; case Okular::PageTransition::Fade: { enum {FADE_TRANSITION_FPS = 20}; const int steps = totalTime * FADE_TRANSITION_FPS; m_transitionSteps = steps; QPainter pixmapPainter; m_currentPixmapOpacity = (double) 1 / steps; m_transitionDelay = (int)( totalTime * 1000 ) / steps; m_lastRenderedPixmap = QPixmap( m_lastRenderedPixmap.size() ); m_lastRenderedPixmap.fill( Qt::transparent ); pixmapPainter.begin( &m_lastRenderedPixmap ); pixmapPainter.setCompositionMode( QPainter::CompositionMode_Source ); pixmapPainter.setOpacity( 1 - m_currentPixmapOpacity ); pixmapPainter.drawPixmap( 0, 0, m_previousPagePixmap ); pixmapPainter.setOpacity( m_currentPixmapOpacity ); pixmapPainter.drawPixmap( 0, 0, m_currentPagePixmap ); pixmapPainter.end(); update(); } break; // implement missing transitions (a binary raster engine needed here) case Okular::PageTransition::Fly: case Okular::PageTransition::Push: case Okular::PageTransition::Cover: case Okular::PageTransition::Uncover: default: update(); return; } // send the first start to the timer m_transitionTimer->start( 0 ); } void PresentationWidget::slotProcessMovieAction( const Okular::MovieAction *action ) { const Okular::MovieAnnotation *movieAnnotation = action->annotation(); if ( !movieAnnotation ) return; Okular::Movie *movie = movieAnnotation->movie(); if ( !movie ) return; VideoWidget *vw = m_frames[ m_frameIndex ]->videoWidgets.value( movieAnnotation->movie() ); if ( !vw ) return; vw->show(); switch ( action->operation() ) { case Okular::MovieAction::Play: vw->stop(); vw->play(); break; case Okular::MovieAction::Stop: vw->stop(); break; case Okular::MovieAction::Pause: vw->pause(); break; case Okular::MovieAction::Resume: vw->play(); break; }; } void PresentationWidget::slotProcessRenditionAction( const Okular::RenditionAction *action ) { Okular::Movie *movie = action->movie(); if ( !movie ) return; VideoWidget *vw = m_frames[ m_frameIndex ]->videoWidgets.value( movie ); if ( !vw ) return; if ( action->operation() == Okular::RenditionAction::None ) return; vw->show(); switch ( action->operation() ) { case Okular::RenditionAction::Play: vw->stop(); vw->play(); break; case Okular::RenditionAction::Stop: vw->stop(); break; case Okular::RenditionAction::Pause: vw->pause(); break; case Okular::RenditionAction::Resume: vw->play(); break; default: return; }; } void PresentationWidget::slotTogglePlayPause() { m_advanceSlides = !m_advanceSlides; setPlayPauseIcon(); if ( m_advanceSlides ) { startAutoChangeTimer(); } else { m_nextPageTimer->stop(); } } #include "presentationwidget.moc" diff --git a/ui/presentationwidget.h b/ui/presentationwidget.h index 970332d64..5eb0a80b5 100644 --- a/ui/presentationwidget.h +++ b/ui/presentationwidget.h @@ -1,175 +1,175 @@ /*************************************************************************** * Copyright (C) 2004 by Enrico Ros * * * * 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. * ***************************************************************************/ #ifndef _OKULAR_PRESENTATIONWIDGET_H_ #define _OKULAR_PRESENTATIONWIDGET_H_ #include #include #include #include #include #include "core/area.h" #include "core/observer.h" #include "core/pagetransition.h" class QLineEdit; class QToolBar; class QTimer; class QGestureEvent; class KActionCollection; class KSelectAction; class SmoothPathEngine; struct PresentationFrame; class PresentationSearchBar; class DrawingToolActions; namespace Okular { class Action; class Annotation; class Document; class MovieAction; class Page; class RenditionAction; } /** * @short A widget that shows pages as fullscreen slides (with transitions fx). * * This is a fullscreen widget that displays */ class PresentationWidget : public QWidget, public Okular::DocumentObserver { Q_OBJECT public: PresentationWidget( QWidget * parent, Okular::Document * doc, DrawingToolActions * drawingToolActions, KActionCollection * collection ); ~PresentationWidget() override; // inherited from DocumentObserver void notifySetup( const QVector< Okular::Page * > & pages, int setupFlags ) override; void notifyViewportChanged( bool smoothMove ) override; void notifyPageChanged( int pageNumber, int changedFlags ) override; bool canUnloadPixmap( int pageNumber ) const override; void notifyCurrentPageChanged( int previous, int current ) override; public Q_SLOTS: void slotFind(); protected: // widget events bool event( QEvent * e ) override; void keyPressEvent( QKeyEvent * e ) override; void wheelEvent( QWheelEvent * e ) override; void mousePressEvent( QMouseEvent * e ) override; void mouseReleaseEvent( QMouseEvent * e ) override; void mouseMoveEvent( QMouseEvent * e ) override; void paintEvent( QPaintEvent * e ) override; void resizeEvent( QResizeEvent * e ) override; void leaveEvent( QEvent * e ) override; bool gestureEvent (QGestureEvent * e ); // Catch TabletEnterProximity and TabletLeaveProximity events from the QApplication bool eventFilter (QObject * o, QEvent * ev ) override; private: const void * getObjectRect( Okular::ObjectRect::ObjectType type, int x, int y, QRect * geometry = nullptr ) const; const Okular::Action * getLink( int x, int y, QRect * geometry = nullptr ) const; const Okular::Annotation * getAnnotation( int x, int y, QRect * geometry = nullptr ) const; void testCursorOnLink( int x, int y ); - void overlayClick( const QPoint & position ); + void overlayClick( const QPoint position ); void changePage( int newPage ); void generatePage( bool disableTransition = false ); void generateIntroPage( QPainter & p ); void generateContentsPage( int page, QPainter & p ); void generateOverlay(); void initTransition( const Okular::PageTransition *transition ); const Okular::PageTransition defaultTransition() const; const Okular::PageTransition defaultTransition( int ) const; QRect routeMouseDrawingEvent( QMouseEvent * ); void startAutoChangeTimer(); void recalcGeometry(); void repositionContent(); void requestPixmaps(); void setScreen( int ); - void applyNewScreenSize( const QSize & oldSize ); + void applyNewScreenSize( const QSize oldSize ); void inhibitPowerManagement(); void allowPowerManagement(); void showTopBar( bool ); // create actions that interact with this widget void setupActions(); void setPlayPauseIcon(); // cache stuff int m_width; int m_height; QPixmap m_lastRenderedPixmap; QPixmap m_lastRenderedOverlay; QRect m_overlayGeometry; const Okular::Action * m_pressedLink; bool m_handCursor; SmoothPathEngine * m_drawingEngine; QRect m_drawingRect; int m_screen; uint m_screenInhibitCookie; int m_sleepInhibitFd; // transition related QTimer * m_transitionTimer; QTimer * m_overlayHideTimer; QTimer * m_nextPageTimer; int m_transitionDelay; int m_transitionMul; int m_transitionSteps; QList< QRect > m_transitionRects; Okular::PageTransition m_currentTransition; QPixmap m_currentPagePixmap; QPixmap m_previousPagePixmap; double m_currentPixmapOpacity; // misc stuff QWidget * m_parentWidget; Okular::Document * m_document; QVector< PresentationFrame * > m_frames; int m_frameIndex; QStringList m_metaStrings; QToolBar * m_topBar; QLineEdit *m_pagesEdit; PresentationSearchBar *m_searchBar; KActionCollection * m_ac; KSelectAction * m_screenSelect; QDomElement m_currentDrawingToolElement; bool m_isSetup; bool m_blockNotifications; bool m_inBlackScreenMode; bool m_showSummaryView; bool m_advanceSlides; bool m_goToPreviousPageOnRelease; bool m_goToNextPageOnRelease; private Q_SLOTS: void slotNextPage(); void slotPrevPage(); void slotFirstPage(); void slotLastPage(); void slotHideOverlay(); void slotTransitionStep(); void slotDelayedEvents(); void slotPageChanged(); void clearDrawings(); void screenResized( int ); void chooseScreen( QAction * ); void toggleBlackScreenMode( bool ); void slotProcessMovieAction( const Okular::MovieAction *action ); void slotProcessRenditionAction( const Okular::RenditionAction *action ); void slotTogglePlayPause(); void slotChangeDrawingToolEngine( const QDomElement &element ); void slotAddDrawingToolActions(); }; #endif diff --git a/ui/propertiesdialog.cpp b/ui/propertiesdialog.cpp index 182851dc8..112991fe1 100644 --- a/ui/propertiesdialog.cpp +++ b/ui/propertiesdialog.cpp @@ -1,429 +1,429 @@ /*************************************************************************** * Copyright (C) 2004 by Albert Astals Cid * * * * 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. * ***************************************************************************/ #include "propertiesdialog.h" // qt/kde includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // local includes #include "core/document.h" #include "core/fontinfo.h" static const int IsExtractableRole = Qt::UserRole; static const int FontInfoRole = Qt::UserRole + 1; PropertiesDialog::PropertiesDialog(QWidget *parent, Okular::Document *doc) : KPageDialog( parent ), m_document( doc ), m_fontPage( nullptr ), m_fontModel( nullptr ), m_fontInfo( nullptr ), m_fontProgressBar( nullptr ), m_fontScanStarted( false ) { setFaceType( Tabbed ); setWindowTitle( i18n( "Unknown File" ) ); setStandardButtons( QDialogButtonBox::Ok ); // PROPERTIES QFrame *page = new QFrame(); KPageWidgetItem *item = addPage( page, i18n( "&Properties" ) ); item->setIcon( QIcon::fromTheme( QStringLiteral("document-properties") ) ); // get document info const Okular::DocumentInfo info = doc->documentInfo(); QFormLayout *layout = new QFormLayout( page ); // mime name based on mimetype id QString mimeName = info.get( Okular::DocumentInfo::MimeType ).section( QLatin1Char('/'), -1 ).toUpper(); setWindowTitle( i18n( "%1 Properties", mimeName ) ); /* obtains the properties list, conveniently ordered */ QStringList orderedProperties; orderedProperties << Okular::DocumentInfo::getKeyString( Okular::DocumentInfo::FilePath ) << Okular::DocumentInfo::getKeyString( Okular::DocumentInfo::PagesSize ) << Okular::DocumentInfo::getKeyString( Okular::DocumentInfo::DocumentSize ); for (Okular::DocumentInfo::Key ks = Okular::DocumentInfo::Title; ks <= Okular::DocumentInfo::Keywords; ks = Okular::DocumentInfo::Key( ks+1 ) ) { orderedProperties << Okular::DocumentInfo::getKeyString( ks ); } const QStringList infoKeys = info.keys(); for (const QString &ks : infoKeys) { if ( !orderedProperties.contains( ks ) ) { orderedProperties << ks; } } for ( const QString &key : qAsConst(orderedProperties) ) { const QString titleString = info.getKeyTitle( key ); const QString valueString = info.get( key ); if ( titleString.isNull() || valueString.isNull() ) continue; // create labels and layout them QWidget *value = nullptr; if ( key == Okular::DocumentInfo::getKeyString( Okular::DocumentInfo::MimeType ) ) { /// for mime type fields, show icon as well value = new QWidget( page ); /// place icon left of mime type's name QHBoxLayout *hboxLayout = new QHBoxLayout( value ); hboxLayout->setContentsMargins( 0, 0, 0, 0 ); /// retrieve icon and place it in a QLabel QMimeDatabase db; QMimeType mimeType = db.mimeTypeForName( valueString ); KSqueezedTextLabel *squeezed; if (mimeType.isValid()) { /// retrieve icon and place it in a QLabel QLabel *pixmapLabel = new QLabel( value ); hboxLayout->addWidget( pixmapLabel, 0 ); pixmapLabel->setPixmap( KIconLoader::global()->loadMimeTypeIcon( mimeType.iconName(), KIconLoader::Small ) ); /// mime type's name and label squeezed = new KSqueezedTextLabel( i18nc( "mimetype information, example: \"PDF Document (application/pdf)\"", "%1 (%2)", mimeType.comment(), valueString ), value ); } else { /// only mime type name squeezed = new KSqueezedTextLabel( valueString, value ); } squeezed->setTextInteractionFlags( Qt::TextSelectableByMouse ); hboxLayout->addWidget( squeezed, 1 ); } else { /// default for any other document information KSqueezedTextLabel *label = new KSqueezedTextLabel( valueString, page ); label->setTextInteractionFlags( Qt::TextSelectableByMouse ); value = label; } layout->addRow( new QLabel( i18n( "%1:", titleString ) ), value); } // FONTS if ( doc->canProvideFontInformation() ) { // create fonts tab and layout it QFrame *page2 = new QFrame(); m_fontPage = addPage(page2, i18n("&Fonts")); m_fontPage->setIcon( QIcon::fromTheme( QStringLiteral("preferences-desktop-font") ) ); QVBoxLayout *page2Layout = new QVBoxLayout(page2); // add a tree view QTreeView *view = new QTreeView(page2); view->setContextMenuPolicy(Qt::CustomContextMenu); connect(view, &QTreeView::customContextMenuRequested, this, &PropertiesDialog::showFontsMenu); page2Layout->addWidget(view); view->setRootIsDecorated(false); view->setAlternatingRowColors(true); view->setSortingEnabled( true ); // creating a proxy model so we can sort the data QSortFilterProxyModel *proxymodel = new QSortFilterProxyModel(view); proxymodel->setDynamicSortFilter( true ); proxymodel->setSortCaseSensitivity( Qt::CaseInsensitive ); m_fontModel = new FontsListModel(view); proxymodel->setSourceModel(m_fontModel); view->setModel(proxymodel); view->sortByColumn( 0, Qt::AscendingOrder ); m_fontInfo = new QLabel( this ); page2Layout->addWidget( m_fontInfo ); m_fontInfo->setText( i18n( "Reading font information..." ) ); m_fontInfo->hide(); m_fontProgressBar = new QProgressBar( this ); page2Layout->addWidget( m_fontProgressBar ); m_fontProgressBar->setRange( 0, 100 ); m_fontProgressBar->setValue( 0 ); m_fontProgressBar->hide(); } // KPageDialog is a bit buggy, it doesn't fix its own sizeHint, so we have to manually resize resize(layout->sizeHint()); connect( pageWidget(), QOverload::of(&KPageWidget::currentPageChanged), this, &PropertiesDialog::pageChanged ); } PropertiesDialog::~PropertiesDialog() { m_document->stopFontReading(); } void PropertiesDialog::pageChanged( KPageWidgetItem *current, KPageWidgetItem * ) { if ( current == m_fontPage && !m_fontScanStarted ) { connect(m_document, &Okular::Document::gotFont, m_fontModel, &FontsListModel::addFont); connect(m_document, &Okular::Document::fontReadingProgress, this, &PropertiesDialog::slotFontReadingProgress); connect(m_document, &Okular::Document::fontReadingEnded, this, &PropertiesDialog::slotFontReadingEnded); QTimer::singleShot( 0, this, &PropertiesDialog::reallyStartFontReading ); m_fontScanStarted = true; } } void PropertiesDialog::slotFontReadingProgress( int page ) { m_fontProgressBar->setValue( m_fontProgressBar->maximum() * ( page + 1 ) / m_document->pages() ); } void PropertiesDialog::slotFontReadingEnded() { m_fontInfo->hide(); m_fontProgressBar->hide(); } void PropertiesDialog::reallyStartFontReading() { m_fontInfo->show(); m_fontProgressBar->show(); m_document->startFontReading(); } -void PropertiesDialog::showFontsMenu(const QPoint &pos) +void PropertiesDialog::showFontsMenu(const QPoint pos) { QTreeView *view = static_cast(sender()); QModelIndex index = view->indexAt(pos); if (index.data(IsExtractableRole).toBool()) { QMenu *menu = new QMenu(this); menu->addAction( i18nc("@action:inmenu", "&Extract Font") ); QAction *result = menu->exec(view->viewport()->mapToGlobal(pos)); if (result) { Okular::FontInfo fi = index.data(FontInfoRole).value(); const QString caption = i18n( "Where do you want to save %1?", fi.name() ); const QString path = QFileDialog::getSaveFileName( this, caption, fi.name() ); if ( path.isEmpty() ) return; QFile f( path ); if ( f.open( QIODevice::WriteOnly ) ) { f.write( m_document->fontData(fi) ); f.close(); } else { KMessageBox::error( this, i18n( "Could not open \"%1\" for writing. File was not saved.", path ) ); } } } } FontsListModel::FontsListModel( QObject * parent ) : QAbstractTableModel( parent ) { } FontsListModel::~FontsListModel() { } void FontsListModel::addFont( const Okular::FontInfo &fi ) { beginInsertRows( QModelIndex(), m_fonts.size(), m_fonts.size() ); m_fonts << fi; endInsertRows(); } int FontsListModel::columnCount( const QModelIndex &parent ) const { return parent.isValid() ? 0 : 3; } static QString descriptionForFontType( Okular::FontInfo::FontType type ) { switch ( type ) { case Okular::FontInfo::Type1: return i18n("Type 1"); break; case Okular::FontInfo::Type1C: return i18n("Type 1C"); break; case Okular::FontInfo::Type1COT: return i18nc("OT means OpenType", "Type 1C (OT)"); break; case Okular::FontInfo::Type3: return i18n("Type 3"); break; case Okular::FontInfo::TrueType: return i18n("TrueType"); break; case Okular::FontInfo::TrueTypeOT: return i18nc("OT means OpenType", "TrueType (OT)"); break; case Okular::FontInfo::CIDType0: return i18n("CID Type 0"); break; case Okular::FontInfo::CIDType0C: return i18n("CID Type 0C"); break; case Okular::FontInfo::CIDType0COT: return i18nc("OT means OpenType", "CID Type 0C (OT)"); break; case Okular::FontInfo::CIDTrueType: return i18n("CID TrueType"); break; case Okular::FontInfo::CIDTrueTypeOT: return i18nc("OT means OpenType", "CID TrueType (OT)"); break; case Okular::FontInfo::TeXPK: return i18n("TeX PK"); break; case Okular::FontInfo::TeXVirtual: return i18n("TeX virtual"); break; case Okular::FontInfo::TeXFontMetric: return i18n("TeX Font Metric"); break; case Okular::FontInfo::TeXFreeTypeHandled: return i18n("TeX FreeType-handled"); break; case Okular::FontInfo::Unknown: return i18nc("Unknown font type", "Unknown"); break; } return QString(); } static QString pathOrDescription( const Okular::FontInfo &font ) { switch ( font.embedType() ) { case Okular::FontInfo::NotEmbedded: return font.file(); break; case Okular::FontInfo::EmbeddedSubset: return i18n("Embedded (subset)"); break; case Okular::FontInfo::FullyEmbedded: return i18n("Fully embedded"); break; } return QString(); } static QString descriptionForEmbedType( Okular::FontInfo::EmbedType type ) { switch ( type ) { case Okular::FontInfo::NotEmbedded: return i18n("No"); break; case Okular::FontInfo::EmbeddedSubset: return i18n("Yes (subset)"); break; case Okular::FontInfo::FullyEmbedded: return i18n("Yes"); break; } return QString(); } QVariant FontsListModel::data( const QModelIndex &index, int role ) const { if ( !index.isValid() || index.row() < 0 || index.row() >= m_fonts.count() ) return QVariant(); switch ( role ) { case Qt::DisplayRole: switch ( index.column() ) { case 0: { const Okular::FontInfo &fi = m_fonts.at( index.row() ); const QString fontname = fi.name(); const QString substituteName = fi.substituteName(); if ( fi.embedType() == Okular::FontInfo::NotEmbedded && !substituteName.isEmpty() && !fontname.isEmpty() && substituteName != fontname ) { return i18nc("Replacing missing font with another one", "%1 (substituting with %2)", fontname, substituteName); } return fontname.isEmpty() ? i18nc( "font name not available (empty)", "[n/a]" ) : fontname; break; } case 1: return descriptionForFontType( m_fonts.at( index.row() ).type() ); break; case 2: return pathOrDescription( m_fonts.at( index.row() ) ); break; } break; case Qt::ToolTipRole: { QString fontname = m_fonts.at( index.row() ).name(); if ( fontname.isEmpty() ) fontname = i18n( "Unknown font" ); QString tooltip = QLatin1String( "" ) + fontname + QLatin1String( "" ); if ( m_fonts.at( index.row() ).embedType() == Okular::FontInfo::NotEmbedded ) tooltip += QStringLiteral( " (%2)" ).arg( fontname, fontname ); tooltip += QLatin1String( "
" ) + i18n( "Embedded: %1", descriptionForEmbedType( m_fonts.at( index.row() ).embedType() ) ); tooltip += QLatin1String( "" ); return tooltip; break; } case IsExtractableRole: { return m_fonts.at( index.row() ).canBeExtracted(); } case FontInfoRole: { QVariant v; v.setValue( m_fonts.at( index.row() ) ); return v; } } return QVariant(); } QVariant FontsListModel::headerData( int section, Qt::Orientation orientation, int role ) const { if ( orientation != Qt::Horizontal ) return QVariant(); if ( role == Qt::TextAlignmentRole ) return QVariant( Qt::AlignLeft ); if ( role != Qt::DisplayRole ) return QVariant(); switch ( section ) { case 0: return i18n( "Name" ); break; case 1: return i18n( "Type" ); break; case 2: return i18n( "File" ); break; default: return QVariant(); } } int FontsListModel::rowCount( const QModelIndex &parent ) const { return parent.isValid() ? 0 : m_fonts.size(); } #include "moc_propertiesdialog.cpp" /* kate: replace-tabs on; indent-width 4; */ diff --git a/ui/propertiesdialog.h b/ui/propertiesdialog.h index e603dd387..fac33abd3 100644 --- a/ui/propertiesdialog.h +++ b/ui/propertiesdialog.h @@ -1,76 +1,76 @@ /*************************************************************************** * Copyright (C) 2004 by Albert Astals Cid * * * * 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. * ***************************************************************************/ #ifndef _PROPERTIESDIALOG_H_ #define _PROPERTIESDIALOG_H_ #include #include #include #include "core/fontinfo.h" class QLabel; class QProgressBar; class FontsListModel; namespace Okular { class Document; } class PropertiesDialog : public KPageDialog { Q_OBJECT public: PropertiesDialog( QWidget *parent, Okular::Document *doc ); ~PropertiesDialog() override; private Q_SLOTS: void pageChanged( KPageWidgetItem *, KPageWidgetItem * ); void slotFontReadingProgress( int page ); void slotFontReadingEnded(); void reallyStartFontReading(); - void showFontsMenu(const QPoint &pos); + void showFontsMenu(const QPoint pos); private: Okular::Document * m_document; KPageWidgetItem * m_fontPage; FontsListModel * m_fontModel; QLabel * m_fontInfo; QProgressBar * m_fontProgressBar; bool m_fontScanStarted; }; class FontsListModel : public QAbstractTableModel { Q_OBJECT public: explicit FontsListModel( QObject * parent = nullptr ); ~FontsListModel() override; // reimplementations from QAbstractTableModel int columnCount( const QModelIndex &parent = QModelIndex() ) const override; QVariant data( const QModelIndex &index, int role = Qt::DisplayRole ) const override; QVariant headerData( int section, Qt::Orientation orientation, int role = Qt::DisplayRole ) const override; int rowCount( const QModelIndex &parent = QModelIndex() ) const override; public Q_SLOTS: void addFont( const Okular::FontInfo &fi ); private: QList m_fonts; }; #endif /* kate: replace-tabs on; indent-width 4; */ diff --git a/ui/side_reviews.cpp b/ui/side_reviews.cpp index a4a203f2c..40d6bbfb7 100644 --- a/ui/side_reviews.cpp +++ b/ui/side_reviews.cpp @@ -1,309 +1,309 @@ /*************************************************************************** * Copyright (C) 2005 by Enrico Ros * * * * 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. * ***************************************************************************/ #include "side_reviews.h" // qt/kde includes #include #include #include #include #include #include #include #include #include #include #include #include #include // local includes #include "core/annotations.h" #include "core/document.h" #include "core/page.h" #include "settings.h" #include "annotationpopup.h" #include "annotationproxymodels.h" #include "annotationmodel.h" #include "ktreeviewsearchline.h" class TreeView : public QTreeView { Q_OBJECT public: TreeView( Okular::Document *document, QWidget *parent = Q_NULLPTR ) : QTreeView( parent ), m_document( document ) { } protected: void paintEvent( QPaintEvent *event ) override { bool hasAnnotations = false; for ( uint i = 0; i < m_document->pages(); ++i ) if ( m_document->page( i )->hasAnnotations() ) { hasAnnotations = true; break; } if ( !hasAnnotations ) { QPainter p( viewport() ); p.setRenderHint( QPainter::Antialiasing, true ); p.setClipRect( event->rect() ); QTextDocument document; document.setHtml( i18n( "

No annotations

" "To create new annotations press F6 or select Tools -> Review" " from the menu.
" ) ); document.setTextWidth( width() - 50 ); const uint w = document.size().width() + 20; const uint h = document.size().height() + 20; p.setBrush( palette().window() ); p.translate( 0.5, 0.5 ); p.drawRoundRect( 15, 15, w, h, (8*200)/w, (8*200)/h ); p.translate( 20, 20 ); document.drawContents( &p ); } else { QTreeView::paintEvent( event ); } } private: Okular::Document *m_document; }; Reviews::Reviews( QWidget * parent, Okular::Document * document ) : QWidget( parent ), m_document( document ) { // create widgets and layout them vertically QVBoxLayout * vLayout = new QVBoxLayout( this ); vLayout->setContentsMargins( 0, 0, 0, 0 ); vLayout->setSpacing( 6 ); m_view = new TreeView( m_document, this ); m_view->setAlternatingRowColors( true ); m_view->setSelectionMode( QAbstractItemView::ExtendedSelection ); m_view->header()->hide(); QToolBar *toolBar = new QToolBar( this ); toolBar->setObjectName( QStringLiteral( "reviewOptsBar" ) ); QSizePolicy sp = toolBar->sizePolicy(); sp.setVerticalPolicy( QSizePolicy::Minimum ); toolBar->setSizePolicy( sp ); m_model = new AnnotationModel( m_document, m_view ); m_filterProxy = new PageFilterProxyModel( m_view ); m_groupProxy = new PageGroupProxyModel( m_view ); m_authorProxy = new AuthorGroupProxyModel( m_view ); m_filterProxy->setSourceModel( m_model ); m_groupProxy->setSourceModel( m_filterProxy ); m_authorProxy->setSourceModel( m_groupProxy ); m_view->setModel( m_authorProxy ); m_searchLine = new KTreeViewSearchLine( this, m_view ); m_searchLine->setPlaceholderText(i18n( "Search..." )); m_searchLine->setCaseSensitivity( Okular::Settings::self()->reviewsSearchCaseSensitive() ? Qt::CaseSensitive : Qt::CaseInsensitive ); m_searchLine->setRegularExpression( Okular::Settings::self()->reviewsSearchRegularExpression() ); connect(m_searchLine, &KTreeViewSearchLine::searchOptionsChanged, this, &Reviews::saveSearchOptions); vLayout->addWidget( m_searchLine ); vLayout->addWidget( m_view ); vLayout->addWidget( toolBar ); toolBar->setIconSize( QSize( 16, 16 ) ); toolBar->setMovable( false ); // - add Page button QAction * groupByPageAction = toolBar->addAction( QIcon::fromTheme( QStringLiteral("text-x-generic") ), i18n( "Group by Page" ) ); groupByPageAction->setCheckable( true ); connect(groupByPageAction, &QAction::toggled, this, &Reviews::slotPageEnabled); groupByPageAction->setChecked( Okular::Settings::groupByPage() ); // - add Author button QAction * groupByAuthorAction = toolBar->addAction( QIcon::fromTheme( QStringLiteral("user-identity") ), i18n( "Group by Author" ) ); groupByAuthorAction->setCheckable( true ); connect(groupByAuthorAction, &QAction::toggled, this, &Reviews::slotAuthorEnabled); groupByAuthorAction->setChecked( Okular::Settings::groupByAuthor() ); // - add separator toolBar->addSeparator(); // - add Current Page Only button QAction * curPageOnlyAction = toolBar->addAction( QIcon::fromTheme( QStringLiteral("arrow-down") ), i18n( "Show reviews for current page only" ) ); curPageOnlyAction->setCheckable( true ); connect(curPageOnlyAction, &QAction::toggled, this, &Reviews::slotCurrentPageOnly); curPageOnlyAction->setChecked( Okular::Settings::currentPageOnly() ); // Adds space between left actions, so that the next two buttons are aligned to the right QWidget * spacer = new QWidget(); spacer->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Expanding ); toolBar->addWidget( spacer ); QAction * expandAll = toolBar->addAction( QIcon::fromTheme( QStringLiteral("expand-all") ), i18n( "Expand all elements" ) ); connect(expandAll, &QAction::triggered, this, &Reviews::slotExpandAll); QAction * collapseAll = toolBar->addAction( QIcon::fromTheme( QStringLiteral("collapse-all") ), i18n( "Collapse all elements" ) ); connect(collapseAll, &QAction::triggered, this, &Reviews::slotCollapseAll); connect(m_view, &TreeView::activated, this, &Reviews::activated); m_view->setContextMenuPolicy( Qt::CustomContextMenu ); connect(m_view, &TreeView::customContextMenuRequested, this, &Reviews::contextMenuRequested); } Reviews::~Reviews() { m_document->removeObserver( this ); } //BEGIN DocumentObserver Notifies void Reviews::notifyCurrentPageChanged( int previousPage, int currentPage ) { Q_UNUSED( previousPage ) m_filterProxy->setCurrentPage( currentPage ); } //END DocumentObserver Notifies void Reviews::reparseConfig() { m_searchLine->setCaseSensitivity( Okular::Settings::reviewsSearchCaseSensitive() ? Qt::CaseSensitive : Qt::CaseInsensitive ); m_searchLine->setRegularExpression( Okular::Settings::reviewsSearchRegularExpression() ); m_view->update(); } //BEGIN GUI Slots -> requestListViewUpdate void Reviews::slotPageEnabled( bool on ) { // store toggle state in Settings and update the listview Okular::Settings::setGroupByPage( on ); m_groupProxy->groupByPage( on ); m_view->expandAll(); } void Reviews::slotAuthorEnabled( bool on ) { // store toggle state in Settings and update the listview Okular::Settings::setGroupByAuthor( on ); m_authorProxy->groupByAuthor( on ); m_view->expandAll(); } void Reviews::slotCurrentPageOnly( bool on ) { // store toggle state in Settings and update the listview Okular::Settings::setCurrentPageOnly( on ); m_filterProxy->groupByCurrentPage( on ); m_view->expandAll(); } void Reviews::slotExpandAll() { m_view->expandAll(); } void Reviews::slotCollapseAll() { m_view->collapseAll(); } //END GUI Slots void Reviews::activated( const QModelIndex &index ) { const QModelIndex authorIndex = m_authorProxy->mapToSource( index ); const QModelIndex filterIndex = m_groupProxy->mapToSource( authorIndex ); const QModelIndex annotIndex = m_filterProxy->mapToSource( filterIndex ); Okular::Annotation *annotation = m_model->annotationForIndex( annotIndex ); if ( !annotation ) return; int pageNumber = m_model->data( annotIndex, AnnotationModel::PageRole ).toInt(); const Okular::Page * page = m_document->page( pageNumber ); // calculating the right coordinates to center the view on the annotation QRect rect = Okular::AnnotationUtils::annotationGeometry( annotation, page->width(), page->height() ); Okular::NormalizedRect nr( rect, (int)page->width(), (int)page->height() ); // set the viewport parameters Okular::DocumentViewport vp; vp.pageNumber = pageNumber; vp.rePos.enabled = true; vp.rePos.pos = Okular::DocumentViewport::Center; vp.rePos.normalizedX = ( nr.right + nr.left ) / 2.0; vp.rePos.normalizedY = ( nr.bottom + nr.top ) / 2.0; // setting the viewport m_document->setViewport( vp, nullptr, true ); } QModelIndexList Reviews::retrieveAnnotations(const QModelIndex& idx) const { QModelIndexList ret; if ( idx.isValid() ) { if ( idx.model()->hasChildren( idx ) ) { int rowCount = idx.model()->rowCount( idx ); for ( int i = 0; i < rowCount; i++ ) { ret += retrieveAnnotations( idx.child( i, idx.column() ) ); } } else { ret += idx; } } return ret; } -void Reviews::contextMenuRequested( const QPoint &pos ) +void Reviews::contextMenuRequested( const QPoint pos ) { AnnotationPopup popup( m_document, AnnotationPopup::SingleAnnotationMode, this ); connect(&popup, &AnnotationPopup::openAnnotationWindow, this, &Reviews::openAnnotationWindow); const QModelIndexList indexes = m_view->selectionModel()->selectedIndexes(); for ( const QModelIndex &index : indexes ) { const QModelIndexList annotations = retrieveAnnotations(index); for ( const QModelIndex &idx : annotations ) { const QModelIndex authorIndex = m_authorProxy->mapToSource( idx ); const QModelIndex filterIndex = m_groupProxy->mapToSource( authorIndex ); const QModelIndex annotIndex = m_filterProxy->mapToSource( filterIndex ); Okular::Annotation *annotation = m_model->annotationForIndex( annotIndex ); if ( annotation ) { const int pageNumber = m_model->data( annotIndex, AnnotationModel::PageRole ).toInt(); popup.addAnnotation( annotation, pageNumber ); } } } popup.exec( m_view->viewport()->mapToGlobal( pos ) ); } void Reviews::saveSearchOptions() { Okular::Settings::setReviewsSearchRegularExpression( m_searchLine->regularExpression() ); Okular::Settings::setReviewsSearchCaseSensitive( m_searchLine->caseSensitivity() == Qt::CaseSensitive ? true : false ); Okular::Settings::self()->save(); } #include "side_reviews.moc" diff --git a/ui/side_reviews.h b/ui/side_reviews.h index 1a28bc488..7efd753e4 100644 --- a/ui/side_reviews.h +++ b/ui/side_reviews.h @@ -1,77 +1,77 @@ /*************************************************************************** * Copyright (C) 2005 by Enrico Ros * * * * 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. * ***************************************************************************/ #ifndef _OKULAR_SIDE_REVIEWS_H_ #define _OKULAR_SIDE_REVIEWS_H_ #include #include #include #include "core/observer.h" class QModelIndex; namespace Okular { class Annotation; class Document; } class AnnotationModel; class AuthorGroupProxyModel; class PageFilterProxyModel; class PageGroupProxyModel; class KTreeViewSearchLine; class TreeView; /** * @short ... */ class Reviews : public QWidget, public Okular::DocumentObserver { Q_OBJECT public: Reviews( QWidget * parent, Okular::Document * document ); ~Reviews() override; // [INHERITED] from DocumentObserver void notifyCurrentPageChanged( int previous, int current ) override; void reparseConfig(); public Q_SLOTS: void slotPageEnabled( bool ); void slotAuthorEnabled( bool ); void slotCurrentPageOnly( bool ); void slotExpandAll(); void slotCollapseAll(); Q_SIGNALS: void openAnnotationWindow( Okular::Annotation *annotation, int pageNumber ); private Q_SLOTS: void activated( const QModelIndex& ); - void contextMenuRequested( const QPoint& ); + void contextMenuRequested( const QPoint ); void saveSearchOptions(); private: QModelIndexList retrieveAnnotations(const QModelIndex& idx) const; // data fields (GUI) KTreeViewSearchLine *m_searchLine; TreeView * m_view; // internal storage Okular::Document * m_document; AnnotationModel * m_model; AuthorGroupProxyModel * m_authorProxy; PageFilterProxyModel * m_filterProxy; PageGroupProxyModel * m_groupProxy; }; #endif diff --git a/ui/sidebar.cpp b/ui/sidebar.cpp index 282e095b1..d8748599d 100644 --- a/ui/sidebar.cpp +++ b/ui/sidebar.cpp @@ -1,824 +1,824 @@ /*************************************************************************** * Copyright (C) 2007 by Pino Toscano * * Copyright (C) 2009 by Eike Hein * * * * 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. * ***************************************************************************/ #include "sidebar.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "settings.h" static const int SidebarItemType = QListWidgetItem::UserType + 1; /* List item representing a sidebar entry. */ class SidebarItem : public QListWidgetItem { public: SidebarItem( QWidget* w, const QIcon &icon, const QString &text ) : QListWidgetItem( nullptr, SidebarItemType ), m_widget( w ) { setFlags( Qt::ItemIsSelectable | Qt::ItemIsEnabled ); setIcon( icon ); setText( text ); setToolTip( text ); } QWidget* widget() const { return m_widget; } private: QWidget *m_widget; }; /* A simple delegate to paint the icon of each item */ #define ITEM_MARGIN_LEFT 5 #define ITEM_MARGIN_TOP 5 #define ITEM_MARGIN_RIGHT 5 #define ITEM_MARGIN_BOTTOM 5 #define ITEM_PADDING 5 class SidebarDelegate : public QAbstractItemDelegate { Q_OBJECT public: SidebarDelegate( QObject *parent = Q_NULLPTR ); ~SidebarDelegate() override; void setShowText( bool show ); bool isTextShown() const; // from QAbstractItemDelegate void paint( QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index ) const override; QSize sizeHint( const QStyleOptionViewItem &option, const QModelIndex &index ) const override; private slots: void updateBrushCache(); private: bool m_showText; QScopedPointer m_windowBackground; QScopedPointer m_windowForeground; QScopedPointer m_selectionBackground; QScopedPointer m_selectionForeground; }; SidebarDelegate::SidebarDelegate( QObject *parent ) : QAbstractItemDelegate( parent ), m_showText( true ), m_windowBackground( nullptr ), m_windowForeground( nullptr ), m_selectionBackground( nullptr ), m_selectionForeground( nullptr ) { updateBrushCache(); connect(qApp, &QGuiApplication::paletteChanged, this, &SidebarDelegate::updateBrushCache); } SidebarDelegate::~SidebarDelegate() { } void SidebarDelegate::setShowText( bool show ) { m_showText = show; } bool SidebarDelegate::isTextShown() const { return m_showText; } void SidebarDelegate::updateBrushCache() { m_windowBackground.reset(new KStatefulBrush(KColorScheme::Window, KColorScheme::NormalBackground)); m_windowForeground.reset(new KStatefulBrush(KColorScheme::Window, KColorScheme::NormalText)); m_selectionBackground.reset(new KStatefulBrush(KColorScheme::Selection, KColorScheme::NormalBackground)); m_selectionForeground.reset(new KStatefulBrush(KColorScheme::Selection, KColorScheme::NormalText)); } void SidebarDelegate::paint( QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index ) const { QBrush backBrush; QColor foreColor; bool disabled = false; bool hover = false; if ( !( option.state & QStyle::State_Enabled ) ) { backBrush = m_windowBackground->brush(QPalette::Disabled); foreColor = m_windowForeground->brush(QPalette::Disabled).color(); disabled = true; } else if ( option.state & QStyle::State_HasFocus ) { backBrush = m_selectionBackground->brush(option.palette); foreColor = m_selectionForeground->brush(option.palette).color(); } else if ( option.state & QStyle::State_Selected ) { backBrush = m_selectionBackground->brush(option.palette); foreColor = m_windowForeground->brush(option.palette).color(); } else if ( option.state & QStyle::State_MouseOver ) { backBrush = m_selectionBackground->brush(option.palette).color().lighter( 115 ); foreColor = m_windowForeground->brush(option.palette).color(); hover = true; } else /*if ( option.state & QStyle::State_Enabled )*/ { backBrush = m_windowBackground->brush(option.palette); foreColor = m_windowForeground->brush(option.palette).color(); } QStyle *style = QApplication::style(); QStyleOptionViewItem opt( option ); // KStyle provides an "hover highlight" effect for free; // but we want that for non-KStyle-based styles too if ( !style->inherits( "KStyle" ) && hover ) { Qt::BrushStyle bs = opt.backgroundBrush.style(); if ( bs > Qt::NoBrush && bs < Qt::TexturePattern ) opt.backgroundBrush = opt.backgroundBrush.color().lighter( 115 ); else opt.backgroundBrush = backBrush; } painter->save(); style->drawPrimitive( QStyle::PE_PanelItemViewItem, &opt, painter, nullptr ); painter->restore(); QIcon icon = index.data( Qt::DecorationRole ).value< QIcon >(); if ( !icon.isNull() ) { QPoint iconpos( ( option.rect.width() - option.decorationSize.width() ) / 2, ITEM_MARGIN_TOP ); iconpos += option.rect.topLeft(); QIcon::Mode iconmode = disabled ? QIcon::Disabled : QIcon::Normal; painter->drawPixmap( iconpos, icon.pixmap( option.decorationSize, iconmode ) ); } if ( m_showText ) { QString text = index.data( Qt::DisplayRole ).toString(); QRect fontBoundaries = QFontMetrics( option.font ).boundingRect( QRect(), Qt::AlignCenter, text ); QPoint textPos( ITEM_MARGIN_LEFT + ( option.rect.width() - ITEM_MARGIN_LEFT - ITEM_MARGIN_RIGHT - fontBoundaries.width() ) / 2, ITEM_MARGIN_TOP + option.decorationSize.height() + ITEM_PADDING ); fontBoundaries.translate( -fontBoundaries.topLeft() ); fontBoundaries.translate( textPos ); fontBoundaries.translate( option.rect.topLeft() ); painter->setPen( foreColor ); painter->drawText( fontBoundaries, Qt::AlignCenter, text ); } } QSize SidebarDelegate::sizeHint( const QStyleOptionViewItem &option, const QModelIndex &index ) const { QSize baseSize( option.decorationSize.width(), option.decorationSize.height() ); if ( m_showText ) { QRect fontBoundaries = QFontMetrics( option.font ).boundingRect( QRect(), Qt::AlignCenter, index.data( Qt::DisplayRole ).toString() ); baseSize.setWidth( qMax( fontBoundaries.width(), baseSize.width() ) ); baseSize.setHeight( baseSize.height() + fontBoundaries.height() + ITEM_PADDING ); } return baseSize + QSize( ITEM_MARGIN_LEFT + ITEM_MARGIN_RIGHT, ITEM_MARGIN_TOP + ITEM_MARGIN_BOTTOM ); } /* A custom list widget that ignores the events for disabled items */ class SidebarListWidget : public QListWidget { public: SidebarListWidget( QWidget *parent = Q_NULLPTR ); ~SidebarListWidget() override; int countVisible() const { int ret = 0; for ( int i = 0, c = count(); i < c; ++i ) { ret += !item(i)->isHidden(); } return ret; } protected: // from QListWidget void mouseDoubleClickEvent( QMouseEvent *event ) override; void mouseMoveEvent( QMouseEvent *event ) override; void mousePressEvent( QMouseEvent *event ) override; void mouseReleaseEvent( QMouseEvent *event ) override; QModelIndex moveCursor( QAbstractItemView::CursorAction cursorAction, Qt::KeyboardModifiers modifiers ) override; private: // These two are used to keep track of the row an initial mousePress- // Event() occurs on and the row the cursor moves over while the left // mouse button is pressed, respectively, as well as for event compre- // ssion, to avoid calling SideBar::itemClicked() multiple times for // the same item in a row on mouseMoveEvent()'s. This code is written // under the assumption that the number and positions of items in the // list won't change while the user interacts with it using the mouse. // Future work here must see to that this assumption continues to hold // up, or achieve calling SideBar::itemClicked() differently. int mousePressedRow; int rowUnderMouse; }; SidebarListWidget::SidebarListWidget( QWidget *parent ) : QListWidget( parent ) { mousePressedRow = -1; rowUnderMouse = -1; } SidebarListWidget::~SidebarListWidget() { } void SidebarListWidget::mouseDoubleClickEvent( QMouseEvent *event ) { QModelIndex index = indexAt( event->pos() ); if ( index.isValid() && !( index.flags() & Qt::ItemIsSelectable ) ) return; QListWidget::mouseDoubleClickEvent( event ); } void SidebarListWidget::mouseMoveEvent( QMouseEvent *event ) { QModelIndex index = indexAt( event->pos() ); if ( index.isValid() ) { if ( index.flags() & Qt::ItemIsSelectable ) { if ( event->buttons() & Qt::LeftButton && index.row() != mousePressedRow && index.row() != rowUnderMouse ) { mousePressedRow = -1; rowUnderMouse = index.row(); QMetaObject::invokeMethod(parent(), "itemClicked", Qt::DirectConnection, Q_ARG(QListWidgetItem*, item(index.row()))); } } else return; } QListWidget::mouseMoveEvent( event ); } void SidebarListWidget::mousePressEvent( QMouseEvent *event ) { QModelIndex index = indexAt( event->pos() ); if ( index.isValid() ) { if ( index.flags() & Qt::ItemIsSelectable ) { if ( event->buttons() & Qt::LeftButton ) mousePressedRow = index.row(); } else return; } QListWidget::mousePressEvent( event ); } void SidebarListWidget::mouseReleaseEvent( QMouseEvent *event ) { QModelIndex index = indexAt( event->pos() ); if ( index.isValid() ) { if ( index.flags() & Qt::ItemIsSelectable ) { if ( event->button() == Qt::LeftButton && index.row() != rowUnderMouse ) { QMetaObject::invokeMethod(parent(), "itemClicked", Qt::DirectConnection, Q_ARG(QListWidgetItem*, item(index.row()))); } } else { mousePressedRow = -1; rowUnderMouse = -1; return; } } mousePressedRow = -1; rowUnderMouse = -1; QListWidget::mouseReleaseEvent( event ); } QModelIndex SidebarListWidget::moveCursor( QAbstractItemView::CursorAction cursorAction, Qt::KeyboardModifiers modifiers ) { Q_UNUSED( modifiers ) QModelIndex oldindex = currentIndex(); QModelIndex newindex = oldindex; switch ( cursorAction ) { case MoveUp: case MovePrevious: { int row = oldindex.row() - 1; while ( row > -1 && !( model()->index( row, 0 ).flags() & Qt::ItemIsSelectable ) ) --row; if ( row > -1 ) newindex = model()->index( row, 0 ); break; } case MoveDown: case MoveNext: { int row = oldindex.row() + 1; int max = model()->rowCount(); while ( row < max && !( model()->index( row, 0 ).flags() & Qt::ItemIsSelectable ) ) ++row; if ( row < max ) newindex = model()->index( row, 0 ); break; } case MoveHome: case MovePageUp: { int row = 0; while ( row < oldindex.row() && !( model()->index( row, 0 ).flags() & Qt::ItemIsSelectable ) ) ++row; if ( row < oldindex.row() ) newindex = model()->index( row, 0 ); break; } case MoveEnd: case MovePageDown: { int row = model()->rowCount() - 1; while ( row > oldindex.row() && !( model()->index( row, 0 ).flags() & Qt::ItemIsSelectable ) ) --row; if ( row > oldindex.row() ) newindex = model()->index( row, 0 ); break; } // no navigation possible for these case MoveLeft: case MoveRight: break; } // dirty hack to change item when the key cursor changes item if ( oldindex != newindex ) { emit itemClicked( itemFromIndex( newindex ) ); } return newindex; } /* Private storage. */ class Sidebar::Private { public: Private() : sideWidget( nullptr ), bottomWidget( nullptr ), splitterSizesSet( false ), itemsHeight( 0 ) { } int indexOf(QWidget *w) const { for (int i = 0; i < pages.count(); ++i) { if (pages[i]->widget() == w) return i; } return -1; } void adjustListSize( bool recalc, bool expand = true ); SidebarListWidget *list; QSplitter *splitter; QStackedWidget *stack; QWidget *sideContainer; QLabel *sideTitle; QVBoxLayout *vlay; QWidget *sideWidget; QWidget *bottomWidget; QList< SidebarItem* > pages; bool splitterSizesSet; int itemsHeight; SidebarDelegate *sideDelegate; }; void Sidebar::Private::adjustListSize( bool recalc, bool expand ) { QSize bottomElemSize( list->sizeHintForIndex( list->model()->index( list->count() - 1, 0 ) ) ); if ( recalc ) { int w = 0; for ( int i = 0; i < list->count(); ++i ) { QSize s = list->sizeHintForIndex( list->model()->index( i, 0 ) ); if ( s.width() > w ) w = s.width(); } bottomElemSize.setWidth( w ); } itemsHeight = bottomElemSize.height() * list->countVisible(); list->setMinimumHeight( itemsHeight + list->frameWidth() * 2 ); int curWidth = list->minimumWidth(); int newWidth = expand ? qMax( bottomElemSize.width() + list->frameWidth() * 2, curWidth ) : qMin( bottomElemSize.width() + list->frameWidth() * 2, curWidth ); list->setFixedWidth( newWidth ); } Sidebar::Sidebar( QWidget *parent ) : QWidget( parent ), d( new Private ) { QHBoxLayout *mainlay = new QHBoxLayout( this ); mainlay->setContentsMargins( 0, 0, 0, 0 ); mainlay->setSpacing( 0 ); setAutoFillBackground( true ); setAcceptDrops( true ); d->list = new SidebarListWidget( this ); mainlay->addWidget( d->list ); d->list->setMouseTracking( true ); d->list->viewport()->setAttribute( Qt::WA_Hover ); d->sideDelegate = new SidebarDelegate( d->list ); d->sideDelegate->setShowText( Okular::Settings::sidebarShowText() ); d->list->setItemDelegate( d->sideDelegate ); d->list->setUniformItemSizes( true ); d->list->setSelectionMode( QAbstractItemView::SingleSelection ); int iconsize = Okular::Settings::sidebarIconSize(); d->list->setIconSize( QSize( iconsize, iconsize ) ); d->list->setHorizontalScrollBarPolicy( Qt::ScrollBarAlwaysOff ); d->list->setVerticalScrollBarPolicy( Qt::ScrollBarAlwaysOff ); d->list->setContextMenuPolicy( Qt::CustomContextMenu ); d->list->viewport()->setAutoFillBackground( false ); d->splitter = new QSplitter( this ); mainlay->addWidget( d->splitter ); d->splitter->setOpaqueResize( true ); d->splitter->setChildrenCollapsible( false ); d->sideContainer = new QWidget( d->splitter ); d->sideContainer->setMinimumWidth( 90 ); d->sideContainer->setMaximumWidth( 600 ); d->vlay = new QVBoxLayout( d->sideContainer ); d->vlay->setContentsMargins( 0, 0, 0, 0 ); d->sideTitle = new QLabel( d->sideContainer ); d->vlay->addWidget( d->sideTitle ); QFont tf = d->sideTitle->font(); tf.setBold( true ); d->sideTitle->setFont( tf ); d->sideTitle->setMargin( 3 ); d->sideTitle->setIndent( 3 ); d->stack = new QStackedWidget( d->sideContainer ); d->vlay->addWidget( d->stack ); connect(d->list, &SidebarListWidget::customContextMenuRequested, this, &Sidebar::listContextMenu); connect(d->splitter, &QSplitter::splitterMoved, this, &Sidebar::splitterMoved); setCollapsed( true ); setFocusProxy( d->list ); } Sidebar::~Sidebar() { delete d; } int Sidebar::addItem( QWidget *widget, const QIcon &icon, const QString &text ) { if ( !widget ) return -1; SidebarItem *newitem = new SidebarItem( widget, icon, text ); d->list->addItem( newitem ); d->pages.append( newitem ); widget->setParent( d->stack ); d->stack->addWidget( widget ); // updating the minimum height of the icon view, so all are visible with no scrolling d->adjustListSize( false, true ); return d->pages.count() - 1; } void Sidebar::setMainWidget( QWidget *widget ) { delete d->sideWidget; d->sideWidget = widget; if ( d->sideWidget ) { // setting the splitter as parent for the widget automatically plugs it // into the splitter, neat! d->sideWidget->setParent( d->splitter ); if ( !d->splitterSizesSet ) { QList splitterSizes = Okular::Settings::splitterSizes(); if ( !splitterSizes.count() ) { // the first time use 1/10 for the panel and 9/10 for the pageView splitterSizes.push_back( 50 ); splitterSizes.push_back( 500 ); } d->splitter->setSizes( splitterSizes ); d->splitterSizesSet = true; } } } void Sidebar::setBottomWidget( QWidget *widget ) { delete d->bottomWidget; d->bottomWidget = widget; if ( d->bottomWidget ) { d->bottomWidget->setParent( this ); d->vlay->addWidget( d->bottomWidget ); } } void Sidebar::setItemEnabled( QWidget *widget, bool enabled ) { const int index = d->indexOf( widget ); setIndexEnabled( index, enabled ); } void Sidebar::setIndexEnabled( int index, bool enabled ) { if ( index < 0 || index >= d->pages.count() ) return; Qt::ItemFlags f = d->pages.at( index )->flags(); if ( enabled ) { f |= Qt::ItemIsEnabled; f |= Qt::ItemIsSelectable; } else { f &= ~Qt::ItemIsEnabled; f &= ~Qt::ItemIsSelectable; } d->pages.at( index )->setFlags( f ); if ( !enabled && index == d->list->currentRow() && isSidebarVisible() ) // find an enabled item, and select that one for ( int i = 0; i < d->pages.count(); ++i ) if ( d->pages.at(i)->flags() & Qt::ItemIsEnabled ) { setCurrentIndex( i ); break; } } bool Sidebar::isItemEnabled( QWidget *widget ) const { const int index = d->indexOf( widget ); return isIndexEnabled( index ); } bool Sidebar::isIndexEnabled( int index ) const { if ( index < 0 ) return false; Qt::ItemFlags f = d->pages.at( index )->flags(); return ( f & Qt::ItemIsEnabled ) == Qt::ItemIsEnabled; } void Sidebar::setCurrentItem( QWidget *widget, SetCurrentItemBehaviour b ) { const int index = d->indexOf( widget ); setCurrentIndex( index, b ); } void Sidebar::setCurrentIndex( int index, SetCurrentItemBehaviour b ) { if ( index < 0 || !isIndexEnabled( index ) ) return; itemClicked( d->pages.at( index ), b ); QModelIndex modelindex = d->list->model()->index( index, 0 ); d->list->setCurrentIndex( modelindex ); d->list->selectionModel()->select( modelindex, QItemSelectionModel::ClearAndSelect ); } QWidget *Sidebar::currentItem() const { const int row = d->list->currentRow(); if (row < 0 || row >= d->pages.count()) return nullptr; return d->pages[row]->widget(); } void Sidebar::setSidebarVisibility( bool visible ) { if ( visible != d->list->isHidden() ) return; static bool wasCollapsed = isCollapsed(); d->list->setHidden( !visible ); if ( visible ) { setCollapsed( wasCollapsed ); wasCollapsed = false; } else { wasCollapsed = isCollapsed(); setCollapsed( true ); } } bool Sidebar::isSidebarVisible() const { return !d->list->isHidden(); } void Sidebar::setCollapsed( bool collapsed ) { d->sideContainer->setHidden( collapsed ); } bool Sidebar::isCollapsed() const { return d->sideContainer->isHidden(); } void Sidebar::moveSplitter(int sideWidgetSize) { QList splitterSizeList = d->splitter->sizes(); const int total = splitterSizeList.at( 0 ) + splitterSizeList.at( 1 ); splitterSizeList.replace( 0, total - sideWidgetSize ); splitterSizeList.replace( 1, sideWidgetSize ); d->splitter->setSizes( splitterSizeList ); } void Sidebar::setItemVisible( QWidget *widget, bool visible ) { const int index = d->indexOf( widget ); if ( index < 0 ) return; d->list->setRowHidden( index, !visible ); setIndexEnabled( index, visible ); } void Sidebar::itemClicked( QListWidgetItem *item ) { itemClicked( item, UncollapseIfCollapsed ); } void Sidebar::itemClicked( QListWidgetItem *item, SetCurrentItemBehaviour b ) { if ( !item ) return; SidebarItem* sbItem = dynamic_cast< SidebarItem* >( item ); if ( !sbItem ) return; if ( sbItem->widget() == d->stack->currentWidget() ) { if ( !isCollapsed() ) { d->list->selectionModel()->clear(); setCollapsed( true ); } else { if ( b == UncollapseIfCollapsed ) { setCollapsed( false ); d->list->show(); } } } else { if ( isCollapsed() && b == UncollapseIfCollapsed ) { setCollapsed( false ); d->list->show(); } d->stack->setCurrentWidget( sbItem->widget() ); d->sideTitle->setText( sbItem->toolTip() ); } } void Sidebar::splitterMoved( int /*pos*/, int index ) { // if the side panel has been resized, save splitter sizes if ( index == 1 ) saveSplitterSize(); } void Sidebar::saveSplitterSize() const { Okular::Settings::setSplitterSizes( d->splitter->sizes() ); Okular::Settings::self()->save(); } -void Sidebar::listContextMenu( const QPoint &pos ) +void Sidebar::listContextMenu( const QPoint pos ) { QMenu menu( this ); menu.setTitle( i18n( "Okular" ) ); QAction *showTextAct = menu.addAction( i18n( "Show Text" ) ); showTextAct->setCheckable( true ); showTextAct->setChecked( d->sideDelegate->isTextShown() ); connect(showTextAct, &QAction::toggled, this, &Sidebar::showTextToggled); menu.addSeparator(); QActionGroup *sizeGroup = new QActionGroup( &menu ); int curSize = d->list->iconSize().width(); #define ADD_SIZE_ACTION( text, _itssize ) \ { \ const int itssize = static_cast< int >( _itssize ); \ QAction *sizeAct = menu.addAction( text ); \ sizeAct->setCheckable( true ); \ sizeAct->setData( QVariant::fromValue( itssize ) ); \ sizeAct->setChecked( itssize == curSize ); \ sizeGroup->addAction( sizeAct ); \ } ADD_SIZE_ACTION( i18n( "Small Icons" ), KIconLoader::SizeSmallMedium ) ADD_SIZE_ACTION( i18n( "Normal Icons" ), KIconLoader::SizeMedium ) ADD_SIZE_ACTION( i18n( "Large Icons" ), KIconLoader::SizeLarge ) #undef ADD_SIZE_ACTION connect(sizeGroup, &QActionGroup::triggered, this, &Sidebar::iconSizeChanged); menu.exec( mapToGlobal( pos ) ); } void Sidebar::showTextToggled( bool on ) { d->sideDelegate->setShowText( on ); d->adjustListSize( true, on ); d->list->reset(); d->list->update(); Okular::Settings::setSidebarShowText( on ); Okular::Settings::self()->save(); } void Sidebar::iconSizeChanged( QAction *action ) { int size = action->data().toInt(); int oldSize = d->list->iconSize().width(); d->list->setIconSize( QSize( size, size ) ); d->adjustListSize( true, size > oldSize ); d->list->reset(); d->list->update(); Okular::Settings::setSidebarIconSize( size ); Okular::Settings::self()->save(); } void Sidebar::dragEnterEvent( QDragEnterEvent* event ) { event->setAccepted( event->mimeData()->hasUrls() ); } void Sidebar::dropEvent( QDropEvent* event ) { const QList list = KUrlMimeData::urlsFromMimeData( event->mimeData() ); emit urlsDropped( list ); } #include "sidebar.moc" diff --git a/ui/sidebar.h b/ui/sidebar.h index 7e9697e35..2baa41c2d 100644 --- a/ui/sidebar.h +++ b/ui/sidebar.h @@ -1,75 +1,75 @@ /*************************************************************************** * Copyright (C) 2007 by Pino Toscano * * * * 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. * ***************************************************************************/ #ifndef _SIDEBAR_H_ #define _SIDEBAR_H_ #include #include "okularpart_export.h" class QIcon; class QListWidgetItem; class OKULARPART_EXPORT Sidebar : public QWidget { Q_OBJECT public: explicit Sidebar( QWidget *parent = nullptr ); ~Sidebar() override; int addItem( QWidget *widget, const QIcon &icon, const QString &text ); void setMainWidget( QWidget *widget ); void setBottomWidget( QWidget *widget ); void setItemEnabled( QWidget *widget, bool enabled ); bool isItemEnabled( QWidget *widget ) const; void setItemVisible( QWidget *widget, bool visible ); enum SetCurrentItemBehaviour { UncollapseIfCollapsed, DoNotUncollapseIfCollapsed }; void setCurrentItem( QWidget *widget, SetCurrentItemBehaviour b = UncollapseIfCollapsed ); QWidget *currentItem() const; void setSidebarVisibility( bool visible ); bool isSidebarVisible() const; void setCollapsed( bool collapsed ); bool isCollapsed() const; void moveSplitter( int sideWidgetSize ); Q_SIGNALS: void urlsDropped( const QList& urls ); protected: void dragEnterEvent( QDragEnterEvent* event ) override; void dropEvent( QDropEvent* event ) override; private Q_SLOTS: void itemClicked( QListWidgetItem *item ); void splitterMoved( int pos, int index ); - void listContextMenu( const QPoint & ); + void listContextMenu( const QPoint ); void showTextToggled( bool ); void iconSizeChanged( QAction *action ); private: void setIndexEnabled( int index, bool enabled ); void setCurrentIndex( int index, SetCurrentItemBehaviour b = UncollapseIfCollapsed ); bool isIndexEnabled( int index ) const; void itemClicked( QListWidgetItem *item, SetCurrentItemBehaviour b ); void saveSplitterSize() const; // private storage class Private; Private *d; }; #endif diff --git a/ui/thumbnaillist.cpp b/ui/thumbnaillist.cpp index d32ed3d6d..c2bd89b94 100644 --- a/ui/thumbnaillist.cpp +++ b/ui/thumbnaillist.cpp @@ -1,1008 +1,1023 @@ /*************************************************************************** * Copyright (C) 2004-2006 by Albert Astals Cid * * * * 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. * ***************************************************************************/ #include "thumbnaillist.h" // qt/kde includes #include #include #include #include #include #include #include #include #include #include #include #include // local includes #include "pagepainter.h" #include "core/area.h" #include "core/bookmarkmanager.h" #include "core/document.h" #include "core/generator.h" #include "core/page.h" #include "settings.h" #include "priorities.h" class ThumbnailWidget; class ThumbnailListPrivate : public QWidget { public: ThumbnailListPrivate( ThumbnailList *qq, Okular::Document *document ); ~ThumbnailListPrivate() override; enum ChangePageDirection { Null, Left, Right, Up, Down }; ThumbnailList *q; Okular::Document *m_document; ThumbnailWidget *m_selected; QTimer *m_delayTimer; QPixmap *m_bookmarkOverlay; QVector m_thumbnails; QList m_visibleThumbnails; int m_vectorIndex; // Grabbing variables QPoint m_mouseGrabPos; ThumbnailWidget *m_mouseGrabItem; int m_pageCurrentlyGrabbed; // resize thumbnails to fit the width void viewportResizeEvent( QResizeEvent * ); // called by ThumbnailWidgets to get the overlay bookmark pixmap const QPixmap * getBookmarkOverlay() const; // called by ThumbnailWidgets to send (forward) the mouse move signals - ChangePageDirection forwardTrack( const QPoint &, const QSize & ); + ChangePageDirection forwardTrack( const QPoint, const QSize ); - ThumbnailWidget* itemFor( const QPoint & p ) const; + ThumbnailWidget* itemFor( const QPoint p ) const; void delayedRequestVisiblePixmaps( int delayMs = 0 ); // SLOTS: // make requests for generating pixmaps for visible thumbnails void slotRequestVisiblePixmaps(); // delay timeout: resize overlays and requests pixmaps void slotDelayTimeout(); ThumbnailWidget* getPageByNumber( int page ) const; int getNewPageOffset( int n, ThumbnailListPrivate::ChangePageDirection dir ) const; ThumbnailWidget *getThumbnailbyOffset( int current, int offset ) const; protected: void mousePressEvent( QMouseEvent * e ) override; void mouseReleaseEvent( QMouseEvent * e ) override; void mouseMoveEvent( QMouseEvent * e ) override; void wheelEvent( QWheelEvent * e ) override; void contextMenuEvent( QContextMenuEvent * e ) override; void paintEvent( QPaintEvent * e ) override; }; // ThumbnailWidget represents a single thumbnail in the ThumbnailList class ThumbnailWidget { public: ThumbnailWidget( ThumbnailListPrivate * parent, const Okular::Page * page ); // set internal parameters to fit the page in the given width void resizeFitWidth( int width ); // set thumbnail's selected state void setSelected( bool selected ); // set the visible rect of the current page void setVisibleRect( const Okular::NormalizedRect & rect ); // query methods int heightHint() const { return m_pixmapHeight + m_labelHeight + m_margin; } int pixmapWidth() const { return m_pixmapWidth; } int pixmapHeight() const { return m_pixmapHeight; } int pageNumber() const { return m_page->number(); } const Okular::Page * page() const { return m_page; } QRect visibleRect() const { return m_visibleRect.geometry( m_pixmapWidth, m_pixmapHeight ); } - void paint( QPainter &p, const QRect &clipRect ); + void paint( QPainter &p, const QRect clipRect ); static int margin() { return m_margin; } // simulating QWidget QRect rect() const { return m_rect; } int height() const { return m_rect.height(); } int width() const { return m_rect.width(); } QPoint pos() const { return m_rect.topLeft(); } void move( int x, int y ) { m_rect.setTopLeft( QPoint( x, y ) ); } void update() { m_parent->update( m_rect ); } - void update( const QRect & rect ) { m_parent->update( rect.translated( m_rect.topLeft() ) ); } + void update( const QRect rect ) { m_parent->update( rect.translated( m_rect.topLeft() ) ); } private: // the margin around the widget static int const m_margin = 16; ThumbnailListPrivate * m_parent; const Okular::Page * m_page; bool m_selected; int m_pixmapWidth, m_pixmapHeight; int m_labelHeight, m_labelNumber; Okular::NormalizedRect m_visibleRect; QRect m_rect; }; ThumbnailListPrivate::ThumbnailListPrivate( ThumbnailList *qq, Okular::Document *document ) : QWidget(), q( qq ), m_document( document ), m_selected( nullptr ), m_delayTimer( nullptr ), m_bookmarkOverlay( nullptr ), m_vectorIndex( 0 ) { setMouseTracking( true ); m_mouseGrabItem = nullptr; } ThumbnailWidget* ThumbnailListPrivate::getPageByNumber( int page ) const { QVector< ThumbnailWidget * >::const_iterator tIt = m_thumbnails.constBegin(), tEnd = m_thumbnails.constEnd(); for ( ; tIt != tEnd; ++tIt ) { if ( (*tIt)->pageNumber() == page ) return (*tIt); } return nullptr; } ThumbnailListPrivate::~ThumbnailListPrivate() { } -ThumbnailWidget* ThumbnailListPrivate::itemFor( const QPoint & p ) const +ThumbnailWidget* ThumbnailListPrivate::itemFor( const QPoint p ) const { QVector< ThumbnailWidget * >::const_iterator tIt = m_thumbnails.constBegin(), tEnd = m_thumbnails.constEnd(); for ( ; tIt != tEnd; ++tIt ) { if ( (*tIt)->rect().contains( p ) ) return (*tIt); } return nullptr; } void ThumbnailListPrivate::paintEvent( QPaintEvent * e ) { QPainter painter( this ); QVector::const_iterator tIt = m_thumbnails.constBegin(), tEnd = m_thumbnails.constEnd(); for ( ; tIt != tEnd; ++tIt ) { QRect rect = e->rect().intersected( (*tIt)->rect() ); if ( !rect.isNull() ) { rect.translate( -(*tIt)->pos() ); painter.save(); painter.translate( (*tIt)->pos() ); (*tIt)->paint( painter, rect ); painter.restore(); } } } /** ThumbnailList implementation **/ ThumbnailList::ThumbnailList( QWidget *parent, Okular::Document *document ) : QScrollArea( parent ), d( new ThumbnailListPrivate( this, document ) ) { setObjectName( QStringLiteral( "okular::Thumbnails" ) ); // set scrollbars setHorizontalScrollBarPolicy( Qt::ScrollBarAlwaysOff ); setVerticalScrollBarPolicy( Qt::ScrollBarAlwaysOn ); verticalScrollBar()->setEnabled( false ); setAttribute( Qt::WA_StaticContents ); viewport()->setBackgroundRole( QPalette::Base ); setWidget( d ); // widget setup: can be focused by mouse click (not wheel nor tab) widget()->setFocusPolicy( Qt::ClickFocus ); widget()->show(); widget()->setBackgroundRole( QPalette::Base ); connect( verticalScrollBar(), &QScrollBar::valueChanged, d, &ThumbnailListPrivate::slotRequestVisiblePixmaps ); } ThumbnailList::~ThumbnailList() { d->m_document->removeObserver( this ); delete d->m_bookmarkOverlay; } //BEGIN DocumentObserver inherited methods void ThumbnailList::notifySetup( const QVector< Okular::Page * > & pages, int setupFlags ) { // if there was a widget selected, save its pagenumber to restore // its selection (if available in the new set of pages) int prevPage = -1; if ( !( setupFlags & Okular::DocumentObserver::DocumentChanged ) && d->m_selected ) { prevPage = d->m_selected->page()->number(); } else prevPage = d->m_document->viewport().pageNumber; // delete all the Thumbnails QVector::const_iterator tIt = d->m_thumbnails.constBegin(), tEnd = d->m_thumbnails.constEnd(); for ( ; tIt != tEnd; ++tIt ) delete *tIt; d->m_thumbnails.clear(); d->m_visibleThumbnails.clear(); d->m_selected = nullptr; d->m_mouseGrabItem = nullptr; if ( pages.count() < 1 ) { widget()->resize( 0, 0 ); return; } // show pages containing highlighted text or bookmarked ones //RESTORE THIS int flags = Okular::Settings::filterBookmarks() ? Okular::Page::Bookmark : Okular::Page::Highlight; // if no page matches filter rule, then display all pages QVector< Okular::Page * >::const_iterator pIt = pages.constBegin(), pEnd = pages.constEnd(); bool skipCheck = true; for ( ; pIt != pEnd ; ++pIt ) //if ( (*pIt)->attributes() & flags ) if ( (*pIt)->hasHighlights( SW_SEARCH_ID ) ) skipCheck = false; // generate Thumbnails for the given set of pages const int width = viewport()->width(); int height = 0; int centerHeight = 0; for ( pIt = pages.constBegin(); pIt != pEnd ; ++pIt ) //if ( skipCheck || (*pIt)->attributes() & flags ) if ( skipCheck || (*pIt)->hasHighlights( SW_SEARCH_ID ) ) { ThumbnailWidget * t = new ThumbnailWidget( d, *pIt ); t->move(0, height); // add to the internal queue d->m_thumbnails.push_back( t ); // update total height (asking widget its own height) t->resizeFitWidth( width ); // restoring the previous selected page, if any if ( (*pIt)->number() < prevPage ) { centerHeight = height + t->height() + this->style()->layoutSpacing(QSizePolicy::Frame, QSizePolicy::Frame, Qt::Vertical)/2; } if ( (*pIt)->number() == prevPage ) { d->m_selected = t; d->m_selected->setSelected( true ); centerHeight = height + t->height() / 2; } height += t->height() + this->style()->layoutSpacing(QSizePolicy::Frame, QSizePolicy::Frame, Qt::Vertical); } // update scrollview's contents size (sets scrollbars limits) height -= this->style()->layoutSpacing(QSizePolicy::Frame, QSizePolicy::Frame, Qt::Vertical); widget()->resize( width, height ); // enable scrollbar when there's something to scroll verticalScrollBar()->setEnabled( viewport()->height() < height ); verticalScrollBar()->setValue(centerHeight - viewport()->height() / 2); // request for thumbnail generation d->delayedRequestVisiblePixmaps( 200 ); } void ThumbnailList::notifyCurrentPageChanged( int previousPage, int currentPage ) { Q_UNUSED( previousPage ) // skip notifies for the current page (already selected) if ( d->m_selected && d->m_selected->pageNumber() == currentPage ) return; // deselect previous thumbnail if ( d->m_selected ) d->m_selected->setSelected( false ); d->m_selected = nullptr; // select the page with viewport and ensure it's centered in the view d->m_vectorIndex = 0; QVector::const_iterator tIt = d->m_thumbnails.constBegin(), tEnd = d->m_thumbnails.constEnd(); for ( ; tIt != tEnd; ++tIt ) { if ( (*tIt)->pageNumber() == currentPage ) { d->m_selected = *tIt; d->m_selected->setSelected( true ); if ( Okular::Settings::syncThumbnailsViewport() ) { int yOffset = qMax( viewport()->height() / 4, d->m_selected->height() / 2 ); ensureVisible( 0, d->m_selected->pos().y() + d->m_selected->height()/2, 0, yOffset ); } break; } d->m_vectorIndex++; } } void ThumbnailList::notifyPageChanged( int pageNumber, int changedFlags ) { static const int interestingFlags = DocumentObserver::Pixmap | DocumentObserver::Bookmark | DocumentObserver::Highlights | DocumentObserver::Annotations; // only handle change notifications we are interested in if ( !( changedFlags & interestingFlags ) ) return; // iterate over visible items: if page(pageNumber) is one of them, repaint it QList::const_iterator vIt = d->m_visibleThumbnails.constBegin(), vEnd = d->m_visibleThumbnails.constEnd(); for ( ; vIt != vEnd; ++vIt ) if ( (*vIt)->pageNumber() == pageNumber ) { (*vIt)->update(); break; } } void ThumbnailList::notifyContentsCleared( int changedFlags ) { // if pixmaps were cleared, re-ask them if ( changedFlags & DocumentObserver::Pixmap ) d->slotRequestVisiblePixmaps(); } void ThumbnailList::notifyVisibleRectsChanged() { bool found = false; const QVector & visibleRects = d->m_document->visiblePageRects(); QVector::const_iterator tIt = d->m_thumbnails.constBegin(), tEnd = d->m_thumbnails.constEnd(); QVector::const_iterator vEnd = visibleRects.end(); for ( ; tIt != tEnd; ++tIt ) { found = false; QVector::const_iterator vIt = visibleRects.begin(); for ( ; ( vIt != vEnd ) && !found; ++vIt ) { if ( (*tIt)->pageNumber() == (*vIt)->pageNumber ) { (*tIt)->setVisibleRect( (*vIt)->rect ); found = true; } } if ( !found ) { (*tIt)->setVisibleRect( Okular::NormalizedRect() ); } } } bool ThumbnailList::canUnloadPixmap( int pageNumber ) const { // if the thumbnail 'pageNumber' is one of the visible ones, forbid unloading QList::const_iterator vIt = d->m_visibleThumbnails.constBegin(), vEnd = d->m_visibleThumbnails.constEnd(); for ( ; vIt != vEnd; ++vIt ) if ( (*vIt)->pageNumber() == pageNumber ) return false; // if hidden permit unloading return true; } //END DocumentObserver inherited methods void ThumbnailList::updateWidgets() { // Update all visible widgets QList::const_iterator vIt = d->m_visibleThumbnails.constBegin(), vEnd = d->m_visibleThumbnails.constEnd(); for ( ; vIt != vEnd; ++vIt ) { ThumbnailWidget * t = *vIt; t->update(); } } int ThumbnailListPrivate::getNewPageOffset(int n, ThumbnailListPrivate::ChangePageDirection dir) const { int reason = 1; int facingFirst = 0; //facingFirstCentered cornercase if ( Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Facing ) reason = 2; else if (Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::FacingFirstCentered ) { facingFirst = 1; reason = 2; } else if ( Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Summary ) reason = 3; if ( dir == ThumbnailListPrivate::Up ) { if ( facingFirst && n == 1 ) return -1; return -reason; } if ( dir == ThumbnailListPrivate::Down ) return reason; if ( dir == ThumbnailListPrivate::Left && reason > 1 && (n + facingFirst) % reason ) return -1; if ( dir == ThumbnailListPrivate::Right && reason > 1 && (n + 1 + facingFirst) % reason ) return 1; return 0; } ThumbnailWidget *ThumbnailListPrivate::getThumbnailbyOffset(int current, int offset) const { QVector::const_iterator it = m_thumbnails.begin(); QVector::const_iterator itE = m_thumbnails.end(); int idx = 0; while ( it != itE ) { if ( (*it)->pageNumber() == current ) break; ++idx; ++it; } if ( it == itE ) return nullptr; idx += offset; if ( idx < 0 || idx >= m_thumbnails.size() ) return nullptr; return m_thumbnails[idx]; } -ThumbnailListPrivate::ChangePageDirection ThumbnailListPrivate::forwardTrack(const QPoint &point, const QSize &r ) +ThumbnailListPrivate::ChangePageDirection ThumbnailListPrivate::forwardTrack(const QPoint point, const QSize r ) { Okular::DocumentViewport vp = m_document->viewport(); const double deltaX = (double)point.x() / r.width(), deltaY = (double)point.y() / r.height(); vp.rePos.normalizedX -= deltaX; vp.rePos.normalizedY -= deltaY; if ( vp.rePos.normalizedY > 1.0 ) return ThumbnailListPrivate::Down; if ( vp.rePos.normalizedY < 0.0 ) return ThumbnailListPrivate::Up; if ( vp.rePos.normalizedX > 1.0 ) return ThumbnailListPrivate::Right; if ( vp.rePos.normalizedX < 0.0 ) return ThumbnailListPrivate::Left; vp.rePos.enabled = true; m_document->setViewport( vp ); return ThumbnailListPrivate::Null; } const QPixmap * ThumbnailListPrivate::getBookmarkOverlay() const { return m_bookmarkOverlay; } void ThumbnailList::slotFilterBookmarks( bool filterOn ) { // save state Okular::Settings::setFilterBookmarks( filterOn ); Okular::Settings::self()->save(); // ask for the 'notifySetup' with a little trick (on reinsertion the // document sends the list again) d->m_document->removeObserver( this ); d->m_document->addObserver( this ); } //BEGIN widget events void ThumbnailList::keyPressEvent( QKeyEvent * keyEvent ) { - if ( d->m_thumbnails.count() < 1 ) - return keyEvent->ignore(); + if ( d->m_thumbnails.count() < 1 ) { + keyEvent->ignore(); + return; + } int nextPage = -1; if ( keyEvent->key() == Qt::Key_Up ) { if ( !d->m_selected ) nextPage = 0; else if ( d->m_vectorIndex > 0 ) nextPage = d->m_thumbnails[ d->m_vectorIndex - 1 ]->pageNumber(); } else if ( keyEvent->key() == Qt::Key_Down ) { if ( !d->m_selected ) nextPage = 0; else if ( d->m_vectorIndex < (int)d->m_thumbnails.count() - 1 ) nextPage = d->m_thumbnails[ d->m_vectorIndex + 1 ]->pageNumber(); } else if ( keyEvent->key() == Qt::Key_PageUp ) verticalScrollBar()->triggerAction( QScrollBar::SliderPageStepSub ); else if ( keyEvent->key() == Qt::Key_PageDown ) verticalScrollBar()->triggerAction( QScrollBar::SliderPageStepAdd ); else if ( keyEvent->key() == Qt::Key_Home ) nextPage = d->m_thumbnails[ 0 ]->pageNumber(); else if ( keyEvent->key() == Qt::Key_End ) nextPage = d->m_thumbnails[ d->m_thumbnails.count() - 1 ]->pageNumber(); - if ( nextPage == -1 ) - return keyEvent->ignore(); + if ( nextPage == -1 ) { + keyEvent->ignore(); + return; + } keyEvent->accept(); if ( d->m_selected ) d->m_selected->setSelected( false ); d->m_selected = nullptr; d->m_document->setViewportPage( nextPage ); } bool ThumbnailList::viewportEvent( QEvent * e ) { switch ( e->type() ) { case QEvent::Resize: { d->viewportResizeEvent( (QResizeEvent*)e ); break; } default: ; } return QScrollArea::viewportEvent( e ); } void ThumbnailListPrivate::viewportResizeEvent( QResizeEvent * e ) { if ( m_thumbnails.count() < 1 || width() < 1 ) return; // if width changed resize all the Thumbnails, reposition them to the // right place and recalculate the contents area if ( e->size().width() != e->oldSize().width() ) { // runs the timer avoiding a thumbnail regeneration by 'contentsMoving' delayedRequestVisiblePixmaps( 2000 ); // resize and reposition items const int newWidth = q->viewport()->width(); int newHeight = 0; QVector::const_iterator tIt = m_thumbnails.constBegin(), tEnd = m_thumbnails.constEnd(); for ( ; tIt != tEnd; ++tIt ) { ThumbnailWidget *t = *tIt; t->move(0, newHeight); t->resizeFitWidth( newWidth ); newHeight += t->height() + this->style()->layoutSpacing(QSizePolicy::Frame, QSizePolicy::Frame, Qt::Vertical); } // update scrollview's contents size (sets scrollbars limits) newHeight -= this->style()->layoutSpacing(QSizePolicy::Frame, QSizePolicy::Frame, Qt::Vertical); const int oldHeight = q->widget()->height(); const int oldYCenter = q->verticalScrollBar()->value() + q->viewport()->height() / 2; q->widget()->resize( newWidth, newHeight ); // enable scrollbar when there's something to scroll q->verticalScrollBar()->setEnabled( q->viewport()->height() < newHeight ); // ensure that what was visible before remains visible now q->ensureVisible( 0, int( (qreal)oldYCenter * q->widget()->height() / oldHeight ), 0, q->viewport()->height() / 2 ); } else if ( e->size().height() <= e->oldSize().height() ) return; // invalidate the bookmark overlay if ( m_bookmarkOverlay ) { delete m_bookmarkOverlay; m_bookmarkOverlay = nullptr; } // update Thumbnails since width has changed or height has increased delayedRequestVisiblePixmaps( 500 ); } //END widget events //BEGIN internal SLOTS void ThumbnailListPrivate::slotRequestVisiblePixmaps() { // if an update is already scheduled or the widget is hidden, don't proceed if ( ( m_delayTimer && m_delayTimer->isActive() ) || q->isHidden() ) return; // scroll from the top to the last visible thumbnail m_visibleThumbnails.clear(); QLinkedList< Okular::PixmapRequest * > requestedPixmaps; QVector::const_iterator tIt = m_thumbnails.constBegin(), tEnd = m_thumbnails.constEnd(); const QRect viewportRect = q->viewport()->rect().translated( q->horizontalScrollBar()->value(), q->verticalScrollBar()->value() ); for ( ; tIt != tEnd; ++tIt ) { ThumbnailWidget * t = *tIt; const QRect thumbRect = t->rect(); if ( !thumbRect.intersects( viewportRect ) ) continue; // add ThumbnailWidget to visible list m_visibleThumbnails.push_back( t ); // if pixmap not present add it to requests if ( !t->page()->hasPixmap( q, t->pixmapWidth(), t->pixmapHeight() ) ) { Okular::PixmapRequest * p = new Okular::PixmapRequest( q, t->pageNumber(), t->pixmapWidth(), t->pixmapHeight(), THUMBNAILS_PRIO, Okular::PixmapRequest::Asynchronous ); requestedPixmaps.push_back( p ); } } // actually request pixmaps if ( !requestedPixmaps.isEmpty() ) m_document->requestPixmaps( requestedPixmaps ); } void ThumbnailListPrivate::slotDelayTimeout() { // resize the bookmark overlay delete m_bookmarkOverlay; const int expectedWidth = q->viewport()->width() / 4; if ( expectedWidth > 10 ) m_bookmarkOverlay = new QPixmap( QIcon::fromTheme( QStringLiteral("bookmarks") ).pixmap( expectedWidth ) ); else m_bookmarkOverlay = nullptr; // request pixmaps slotRequestVisiblePixmaps(); } //END internal SLOTS void ThumbnailListPrivate::delayedRequestVisiblePixmaps( int delayMs ) { if ( !m_delayTimer ) { m_delayTimer = new QTimer( q ); m_delayTimer->setSingleShot( true ); connect( m_delayTimer, &QTimer::timeout, this, &ThumbnailListPrivate::slotDelayTimeout ); } m_delayTimer->start( delayMs ); } /** ThumbnailWidget implementation **/ ThumbnailWidget::ThumbnailWidget( ThumbnailListPrivate * parent, const Okular::Page * page ) : m_parent( parent ), m_page( page ), m_selected( false ), m_pixmapWidth( 10 ), m_pixmapHeight( 10 ) { m_labelNumber = m_page->number() + 1; m_labelHeight = QFontMetrics( m_parent->font() ).height(); } void ThumbnailWidget::resizeFitWidth( int width ) { m_pixmapWidth = width - m_margin; m_pixmapHeight = qRound( m_page->ratio() * (double)m_pixmapWidth ); m_rect.setSize( QSize( width, heightHint() ) ); } void ThumbnailWidget::setSelected( bool selected ) { // update selected state if ( m_selected != selected ) { m_selected = selected; update(); } } void ThumbnailWidget::setVisibleRect( const Okular::NormalizedRect & rect ) { if ( rect == m_visibleRect ) return; m_visibleRect = rect; update(); } void ThumbnailListPrivate::mousePressEvent( QMouseEvent * e ) { ThumbnailWidget* item = itemFor( e->pos() ); - if ( !item ) // mouse on the spacing between items - return e->ignore(); + if ( !item ) { // mouse on the spacing between items + e->ignore(); + return; + } const QRect r = item->visibleRect(); const int margin = ThumbnailWidget::margin(); const QPoint p = e->pos() - item->pos(); if ( e->button() != Qt::RightButton && r.contains( p - QPoint( margin / 2, margin / 2 ) ) ) { m_mouseGrabPos.setX( 0 ); m_mouseGrabPos.setY( 0 ); m_mouseGrabItem = item; m_pageCurrentlyGrabbed = item->pageNumber(); m_mouseGrabItem = item; } else { m_mouseGrabPos.setX( 0 ); m_mouseGrabPos.setY( 0 ); m_mouseGrabItem = nullptr; } } void ThumbnailListPrivate::mouseReleaseEvent( QMouseEvent * e ) { ThumbnailWidget* item = itemFor( e->pos() ); m_mouseGrabItem = item; - if ( !item ) // mouse on the spacing between items - return e->ignore(); + if ( !item ) { // mouse on the spacing between items + e->ignore(); + return; + } QRect r = item->visibleRect(); const QPoint p = e->pos() - item->pos(); // jump center of viewport to cursor if it wasn't dragged if ( m_mouseGrabPos.isNull() ) { r = item->visibleRect(); Okular::DocumentViewport vp = Okular::DocumentViewport( item->pageNumber() ); vp.rePos.normalizedX = double(p.x()) / double(item->rect().width()); vp.rePos.normalizedY = double(p.y()) / double(item->rect().height()); vp.rePos.pos = Okular::DocumentViewport::Center; vp.rePos.enabled = true; m_document->setViewport( vp, nullptr, true); } setCursor( Qt::OpenHandCursor ); m_mouseGrabPos.setX( 0 ); m_mouseGrabPos.setY( 0 ); } void ThumbnailListPrivate::mouseMoveEvent( QMouseEvent * e ) { if ( e->buttons() == Qt::NoButton ) { ThumbnailWidget* item = itemFor( e->pos() ); - if ( !item ) // mouse on the spacing between items - return e->ignore(); + if ( !item ) { // mouse on the spacing between items + e->ignore(); + return; + } QRect r = item->visibleRect(); const int margin = ThumbnailWidget::margin(); const QPoint p = e->pos() - item->pos(); if ( r.contains( p - QPoint( margin / 2, margin / 2 ) ) ) { setCursor( Qt::OpenHandCursor ); } else { setCursor( Qt::ArrowCursor ); } - return e->ignore(); + e->ignore(); + return; } // no item under the mouse or previously selected - if ( !m_mouseGrabItem ) - return e->ignore(); + if ( !m_mouseGrabItem ) { + e->ignore(); + return; + } const QRect r = m_mouseGrabItem->rect(); if ( !m_mouseGrabPos.isNull() ) { const QPoint mousePos = e->pos(); const QPoint delta = m_mouseGrabPos - mousePos; m_mouseGrabPos = e->pos(); // don't handle the mouse move, forward it to the thumbnail list ThumbnailListPrivate::ChangePageDirection direction; if (( direction = forwardTrack( delta, r.size() )) != ThumbnailListPrivate::Null ) { // Changing the selected page const int offset = getNewPageOffset( m_pageCurrentlyGrabbed, direction ); const ThumbnailWidget *newThumb = getThumbnailbyOffset( m_pageCurrentlyGrabbed, offset ); if ( !newThumb ) return; int newPageOn = newThumb->pageNumber(); if ( newPageOn == m_pageCurrentlyGrabbed || newPageOn < 0 || newPageOn >= (int)m_document->pages() ) { return; } Okular::DocumentViewport vp = m_document->viewport(); const float origNormalX = vp.rePos.normalizedX; const float origNormalY = vp.rePos.normalizedY; vp = Okular::DocumentViewport( newPageOn ); vp.rePos.normalizedX = origNormalX; vp.rePos.normalizedY = origNormalY; if ( direction == ThumbnailListPrivate::Up ) { vp.rePos.normalizedY = 1.0; if ( Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::FacingFirstCentered && !newPageOn) { if ( m_pageCurrentlyGrabbed == 1 ) vp.rePos.normalizedX = origNormalX - 0.5; else vp.rePos.normalizedX = origNormalX + 0.5; if ( vp.rePos.normalizedX < 0.0 ) vp.rePos.normalizedX = 0.0; else if ( vp.rePos.normalizedX > 1.0 ) vp.rePos.normalizedX = 1.0; } } else if ( direction == ThumbnailListPrivate::Down ) { vp.rePos.normalizedY = 0.0; if ( Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::FacingFirstCentered && !m_pageCurrentlyGrabbed) { if ( origNormalX < 0.5 ) { vp = Okular::DocumentViewport( --newPageOn ); vp.rePos.normalizedX = origNormalX + 0.5; } else vp.rePos.normalizedX = origNormalX - 0.5; if ( vp.rePos.normalizedX < 0.0 ) vp.rePos.normalizedX = 0.0; else if ( vp.rePos.normalizedX > 1.0 ) vp.rePos.normalizedX = 1.0; } } else if ( Okular::Settings::viewMode() != Okular::Settings::EnumViewMode::Single ) { if ( direction == ThumbnailListPrivate::Left ) vp.rePos.normalizedX = 1.0; else vp.rePos.normalizedX = 0.0; } vp.rePos.pos = Okular::DocumentViewport::Center; vp.rePos.enabled = true; m_document->setViewport( vp ); m_mouseGrabPos.setX( 0 ); m_mouseGrabPos.setY( 0 ); m_pageCurrentlyGrabbed = newPageOn; m_mouseGrabItem = getPageByNumber( m_pageCurrentlyGrabbed ); } // wrap mouse from top to bottom const QRect mouseContainer = QApplication::desktop()->screenGeometry( this ); QPoint currentMousePos = QCursor::pos(); if ( currentMousePos.y() <= mouseContainer.top() + 4 ) { currentMousePos.setY( mouseContainer.bottom() - 5 ); QCursor::setPos( currentMousePos ); m_mouseGrabPos.setX( 0 ); m_mouseGrabPos.setY( 0 ); } // wrap mouse from bottom to top else if ( currentMousePos.y() >= mouseContainer.bottom() - 4 ) { currentMousePos.setY( mouseContainer.top() + 5 ); QCursor::setPos( currentMousePos ); m_mouseGrabPos.setX( 0 ); m_mouseGrabPos.setY( 0 ); } } else { setCursor( Qt::ClosedHandCursor ); m_mouseGrabPos = e->pos(); } } void ThumbnailListPrivate::wheelEvent( QWheelEvent * e ) { const ThumbnailWidget* item = itemFor( e->pos() ); - if ( !item ) // wheeling on the spacing between items - return e->ignore(); + if ( !item ) { // wheeling on the spacing between items + e->ignore(); + return; + } const QRect r = item->visibleRect(); const int margin = ThumbnailWidget::margin(); if ( r.contains( e->pos() - QPoint( margin / 2, margin / 2 ) ) && e->orientation() == Qt::Vertical && e->modifiers() == Qt::ControlModifier ) { m_document->setZoom( e->angleDelta().y() ); } else { e->ignore(); } } void ThumbnailListPrivate::contextMenuEvent( QContextMenuEvent * e ) { const ThumbnailWidget* item = itemFor( e->pos() ); if ( item ) { emit q->rightClick( item->page(), e->globalPos() ); } } -void ThumbnailWidget::paint( QPainter &p, const QRect &_clipRect ) +void ThumbnailWidget::paint( QPainter &p, const QRect _clipRect ) { const int width = m_pixmapWidth + m_margin; QRect clipRect = _clipRect; const QPalette pal = m_parent->palette(); // draw the bottom label + highlight mark const QColor fillColor = m_selected ? pal.color( QPalette::Active, QPalette::Highlight ) : pal.color( QPalette::Active, QPalette::Base ); p.fillRect( clipRect, fillColor ); p.setPen( m_selected ? pal.color( QPalette::Active, QPalette::HighlightedText ) : pal.color( QPalette::Active, QPalette::Text ) ); p.drawText( 0, m_pixmapHeight + (m_margin - 3), width, m_labelHeight, Qt::AlignCenter, QString::number( m_labelNumber ) ); // draw page outline and pixmap if ( clipRect.top() < m_pixmapHeight + m_margin ) { // if page is bookmarked draw a colored border const bool isBookmarked = m_parent->m_document->bookmarkManager()->isBookmarked( pageNumber() ); // draw the inner rect p.setPen( isBookmarked ? QColor( 0xFF8000 ) : Qt::black ); p.drawRect( m_margin/2 - 1, m_margin/2 - 1, m_pixmapWidth + 1, m_pixmapHeight + 1 ); // draw the clear rect p.setPen( isBookmarked ? QColor( 0x804000 ) : pal.color( QPalette::Active, QPalette::Base ) ); // draw the bottom and right shadow edges if ( !isBookmarked ) { int left, right, bottom, top; left = m_margin/2 + 1; right = m_margin/2 + m_pixmapWidth + 1; bottom = m_pixmapHeight + m_margin/2 + 1; top = m_margin/2 + 1; p.setPen( Qt::gray ); p.drawLine( left, bottom, right, bottom ); p.drawLine( right, top, right, bottom ); } // draw the page using the shared PagePainter class p.translate( m_margin/2.0, m_margin/2.0 ); clipRect.translate( -m_margin/2, -m_margin/2 ); clipRect = clipRect.intersected( QRect( 0, 0, m_pixmapWidth, m_pixmapHeight ) ); if ( clipRect.isValid() ) { int flags = PagePainter::Accessibility | PagePainter::Highlights | PagePainter::Annotations; PagePainter::paintPageOnPainter( &p, m_page, m_parent->q, flags, m_pixmapWidth, m_pixmapHeight, clipRect ); } if ( !m_visibleRect.isNull() ) { p.save(); p.setPen( QColor( 255, 255, 0, 200 ) ); p.setBrush( QColor( 0, 0, 0, 100 ) ); p.drawRect( m_visibleRect.geometry( m_pixmapWidth, m_pixmapHeight ).adjusted( 0, 0, -1, -1 ) ); p.restore(); } // draw the bookmark overlay on the top-right corner const QPixmap * bookmarkPixmap = m_parent->getBookmarkOverlay(); if ( isBookmarked && bookmarkPixmap ) { int pixW = bookmarkPixmap->width(), pixH = bookmarkPixmap->height(); clipRect = clipRect.intersected( QRect( m_pixmapWidth - pixW, 0, pixW, pixH ) ); if ( clipRect.isValid() ) p.drawPixmap( m_pixmapWidth - pixW, -pixH/8, *bookmarkPixmap ); } } } /** ThumbnailsController implementation **/ #define FILTERB_ID 1 ThumbnailController::ThumbnailController( QWidget * parent, ThumbnailList * list ) : QToolBar( parent ) { setObjectName( QStringLiteral( "ThumbsControlBar" ) ); // change toolbar appearance setIconSize( QSize( 16, 16 ) ); setMovable( false ); QSizePolicy sp = sizePolicy(); sp.setVerticalPolicy( QSizePolicy::Minimum ); setSizePolicy( sp ); // insert a togglebutton [show only bookmarked pages] //insertSeparator(); QAction * showBoomarkOnlyAction = addAction( QIcon::fromTheme( QStringLiteral("bookmarks") ), i18n( "Show bookmarked pages only" ) ); showBoomarkOnlyAction->setCheckable( true ); connect( showBoomarkOnlyAction, &QAction::toggled, list, &ThumbnailList::slotFilterBookmarks ); showBoomarkOnlyAction->setChecked( Okular::Settings::filterBookmarks() ); //insertLineSeparator(); } #include "moc_thumbnaillist.cpp" /* kate: replace-tabs on; indent-width 4; */ diff --git a/ui/thumbnaillist.h b/ui/thumbnaillist.h index 50c340492..14cb6205e 100644 --- a/ui/thumbnaillist.h +++ b/ui/thumbnaillist.h @@ -1,100 +1,100 @@ /*************************************************************************** * Copyright (C) 2004 by Albert Astals Cid * * * * 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. * ***************************************************************************/ #ifndef _OKULAR_THUMBNAILLIST_H_ #define _OKULAR_THUMBNAILLIST_H_ #include #include #include #include "core/observer.h" class ThumbnailListPrivate; namespace Okular { class Document; } /** * @short A scrollview that displays page pixmap previews (aka thumbnails). * * ... */ class ThumbnailList : public QScrollArea, public Okular::DocumentObserver { Q_OBJECT public: ThumbnailList(QWidget *parent, Okular::Document *document); ~ThumbnailList() override; // inherited: create thumbnails ( inherited as a DocumentObserver ) void notifySetup( const QVector< Okular::Page * > & pages, int setupFlags ) override; // inherited: hilihght current thumbnail ( inherited as DocumentObserver ) void notifyCurrentPageChanged( int previous, int current ) override; // inherited: redraw thumbnail ( inherited as DocumentObserver ) void notifyPageChanged( int pageNumber, int changedFlags ) override; // inherited: request all visible pixmap (due to a global change or so..) void notifyContentsCleared( int changedFlags ) override; // inherited: the visible areas of the page have changed void notifyVisibleRectsChanged() override; // inherited: tell if pixmap is hidden and can be unloaded bool canUnloadPixmap( int pageNumber ) const override; // redraw visible widgets (useful for refreshing contents...) void updateWidgets(); public Q_SLOTS: // these are connected to ThumbnailController buttons void slotFilterBookmarks( bool filterOn ); protected: // scroll up/down the view void keyPressEvent( QKeyEvent * keyEvent ) override; // catch the viewport event and filter them if necessary bool viewportEvent( QEvent * ) override; Q_SIGNALS: - void rightClick( const Okular::Page *, const QPoint & ); + void rightClick( const Okular::Page *, const QPoint ); private: friend class ThumbnailListPrivate; ThumbnailListPrivate *d; }; /** * @short A vertical boxed container with zero size hint (for insertion on left toolbox) */ class ThumbnailsBox : public QWidget { Q_OBJECT public: explicit ThumbnailsBox( QWidget * parent ) : QWidget( parent ) { QVBoxLayout *vbox = new QVBoxLayout(this); vbox->setContentsMargins(0, 0, 0, 0); vbox->setSpacing(0);} QSize sizeHint() const override { return QSize(); } }; /** * @short A toolbar that sets ThumbnailList properties when clicking on items * * This class is the small toolbar that resides in the bottom of the * ThumbnailsBox container (below ThumbnailList and the SearchLine) and * emits signals whenever a button is pressed. A click action results * in invoking some method (or slot) in ThumbnailList. */ class ThumbnailController : public QToolBar { Q_OBJECT public: ThumbnailController( QWidget * parent, ThumbnailList * thumbnailList ); }; #endif diff --git a/ui/toc.h b/ui/toc.h index e93f3bd78..7a15757d3 100644 --- a/ui/toc.h +++ b/ui/toc.h @@ -1,75 +1,75 @@ /*************************************************************************** * Copyright (C) 2004-2006 by Albert Astals Cid * * * * 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. * ***************************************************************************/ #ifndef _OKULAR_TOC_H_ #define _OKULAR_TOC_H_ #include #include "core/observer.h" #include "core/document.h" #include #include "okularpart_export.h" class QModelIndex; class QTreeView; class KTreeViewSearchLine; class TOCModel; namespace Okular { class Document; class PartTest; } class OKULARPART_EXPORT TOC : public QWidget, public Okular::DocumentObserver { Q_OBJECT friend class Okular::PartTest; public: TOC(QWidget *parent, Okular::Document *document); ~TOC() override; // inherited from DocumentObserver void notifySetup( const QVector< Okular::Page * > & pages, int setupFlags ) override; void notifyCurrentPageChanged( int previous, int current ) override; void reparseConfig(); void prepareForReload(); void rollbackReload(); void finishReload(); public Q_SLOTS: void expandRecursively(); void collapseRecursively(); void expandAll(); void collapseAll(); Q_SIGNALS: void hasTOC(bool has); - void rightClick( const Okular::DocumentViewport &, const QPoint &, const QString & ); + void rightClick( const Okular::DocumentViewport &, const QPoint , const QString & ); private Q_SLOTS: void slotExecuted( const QModelIndex & ); void saveSearchOptions(); protected: void contextMenuEvent( QContextMenuEvent * e ) override; private: QVector expandedNodes( const QModelIndex & parent=QModelIndex() ) const; Okular::Document *m_document; QTreeView *m_treeView; KTreeViewSearchLine *m_searchLine; TOCModel *m_model; }; #endif