From c76bc5eb4e8921792dc4f098051527970b46b898 Mon Sep 17 00:00:00 2001 From: Beat Hangartner Date: Sun, 4 Oct 2026 19:24:38 +0200 Subject: [PATCH] Fit dropped pictures into the folio frame, with a margin A dropped picture too large for the folio is now scaled to leave 20 % of the drawing area free on every side, instead of being fitted to the visible part of the view, and every dropped picture is kept inside the frame -- a scaled-down one also off that margin. Co-Authored-By: Claude Opus 5.5 --- sources/diagramview.cpp | 22 ++++++++------ sources/imagedrop.cpp | 30 +++++++++++++++++--- sources/imagedrop.h | 19 ++++++++++--- tests/qttest/tst_imagedrop.cpp | 52 +++++++++++++++++++++++++++------- 4 files changed, 96 insertions(+), 27 deletions(-) diff --git a/sources/diagramview.cpp b/sources/diagramview.cpp index 21e651701..7602a1ca8 100644 --- a/sources/diagramview.cpp +++ b/sources/diagramview.cpp @@ -430,9 +430,10 @@ void DiagramView::handleTextDrop(QDropEvent *e) { /** @brief DiagramView::handleImageFilesDrop Add the picture files dropped from the file manager. The first one is - centred on the drop point and the others are cascaded from it; a - picture larger than the visible part of the folio is scaled down to - fit it. One undo step removes them all. Files that cannot be used are + centred on the drop point and the others are cascaded from it. A + picture too large for the folio is scaled down to leave a free margin + of ImageDrop::frameMargin to the frame on every side, and every + picture is kept inside the frame. One undo step removes them all. Files that cannot be used are listed once, after the others have been placed. @param e the QDropEvent describing the current drag'n drop */ @@ -443,7 +444,7 @@ void DiagramView::handleImageFilesDrop(QDropEvent *e) const QStringList files = ImageDrop::imageFiles(e -> mimeData()); const QPointF drop_pos = mapToScene(e -> position().toPoint()); - const QSizeF available = viewedSceneRect().size(); + const QRectF frame = m_diagram -> border_and_titleblock.insideBorderRect(); auto *undo = new QUndoCommand(); QStringList refused; @@ -458,15 +459,18 @@ void DiagramView::handleImageFilesDrop(QDropEvent *e) } auto *item = new DiagramImageItem(QPixmap::fromImage(image)); - const qreal scale = ImageDrop::fitScale(item -> boundingRect().size(), available); + const qreal scale = ImageDrop::fitScale(item -> boundingRect().size(), frame); if (scale < 1.0) { item -> setScaleFactorX(scale); item -> setScaleFactorY(scale); } - // Centre the picture, as transformed, on its drop point. - const QPointF centre = item -> mapRectToScene(item -> boundingRect()).center() - item -> pos(); - new AddGraphicsObjectCommand(item, m_diagram, - drop_pos + ImageDrop::cascadeOffset(placed) - centre, undo); + // Centre the picture, as transformed, on its drop point, then keep + // it inside the frame -- a scaled-down one also off the margin. + QRectF rect = item -> mapRectToScene(item -> boundingRect()); + const QPointF offset = rect.topLeft() - item -> pos(); + rect.moveCenter(drop_pos + ImageDrop::cascadeOffset(placed)); + rect = ImageDrop::keepInside(rect, scale < 1.0 ? ImageDrop::innerFrame(frame) : frame); + new AddGraphicsObjectCommand(item, m_diagram, rect.topLeft() - offset, undo); ++placed; } diff --git a/sources/imagedrop.cpp b/sources/imagedrop.cpp index 0c36f25f4..871936662 100644 --- a/sources/imagedrop.cpp +++ b/sources/imagedrop.cpp @@ -88,15 +88,37 @@ QImage ImageDrop::load(const QString &path, QString *error) return image; } -qreal ImageDrop::fitScale(const QSizeF &size, const QSizeF &available) +QRectF ImageDrop::innerFrame(const QRectF &frame) { - if (size.isEmpty() || available.isEmpty()) + return frame.adjusted(frame.width() * frameMargin, frame.height() * frameMargin, + -frame.width() * frameMargin, -frame.height() * frameMargin); +} + +qreal ImageDrop::fitScale(const QSizeF &size, const QRectF &frame) +{ + const QRectF inner = innerFrame(frame); + if (size.isEmpty() || inner.isEmpty()) return 1.0; - const qreal scale = std::min(available.width() * fitMargin / size.width(), - available.height() * fitMargin / size.height()); + const qreal scale = std::min(inner.width() / size.width(), + inner.height() / size.height()); return std::min(1.0, scale); } +QRectF ImageDrop::keepInside(const QRectF &picture, const QRectF &area) +{ + if (area.isEmpty()) + return picture; + auto axis = [](qreal start, qreal length, qreal area_start, qreal area_length) { + if (length >= area_length) + return area_start + (area_length - length) / 2; + return std::clamp(start, area_start, area_start + area_length - length); + }; + QRectF result = picture; + result.moveLeft(axis(picture.left(), picture.width(), area.left(), area.width())); + result.moveTop(axis(picture.top(), picture.height(), area.top(), area.height())); + return result; +} + QPointF ImageDrop::cascadeOffset(int index) { return QPointF(cascadeStep * index, cascadeStep * index); diff --git a/sources/imagedrop.h b/sources/imagedrop.h index 9d7afaab7..431791d86 100644 --- a/sources/imagedrop.h +++ b/sources/imagedrop.h @@ -20,6 +20,7 @@ #include #include +#include #include #include @@ -38,8 +39,9 @@ namespace ImageDrop constexpr qint64 maxPixels = 64LL * 1024 * 1024; /// Offset between the pictures of one drop, in scene units. constexpr qreal cascadeStep = 20.0; - /// Share of the visible area a large dropped picture is fitted into. - constexpr qreal fitMargin = 0.9; + /// Free margin left between a scaled-down picture and the folio frame, + /// as a share of the frame on each side. + constexpr qreal frameMargin = 0.2; /// The suffixes the "add image" file dialog offers, lower case. QStringList supportedSuffixes(); @@ -58,12 +60,21 @@ namespace ImageDrop */ QImage load(const QString &path, QString *error = nullptr); + /// @a frame shrunk by frameMargin on every side. + QRectF innerFrame(const QRectF &frame); + /** - Scale that fits a picture of @a size into fitMargin of @a available, + Scale that fits a picture of @a size into innerFrame(@a frame), keeping its proportions; 1.0 when it already fits. Pictures are never enlarged. */ - qreal fitScale(const QSizeF &size, const QSizeF &available); + qreal fitScale(const QSizeF &size, const QRectF &frame); + + /** + @a picture moved by the least amount that puts it inside @a area; + centred on @a area along an axis where it is larger. + */ + QRectF keepInside(const QRectF &picture, const QRectF &area); /// Offset of the @a index-th picture of one drop from the first one. QPointF cascadeOffset(int index); diff --git a/tests/qttest/tst_imagedrop.cpp b/tests/qttest/tst_imagedrop.cpp index 514299639..4c357950f 100644 --- a/tests/qttest/tst_imagedrop.cpp +++ b/tests/qttest/tst_imagedrop.cpp @@ -140,24 +140,56 @@ private slots: QVERIFY2(error.contains(reason), qPrintable(error)); } - void largePicturesAreFittedIntoTheView_data() + // A picture too large for the folio is scaled down until it leaves 20 % + // of the frame free on every side, i.e. into 60 % of its width and + // height; a picture that already fits keeps its size. + void largePicturesLeaveAMarginToTheFrame_data() { QTest::addColumn("size"); - QTest::addColumn("available"); QTest::addColumn("scale"); - QTest::newRow("small, kept") << QSizeF(100, 50) << QSizeF(1000, 800) << 1.0; - QTest::newRow("wide") << QSizeF(4000, 1000) << QSizeF(1000, 800) << 0.225; - QTest::newRow("tall") << QSizeF(500, 3200) << QSizeF(1000, 800) << 0.225; - QTest::newRow("exactly the margin") << QSizeF(900, 720) << QSizeF(1000, 800) << 1.0; - QTest::newRow("empty view") << QSizeF(4000, 1000) << QSizeF() << 1.0; + // frame: 1000 x 800, so the inner frame is 600 x 480 + QTest::newRow("small, kept") << QSizeF(100, 50) << 1.0; + QTest::newRow("exactly the inner frame") << QSizeF(600, 480) << 1.0; + QTest::newRow("wide") << QSizeF(3000, 1000) << 0.2; + QTest::newRow("tall photo") << QSizeF(3024, 4032) << 480.0 / 4032; } - void largePicturesAreFittedIntoTheView() + void largePicturesLeaveAMarginToTheFrame() { QFETCH(QSizeF, size); - QFETCH(QSizeF, available); QFETCH(qreal, scale); - QCOMPARE(ImageDrop::fitScale(size, available), scale); + const QRectF frame(50, 30, 1000, 800); + QCOMPARE(ImageDrop::innerFrame(frame), QRectF(250, 190, 600, 480)); + QCOMPARE(ImageDrop::fitScale(size, frame), scale); + const QSizeF scaled = size * ImageDrop::fitScale(size, frame); + QVERIFY(scaled.width() <= 600.0001 && scaled.height() <= 480.0001); + } + + void noFrameMeansNoScaling() + { + QCOMPARE(ImageDrop::fitScale(QSizeF(4000, 3000), QRectF()), 1.0); + } + + // A picture dropped near an edge is moved back inside, by the least + // amount; one larger than the area is centred on it. + void picturesAreKeptInsideTheArea_data() + { + QTest::addColumn("picture"); + QTest::addColumn("expected"); + const QRectF inside(300, 300, 100, 50); + QTest::newRow("already inside") << inside << inside; + QTest::newRow("over the top edge") << QRectF(300, -20, 100, 50) << QRectF(300, 0, 100, 50); + QTest::newRow("past the right and bottom") << QRectF(980, 790, 100, 50) + << QRectF(900, 750, 100, 50); + QTest::newRow("wider than the area") << QRectF(-50, 100, 1200, 50) + << QRectF(-100, 100, 1200, 50); + } + + void picturesAreKeptInsideTheArea() + { + QFETCH(QRectF, picture); + QFETCH(QRectF, expected); + QCOMPARE(ImageDrop::keepInside(picture, QRectF(0, 0, 1000, 800)), expected); } void picturesOfOneDropCascade()