Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation of basic image attachment sketching capability #5149

Merged
merged 32 commits into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
74014a7
Create ImageCanvas skeleton
nirvn Mar 26, 2024
0793a3c
Add offset property to ImageCanvas
nirvn Mar 26, 2024
6bbae64
Implement offset and zoom interaction
nirvn Mar 26, 2024
4741a48
Animated zoom transition
nirvn Mar 26, 2024
b7df93b
Add offseted background shield to indicate boundaries
nirvn Mar 26, 2024
8ed6f56
Rename ImageCanvas to DrawingCanvas
nirvn Mar 27, 2024
dc84d0d
Add frameColor property to further identify boundaries and reinforce …
nirvn Mar 27, 2024
837f32a
Add stroke sketching functionality
nirvn Mar 27, 2024
fd4b2f2
Add posibility to change stroke color, fix offset ignored when conver…
nirvn Mar 27, 2024
52c0e2e
Implement a sketcher popup
nirvn Mar 27, 2024
dc7e510
Implement sketching over image attachments
nirvn Mar 27, 2024
e7d6a61
Better sketcher background color, insure map canvas doesn't interfere
nirvn Mar 27, 2024
3e612c1
Implement stroke color picker UI
nirvn Mar 27, 2024
0d36f06
Fix geotag badge position
nirvn Mar 27, 2024
29164fd
Fix fit to canvas height value
nirvn Mar 27, 2024
303eebe
Set pincharea target to null, reverse pan points
nirvn Mar 27, 2024
eeb965e
Refine touch events some more
nirvn Mar 27, 2024
de9a41c
Fix grayscale image crasher, make popup full screen, improve pinch re…
nirvn Mar 28, 2024
6876d05
Make sketcher fullscreen
nirvn Mar 28, 2024
8996667
Make stroke width take zoom into account to offer variable stroke width
nirvn Mar 28, 2024
5b82a98
Fix sketching threshold
nirvn Mar 28, 2024
0e9d582
Fix sketcher's tool button clicks with stylus
nirvn Mar 29, 2024
25877ba
Add outline to color picker to avoid white and black becoming invisib…
nirvn Mar 29, 2024
cefbb2b
Add sketch/drawing template
nirvn Mar 29, 2024
5903258
Revamp handling of attachment widget buttons, only ever show one, add…
nirvn Mar 29, 2024
d7f2020
Theme-based backgroud color for the template picker
nirvn Mar 29, 2024
1d0bdf3
... handlers ...
nirvn Mar 30, 2024
cc759c9
Add classes documentation
nirvn Mar 31, 2024
6de2fdd
Take on lesson learned from the drawing canvas, avoid grabPermission …
nirvn Mar 31, 2024
9854cc3
Avoid overloading stroke with a large number of points
nirvn Apr 4, 2024
efb51b8
Thanks clang-tidy!
nirvn Apr 6, 2024
de24f0c
Exclude a few clang-tidy checks
nirvn Apr 6, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
4 changes: 4 additions & 0 deletions src/core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ set(QFIELD_CORE_SRCS
deltalistmodel.cpp
digitizinglogger.cpp
distancearea.cpp
drawingcanvas.cpp
drawingtemplatemodel.cpp
expressionevaluator.cpp
expressionvariablemodel.cpp
featurechecklistmodel.cpp
Expand Down Expand Up @@ -152,6 +154,8 @@ set(QFIELD_CORE_HDRS
deltalistmodel.h
digitizinglogger.h
distancearea.h
drawingcanvas.h
drawingtemplatemodel.h
expressionevaluator.h
expressionvariablemodel.h
featurechecklistmodel.h
Expand Down
327 changes: 327 additions & 0 deletions src/core/drawingcanvas.cpp
Original file line number Diff line number Diff line change
@@ -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 <QPainter>
#include <QPainterPath>
#include <QStandardPaths>

#include <cmath>

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 );
nirvn marked this conversation as resolved.
Show resolved Hide resolved

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;
nirvn marked this conversation as resolved.
Show resolved Hide resolved
}

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 )
nirvn marked this conversation as resolved.
Show resolved Hide resolved
{
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 );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: variable 'brush' of type 'QBrush' can be declared 'const' [misc-const-correctness]

Suggested change
QBrush brush( stroke.fillColor );
QBrush const 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 );
}
}
Loading
Loading