mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-11 06:34:14 +02:00
Merge pull request #1432 from bhangart/feature/image-crop-on-folio
Crop a picture directly on the folio
This commit is contained in:
@@ -581,6 +581,7 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/qetgraphicsitem/crossrefitem.h
|
||||
${QET_DIR}/sources/qetgraphicsitem/diagramimageitem.cpp
|
||||
${QET_DIR}/sources/qetgraphicsitem/diagramimageitem.h
|
||||
${QET_DIR}/sources/qetgraphicsitem/cropgeometry.h
|
||||
${QET_DIR}/sources/qetgraphicsitem/diagramtextitem.cpp
|
||||
${QET_DIR}/sources/qetgraphicsitem/diagramtextitem.h
|
||||
${QET_DIR}/sources/qetgraphicsitem/dynamicelementtextitem.cpp
|
||||
|
||||
@@ -2212,11 +2212,21 @@ Continuer ?</translation>
|
||||
<source>rotate/skew</source>
|
||||
<translation>pivoter/incliner</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/qetgraphicsitem/diagramimageitem.cpp" line="675"/>
|
||||
<source>crop</source>
|
||||
<translation>rogner</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/qetgraphicsitem/diagramimageitem.cpp" line="535"/>
|
||||
<source>Click: mode %1</source>
|
||||
<translation>Cliquer : mode %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/qetgraphicsitem/diagramimageitem.cpp" line="731"/>
|
||||
<source>Crop: drag the handles or the frame -- Enter, double-click or click elsewhere: apply; Escape: cancel</source>
|
||||
<translation>Rogner : glisser les poignées ou le cadre -- Entrée, double-clic ou clic à côté : appliquer ; Échap : annuler</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/qetgraphicsitem/diagramimageitem.cpp" line="573"/>
|
||||
<source>Drag a corner/edge: resize (Ctrl = from center, Shift = keep proportions)</source>
|
||||
@@ -2257,6 +2267,11 @@ Continuer ?</translation>
|
||||
<source>Drag: move the rotation center</source>
|
||||
<translation>Glisser : déplacer le centre de rotation</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/qetgraphicsitem/diagramimageitem.cpp" line="811"/>
|
||||
<source>Drag: crop (Enter = apply, Escape = cancel)</source>
|
||||
<translation>Glisser : rogner (Entrée = appliquer, Échap = annuler)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/qetgraphicsitem/diagramimageitem.cpp" line="899"/>
|
||||
<source>Resize an image</source>
|
||||
@@ -2359,11 +2374,21 @@ Continuer ?</translation>
|
||||
<source>Transparent color...</source>
|
||||
<translation>Couleur transparente...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/qetgraphicsitem/diagramimageitem.cpp" line="2101"/>
|
||||
<source>Crop on the folio</source>
|
||||
<translation>Rogner sur le folio</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/qetgraphicsitem/diagramimageitem.cpp" line="1806"/>
|
||||
<source>Crop...</source>
|
||||
<translation>Rogner...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/qetgraphicsitem/diagramimageitem.cpp" line="2109"/>
|
||||
<source>Reset crop</source>
|
||||
<translation>Réinitialiser le rognage</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/qetgraphicsitem/diagramimageitem.cpp" line="1810"/>
|
||||
<source>Horizontal mirror</source>
|
||||
@@ -2379,6 +2404,11 @@ Continuer ?</translation>
|
||||
<source>Restore proportions</source>
|
||||
<translation>Restaurer les proportions</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/qetgraphicsitem/diagramimageitem.cpp" line="2125"/>
|
||||
<source>Properties...</source>
|
||||
<translation>Propriétés...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/qetgraphicsitem/diagramimageitem.cpp" line="1852"/>
|
||||
<source>Select an image ...</source>
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech 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.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef CROPGEOMETRY_H
|
||||
#define CROPGEOMETRY_H
|
||||
|
||||
#include <QPointF>
|
||||
#include <QRect>
|
||||
#include <QRectF>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
/**
|
||||
The geometry of cropping a picture directly on the folio, in the
|
||||
picture's own pixels. Free of any item so that it can be tested on
|
||||
its own.
|
||||
*/
|
||||
namespace CropGeometry
|
||||
{
|
||||
/// Which edges of the crop frame a handle moves.
|
||||
struct Edges
|
||||
{
|
||||
bool left = false;
|
||||
bool top = false;
|
||||
bool right = false;
|
||||
bool bottom = false;
|
||||
};
|
||||
|
||||
/**
|
||||
Edges moved by the handle at @a handle on a frame of @a size, from
|
||||
where that handle sits: on the left side it moves the left edge,
|
||||
in the middle of a side only that side's edge.
|
||||
*/
|
||||
inline Edges edgesForHandle(const QPointF &handle, const QSizeF &size)
|
||||
{
|
||||
Edges e;
|
||||
e.left = qFuzzyIsNull(handle.x());
|
||||
e.right = qFuzzyCompare(handle.x(), size.width());
|
||||
e.top = qFuzzyIsNull(handle.y());
|
||||
e.bottom = qFuzzyCompare(handle.y(), size.height());
|
||||
return e;
|
||||
}
|
||||
|
||||
/**
|
||||
@a crop with the @a edges moved to @a point, kept inside @a bounds
|
||||
and never smaller than @a minimum on either side: an edge stops
|
||||
at the opposite one instead of crossing it.
|
||||
*/
|
||||
inline QRectF dragEdges(const QRectF &crop, Edges edges, const QPointF &point,
|
||||
const QRectF &bounds, qreal minimum)
|
||||
{
|
||||
const qreal x = std::clamp(point.x(), bounds.left(), bounds.right());
|
||||
const qreal y = std::clamp(point.y(), bounds.top(), bounds.bottom());
|
||||
QRectF r = crop;
|
||||
if (edges.left) r.setLeft(std::min(x, crop.right() - minimum));
|
||||
if (edges.right) r.setRight(std::max(x, crop.left() + minimum));
|
||||
if (edges.top) r.setTop(std::min(y, crop.bottom() - minimum));
|
||||
if (edges.bottom) r.setBottom(std::max(y, crop.top() + minimum));
|
||||
return r;
|
||||
}
|
||||
|
||||
/// @a crop moved by @a delta, but no further than @a bounds allow.
|
||||
inline QRectF moveWithin(const QRectF &crop, const QPointF &delta, const QRectF &bounds)
|
||||
{
|
||||
QRectF r = crop.translated(delta);
|
||||
r.moveLeft(std::clamp(r.left(), bounds.left(), bounds.right() - r.width()));
|
||||
r.moveTop(std::clamp(r.top(), bounds.top(), bounds.bottom() - r.height()));
|
||||
return r;
|
||||
}
|
||||
|
||||
/// The whole-pixel crop rectangle closest to @a crop.
|
||||
inline QRect toPixels(const QRectF &crop)
|
||||
{
|
||||
const int left = int(std::lround(crop.left()));
|
||||
const int top = int(std::lround(crop.top()));
|
||||
return QRect(left, top,
|
||||
int(std::lround(crop.right())) - left,
|
||||
int(std::lround(crop.bottom())) - top);
|
||||
}
|
||||
}
|
||||
|
||||
#endif // CROPGEOMETRY_H
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
#include "diagramimageitem.h"
|
||||
#include "../darkimagerendering.h"
|
||||
#include "cropgeometry.h"
|
||||
#include "../shownkinds.h"
|
||||
|
||||
#include "../PropertiesEditor/propertieseditordialog.h"
|
||||
@@ -41,6 +42,9 @@
|
||||
#include <QFontMetricsF>
|
||||
#include <QGraphicsSceneContextMenuEvent>
|
||||
#include <QImageWriter>
|
||||
#include <QPainterPath>
|
||||
#include <QtMath>
|
||||
#include <QGraphicsView>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <QTextStream>
|
||||
@@ -99,6 +103,70 @@ DiagramImageItem::~DiagramImageItem()
|
||||
@param widget the QWidget where we draw the pixmap
|
||||
*/
|
||||
void DiagramImageItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) {
|
||||
if (isCropping())
|
||||
{
|
||||
// The whole original, dimmed, with the region being kept drawn
|
||||
// over it at full strength and outlined. In a dark scene this is
|
||||
// a picture like any other: kept out of the scene's inversion
|
||||
// unless the picture adapts to the dark theme, as
|
||||
// DarkImageRendering::paintPixmap() does for the plain picture.
|
||||
Q_UNUSED(option); Q_UNUSED(widget);
|
||||
const QPointF origin = -QPointF(m_crop_rect.topLeft());
|
||||
const QRectF previewRect(origin, QSizeF(m_crop_preview.size()));
|
||||
DarkImageRendering::Context *context = DarkImageRendering::current;
|
||||
const bool boundary = context && context->painter == painter
|
||||
&& m_adapt_to_dark_theme != context->invertScene;
|
||||
if (boundary) context->flush(context->invertScene, QRect());
|
||||
painter -> save();
|
||||
painter -> setOpacity(0.3);
|
||||
painter -> drawPixmap(origin, m_crop_preview);
|
||||
painter -> setOpacity(1.0);
|
||||
painter -> drawPixmap(cropFrameLocal(), m_crop_preview, m_pending_crop);
|
||||
const QRectF f = cropFrameLocal();
|
||||
QPen frame(Qt::black);
|
||||
frame.setCosmetic(true);
|
||||
frame.setWidth(1);
|
||||
painter -> setPen(frame);
|
||||
painter -> setBrush(Qt::NoBrush);
|
||||
painter -> drawRect(f);
|
||||
|
||||
// Crop bars, as photo editors draw them: an L at each corner and a
|
||||
// short bar in the middle of each side, black on a white outline
|
||||
// so they show on dark pictures too, and the same size on screen
|
||||
// whatever the zoom or the picture's own scale.
|
||||
const QTransform t = painter -> worldTransform();
|
||||
const qreal px = 1.0 / qMax(1e-6, qSqrt(qAbs(t.m11() * t.m22() - t.m12() * t.m21())));
|
||||
const qreal corner = qMin(18 * px, qMin(f.width(), f.height()) / 2);
|
||||
const qreal side = qMin(10 * px, qMin(f.width(), f.height()) / 4);
|
||||
QPainterPath bars;
|
||||
auto l = [&bars](QPointF a, QPointF b, QPointF c) {
|
||||
bars.moveTo(a); bars.lineTo(b); bars.lineTo(c);
|
||||
};
|
||||
l(f.topLeft() + QPointF(0, corner), f.topLeft(), f.topLeft() + QPointF(corner, 0));
|
||||
l(f.topRight() - QPointF(corner, 0), f.topRight(), f.topRight() + QPointF(0, corner));
|
||||
l(f.bottomRight() - QPointF(0, corner), f.bottomRight(), f.bottomRight() - QPointF(corner, 0));
|
||||
l(f.bottomLeft() + QPointF(corner, 0), f.bottomLeft(), f.bottomLeft() - QPointF(0, corner));
|
||||
const QPointF c = f.center();
|
||||
bars.moveTo(c.x() - side, f.top()); bars.lineTo(c.x() + side, f.top());
|
||||
bars.moveTo(c.x() - side, f.bottom()); bars.lineTo(c.x() + side, f.bottom());
|
||||
bars.moveTo(f.left(), c.y() - side); bars.lineTo(f.left(), c.y() + side);
|
||||
bars.moveTo(f.right(), c.y() - side); bars.lineTo(f.right(), c.y() + side);
|
||||
painter -> setRenderHint(QPainter::Antialiasing, true);
|
||||
QPen outline(Qt::white, 7, Qt::SolidLine, Qt::SquareCap, Qt::MiterJoin);
|
||||
outline.setCosmetic(true);
|
||||
painter -> setPen(outline);
|
||||
painter -> drawPath(bars);
|
||||
QPen bar(Qt::black, 4, Qt::SolidLine, Qt::SquareCap, Qt::MiterJoin);
|
||||
bar.setCosmetic(true);
|
||||
painter -> setPen(bar);
|
||||
painter -> drawPath(bars);
|
||||
painter -> restore();
|
||||
if (boundary)
|
||||
context->flush(m_adapt_to_dark_theme, painter->worldTransform().mapRect(previewRect)
|
||||
.toAlignedRect().adjusted(-2, -2, 2, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
DarkImageRendering::paintPixmap(painter, pixmap_, m_adapt_to_dark_theme);
|
||||
|
||||
Q_UNUSED(option); Q_UNUSED(widget);
|
||||
@@ -213,6 +281,11 @@ void DiagramImageItem::setPixmap(const QPixmap &pixmap) {
|
||||
*/
|
||||
void DiagramImageItem::setImageSource(const ImageSource &source)
|
||||
{
|
||||
// An undo or redo that changes the picture under a crop being edited
|
||||
// ends that crop: its frame and its preview were made for the source
|
||||
// that is replaced here. Nothing is applied, since this may run
|
||||
// inside an undo command.
|
||||
finishCropMode(false);
|
||||
m_base_pixmap = source.base;
|
||||
m_crop_rect = source.crop;
|
||||
m_transparent_colors = source.colors;
|
||||
@@ -461,6 +534,11 @@ QPointF DiagramImageItem::handlePosition(int index) const
|
||||
{
|
||||
const qreal w = pixmap_.width(), h = pixmap_.height();
|
||||
const HandleRole role = m_handleRoles.at(index);
|
||||
if (role == HandleRole::CropEdge)
|
||||
{
|
||||
const QRectF frame = cropFrameLocal();
|
||||
return frame.topLeft() + handleNaturalPosition(index, frame.width(), frame.height());
|
||||
}
|
||||
if (role == HandleRole::Resize)
|
||||
return handleNaturalPosition(index, w, h);
|
||||
if (role == HandleRole::Rotate)
|
||||
@@ -479,6 +557,24 @@ QPointF DiagramImageItem::handlePosition(int index) const
|
||||
*/
|
||||
void DiagramImageItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
||||
{
|
||||
if (isCropping())
|
||||
{
|
||||
// Inside the crop frame: move it. On the dimmed rest: done.
|
||||
if (event->button() == Qt::LeftButton)
|
||||
{
|
||||
if (cropFrameLocal().contains(event->pos()))
|
||||
{
|
||||
m_crop_moving = true;
|
||||
m_crop_move_start = event->pos();
|
||||
m_crop_move_origin = m_pending_crop;
|
||||
}
|
||||
else
|
||||
finishCropMode(true);
|
||||
}
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
|
||||
const bool wasAlreadySelected = isSelected();
|
||||
event->ignore();
|
||||
QetGraphicsItem::mousePressEvent(event);
|
||||
@@ -491,12 +587,67 @@ void DiagramImageItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramImageItem::mouseMoveEvent
|
||||
In crop mode, a drag inside the crop frame moves it over the
|
||||
original; the picture itself stays where it is.
|
||||
*/
|
||||
void DiagramImageItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
|
||||
{
|
||||
if (isCropping())
|
||||
{
|
||||
if (m_crop_moving)
|
||||
{
|
||||
m_pending_crop = CropGeometry::moveWithin(m_crop_move_origin,
|
||||
event->pos() - m_crop_move_start, QRectF(m_base_pixmap.rect()));
|
||||
repositionHandles();
|
||||
update();
|
||||
}
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
QetGraphicsItem::mouseMoveEvent(event);
|
||||
}
|
||||
|
||||
void DiagramImageItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
||||
{
|
||||
if (isCropping())
|
||||
{
|
||||
m_crop_moving = false;
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
QetGraphicsItem::mouseReleaseEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramImageItem::mouseDoubleClickEvent
|
||||
Double-click crops the picture directly on the folio, and applies the
|
||||
crop when it is already being edited. The properties stay in the
|
||||
context menu.
|
||||
*/
|
||||
void DiagramImageItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
|
||||
{
|
||||
if (event->button() != Qt::LeftButton)
|
||||
{
|
||||
QetGraphicsItem::mouseDoubleClickEvent(event);
|
||||
return;
|
||||
}
|
||||
if (isCropping())
|
||||
finishCropMode(true);
|
||||
else
|
||||
enterCropMode();
|
||||
event->accept();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramImageItem::toggleHandleMode
|
||||
*/
|
||||
void DiagramImageItem::toggleHandleMode()
|
||||
{
|
||||
prepareGeometryChange();
|
||||
if (isCropping())
|
||||
return;
|
||||
m_handleMode = (m_handleMode == HandleMode::Size) ? HandleMode::RotateSkew : HandleMode::Size;
|
||||
rebuildHandles();
|
||||
refreshInteractionHints();
|
||||
@@ -517,7 +668,13 @@ DiagramImageItem::HandleMode DiagramImageItem::nextHandleMode() const
|
||||
*/
|
||||
QString DiagramImageItem::handleModeLabel(HandleMode mode)
|
||||
{
|
||||
return (mode == HandleMode::Size) ? tr("resize") : tr("rotate/skew");
|
||||
switch (mode)
|
||||
{
|
||||
case HandleMode::Size: return tr("resize");
|
||||
case HandleMode::RotateSkew: return tr("rotate/skew");
|
||||
case HandleMode::Crop: return tr("crop");
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -570,6 +727,8 @@ void DiagramImageItem::refreshInteractionHints()
|
||||
*/
|
||||
QString DiagramImageItem::currentModeStatusHint() const
|
||||
{
|
||||
if (isCropping())
|
||||
return tr("Crop: drag the handles or the frame -- Enter, double-click or click elsewhere: apply; Escape: cancel");
|
||||
QString hint = (m_handleMode == HandleMode::Size)
|
||||
? tr("Drag a corner/edge: resize (Ctrl = from center, Shift = keep proportions)")
|
||||
: tr("Drag a corner: rotate (Shift = 15° steps); drag an edge: skew (Shift = 15° steps); red point: move the rotation center");
|
||||
@@ -636,6 +795,7 @@ QColor DiagramImageItem::colorForHandleRole(HandleRole role)
|
||||
case HandleRole::Rotate: return Qt::darkGreen;
|
||||
case HandleRole::SkewEdge: return QColor(255, 140, 0);
|
||||
case HandleRole::Pivot: return Qt::red;
|
||||
case HandleRole::CropEdge: return Qt::transparent; // paint() draws crop bars instead
|
||||
}
|
||||
return Qt::blue;
|
||||
}
|
||||
@@ -648,6 +808,7 @@ QString DiagramImageItem::hintForHandleRole(HandleRole role)
|
||||
case HandleRole::Rotate: return tr("Drag: rotate (Shift = 15° steps)");
|
||||
case HandleRole::SkewEdge: return tr("Drag: skew (Shift = 15° steps)");
|
||||
case HandleRole::Pivot: return tr("Drag: move the rotation center");
|
||||
case HandleRole::CropEdge: return tr("Drag: crop (Enter = apply, Escape = cancel)");
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
@@ -656,7 +817,12 @@ void DiagramImageItem::rebuildHandles()
|
||||
{
|
||||
clearHandles();
|
||||
|
||||
if (m_handleMode == HandleMode::Size)
|
||||
if (m_handleMode == HandleMode::Crop)
|
||||
{
|
||||
for (int i = 0; i < 8; ++i)
|
||||
m_handleRoles << HandleRole::CropEdge;
|
||||
}
|
||||
else if (m_handleMode == HandleMode::Size)
|
||||
{
|
||||
for (int i = 0; i < 8; ++i)
|
||||
m_handleRoles << HandleRole::Resize;
|
||||
@@ -684,6 +850,13 @@ void DiagramImageItem::rebuildHandles()
|
||||
h->setColor(colorForHandleRole(m_handleRoles.at(i)));
|
||||
h->setToolTip(hintForHandleRole(m_handleRoles.at(i)));
|
||||
h->setAcceptHoverEvents(true);
|
||||
if (m_handleRoles.at(i) == HandleRole::CropEdge)
|
||||
{
|
||||
static const Qt::CursorShape cursors[8] = {
|
||||
Qt::SizeFDiagCursor, Qt::SizeVerCursor, Qt::SizeBDiagCursor, Qt::SizeHorCursor,
|
||||
Qt::SizeHorCursor, Qt::SizeBDiagCursor, Qt::SizeVerCursor, Qt::SizeFDiagCursor};
|
||||
h->setCursor(cursors[i]);
|
||||
}
|
||||
scene()->addItem(h);
|
||||
h->installSceneEventFilter(this);
|
||||
}
|
||||
@@ -849,7 +1022,9 @@ void DiagramImageItem::handlerMouseMoveEvent(int index, QGraphicsSceneMouseEvent
|
||||
{
|
||||
const HandleRole role = m_handleRoles.at(index);
|
||||
|
||||
if (role == HandleRole::Resize)
|
||||
if (role == HandleRole::CropEdge)
|
||||
dragCropHandle(index, event->scenePos());
|
||||
else if (role == HandleRole::Resize)
|
||||
dragResize(index, mapFromScene(event->scenePos()), event->modifiers());
|
||||
else if (role == HandleRole::Rotate)
|
||||
dragRotateHandle(index, event->scenePos(), event->modifiers()); // index IS the corner slot (0-3): Rotate handles are always first
|
||||
@@ -945,6 +1120,9 @@ void DiagramImageItem::handlerMouseReleaseEvent(int index)
|
||||
else
|
||||
m_pivotIsCustom = m_original_pivotIsCustom; // dragged back to where it was: nothing to undo
|
||||
break;
|
||||
|
||||
case HandleRole::CropEdge:
|
||||
break; // nothing is undone until the crop is applied
|
||||
}
|
||||
|
||||
if (undo)
|
||||
@@ -981,11 +1159,25 @@ void DiagramImageItem::handlerMouseReleaseEvent(int index)
|
||||
/**
|
||||
@brief DiagramImageItem::keyPressEvent
|
||||
The view sends keys here only while one of the picture's handles is
|
||||
dragged (see handlerMousePressEvent()): Escape cancels that drag. Any
|
||||
other key is left to the view.
|
||||
dragged (see handlerMousePressEvent()) or while it is cropped (see
|
||||
enterCropMode()). While cropping, Enter applies the crop and Escape
|
||||
drops it; otherwise Escape cancels the drag. Any other key is left to
|
||||
the view.
|
||||
*/
|
||||
void DiagramImageItem::keyPressEvent(QKeyEvent *event)
|
||||
{
|
||||
if (isCropping())
|
||||
{
|
||||
const int key = event->key();
|
||||
if (key == Qt::Key_Escape || key == Qt::Key_Return || key == Qt::Key_Enter)
|
||||
{
|
||||
if (m_vector_index != -1)
|
||||
m_drag_cancelled = true; // swallow the rest of a handle drag
|
||||
finishCropMode(key != Qt::Key_Escape);
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (event->key() == Qt::Key_Escape && m_vector_index != -1)
|
||||
{
|
||||
cancelHandleDrag();
|
||||
@@ -1003,6 +1195,16 @@ void DiagramImageItem::keyPressEvent(QKeyEvent *event)
|
||||
void DiagramImageItem::endHandleDrag()
|
||||
{
|
||||
m_vector_index = -1;
|
||||
if (!isCropping()) // crop mode keeps the keys until it ends
|
||||
releaseKeyboardItem();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramImageItem::releaseKeyboardItem
|
||||
The view's shortcuts get the keys again.
|
||||
*/
|
||||
void DiagramImageItem::releaseKeyboardItem()
|
||||
{
|
||||
if (diagram() && diagram()->keyboardItem() == this)
|
||||
diagram()->setKeyboardItem(nullptr);
|
||||
}
|
||||
@@ -1410,6 +1612,9 @@ QVariant DiagramImageItem::itemChange(GraphicsItemChange change, const QVariant
|
||||
rebuildHandles();
|
||||
else
|
||||
{
|
||||
// Clicking elsewhere on the folio applies a crop in progress.
|
||||
if (isCropping())
|
||||
finishCropMode(true);
|
||||
prepareGeometryChange();
|
||||
clearHandles();
|
||||
m_handleMode = HandleMode::Size;
|
||||
@@ -1426,6 +1631,13 @@ QVariant DiagramImageItem::itemChange(GraphicsItemChange change, const QVariant
|
||||
if (!m_deferHandleReposition)
|
||||
repositionHandles();
|
||||
}
|
||||
else if (change == ItemSceneChange)
|
||||
{
|
||||
// Leaving the folio (undoing the picture's insertion, for one)
|
||||
// drops a crop in progress while diagram() is still there to
|
||||
// give the keys back to the view.
|
||||
finishCropMode(false);
|
||||
}
|
||||
else if (change == ItemSceneHasChanged)
|
||||
{
|
||||
if (!scene())
|
||||
@@ -1467,6 +1679,20 @@ QPixmap DiagramImageItem::computeDisplayPixmap(const QPixmap &base, const QRect
|
||||
*/
|
||||
QRectF DiagramImageItem::boundingRect() const
|
||||
{
|
||||
if (isCropping())
|
||||
{
|
||||
// Room for the crop bars, which are drawn a few screen pixels
|
||||
// wide over the edge of the original.
|
||||
qreal zoom = 1.0;
|
||||
if (scene() && !scene()->views().isEmpty())
|
||||
{
|
||||
const QTransform t = scene()->views().first()->transform() * sceneTransform();
|
||||
zoom = qSqrt(qAbs(t.m11() * t.m22() - t.m12() * t.m21()));
|
||||
}
|
||||
const qreal m = 8.0 / qMax(1e-6, zoom);
|
||||
return imageRect().united(QRectF(-QPointF(m_crop_rect.topLeft()), m_base_pixmap.size()))
|
||||
.adjusted(-m, -m, m, m);
|
||||
}
|
||||
if (m_label.isEmpty())
|
||||
return imageRect();
|
||||
return imageRect().united(labelRect());
|
||||
@@ -1877,10 +2103,18 @@ void DiagramImageItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
|
||||
transparentColor->setIcon(QET::Icons::EditOpacity);
|
||||
connect(transparentColor, &QAction::triggered, this, &DiagramImageItem::setTransparentColor);
|
||||
|
||||
QAction *cropHere = menu.data()->addAction(tr("Crop on the folio"));
|
||||
cropHere->setIcon(QET::Icons::TransformCrop);
|
||||
connect(cropHere, &QAction::triggered, this, &DiagramImageItem::enterCropMode);
|
||||
|
||||
QAction *cropAction = menu.data()->addAction(tr("Crop..."));
|
||||
cropAction->setIcon(QET::Icons::TransformCrop);
|
||||
connect(cropAction, &QAction::triggered, this, &DiagramImageItem::crop);
|
||||
|
||||
QAction *resetCropAction = menu.data()->addAction(tr("Reset crop"));
|
||||
resetCropAction->setEnabled(m_crop_rect != m_base_pixmap.rect());
|
||||
connect(resetCropAction, &QAction::triggered, this, &DiagramImageItem::resetCrop);
|
||||
|
||||
QAction *mirrorH = menu.data()->addAction(tr("Horizontal mirror"));
|
||||
mirrorH->setIcon(QET::Icons::ImageFlipHorizontal);
|
||||
QAction *mirrorV = menu.data()->addAction(tr("Vertical mirror"));
|
||||
@@ -1891,9 +2125,10 @@ void DiagramImageItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
|
||||
QAction *restoreRatio = menu.data()->addAction(tr("Restore proportions"));
|
||||
connect(restoreRatio, &QAction::triggered, this, &DiagramImageItem::restoreAspectRatio);
|
||||
|
||||
// menu.data()->addSeparator();
|
||||
// QAction *properties = menu.data()->addAction(tr("Propriétés..."));
|
||||
// connect(properties, &QAction::triggered, this, &DiagramImageItem::editProperty);
|
||||
// Double-click crops now, so the properties are opened from here.
|
||||
menu.data()->addSeparator();
|
||||
QAction *properties = menu.data()->addAction(tr("Properties..."));
|
||||
connect(properties, &QAction::triggered, this, &DiagramImageItem::editProperty);
|
||||
|
||||
menu.data()->addSeparator();
|
||||
menu.data()->addActions(d_view->contextMenuActions());
|
||||
@@ -2063,6 +2298,94 @@ void DiagramImageItem::crop()
|
||||
applyCrop(dialog.cropRect());
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramImageItem::enterCropMode
|
||||
Crop the picture directly on the folio: the whole original is shown,
|
||||
dimmed outside the region kept, and handles move the crop's edges.
|
||||
Nothing changes until finishCropMode() applies it.
|
||||
*/
|
||||
void DiagramImageItem::enterCropMode()
|
||||
{
|
||||
if (!diagram() || diagram()->isReadOnly() || m_base_pixmap.isNull() || isCropping())
|
||||
return;
|
||||
setSelected(true);
|
||||
prepareGeometryChange();
|
||||
m_pending_crop = QRectF(m_crop_rect);
|
||||
m_crop_preview = computeDisplayPixmap(m_base_pixmap, m_base_pixmap.rect(), m_transparent_colors);
|
||||
m_handleMode = HandleMode::Crop;
|
||||
rebuildHandles();
|
||||
// Enter and Escape end the crop: the view sends keys here first, before
|
||||
// its own Enter (repeat the last command) and Escape (clear the
|
||||
// selection, which would apply the crop).
|
||||
diagram()->setKeyboardItem(this);
|
||||
refreshInteractionHints();
|
||||
showStatusHint(currentModeStatusHint());
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramImageItem::finishCropMode
|
||||
Leave crop mode, applying the crop when @a apply is true -- one undo
|
||||
step, see applyCrop() -- and dropping it otherwise.
|
||||
*/
|
||||
void DiagramImageItem::finishCropMode(bool apply)
|
||||
{
|
||||
if (!isCropping())
|
||||
return;
|
||||
prepareGeometryChange();
|
||||
const QRect crop = CropGeometry::toPixels(m_pending_crop);
|
||||
m_handleMode = HandleMode::Size;
|
||||
releaseKeyboardItem();
|
||||
m_crop_moving = false;
|
||||
m_crop_preview = QPixmap();
|
||||
if (isSelected())
|
||||
rebuildHandles();
|
||||
else
|
||||
clearHandles();
|
||||
clearStatusHint();
|
||||
refreshInteractionHints();
|
||||
update();
|
||||
if (apply)
|
||||
applyCrop(crop);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramImageItem::resetCrop
|
||||
Show the whole original again, as one undoable step.
|
||||
*/
|
||||
void DiagramImageItem::resetCrop()
|
||||
{
|
||||
finishCropMode(false);
|
||||
applyCrop(m_base_pixmap.rect());
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramImageItem::cropFrameLocal
|
||||
@return the crop being edited, in the item's own coordinates, where
|
||||
the current crop's top-left corner is (0, 0).
|
||||
*/
|
||||
QRectF DiagramImageItem::cropFrameLocal() const
|
||||
{
|
||||
return m_pending_crop.translated(-QPointF(m_crop_rect.topLeft()));
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramImageItem::dragCropHandle
|
||||
Move the crop edges held by handle @a index to @a scenePos, mapped
|
||||
through the picture's transform, so a rotated or scaled picture is
|
||||
cropped along its own sides.
|
||||
*/
|
||||
void DiagramImageItem::dragCropHandle(int index, const QPointF &scenePos)
|
||||
{
|
||||
const QPointF basePoint = mapFromScene(scenePos) + QPointF(m_crop_rect.topLeft());
|
||||
const CropGeometry::Edges edges = CropGeometry::edgesForHandle(
|
||||
handleNaturalPosition(index, 2, 2), QSizeF(2, 2));
|
||||
m_pending_crop = CropGeometry::dragEdges(m_pending_crop, edges, basePoint,
|
||||
QRectF(m_base_pixmap.rect()), 4.0);
|
||||
repositionHandles();
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramImageItem::applyCrop
|
||||
Show @a cropRect of the original (in the original's own pixels),
|
||||
|
||||
@@ -88,6 +88,10 @@ class DiagramImageItem : public QetGraphicsItem {
|
||||
void setImageSourceVariant(const QVariant &source);
|
||||
QRect cropRect() const { return m_crop_rect; }
|
||||
bool applyCrop(const QRect &cropRect);
|
||||
bool isCropping() const { return m_handleMode == HandleMode::Crop; }
|
||||
void enterCropMode();
|
||||
void finishCropMode(bool apply);
|
||||
void resetCrop();
|
||||
|
||||
// attributes
|
||||
public:
|
||||
@@ -103,8 +107,8 @@ class DiagramImageItem : public QetGraphicsItem {
|
||||
// not shared code, since the two classes' broader handle roles
|
||||
// differ too much (no path/polygon/arc concepts here) for sharing
|
||||
// the dispatcher itself to be worth an indirect abstraction.
|
||||
enum class HandleMode { Size, RotateSkew };
|
||||
enum class HandleRole { Resize, Rotate, SkewEdge, Pivot };
|
||||
enum class HandleMode { Size, RotateSkew, Crop };
|
||||
enum class HandleRole { Resize, Rotate, SkewEdge, Pivot, CropEdge };
|
||||
|
||||
// methods
|
||||
public:
|
||||
@@ -171,6 +175,9 @@ class DiagramImageItem : public QetGraphicsItem {
|
||||
void paint(QPainter *, const QStyleOptionGraphicsItem *, QWidget *) override;
|
||||
void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override;
|
||||
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
|
||||
void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
|
||||
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;
|
||||
void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override;
|
||||
void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override;
|
||||
void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override;
|
||||
bool sceneEventFilter(QGraphicsItem *watched, QEvent *event) override;
|
||||
@@ -206,6 +213,7 @@ class DiagramImageItem : public QetGraphicsItem {
|
||||
void handlerMouseReleaseEvent(int index);
|
||||
void cancelHandleDrag();
|
||||
void endHandleDrag();
|
||||
void releaseKeyboardItem();
|
||||
void dragResize(int index, const QPointF &localPos, Qt::KeyboardModifiers mods);
|
||||
void dragRotateHandle(int cornerIndex, const QPointF &scenePos, Qt::KeyboardModifiers mods);
|
||||
void dragSkewHandle(int edgeIndex, const QPointF &scenePos, Qt::KeyboardModifiers mods);
|
||||
@@ -268,6 +276,16 @@ class DiagramImageItem : public QetGraphicsItem {
|
||||
int m_vector_index = -1;
|
||||
QPointF m_original_pos; // scene position at the start of a resize/rotate/pivot drag, for Escape-to-cancel
|
||||
bool m_drag_cancelled = false; // Escape pressed: ignore the rest of this mouse gesture
|
||||
// Crop mode: the crop being edited, in the original's pixels; the
|
||||
// whole original, colour-keyed, shown dimmed around it; and the
|
||||
// state of a drag that moves the crop window.
|
||||
QRectF m_pending_crop;
|
||||
QPixmap m_crop_preview;
|
||||
bool m_crop_moving = false;
|
||||
QPointF m_crop_move_start;
|
||||
QRectF m_crop_move_origin;
|
||||
QRectF cropFrameLocal() const;
|
||||
void dragCropHandle(int index, const QPointF &scenePos);
|
||||
ShapeTransform m_original_transform;
|
||||
bool m_original_pivotIsCustom = false;
|
||||
bool m_deferHandleReposition = false; // see setPivot()'s comment
|
||||
|
||||
@@ -1118,6 +1118,15 @@ add_test(NAME tst_diagramcontext COMMAND tst_diagramcontext)
|
||||
target_include_directories(tst_diagramcontext PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_diagramcontext PRIVATE Qt::Test Qt::Widgets Qt::Xml pugixml::pugixml)
|
||||
|
||||
# Cropping a picture on the folio: which edges a handle moves, how far,
|
||||
# and moving the crop window over the original (CropGeometry).
|
||||
add_executable(
|
||||
tst_cropgeometry
|
||||
tst_cropgeometry.cpp)
|
||||
add_test(NAME tst_cropgeometry COMMAND tst_cropgeometry)
|
||||
target_include_directories(tst_cropgeometry PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_cropgeometry PRIVATE Qt::Test Qt::Core)
|
||||
|
||||
# XRefProperties: an empty or unknown cross-reference position, as older
|
||||
# versions saved it, reads as the default instead of drawing the
|
||||
# cross-reference over the element's label (issue #1238).
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech 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.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "qetgraphicsitem/cropgeometry.h"
|
||||
|
||||
#include <QtTest>
|
||||
|
||||
/**
|
||||
Cropping a picture on the folio: which edges a handle moves, how far
|
||||
they may go, and moving the crop window over the original.
|
||||
*/
|
||||
class tst_cropgeometry : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
const QRectF m_bounds{0, 0, 400, 300}; // the original, in its pixels
|
||||
const QRectF m_crop{100, 50, 200, 100};
|
||||
|
||||
private slots:
|
||||
void handlesMoveTheEdgesTheySitOn_data()
|
||||
{
|
||||
QTest::addColumn<QPointF>("handle");
|
||||
QTest::addColumn<bool>("left");
|
||||
QTest::addColumn<bool>("top");
|
||||
QTest::addColumn<bool>("right");
|
||||
QTest::addColumn<bool>("bottom");
|
||||
QTest::newRow("top-left corner") << QPointF(0, 0) << true << true << false << false;
|
||||
QTest::newRow("top edge") << QPointF(1, 0) << false << true << false << false;
|
||||
QTest::newRow("right edge") << QPointF(2, 1) << false << false << true << false;
|
||||
QTest::newRow("bottom-right corner") << QPointF(2, 2) << false << false << true << true;
|
||||
}
|
||||
|
||||
void handlesMoveTheEdgesTheySitOn()
|
||||
{
|
||||
QFETCH(QPointF, handle);
|
||||
QFETCH(bool, left);
|
||||
QFETCH(bool, top);
|
||||
QFETCH(bool, right);
|
||||
QFETCH(bool, bottom);
|
||||
const CropGeometry::Edges e = CropGeometry::edgesForHandle(handle, QSizeF(2, 2));
|
||||
QCOMPARE(e.left, left);
|
||||
QCOMPARE(e.top, top);
|
||||
QCOMPARE(e.right, right);
|
||||
QCOMPARE(e.bottom, bottom);
|
||||
}
|
||||
|
||||
void aCornerMovesTwoEdges()
|
||||
{
|
||||
CropGeometry::Edges e;
|
||||
e.left = e.top = true;
|
||||
QCOMPARE(CropGeometry::dragEdges(m_crop, e, QPointF(120, 60), m_bounds, 4),
|
||||
QRectF(QPointF(120, 60), m_crop.bottomRight()));
|
||||
}
|
||||
|
||||
void edgesStayOnTheOriginal()
|
||||
{
|
||||
CropGeometry::Edges e;
|
||||
e.right = e.bottom = true;
|
||||
QCOMPARE(CropGeometry::dragEdges(m_crop, e, QPointF(900, 900), m_bounds, 4),
|
||||
QRectF(m_crop.topLeft(), m_bounds.bottomRight()));
|
||||
CropGeometry::Edges l;
|
||||
l.left = true;
|
||||
QCOMPARE(CropGeometry::dragEdges(m_crop, l, QPointF(-50, 0), m_bounds, 4).left(), 0.0);
|
||||
}
|
||||
|
||||
// Dragged past the opposite edge, an edge stops a minimum away
|
||||
// instead of turning the frame inside out.
|
||||
void edgesNeverCross()
|
||||
{
|
||||
CropGeometry::Edges e;
|
||||
e.left = true;
|
||||
const QRectF r = CropGeometry::dragEdges(m_crop, e, QPointF(380, 80), m_bounds, 4);
|
||||
QCOMPARE(r.left(), m_crop.right() - 4);
|
||||
QCOMPARE(r.width(), 4.0);
|
||||
}
|
||||
|
||||
void theWindowMovesWithinTheOriginal()
|
||||
{
|
||||
QCOMPARE(CropGeometry::moveWithin(m_crop, QPointF(30, -20), m_bounds),
|
||||
m_crop.translated(30, -20));
|
||||
QCOMPARE(CropGeometry::moveWithin(m_crop, QPointF(500, -500), m_bounds),
|
||||
QRectF(QPointF(200, 0), m_crop.size()));
|
||||
}
|
||||
|
||||
void cropsLandOnWholePixels()
|
||||
{
|
||||
QCOMPARE(CropGeometry::toPixels(QRectF(10.4, 9.6, 20.2, 30.3)), QRect(10, 10, 21, 30));
|
||||
QCOMPARE(CropGeometry::toPixels(m_bounds), m_bounds.toRect());
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_GUILESS_MAIN(tst_cropgeometry)
|
||||
#include "tst_cropgeometry.moc"
|
||||
Reference in New Issue
Block a user