QGIS/tests/src/core/testqgslayoutmultiframe.cpp
Nyall Dawson 49eaebbf40 Remove outdated icon support for item metadata
Now handled in QgsLayoutItem directly
2018-01-05 11:15:03 +10:00

561 lines
19 KiB
C++

/***************************************************************************
testqgslayoutmultiframe.cpp
---------------------------
begin : October 2017
copyright : (C) 2017 by Nyall Dawson
email : nyall dot dawson at gmail dot 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 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#include "qgslayoutframe.h"
#include "qgslayoutmultiframe.h"
#include "qgslayoutitemlabel.h"
#include "qgslayout.h"
#include "qgsmultirenderchecker.h"
#include "qgsapplication.h"
#include "qgsproject.h"
#include "qgslayoutitemhtml.h"
#include "qgslayoutpagecollection.h"
#include "qgslayoutundostack.h"
#include <QObject>
#include "qgstest.h"
class TestQgsLayoutMultiFrame : public QObject
{
Q_OBJECT
private slots:
void initTestCase();// will be called before the first testfunction is executed.
void cleanupTestCase();// will be called after the last testfunction was executed.
void init();// will be called before each testfunction is executed.
void cleanup();// will be called after every testfunction.
void layoutMethods();
void addFrame(); //test creating new frame inherits all properties of existing frame
void displayName();
void frameIsEmpty(); //test if frame is empty works
void addRemovePage(); //test if page is added and removed for RepeatUntilFinished mode
void undoRedo(); //test that combinations of frame/multiframe undo/redo don't crash
void undoRedoRemovedFrame(); //test that undo doesn't crash with removed frames
void undoRedoRemovedFrame2();
void registry();
private:
QgsLayout *mLayout = nullptr;
QString mReport;
};
class TestMultiFrame : public QgsLayoutMultiFrame
{
Q_OBJECT
public:
TestMultiFrame( QgsLayout *layout )
: QgsLayoutMultiFrame( layout )
{
}
int type() const override
{
return QgsLayoutItemRegistry::PluginItem + 1;
}
void render( QgsRenderContext &, const QRectF &, int,
const QStyleOptionGraphicsItem * ) override
{
}
QSizeF totalSize() const override
{
return QSizeF();
}
};
void TestQgsLayoutMultiFrame::initTestCase()
{
QgsApplication::init();
QgsApplication::initQgis();
mLayout = new QgsLayout( QgsProject::instance() );
mLayout->initializeDefaults();
mReport = QStringLiteral( "<h1>Layout MultiFrame Tests</h1>\n" );
}
void TestQgsLayoutMultiFrame::cleanupTestCase()
{
delete mLayout;
QString myReportFile = QDir::tempPath() + "/qgistest.html";
QFile myFile( myReportFile );
if ( myFile.open( QIODevice::WriteOnly | QIODevice::Append ) )
{
QTextStream myQTextStream( &myFile );
myQTextStream << mReport;
myFile.close();
}
QgsApplication::exitQgis();
}
void TestQgsLayoutMultiFrame::init()
{
}
void TestQgsLayoutMultiFrame::cleanup()
{
}
void TestQgsLayoutMultiFrame::layoutMethods()
{
QgsLayout *l = new QgsLayout( QgsProject::instance() );
QVERIFY( l->multiFrames().empty() );
l->addMultiFrame( nullptr );
QVERIFY( l->multiFrames().empty() );
TestMultiFrame *mF = new TestMultiFrame( l );
QPointer< TestMultiFrame > pMF( mF );
QCOMPARE( l->multiFrames().count(), 1 );
QVERIFY( l->multiFrames().contains( mF ) );
QVERIFY( !l->multiFrameByUuid( QString() ) );
QCOMPARE( l->multiFrameByUuid( mF->uuid() ), mF );
// try readding
l->addMultiFrame( mF );
QCOMPARE( l->multiFrames().count(), 1 );
l->removeMultiFrame( nullptr );
QCOMPARE( l->multiFrames().count(), 1 );
QVERIFY( l->multiFrames().contains( mF ) );
TestMultiFrame *mF2 = new TestMultiFrame( l );
QPointer< TestMultiFrame > pMF2( mF2 );
l->addMultiFrame( mF2 );
QCOMPARE( l->multiFrames().count(), 2 );
QVERIFY( l->multiFrames().contains( mF ) );
QVERIFY( l->multiFrames().contains( mF2 ) );
QCOMPARE( l->multiFrameByUuid( mF2->uuid() ), mF2 );
l->removeMultiFrame( mF2 );
QCOMPARE( l->multiFrames().count(), 1 );
QVERIFY( l->multiFrames().contains( mF ) );
QVERIFY( !l->multiFrameByUuid( mF2->uuid() ) );
// should not be deleted
QVERIFY( pMF2 );
delete mF2;
QVERIFY( !pMF2 );
delete l;
QVERIFY( !pMF );
}
void TestQgsLayoutMultiFrame::addFrame()
{
TestMultiFrame *multiframe = new TestMultiFrame( mLayout );
QCOMPARE( multiframe->frameCount(), 0 );
QVERIFY( multiframe->frames().empty() );
QVERIFY( !multiframe->frame( -1 ) );
QVERIFY( !multiframe->frame( 0 ) );
QVERIFY( !multiframe->frame( 1 ) );
QCOMPARE( multiframe->frameIndex( nullptr ), -1 );
multiframe->addFrame( nullptr );
QCOMPARE( multiframe->frameCount(), 0 );
QVERIFY( multiframe->frames().empty() );
QgsLayoutFrame *frame1 = new QgsLayoutFrame( mLayout, multiframe );
QCOMPARE( multiframe->frameIndex( frame1 ), -1 );
frame1->attemptSetSceneRect( QRectF( 0, 0, 100, 200 ) );
multiframe->addFrame( frame1 );
QCOMPARE( multiframe->frameCount(), 1 );
QCOMPARE( multiframe->frames().count(), 1 );
QVERIFY( multiframe->frames().contains( frame1 ) );
QVERIFY( !multiframe->frame( -1 ) );
QCOMPARE( multiframe->frame( 0 ), frame1 );
QVERIFY( !multiframe->frame( 1 ) );
QCOMPARE( multiframe->frameIndex( frame1 ), 0 );
QCOMPARE( frame1->multiFrame(), multiframe );
// deferred set multiframe
QgsLayoutFrame *frame1a = new QgsLayoutFrame( mLayout, nullptr );
QVERIFY( !frame1a->multiFrame() );
multiframe->addFrame( frame1a );
QCOMPARE( frame1a->multiFrame(), multiframe );
//should not be inherited
frame1->setHidePageIfEmpty( true );
//should be inherited
frame1->setHideBackgroundIfEmpty( true );
frame1->setFrameStrokeWidth( QgsLayoutMeasurement( 5.0 ) );
frame1->setFrameJoinStyle( Qt::RoundJoin );
frame1->setFrameEnabled( true );
frame1->setFrameStrokeColor( QColor( Qt::red ) );
frame1->setBackgroundEnabled( true );
frame1->setBackgroundColor( QColor( Qt::green ) );
frame1->setBlendMode( QPainter::CompositionMode_ColorBurn );
frame1->setItemOpacity( 0.5 );
QgsLayoutFrame *frame2 = multiframe->createNewFrame( frame1, QPointF( 50, 55 ), QSizeF( 70, 120 ) );
//check frame created in correct place
QCOMPARE( frame2->rect().height(), 120.0 );
QCOMPARE( frame2->rect().width(), 70.0 );
QCOMPARE( frame2->scenePos().x(), 50.0 );
QCOMPARE( frame2->scenePos().y(), 55.0 );
//check frame properties
QCOMPARE( frame2->frameStrokeWidth(), frame1->frameStrokeWidth() );
QCOMPARE( frame2->frameStrokeColor(), frame1->frameStrokeColor() );
QCOMPARE( frame2->frameJoinStyle(), frame1->frameJoinStyle() );
QCOMPARE( frame2->hasBackground(), frame1->hasBackground() );
QCOMPARE( frame2->backgroundColor(), frame1->backgroundColor() );
QCOMPARE( frame2->blendMode(), frame1->blendMode() );
QCOMPARE( frame2->itemOpacity(), frame1->itemOpacity() );
//check non-inherited properties
QVERIFY( !frame2->hidePageIfEmpty() );
mLayout->removeMultiFrame( multiframe );
delete multiframe;
}
void TestQgsLayoutMultiFrame::displayName()
{
TestMultiFrame *multiframe = new TestMultiFrame( mLayout );
QCOMPARE( multiframe->displayName(), QStringLiteral( "<Multiframe>" ) );
QgsLayoutFrame *frame1 = new QgsLayoutFrame( mLayout, nullptr );
QCOMPARE( frame1->displayName(), QStringLiteral( "<Frame>" ) );
multiframe->addFrame( frame1 );
QCOMPARE( frame1->displayName(), QStringLiteral( "<Multiframe>" ) );
frame1->setId( "my frame" );
QCOMPARE( frame1->displayName(), QStringLiteral( "my frame" ) );
}
void TestQgsLayoutMultiFrame::frameIsEmpty()
{
QgsLayoutItemHtml *htmlItem = new QgsLayoutItemHtml( mLayout );
QgsLayoutFrame *frame1 = new QgsLayoutFrame( mLayout, htmlItem );
frame1->attemptSetSceneRect( QRectF( 0, 0, 100, 200 ) );
QgsLayoutFrame *frame2 = new QgsLayoutFrame( mLayout, htmlItem );
frame2->attemptSetSceneRect( QRectF( 0, 0, 100, 200 ) );
htmlItem->addFrame( frame1 );
htmlItem->addFrame( frame2 );
htmlItem->setContentMode( QgsLayoutItemHtml::ManualHtml );
//short content, so frame 2 should be empty
htmlItem->setHtml( QStringLiteral( "<p><i>Test manual <b>html</b></i></p>" ) );
htmlItem->loadHtml();
QCOMPARE( frame1->isEmpty(), false );
QCOMPARE( frame2->isEmpty(), true );
//long content, so frame 2 should not be empty
htmlItem->setHtml( QStringLiteral( "<p style=\"height: 10000px\"><i>Test manual <b>html</b></i></p>" ) );
htmlItem->loadHtml();
QCOMPARE( frame1->isEmpty(), false );
QCOMPARE( frame2->isEmpty(), false );
//..and back again..
htmlItem->setHtml( QStringLiteral( "<p><i>Test manual <b>html</b></i></p>" ) );
htmlItem->loadHtml();
QCOMPARE( frame1->isEmpty(), false );
QCOMPARE( frame2->isEmpty(), true );
mLayout->removeMultiFrame( htmlItem );
delete htmlItem;
}
void TestQgsLayoutMultiFrame::addRemovePage()
{
QgsLayoutItemHtml *htmlItem = new QgsLayoutItemHtml( mLayout );
QgsLayoutFrame *frame1 = new QgsLayoutFrame( mLayout, htmlItem );
frame1->attemptSetSceneRect( QRectF( 0, 0, 100, 200 ) );
htmlItem->addFrame( frame1 );
htmlItem->setContentMode( QgsLayoutItemHtml::ManualHtml );
htmlItem->setResizeMode( QgsLayoutMultiFrame::RepeatUntilFinished );
//short content, so should fit in one frame
htmlItem->setHtml( QStringLiteral( "<p><i>Test manual <b>html</b></i></p>" ) );
htmlItem->loadHtml();
//should be one page
QCOMPARE( htmlItem->frameCount(), 1 );
QCOMPARE( mLayout->pageCollection()->pageCount(), 1 );
//long content, so we require 3 frames
htmlItem->setHtml( QStringLiteral( "<p style=\"height: 2000px\"><i>Test manual <b>html</b></i></p>" ) );
htmlItem->loadHtml();
QCOMPARE( htmlItem->frameCount(), 3 );
QCOMPARE( mLayout->pageCollection()->pageCount(), 3 );
//..and back again..
htmlItem->setHtml( QStringLiteral( "<p><i>Test manual <b>html</b></i></p>" ) );
htmlItem->loadHtml();
QCOMPARE( htmlItem->frameCount(), 1 );
QCOMPARE( mLayout->pageCollection()->pageCount(), 1 );
//get a bit more complicated - add another item to page 3
QgsLayoutItemLabel *label1 = new QgsLayoutItemLabel( mLayout );
mLayout->addLayoutItem( label1 );
label1->attemptResize( QgsLayoutSize( 50, 50 ), false );
//long content, so we require 4 pages
htmlItem->setHtml( QStringLiteral( "<p style=\"height: 3000px\"><i>Test manual <b>html</b></i></p>" ) );
htmlItem->loadHtml();
QCOMPARE( htmlItem->frameCount(), 4 );
QCOMPARE( mLayout->pageCollection()->pageCount(), 4 );
label1->attemptMove( QgsLayoutPoint( 10, 10 ), true, false, 2 );
//..and back again. Since there's an item on page 3, only page 4 should be removed
htmlItem->setHtml( QStringLiteral( "<p><i>Test manual <b>html</b></i></p>" ) );
htmlItem->loadHtml();
QCOMPARE( htmlItem->frameCount(), 1 );
QCOMPARE( mLayout->pageCollection()->pageCount(), 3 );
mLayout->removeMultiFrame( htmlItem );
delete htmlItem;
}
void TestQgsLayoutMultiFrame::undoRedo()
{
QgsLayoutItemHtml *htmlItem = new QgsLayoutItemHtml( mLayout );
QgsLayoutFrame *frame1 = new QgsLayoutFrame( mLayout, htmlItem );
frame1->attemptSetSceneRect( QRectF( 0, 0, 100, 200 ) );
htmlItem->addFrame( frame1 );
htmlItem->setContentMode( QgsLayoutItemHtml::ManualHtml );
htmlItem->setResizeMode( QgsLayoutMultiFrame::RepeatUntilFinished );
//short content, so should fit in one frame
htmlItem->setHtml( QStringLiteral( "<p>Test content</p>" ) );
htmlItem->loadHtml();
//do some combinations of undo/redo commands for both the frame and multiframe
//to try to trigger a crash
frame1->beginCommand( QStringLiteral( "move" ) );
frame1->attemptSetSceneRect( QRectF( 10, 10, 20, 20 ) );
frame1->endCommand();
frame1->beginCommand( QStringLiteral( "stroke" ), QgsLayoutItem::UndoStrokeWidth );
frame1->setFrameStrokeWidth( QgsLayoutMeasurement( 4.0 ) );
frame1->endCommand();
frame1->beginCommand( QStringLiteral( "stroke" ), QgsLayoutItem::UndoStrokeWidth );
frame1->setFrameStrokeWidth( QgsLayoutMeasurement( 7.0 ) );
frame1->endCommand();
//multiframe commands
htmlItem->beginCommand( QStringLiteral( "maxbreak" ) );
htmlItem->setMaxBreakDistance( 100 );
htmlItem->endCommand();
//another frame command
frame1->beginCommand( QStringLiteral( "bgcolor" ), QgsLayoutItem::UndoOpacity );
frame1->setBackgroundColor( QColor( 255, 255, 0 ) );
frame1->endCommand();
frame1->beginCommand( QStringLiteral( "bgcolor" ), QgsLayoutItem::UndoOpacity );
frame1->setBackgroundColor( QColor( 255, 0, 0 ) );
frame1->endCommand();
//undo changes
//frame bg
mLayout->undoStack()->stack()->undo();
//multiframe max break
mLayout->undoStack()->stack()->undo();
//frame stroke width
mLayout->undoStack()->stack()->undo();
//frame move
mLayout->undoStack()->stack()->undo();
//check result
QCOMPARE( htmlItem->maxBreakDistance(), 10.0 );
QCOMPARE( htmlItem->frame( 0 )->frameStrokeWidth().length(), 0.3 );
QCOMPARE( htmlItem->frame( 0 )->pos(), QPointF( 0, 0 ) );
QCOMPARE( htmlItem->frame( 0 )->backgroundColor(), QColor( 255, 255, 255 ) );
//now redo
//frame move
mLayout->undoStack()->stack()->redo();
//frame stroke width
mLayout->undoStack()->stack()->redo();
//multiframe max break
mLayout->undoStack()->stack()->redo();
//frame bg color
mLayout->undoStack()->stack()->redo();
//check result
QCOMPARE( htmlItem->maxBreakDistance(), 100.0 );
QCOMPARE( htmlItem->frame( 0 )->frameStrokeWidth().length(), 7.0 );
QCOMPARE( htmlItem->frame( 0 )->pos(), QPointF( 10, 10 ) );
QCOMPARE( htmlItem->frame( 0 )->backgroundColor(), QColor( 255, 0, 0 ) );
mLayout->removeMultiFrame( htmlItem );
delete htmlItem;
}
void TestQgsLayoutMultiFrame::undoRedoRemovedFrame()
{
QgsLayoutItemHtml *htmlItem = new QgsLayoutItemHtml( mLayout );
QgsLayoutFrame *frame1 = new QgsLayoutFrame( mLayout, htmlItem );
frame1->attemptSetSceneRect( QRectF( 0, 0, 100, 200 ) );
htmlItem->addFrame( frame1 );
htmlItem->setContentMode( QgsLayoutItemHtml::ManualHtml );
htmlItem->setResizeMode( QgsLayoutMultiFrame::RepeatUntilFinished );
//long content, so should require multiple frames
htmlItem->setHtml( QStringLiteral( "<p style=\"height: 2000px\">Test content</p>" ) );
htmlItem->loadHtml();
QVERIFY( htmlItem->frameCount() > 1 );
//do a command on the first frame
htmlItem->frame( 0 )->beginCommand( QStringLiteral( "stroke" ), QgsLayoutItem::UndoStrokeWidth );
htmlItem->frame( 0 )->setFrameStrokeWidth( QgsLayoutMeasurement( 4.0 ) );
htmlItem->frame( 0 )->endCommand();
//do a command on the second frame
htmlItem->frame( 1 )->beginCommand( QStringLiteral( "stroke" ), QgsLayoutItem::UndoStrokeWidth );
htmlItem->frame( 1 )->setFrameStrokeWidth( QgsLayoutMeasurement( 8.0 ) );
htmlItem->frame( 1 )->endCommand();
//do a multiframe command which removes extra frames
htmlItem->beginCommand( QStringLiteral( "source" ) );
htmlItem->setHtml( QStringLiteral( "<p style=\"height: 20px\">Test content</p>" ) );
htmlItem->endCommand();
//wipes the second frame
htmlItem->loadHtml();
QCOMPARE( htmlItem->frameCount(), 1 );
auto dumpStack = [ = ]
{
#if 0 // for debugging
// dump stack
for ( int i = 0; i < mLayout->undoStack()->stack()->count(); ++i )
{
QgsDebugMsg( QString( "%1: %2 %3" ).arg( i ).arg( mLayout->undoStack()->stack()->command( i )->text(), i + 1 == mLayout->undoStack()->stack()->index() ? QString( "<---" ) : QString() ) );
}
#endif
};
dumpStack();
//undo changes
//multiframe command
mLayout->undoStack()->stack()->undo();
dumpStack();
//frame 2 command
mLayout->undoStack()->stack()->undo();
dumpStack();
//frame 1 command
mLayout->undoStack()->stack()->undo();
dumpStack();
//check result
QVERIFY( htmlItem->frameCount() > 1 );
QCOMPARE( htmlItem->frame( 0 )->frameStrokeWidth().length(), 0.3 );
QCOMPARE( htmlItem->frame( 1 )->frameStrokeWidth().length(), 0.3 );
//now redo
//frame 1 command
mLayout->undoStack()->stack()->redo();
dumpStack();
//frame 2 command
mLayout->undoStack()->stack()->redo();
dumpStack();
//check result
QVERIFY( htmlItem->frameCount() > 1 );
QCOMPARE( htmlItem->frame( 0 )->frameStrokeWidth().length(), 4.0 );
QCOMPARE( htmlItem->frame( 1 )->frameStrokeWidth().length(), 8.0 );
//multiframe command
mLayout->undoStack()->stack()->redo();
QCOMPARE( htmlItem->frameCount(), 1 );
mLayout->removeMultiFrame( htmlItem );
delete htmlItem;
}
void TestQgsLayoutMultiFrame::undoRedoRemovedFrame2()
{
QgsLayoutItemHtml *htmlItem = new QgsLayoutItemHtml( mLayout );
QgsLayoutFrame *frame1 = new QgsLayoutFrame( mLayout, htmlItem );
frame1->attemptSetSceneRect( QRectF( 0, 0, 100, 200 ) );
htmlItem->addFrame( frame1 );
}
void TestQgsLayoutMultiFrame::registry()
{
// test QgsLayoutItemRegistry
QgsLayoutItemRegistry registry;
// empty registry
QVERIFY( !registry.multiFrameMetadata( -1 ) );
QVERIFY( registry.itemTypes().isEmpty() );
QVERIFY( !registry.createMultiFrame( 1, nullptr ) );
auto create = []( QgsLayout * layout )->QgsLayoutMultiFrame*
{
return new TestMultiFrame( layout );
};
auto resolve = []( QVariantMap & props, const QgsPathResolver &, bool )
{
props.clear();
};
QSignalSpy spyTypeAdded( &registry, &QgsLayoutItemRegistry::multiFrameTypeAdded );
QgsLayoutMultiFrameMetadata *metadata = new QgsLayoutMultiFrameMetadata( QgsLayoutItemRegistry::PluginItem + 1, QStringLiteral( "TestMultiFrame" ), create, resolve );
QVERIFY( registry.addLayoutMultiFrameType( metadata ) );
QCOMPARE( spyTypeAdded.count(), 1 );
QCOMPARE( spyTypeAdded.value( 0 ).at( 0 ).toInt(), QgsLayoutItemRegistry::PluginItem + 1 );
QCOMPARE( spyTypeAdded.value( 0 ).at( 1 ).toString(), QStringLiteral( "TestMultiFrame" ) );
// duplicate type id
QVERIFY( !registry.addLayoutMultiFrameType( metadata ) );
QCOMPARE( spyTypeAdded.count(), 1 );
//retrieve metadata
QVERIFY( !registry.multiFrameMetadata( -1 ) );
QCOMPARE( registry.multiFrameMetadata( QgsLayoutItemRegistry::PluginItem + 1 )->visibleName(), QStringLiteral( "TestMultiFrame" ) );
QCOMPARE( registry.itemTypes().count(), 1 );
QCOMPARE( registry.itemTypes().value( QgsLayoutItemRegistry::PluginItem + 1 ), QStringLiteral( "TestMultiFrame" ) );
QgsLayout l( QgsProject::instance() );
QgsLayoutMultiFrame *item = registry.createMultiFrame( QgsLayoutItemRegistry::PluginItem + 1, &l );
QVERIFY( item );
QVERIFY( dynamic_cast< TestMultiFrame *>( item ) );
QVariantMap props;
props.insert( QStringLiteral( "a" ), 5 );
registry.resolvePaths( 1, props, QgsPathResolver(), true );
QCOMPARE( props.size(), 1 );
registry.resolvePaths( QgsLayoutItemRegistry::PluginItem + 1, props, QgsPathResolver(), true );
QVERIFY( props.isEmpty() );
}
QGSTEST_MAIN( TestQgsLayoutMultiFrame )
#include "testqgslayoutmultiframe.moc"