Merge pull request #1416 from bhangart/feature/drop-images-on-folio
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 1m40s

Drop picture files on a folio to add them
This commit is contained in:
ispyisail
2026-10-09 22:59:20 +13:00
committed by GitHub
9 changed files with 614 additions and 18 deletions
@@ -17,6 +17,7 @@
*/
#include "diagrameventaddimage.h"
#include "../imagedrop.h"
#include "../qetapp.h"
#include "../qetdiagrameditor.h"
@@ -341,10 +342,11 @@ void DiagramEventAddImage::openDialog()
if (fileName.isEmpty()) return;
QImage image(fileName);
QString error;
const QImage image = ImageDrop::load(fileName, &error);
if(image.isNull())
{
QMessageBox::critical(m_diagram->views().isEmpty()? nullptr : m_diagram->views().first(), QObject::tr("Error"), QObject::tr("Unable to load the image."));
QMessageBox::critical(m_diagram->views().isEmpty()? nullptr : m_diagram->views().first(), QObject::tr("Error"), QObject::tr("Unable to load the image.") + "\n" + error);
return;
}
+100 -3
View File
@@ -30,6 +30,7 @@
#include "qetdiagrameditor.h"
#include "qetgraphicsitem/conductor.h"
#include "qetgraphicsitem/conductortextitem.h"
#include "qetgraphicsitem/diagramimageitem.h"
#include "qetgraphicsitem/independenttextitem.h"
#include "qeticons.h"
#include "qetpalette.h"
@@ -42,6 +43,7 @@
#include "diagram.h"
#include "foliogrid.h"
#include "diagramcontexttoolbar.h"
#include "imagedrop.h"
#include "diagramgestureoverlay.h"
#include "gesturesettings.h"
#include "shortcutbarsettings.h"
@@ -55,6 +57,7 @@
#include "qetversion.h"
#include <QApplication>
#include <QDropEvent>
#include <QMessageBox>
#include <QPainter>
#include <QPointer>
#include <QSet>
@@ -222,7 +225,14 @@ DiagramView::~DiagramView()
@param e le QDragEnterEvent correspondant au drag'n drop tente
*/
void DiagramView::dragEnterEvent(QDragEnterEvent *e) {
if (e -> mimeData() -> hasFormat("application/x-qet-element-uri")) {
// Picture files from the file manager are checked first: a file drag
// also carries its path as text, which would otherwise become a text.
// Any other file (a .qet project...) is left to the main window.
if (!ImageDrop::imageFiles(e -> mimeData()).isEmpty()) {
e -> acceptProposedAction();
} else if (ImageDrop::hasOnlyOtherUrls(e -> mimeData())) {
e -> ignore();
} else if (e -> mimeData() -> hasFormat("application/x-qet-element-uri")) {
e -> acceptProposedAction();
} else if (e -> mimeData() -> hasFormat("application/x-qet-titleblock-uri")) {
e -> acceptProposedAction();
@@ -238,7 +248,9 @@ void DiagramView::dragEnterEvent(QDragEnterEvent *e) {
@param e le QDragMoveEvent correspondant au drag'n drop tente
*/
void DiagramView::dragMoveEvent(QDragMoveEvent *e) {
if (e -> mimeData() -> hasFormat("text/plain")) e -> acceptProposedAction();
if (!ImageDrop::imageFiles(e -> mimeData()).isEmpty()) e -> acceptProposedAction();
else if (ImageDrop::hasOnlyOtherUrls(e -> mimeData())) e -> ignore();
else if (e -> mimeData() -> hasFormat("text/plain")) e -> acceptProposedAction();
else e-> ignore();
}
@@ -248,7 +260,11 @@ void DiagramView::dragMoveEvent(QDragMoveEvent *e) {
*/
void DiagramView::dropEvent(QDropEvent *e) {
if (e -> mimeData() -> hasFormat("application/x-qet-element-uri")) {
if (!ImageDrop::imageFiles(e -> mimeData()).isEmpty()) {
handleImageFilesDrop(e);
} else if (ImageDrop::hasOnlyOtherUrls(e -> mimeData())) {
e -> ignore();
} else if (e -> mimeData() -> hasFormat("application/x-qet-element-uri")) {
handleElementDrop(e);
} else if (e -> mimeData() -> hasFormat("application/x-qet-titleblock-uri")) {
handleTitleBlockDrop(e);
@@ -411,6 +427,87 @@ void DiagramView::handleTextDrop(QDropEvent *e) {
iti, m_diagram, mapToScene(e->position().toPoint())));
}
/**
@brief DiagramView::handleImageFilesDrop
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
*/
void DiagramView::handleImageFilesDrop(QDropEvent *e)
{
if (m_diagram -> isReadOnly()) return;
e -> acceptProposedAction();
const QStringList files = ImageDrop::imageFiles(e -> mimeData());
const QPointF drop_pos = mapToScene(e -> position().toPoint());
const QRectF frame = m_diagram -> border_and_titleblock.insideBorderRect();
QStringList refused;
QList<DiagramImageItem *> items;
for (const QString &file : files)
{
QString error;
const QImage image = ImageDrop::load(file, &error);
if (image.isNull())
refused << QStringLiteral("%1 : %2").arg(QFileInfo(file).fileName(), error);
else
items << new DiagramImageItem(QPixmap::fromImage(image));
}
// 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<QRectF> 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<QSizeF> 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);
}
const QPointF offset = item -> mapRectToScene(item -> boundingRect()).topLeft() - item -> pos();
new AddGraphicsObjectCommand(item, m_diagram, targets.at(i).topLeft() - offset, undo);
}
if (placed) {
undo -> setText(placed == 1 ? tr("Add an image")
: tr("Add %n image(s)", nullptr, placed));
m_diagram -> undoStack().push(undo);
} else {
delete undo;
}
if (!refused.isEmpty())
QMessageBox::warning(this, tr("Images not added"),
tr("These files could not be added:") + "\n\n"
+ refused.join("\n"));
}
/**
Set the Diagram in visualisation mode
*/
+1
View File
@@ -145,6 +145,7 @@ class DiagramView : public PaletteGraphicsView
void updateFolioReportMenu();
void handleTitleBlockDrop(QDropEvent *);
void handleTextDrop(QDropEvent *);
void handleImageFilesDrop(QDropEvent *);
void scrollOnMovement(QKeyEvent *);
bool gestureEvent(QGestureEvent *event);
QRectF viewedSceneRect() const;
+145
View File
@@ -0,0 +1,145 @@
/*
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 "imagedrop.h"
#include <QCoreApplication>
#include <QFileInfo>
#include <QImageReader>
#include <QMimeData>
#include <QUrl>
#include <algorithm>
#include <cmath>
namespace
{
bool isImageFile(const QUrl &url)
{
if (!url.isLocalFile())
return false;
const QString suffix = QFileInfo(url.toLocalFile()).suffix().toLower();
return ImageDrop::supportedSuffixes().contains(suffix);
}
QString tr(const char *text)
{
return QCoreApplication::translate("ImageDrop", text);
}
}
QStringList ImageDrop::supportedSuffixes()
{
return {QStringLiteral("png"), QStringLiteral("jpg"), QStringLiteral("jpeg"),
QStringLiteral("bmp"), QStringLiteral("svg")};
}
QStringList ImageDrop::imageFiles(const QMimeData *mime)
{
QStringList files;
if (!mime || !mime->hasUrls())
return files;
for (const QUrl &url : mime->urls())
if (isImageFile(url))
files << url.toLocalFile();
return files;
}
bool ImageDrop::hasOnlyOtherUrls(const QMimeData *mime)
{
return mime && mime->hasUrls() && imageFiles(mime).isEmpty();
}
QImage ImageDrop::load(const QString &path, QString *error)
{
auto fail = [error](const QString &message) {
if (error)
*error = message;
return QImage();
};
const QFileInfo info(path);
if (!info.isFile())
return fail(tr("not a file"));
if (info.size() > maxFileBytes)
return fail(tr("the file is larger than 10 MB"));
QImageReader reader(path);
const QSize size = reader.size();
if (size.isValid() && qint64(size.width()) * size.height() > maxPixels)
return fail(tr("the image has too many pixels"));
const QImage image = reader.read();
if (image.isNull())
return fail(tr("unable to read the image"));
return image;
}
QRectF ImageDrop::innerFrame(const QRectF &frame)
{
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(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;
}
QList<QRectF> ImageDrop::gridLayout(const QList<QSizeF> &sizes, const QRectF &area)
{
QList<QRectF> 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;
}
+91
View File
@@ -0,0 +1,91 @@
/*
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 IMAGEDROP_H
#define IMAGEDROP_H
#include <QImage>
#include <QPointF>
#include <QRectF>
#include <QSizeF>
#include <QList>
#include <QStringList>
class QMimeData;
/**
Picture files dropped on a folio: which files qualify, how they are
loaded, and where they are placed. Kept free of any QElectroTech class
so that it can be tested on its own.
*/
namespace ImageDrop
{
/// Pictures are embedded in the project, so larger files are refused.
constexpr qint64 maxFileBytes = 10LL * 1024 * 1024;
/// Refuses decompression bombs before any pixel is allocated.
constexpr qint64 maxPixels = 64LL * 1024 * 1024;
/// 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;
/// The suffixes the "add image" file dialog offers, lower case.
QStringList supportedSuffixes();
/// The local picture files among the URLs of @a mime, in drop order.
QStringList imageFiles(const QMimeData *mime);
/// True when @a mime carries URLs and none of them is a picture file:
/// the drop is meant for someone else (a .qet project, an element).
bool hasOnlyOtherUrls(const QMimeData *mime);
/**
Load the picture at @a path, checking that it is a regular file of
at most maxFileBytes and maxPixels. On failure the image is null
and @a error, if given, says why.
*/
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 innerFrame(@a frame),
keeping its proportions; 1.0 when it already fits. Pictures are
never enlarged.
*/
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);
/**
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<QRectF> gridLayout(const QList<QSizeF> &sizes, const QRectF &area);
}
#endif // IMAGEDROP_H
+6 -13
View File
@@ -29,6 +29,7 @@
#include "../diagramcontent.h"
#include "../diagramview.h"
#include "../factory/elementfactory.h"
#include "../imagedrop.h"
#include "../factory/qetgraphicstablefactory.h"
#include "../factory/ui/addtabledialog.h"
#include "../qet.h"
@@ -3437,20 +3438,12 @@ int QetScriptApi::addImage(int folioIndex, const QString &filePath, double x, do
const QList<Diagram *> diagrams = m_project->diagrams();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return -1;
const QFileInfo info(filePath);
if (!info.isFile()) {
log(QStringLiteral("qet.addImage: '%1' is not a file").arg(filePath));
return -1;
}
constexpr qint64 max_bytes = 10LL * 1024 * 1024;
if (info.size() > max_bytes) {
log(QStringLiteral("qet.addImage: '%1' is %2 bytes; images are embedded in the project, "
"so files over 10 MB are refused").arg(filePath).arg(info.size()));
return -1;
}
const QImage image(filePath);
// Same checks as a picture dropped on a folio: a regular file of at
// most 10 MB (it is embedded in the project) and a bounded pixel count.
QString error;
const QImage image = ImageDrop::load(filePath, &error);
if (image.isNull()) {
log(QStringLiteral("qet.addImage: '%1' could not be read as an image").arg(filePath));
log(QStringLiteral("qet.addImage: '%1': %2").arg(filePath, error));
return -1;
}