diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 54a45f949c..fd4681f5e6 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -98,6 +98,7 @@ jobs: with: build_dir: "build" lgtm_comment_body: '' + exclude: 'cppcoreguidelines-avoid-magic-numbers,bugprone-easily-swappable-parameters,misc-non-private-member-variables-in-classes,cppcoreguidelines-macro-usage' - name: Package run: | diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 3fb9d908c5..0da1545fb9 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -45,6 +45,8 @@ set(QFIELD_CORE_SRCS deltalistmodel.cpp digitizinglogger.cpp distancearea.cpp + drawingcanvas.cpp + drawingtemplatemodel.cpp expressionevaluator.cpp expressionvariablemodel.cpp featurechecklistmodel.cpp @@ -152,6 +154,8 @@ set(QFIELD_CORE_HDRS deltalistmodel.h digitizinglogger.h distancearea.h + drawingcanvas.h + drawingtemplatemodel.h expressionevaluator.h expressionvariablemodel.h featurechecklistmodel.h diff --git a/src/core/drawingcanvas.cpp b/src/core/drawingcanvas.cpp new file mode 100644 index 0000000000..7a8074cdf9 --- /dev/null +++ b/src/core/drawingcanvas.cpp @@ -0,0 +1,327 @@ +/*************************************************************************** + drawingcanvas.cpp - DrawingCanvas + + --------------------- + begin : 24.03.2024 + copyright : (C) 2024 by Mathieu Pellerin + email : mathieu (at) opengis.ch + *************************************************************************** + * * + * 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 "drawingcanvas.h" + +#include +#include +#include + +#include + +DrawingCanvas::DrawingCanvas( QQuickItem *parent ) + : QQuickPaintedItem( parent ) +{ + setOpaquePainting( true ); +} + +void DrawingCanvas::createBlankCanvas( int width, int height, QColor backgroundColor ) +{ + mBackgroundImage = QImage( QSize( width, height ), QImage::Format_ARGB32 ); + mBackgroundImage.fill( backgroundColor ); + + mDrawingImage = QImage( QSize( width, height ), QImage::Format_ARGB32 ); + mDrawingImage.fill( Qt::transparent ); + + setIsEmpty( false ); + setIsDirty( false ); + fitCanvas(); +} + +void DrawingCanvas::createCanvasFromImage( const QString &path ) +{ + mBackgroundImage = QImage( path.startsWith( QStringLiteral( "file://" ) ) ? path.mid( 7 ) : path ); + + if ( !mBackgroundImage.isNull() ) + { + if ( mBackgroundImage.format() != QImage::Format_ARGB32 ) + { + mBackgroundImage.convertTo( QImage::Format_ARGB32 ); + } + + mDrawingImage = QImage( mBackgroundImage.size(), QImage::Format_ARGB32 ); + mDrawingImage.fill( Qt::transparent ); + setIsEmpty( false ); + } + else + { + mDrawingImage = QImage(); + setIsEmpty( false ); + } + + setIsDirty( false ); + fitCanvas(); +} + +void DrawingCanvas::clear() +{ + mBackgroundImage = QImage(); + mDrawingImage = QImage(); + + setZoomFactor( 1.0 ); + setOffset( QPointF( 0, 0 ) ); + setIsDirty( false ); + setIsEmpty( true ); + + update(); +} + +QString DrawingCanvas::save() const +{ + QImage image( mBackgroundImage.size(), QImage::Format_ARGB32 ); + image.fill( Qt::transparent ); + + QPainter painter( &image ); + painter.drawImage( 0, 0, mBackgroundImage ); + painter.drawImage( 0, 0, mDrawingImage ); + + QString path = QStandardPaths::writableLocation( QStandardPaths::TempLocation ) + "/sketch.png"; + image.save( path ); + + return path; +} + +void DrawingCanvas::fitCanvas() +{ + double scale = 1.0; + if ( !mBackgroundImage.isNull() ) + { + const QSizeF itemSize( size().width() - 30, size().height() - 100 ); + const QSizeF backgroundImageSize = mBackgroundImage.size(); + if ( backgroundImageSize.width() > itemSize.width() ) + { + scale = itemSize.width() / backgroundImageSize.width(); + } + if ( backgroundImageSize.height() * scale > itemSize.height() ) + { + scale = itemSize.height() / backgroundImageSize.height(); + } + } + + mZoomFactor = scale; + emit zoomFactorChanged(); + + mOffset = QPoint( 0, 0 ); + emit offsetChanged(); + + update(); +} + +void DrawingCanvas::pan( const QPointF &oldPosition, const QPointF &newPosition ) +{ + setOffset( QPointF( mOffset.x() + ( newPosition.x() - oldPosition.x() ), + mOffset.y() + ( newPosition.y() - oldPosition.y() ) ) ); +} + +void DrawingCanvas::zoom( double scale ) +{ + setZoomFactor( mZoomFactor * scale ); +} + +bool DrawingCanvas::isEmpty() const +{ + return mIsEmpty; +} + +void DrawingCanvas::setIsEmpty( bool empty ) +{ + if ( mIsEmpty == empty ) + { + return; + } + + mIsEmpty = empty; + emit isEmptyChanged(); +} + +bool DrawingCanvas::isDirty() const +{ + return mIsDirty; +} + +void DrawingCanvas::setIsDirty( bool dirty ) +{ + if ( mIsDirty == dirty ) + { + return; + } + + mIsDirty = dirty; + emit isDirtyChanged(); +} + +QColor DrawingCanvas::frameColor() const +{ + return mFrameColor; +} + +void DrawingCanvas::setFrameColor( const QColor &color ) +{ + if ( mFrameColor == color ) + { + return; + } + + mFrameColor = color; + emit frameColorChanged(); + + update(); +} + +double DrawingCanvas::zoomFactor() const +{ + return mZoomFactor; +} + +void DrawingCanvas::setZoomFactor( double factor ) +{ + if ( mZoomFactor == factor ) + { + return; + } + + mZoomFactor = factor; + emit zoomFactorChanged(); + + update(); +} + +QPointF DrawingCanvas::offset() const +{ + return mOffset; +} + +void DrawingCanvas::setOffset( const QPointF &offset ) +{ + if ( mOffset == offset ) + { + return; + } + + mOffset = offset; + emit offsetChanged(); + + update(); +} + +void DrawingCanvas::strokeBegin( const QPointF &point, const QColor color ) +{ + mCurrentStroke.points.clear(); + mCurrentStroke.color = color; + mCurrentStroke.width = DEFAULT_STROKE_WIDTH / mZoomFactor; + mCurrentStroke.points << itemToCanvas( point ); +} + +void DrawingCanvas::strokeMove( const QPointF &point ) +{ + if ( mCurrentStroke.points.isEmpty() ) + { + return; + } + + const QPointF lastPoint = canvasToItem( mCurrentStroke.points.last() ); + if ( std::pow( point.x() - lastPoint.x(), 2 ) + std::pow( point.y() - lastPoint.y(), 2 ) >= 0.1 ) + { + mCurrentStroke.points << itemToCanvas( point ); + + update(); + } +} + +void DrawingCanvas::strokeEnd( const QPointF &point ) +{ + if ( mCurrentStroke.points.isEmpty() ) + { + return; + } + + mCurrentStroke.points << itemToCanvas( point ); + + QPainter painter( &mDrawingImage ); + painter.setRenderHint( QPainter::Antialiasing, true ); + drawStroke( &painter, mCurrentStroke ); + + mCurrentStroke.points.clear(); + + setIsDirty( true ); + update(); +} + +void DrawingCanvas::drawStroke( QPainter *painter, Stroke &stroke, bool onCanvas ) +{ + QPainterPath path( onCanvas ? stroke.points.at( 0 ) : canvasToItem( stroke.points.at( 0 ) ) ); + for ( int i = 1; i < stroke.points.size(); i++ ) + { + path.lineTo( onCanvas ? stroke.points.at( i ) : canvasToItem( stroke.points.at( i ) ) ); + } + + QPen pen( stroke.color ); + pen.setWidthF( onCanvas ? stroke.width : stroke.width * mZoomFactor ); + painter->setPen( pen ); + QBrush brush( stroke.fillColor ); + painter->setBrush( brush ); + painter->drawPath( path ); +} + +QPointF DrawingCanvas::itemToCanvas( const QPointF &point ) +{ + const QPointF canvasTopLeft( size().width() / 2 - ( mBackgroundImage.size().width() * mZoomFactor / 2 ) + mOffset.x(), + size().height() / 2 - ( mBackgroundImage.size().height() * mZoomFactor / 2 ) + mOffset.y() ); + return QPointF( ( point.x() - canvasTopLeft.x() ) / mZoomFactor, + ( point.y() - canvasTopLeft.y() ) / mZoomFactor ); +} + +QPointF DrawingCanvas::canvasToItem( const QPointF &point ) +{ + const QPointF canvasTopLeft( size().width() / 2 - ( mBackgroundImage.size().width() * mZoomFactor / 2 ) + mOffset.x(), + size().height() / 2 - ( mBackgroundImage.size().height() * mZoomFactor / 2 ) + mOffset.y() ); + return QPointF( canvasTopLeft.x() + ( point.x() * mZoomFactor ), + canvasTopLeft.y() + ( point.y() * mZoomFactor ) ); +} + +void DrawingCanvas::paint( QPainter *painter ) +{ + if ( !mBackgroundImage.isNull() ) + { + painter->setRenderHint( QPainter::Antialiasing, true ); + painter->setRenderHint( QPainter::SmoothPixmapTransform, true ); + + const QSizeF scaledImageSize = mBackgroundImage.size() * mZoomFactor; + const QRectF imageRect( ( size().width() / 2 - scaledImageSize.width() / 2 ) + mOffset.x(), + ( size().height() / 2 - scaledImageSize.height() / 2 ) + mOffset.y(), + scaledImageSize.width(), + scaledImageSize.height() ); + + QColor shadowColor = mFrameColor; + shadowColor.setAlphaF( shadowColor.alphaF() / 2 ); + + painter->setPen( Qt::NoPen ); + painter->setBrush( QBrush( shadowColor ) ); + painter->drawRect( imageRect.translated( 3, 3 ) ); + + painter->drawImage( imageRect, mBackgroundImage ); + painter->drawImage( imageRect, mDrawingImage ); + + if ( mCurrentStroke.points.size() > 1 ) + { + drawStroke( painter, mCurrentStroke, false ); + } + + painter->setPen( QPen( mFrameColor ) ); + painter->setBrush( Qt::NoBrush ); + painter->drawRect( imageRect ); + } +} diff --git a/src/core/drawingcanvas.h b/src/core/drawingcanvas.h new file mode 100644 index 0000000000..ff24ec591d --- /dev/null +++ b/src/core/drawingcanvas.h @@ -0,0 +1,189 @@ +/*************************************************************************** + drawingcanvas.h - DrawingCanvas + + --------------------- + begin : 24.03.2024 + copyright : (C) 2024 by Mathieu Pellerin + email : mathieu (at) opengis.ch + *************************************************************************** + * * + * 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 DRAWINGCANVAS_H +#define DRAWINGCANVAS_H + +#include +#include +#include + +#define DEFAULT_STROKE_WIDTH 5 + +class DrawingCanvas : public QQuickPaintedItem +{ + Q_OBJECT + + /** + * This property is set to TRUE when no canvas has been created or a previously + * created canvas has been cleared. + */ + Q_PROPERTY( bool isEmpty READ isEmpty NOTIFY isEmptyChanged ) + + /** + * This property is set to TRUE when a created canvas has been modified. + */ + Q_PROPERTY( bool isDirty READ isDirty NOTIFY isDirtyChanged ) + + /** + * This property holds the color of the canvas frame. + */ + Q_PROPERTY( QColor frameColor READ frameColor WRITE setFrameColor NOTIFY frameColorChanged ) + + /** + * This property holds the current zoom factor of the canvas. A value of 1.0 means the canvas + * is at native resolutio (i.e. one screen pixel represents one canvas pixel). + */ + Q_PROPERTY( double zoomFactor READ zoomFactor WRITE setZoomFactor NOTIFY zoomFactorChanged ) + + /** + * This property holds the offset from the center of the canvas as a result of + * panning operations. + */ + Q_PROPERTY( QPointF offset READ offset WRITE setOffset NOTIFY offsetChanged ) + + public: + DrawingCanvas( QQuickItem *parent = nullptr ); + ~DrawingCanvas() = default; + + void paint( QPainter *painter ) override; + + //! \copydoc DrawingCanvas::isEmpty + bool isEmpty() const; + + //! \copydoc DrawingCanvas::isEmpty + void setIsEmpty( bool empty ); + + //! \copydoc DrawingCanvas::isDirty + bool isDirty() const; + + //! \copydoc DrawingCanvas::isDirty + void setIsDirty( bool dirty ); + + //! \copydoc DrawingCanvas::frameColor + QColor frameColor() const; + + //! \copydoc DrawingCanvas::frameColor + void setFrameColor( const QColor &color ); + + //! \copydoc DrawingCanvas::zoomFactor + double zoomFactor() const; + + //! \copydoc DrawingCanvas::zoomFactor + void setZoomFactor( double factor ); + + //! \copydoc DrawingCanvas::offset + QPointF offset() const; + + //! \copydoc DrawingCanvas::offset + void setOffset( const QPointF &offset ); + + /** + * Creates a blank drawing canvas. + * \param width the width of the canvas. + * \param height the height of the canvas. + * \param backgroundColor the background color of the canvas. + */ + Q_INVOKABLE void createBlankCanvas( int width, int height, QColor backgroundColor = QColor( 255, 255, 255 ) ); + + /** + * Creates a drawing canvas from a given image which will be the background on which + * the drawing will be overlayed. + * \param path the image path. + */ + Q_INVOKABLE void createCanvasFromImage( const QString &path ); + + /** + * Clears the drawing canvas. + * \see isEmpty() + */ + Q_INVOKABLE void clear(); + + /** + * Saves the drawing canvas to a temporary location. + * \returns the temporary file path of the saved image. + */ + Q_INVOKABLE QString save() const; + + /** + * Fits the drawing canvas to match available width and height. + */ + Q_INVOKABLE void fitCanvas(); + + /** + * Pans the drawing canvas by the distance between two points. + */ + Q_INVOKABLE void pan( const QPointF &oldPosition, const QPointF &newPosition ); + + /** + * Zooms the drawing canvas by the provided \a scale value. + */ + Q_INVOKABLE void zoom( double scale ); + + /** + * Begins a stroke operation. + * \param point the first point of the stroke + * \param color the color of the stroke + */ + Q_INVOKABLE void strokeBegin( const QPointF &point, const QColor color = QColor( 0, 0, 0 ) ); + + /** + * Adds a new \a point to the current stroke path. + * \note The function strokeBegin must have been called prior to this function. + */ + Q_INVOKABLE void strokeMove( const QPointF &point ); + + /** + * Ends the current stroke with a final \a point added to the stroke path. + * \note The function strokeBegin must have been called prior to this function. + */ + Q_INVOKABLE void strokeEnd( const QPointF &point ); + + signals: + void isEmptyChanged(); + void isDirtyChanged(); + void frameColorChanged(); + void zoomFactorChanged(); + void offsetChanged(); + + private: + struct Stroke + { + double width = 5.0; + QColor color = QColor( 0, 0, 0 ); + QColor fillColor = QColor( Qt::transparent ); + QList points; + }; + + void drawStroke( QPainter *painter, Stroke &stroke, bool onCanvas = true ); + + QPointF itemToCanvas( const QPointF &point ); + QPointF canvasToItem( const QPointF &point ); + + bool mIsEmpty = true; + bool mIsDirty = false; + + QColor mFrameColor; + double mZoomFactor = 1.0; + QPointF mOffset = QPointF( 0, 0 ); + + QImage mBackgroundImage; + QImage mDrawingImage; + + Stroke mCurrentStroke; +}; + +#endif // DRAWINGCANVAS_H diff --git a/src/core/drawingtemplatemodel.cpp b/src/core/drawingtemplatemodel.cpp new file mode 100644 index 0000000000..6b09232dc4 --- /dev/null +++ b/src/core/drawingtemplatemodel.cpp @@ -0,0 +1,154 @@ +/*************************************************************************** + drawingtemplatemodel.cpp + + --------------------- + begin : 28.03.2024 + copyright : (C) 2024 by Mathieu Pellerin + email : mathieu at opengis dot ch + *************************************************************************** + * * + * 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 "drawingtemplatemodel.h" +#include "fileutils.h" +#include "platformutilities.h" + +#include +#include + +DrawingTemplateModel::DrawingTemplateModel( QObject *parent ) + : QAbstractListModel( parent ) +{ + reloadModel(); +} + +QHash DrawingTemplateModel::roleNames() const +{ + QHash roles = QAbstractListModel::roleNames(); + roles[TemplateTypeRole] = "templateType"; + roles[TemplateTitleRole] = "templateTitle"; + roles[TemplatePathRole] = "templatePath"; + + return roles; +} + +void DrawingTemplateModel::reloadModel() +{ + beginResetModel(); + mTemplates.clear(); + + QStringList dirs; + bool hasProjectTemplate = false; + QString projectPath; + + // Project templates + if ( !mProjectFilePath.isEmpty() ) + { + QFileInfo projectInfo( mProjectFilePath ); + projectPath = projectInfo.absolutePath() + QStringLiteral( "/" ); + dirs << projectPath; + } + + // App-wide templates + dirs << PlatformUtilities::instance()->appDataDirs(); + for ( const QString &dir : dirs ) + { + QDir templateDir( dir + QStringLiteral( "drawing_templates/" ) ); + if ( templateDir.exists() ) + { + const QStringList templates = templateDir.entryList( QStringList() << "*.*", QDir::Files ); + for ( const QString &templateFile : templates ) + { + const QFileInfo templateInfo( dir + QStringLiteral( "drawing_templates/" ) + templateFile ); + if ( FileUtils::isImageMimeTypeSupported( FileUtils::mimeTypeName( templateInfo.absoluteFilePath() ) ) ) + { + const TemplateType type = !projectPath.isEmpty() && templateDir == projectPath ? ProjectTemplate : AppTemplate; + mTemplates << Template( type, templateInfo.baseName(), templateInfo.absoluteFilePath() ); + + if ( type == ProjectTemplate ) + { + hasProjectTemplate = true; + } + } + } + } + } + + std::sort( mTemplates.begin(), mTemplates.end(), [=]( const Template &t1, const Template &t2 ) { + if ( t1.type != t2.type ) + { + return t2.type == ProjectTemplate; + } + + return t1.title <= t2.title; + } ); + + // Add blank template + mTemplates.prepend( Template( AppTemplate, tr( "Blank" ), QString() ) ); + + endResetModel(); + + if ( mHasProjectTemplate != hasProjectTemplate ) + { + mHasProjectTemplate = hasProjectTemplate; + emit hasProjectTemplateChanged(); + } +} + +int DrawingTemplateModel::rowCount( const QModelIndex &parent ) const +{ + if ( !parent.isValid() ) + { + return mTemplates.size(); + } + + return 0; +} + +QVariant DrawingTemplateModel::data( const QModelIndex &index, int role ) const +{ + if ( index.row() >= mTemplates.size() || index.row() < 0 ) + { + return QVariant(); + } + + switch ( static_cast( role ) ) + { + case TemplateTypeRole: + return mTemplates.at( index.row() ).type; + case TemplateTitleRole: + return mTemplates.at( index.row() ).title; + case TemplatePathRole: + return mTemplates.at( index.row() ).path; + } + + return QVariant(); +} + +QString DrawingTemplateModel::projectFilePath() const +{ + return mProjectFilePath; +} + +void DrawingTemplateModel::setProjectFilePath( const QString &path ) +{ + if ( mProjectFilePath == path ) + { + return; + } + + mProjectFilePath = path; + emit projectFilePathChanged(); + + reloadModel(); +} + +bool DrawingTemplateModel::hasProjectTemplate() const +{ + return mHasProjectTemplate; +} diff --git a/src/core/drawingtemplatemodel.h b/src/core/drawingtemplatemodel.h new file mode 100644 index 0000000000..3a0c071cf5 --- /dev/null +++ b/src/core/drawingtemplatemodel.h @@ -0,0 +1,95 @@ +/*************************************************************************** + drawingtemplatemodel.h + + --------------------- + begin : 28.03.2024 + copyright : (C) 2024 by Mathieu Pellerin + email : mathieu at opengis dot ch + *************************************************************************** + * * + * 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 DRAWINGTEMPLATEMODEL_H +#define DRAWINGTEMPLATEMODEL_H + +#include + +class DrawingTemplateModel : public QAbstractListModel +{ + Q_OBJECT + + /** + * This property holds the project file path where project templates will be looked for. + */ + Q_PROPERTY( QString projectFilePath READ projectFilePath WRITE setProjectFilePath NOTIFY projectFilePathChanged ) + + /** + * This property holds whether the model contains project templates. + */ + Q_PROPERTY( bool hasProjectTemplate READ hasProjectTemplate NOTIFY hasProjectTemplateChanged ) + + public: + enum TemplateType + { + AppTemplate, + ProjectTemplate, + }; + + enum Role + { + TemplateTypeRole = Qt::UserRole, + TemplateTitleRole, + TemplatePathRole, + }; + Q_ENUM( Role ) + + explicit DrawingTemplateModel( QObject *parent = nullptr ); + + //! \copydoc DrawingTemplateModel::projectFilePath + QString projectFilePath() const; + + //! \copydoc DrawingTemplateModel::projectFilePath + void setProjectFilePath( const QString &path ); + + //! \copydoc DrawingTemplateModel::hasProjectTemplate + bool hasProjectTemplate() const; + + QHash roleNames() const override; + + int rowCount( const QModelIndex &parent ) const override; + QVariant data( const QModelIndex &index, int role ) const override; + + /** + * Reloads the drawing template model. + */ + Q_INVOKABLE void reloadModel(); + + signals: + void projectFilePathChanged(); + void hasProjectTemplateChanged(); + + private: + struct Template + { + Template( TemplateType type, const QString &title, const QString &path ) + : type( type ) + , title( title ) + , path( path ) + {} + + TemplateType type = TemplateType::AppTemplate; + QString title; + QString path; + }; + + QList