mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-09 21:54:13 +02:00
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 <noreply@anthropic.com>
This commit is contained in:
+13
-9
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+26
-4
@@ -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);
|
||||
|
||||
+15
-4
@@ -20,6 +20,7 @@
|
||||
|
||||
#include <QImage>
|
||||
#include <QPointF>
|
||||
#include <QRectF>
|
||||
#include <QSizeF>
|
||||
#include <QStringList>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<QSizeF>("size");
|
||||
QTest::addColumn<QSizeF>("available");
|
||||
QTest::addColumn<qreal>("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<QRectF>("picture");
|
||||
QTest::addColumn<QRectF>("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()
|
||||
|
||||
Reference in New Issue
Block a user