okular/ui/pageviewannotator.cpp
Dileep Sankhla e44ff38706 Support setting text color for typewriter annotations
Summary:
Changing typewriter text color can be done in the typewriter properties dialog, or programmatically via new okular API methods TextAnnotation::textColor and TextAnnotation::setTextColor.

poppler >= 0.69 is required to store text color natively inside PDF documents. For other document types, text color is stored as metadata inside the document archive.

This work was done during GSoC 2018. See https://community.kde.org/GSoC/2018/StatusReports/DileepSankhla for details.

Test Plan:
- properties dialog of typewriter annotation has "Font Color" picker
- saving to PDF results in <r> <g> <b> rg operation in /DA
- saving to archive results in fontColor="rrggbb" attribute in metadata.xml

Reviewers: sander

Reviewed By: sander

Subscribers: kde-doc-english, sander, okular-devel

Tags: #okular, #documentation

Differential Revision: https://phabricator.kde.org/D15205
2018-10-04 20:02:55 +02:00

1280 lines
51 KiB
C++

/***************************************************************************
* Copyright (C) 2005 by Enrico Ros <eros.kde@email.it> *
* *
* 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 <qloggingcategory.h>
#include <qapplication.h>
#include <qfile.h>
#include <qcolor.h>
#include <qevent.h>
#include <qlist.h>
#include <qpainter.h>
#include <qset.h>
#include <qvariant.h>
#include <QInputDialog>
#include <kiconloader.h>
#include <KLocalizedString>
#include <kuser.h>
#include <QDebug>
#include <qmenu.h>
// system includes
#include <math.h>
#include <memory>
#include <QStandardPaths>
// 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, QSize( size, 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 = size / xscale;
const double stampyscale = size / 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.light(), 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<Okular::NormalizedPoint> 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() );
}
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<Okular::NormalizedPoint> 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<Okular::RegularAreaRect> newselection( m_pageView->textSelectionForItem( item(), start, end ) );
if ( newselection && !newselection->isEmpty() )
{
const QList<QRect> geom = newselection->geometry( (int)xScale, (int)yScale );
QRect newrect;
Q_FOREACH ( 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 );
foreach( const Okular::NormalizedRect & r, *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() ) );
foreach ( const Okular::NormalizedRect & r, *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<Okular::RegularAreaRect> 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 <tool></tool> elements
const QStringList userTools = Okular::Settings::annotationTools();
// Populate m_toolsDefinition
QDomDocument doc;
m_toolsDefinition = doc.createElement( QStringLiteral("annotatingTools") );
foreach ( 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 on )
{
if ( !on )
{
// 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 )
{
// 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 QVector<QRect> rects = compoundRegion.united( m_lastDrawnRect ).rects();
const QPoint areaPos = m_pageView->contentAreaPosition();
for ( int i = 0; i < rects.count(); i++ )
m_pageView->viewport()->update( rects[i].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
QList< Okular::Annotation* > annotations = m_engine->end();
// attach the newly filled annotations to the page
foreach ( 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 )
{
// 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
{
return m_engine && m_toolBar && wantedRect.intersects( m_lastDrawnRect ) && m_lockedItem;
}
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 );
}
// consistancy 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 = "@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<qreal>() << 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, QSize( 16, 16 ) );
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; */