Crop a picture directly on the folio

Double-clicking a picture, or "Crop on the folio" in its context
menu, crops it in place: the whole original is shown, dimmed outside
the region kept, and eight handles move the crop's edges; a drag inside
the frame moves the crop window over the original. Enter, a double-click
or a click elsewhere applies the crop as one undo step, keeping the
kept region where it is on the folio; Escape drops it. Rotated and
scaled pictures are cropped along their own sides.

While cropping, the picture is the folio's keyboardItem (see the
Escape commit before this one), so the view hands it Enter and Escape
before its own shortcuts: Enter does not repeat the last placing
command, and Escape does not clear the selection, which would apply
the crop.

The context menu gains "Reset crop", which shows the
whole original again, and "Properties...", since double-click no
longer opens them. The crop dialog stays available.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Beat Hangartner
2026-10-04 19:59:26 +02:00
parent 5f6e74918e
commit d86f2d5461
6 changed files with 500 additions and 11 deletions
+9
View File
@@ -1092,6 +1092,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).
+107
View File
@@ -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"