diff --git a/sources/diagramview.cpp b/sources/diagramview.cpp index 7602a1ca8..ef7618a85 100644 --- a/sources/diagramview.cpp +++ b/sources/diagramview.cpp @@ -429,11 +429,12 @@ 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 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 + Add the picture files dropped from the file manager. A single one is + centred on the drop point and kept inside the frame; a picture too + large for the folio is scaled down to leave a free margin of + ImageDrop::frameMargin to the frame on every side. Several pictures + are spread side by side in a grid over that same area + (ImageDrop::gridLayout). 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 */ @@ -446,32 +447,51 @@ void DiagramView::handleImageFilesDrop(QDropEvent *e) const QPointF drop_pos = mapToScene(e -> position().toPoint()); const QRectF frame = m_diagram -> border_and_titleblock.insideBorderRect(); - auto *undo = new QUndoCommand(); QStringList refused; - int placed = 0; + QList items; for (const QString &file : files) { QString error; const QImage image = ImageDrop::load(file, &error); - if (image.isNull()) { + if (image.isNull()) refused << QStringLiteral("%1 : %2").arg(QFileInfo(file).fileName(), error); - continue; - } + else + items << new DiagramImageItem(QPixmap::fromImage(image)); + } - auto *item = new DiagramImageItem(QPixmap::fromImage(image)); - const qreal scale = ImageDrop::fitScale(item -> boundingRect().size(), frame); + // Where each picture goes, as it will look on the folio: one picture + // is centred on the drop point and fitted to the frame, several are + // spread over the frame in a grid. + QList targets; + if (items.size() == 1) + { + const QSizeF size = items.first() -> mapRectToScene(items.first() -> boundingRect()).size(); + const qreal scale = ImageDrop::fitScale(size, frame); + QRectF r(QPointF(), size * scale); + r.moveCenter(drop_pos); + targets << ImageDrop::keepInside(r, scale < 1.0 ? ImageDrop::innerFrame(frame) : frame); + } + else + { + QList sizes; + for (DiagramImageItem *item : items) + sizes << item -> mapRectToScene(item -> boundingRect()).size(); + targets = ImageDrop::gridLayout(sizes, ImageDrop::innerFrame(frame)); + } + + auto *undo = new QUndoCommand(); + const int placed = int(items.size()); + for (int i = 0 ; i < placed ; ++i) + { + DiagramImageItem *item = items.at(i); + const QRectF natural = item -> mapRectToScene(item -> boundingRect()); + const qreal scale = natural.width() > 0 ? targets.at(i).width() / natural.width() : 1.0; if (scale < 1.0) { item -> setScaleFactorX(scale); item -> setScaleFactorY(scale); } - // 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; + const QPointF offset = item -> mapRectToScene(item -> boundingRect()).topLeft() - item -> pos(); + new AddGraphicsObjectCommand(item, m_diagram, targets.at(i).topLeft() - offset, undo); } if (placed) { diff --git a/sources/imagedrop.cpp b/sources/imagedrop.cpp index 871936662..06aa5ed85 100644 --- a/sources/imagedrop.cpp +++ b/sources/imagedrop.cpp @@ -24,6 +24,7 @@ #include #include +#include namespace { @@ -119,7 +120,26 @@ QRectF ImageDrop::keepInside(const QRectF &picture, const QRectF &area) return result; } -QPointF ImageDrop::cascadeOffset(int index) +QList ImageDrop::gridLayout(const QList &sizes, const QRectF &area) { - return QPointF(cascadeStep * index, cascadeStep * index); + QList rects; + const int count = int(sizes.size()); + if (count == 0) + return rects; + const int columns = int(std::ceil(std::sqrt(qreal(count)))); + const int rows = (count + columns - 1) / columns; + const QSizeF cell(area.width() / columns, area.height() / rows); + const QSizeF room = cell * (1.0 - gridGap); + for (int i = 0; i < count; ++i) + { + const QSizeF size = sizes.at(i); + qreal scale = 1.0; + if (!size.isEmpty() && !room.isEmpty()) + scale = std::min({1.0, room.width() / size.width(), room.height() / size.height()}); + QRectF r(QPointF(), size * scale); + r.moveCenter(area.topLeft() + QPointF(cell.width() * (i % columns + 0.5), + cell.height() * (i / columns + 0.5))); + rects << r; + } + return rects; } diff --git a/sources/imagedrop.h b/sources/imagedrop.h index 431791d86..22b944274 100644 --- a/sources/imagedrop.h +++ b/sources/imagedrop.h @@ -22,6 +22,7 @@ #include #include #include +#include #include class QMimeData; @@ -37,8 +38,9 @@ namespace ImageDrop constexpr qint64 maxFileBytes = 10LL * 1024 * 1024; /// Refuses decompression bombs before any pixel is allocated. constexpr qint64 maxPixels = 64LL * 1024 * 1024; - /// Offset between the pictures of one drop, in scene units. - constexpr qreal cascadeStep = 20.0; + /// Space left between the pictures of one drop laid out in a grid, as + /// a share of a grid cell. + constexpr qreal gridGap = 0.05; /// 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; @@ -76,8 +78,14 @@ namespace ImageDrop */ 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); + /** + Lay out pictures of @a sizes, dropped together, side by side in a + grid filling @a area: as many columns as needed for a roughly + square grid, every picture shrunk -- never enlarged -- into its + cell, proportions kept, and centred in it. + @return where each picture goes, in the order of @a sizes + */ + QList gridLayout(const QList &sizes, const QRectF &area); } #endif // IMAGEDROP_H diff --git a/tests/qttest/tst_imagedrop.cpp b/tests/qttest/tst_imagedrop.cpp index 4c357950f..41945b203 100644 --- a/tests/qttest/tst_imagedrop.cpp +++ b/tests/qttest/tst_imagedrop.cpp @@ -192,11 +192,61 @@ private slots: QCOMPARE(ImageDrop::keepInside(picture, QRectF(0, 0, 1000, 800)), expected); } - void picturesOfOneDropCascade() + // Several pictures dropped together are spread side by side: a + // roughly square grid over the area, each shrunk into its cell. + void severalPicturesAreSpreadInAGrid_data() { - QCOMPARE(ImageDrop::cascadeOffset(0), QPointF(0, 0)); - QCOMPARE(ImageDrop::cascadeOffset(2), QPointF(2 * ImageDrop::cascadeStep, - 2 * ImageDrop::cascadeStep)); + QTest::addColumn("count"); + QTest::addColumn("columns"); + QTest::addColumn("rows"); + QTest::newRow("2") << 2 << 2 << 1; + QTest::newRow("3") << 3 << 2 << 2; + QTest::newRow("4") << 4 << 2 << 2; + QTest::newRow("5") << 5 << 3 << 2; + QTest::newRow("9") << 9 << 3 << 3; + } + + void severalPicturesAreSpreadInAGrid() + { + QFETCH(int, count); + QFETCH(int, columns); + QFETCH(int, rows); + const QRectF area(100, 50, 600, 480); + const QList sizes(count, QSizeF(3024, 4032)); + const QList rects = ImageDrop::gridLayout(sizes, area); + QCOMPARE(rects.size(), count); + + const QSizeF cell(area.width() / columns, area.height() / rows); + for (int i = 0 ; i < count ; ++i) + { + const QRectF r = rects.at(i); + // inside the area, proportions kept + QVERIFY(area.contains(r)); + QVERIFY(qAbs(r.width() / r.height() - 3024.0 / 4032.0) < 1e-6); + // centred in its own cell, row by row + const QPointF centre = area.topLeft() + QPointF(cell.width() * (i % columns + 0.5), + cell.height() * (i / columns + 0.5)); + QVERIFY(qAbs(r.center().x() - centre.x()) < 1e-6); + QVERIFY(qAbs(r.center().y() - centre.y()) < 1e-6); + // and overlapping no other + for (int j = i + 1 ; j < count ; ++j) + QVERIFY2(!r.intersects(rects.at(j)), qPrintable(QString("%1 and %2").arg(i).arg(j))); + } + } + + void smallPicturesAreNotEnlargedInTheGrid() + { + const QList rects = ImageDrop::gridLayout({QSizeF(40, 20), QSizeF(30, 30)}, + QRectF(0, 0, 600, 480)); + QCOMPARE(rects.at(0).size(), QSizeF(40, 20)); + QCOMPARE(rects.at(1).size(), QSizeF(30, 30)); + QCOMPARE(rects.at(0).center(), QPointF(150, 240)); + QCOMPARE(rects.at(1).center(), QPointF(450, 240)); + } + + void emptyDropHasNoLayout() + { + QVERIFY(ImageDrop::gridLayout({}, QRectF(0, 0, 600, 480)).isEmpty()); } };