mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-20 23:44:13 +02:00
Compare commits
6 Commits
3e66d403c8
...
70c665199f
| Author | SHA1 | Date | |
|---|---|---|---|
| 70c665199f | |||
| 6b577ee757 | |||
| 74e878207a | |||
| b72794bea1 | |||
| 11357eb504 | |||
| 85346305e5 |
@@ -318,6 +318,8 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/diagramevent/diagrameventaddimage.h
|
||||
${QET_DIR}/sources/diagramevent/diagrameventaddshape.cpp
|
||||
${QET_DIR}/sources/diagramevent/diagrameventaddshape.h
|
||||
${QET_DIR}/sources/diagramevent/diagrameventaddpath.cpp
|
||||
${QET_DIR}/sources/diagramevent/diagrameventaddpath.h
|
||||
${QET_DIR}/sources/diagramevent/diagrameventaddtext.cpp
|
||||
${QET_DIR}/sources/diagramevent/diagrameventaddtext.h
|
||||
${QET_DIR}/sources/diagramevent/diagrameventinterface.cpp
|
||||
@@ -518,6 +520,8 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/qetgraphicsitem/qetgraphicsitem.h
|
||||
${QET_DIR}/sources/qetgraphicsitem/qetshapeitem.cpp
|
||||
${QET_DIR}/sources/qetgraphicsitem/qetshapeitem.h
|
||||
${QET_DIR}/sources/qetgraphicsitem/shapetransform.cpp
|
||||
${QET_DIR}/sources/qetgraphicsitem/shapetransform.h
|
||||
${QET_DIR}/sources/qetgraphicsitem/qgraphicsitemutility.cpp
|
||||
${QET_DIR}/sources/qetgraphicsitem/qgraphicsitemutility.h
|
||||
${QET_DIR}/sources/qetgraphicsitem/reportelement.cpp
|
||||
@@ -718,8 +722,12 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/ui/elementpropertieswidget.h
|
||||
${QET_DIR}/sources/ui/formulaassistantdialog.cpp
|
||||
${QET_DIR}/sources/ui/formulaassistantdialog.h
|
||||
${QET_DIR}/sources/ui/imagecropdialog.cpp
|
||||
${QET_DIR}/sources/ui/imagecropdialog.h
|
||||
${QET_DIR}/sources/ui/imagepropertieswidget.cpp
|
||||
${QET_DIR}/sources/ui/imagepropertieswidget.h
|
||||
${QET_DIR}/sources/ui/imagetransparentcolordialog.cpp
|
||||
${QET_DIR}/sources/ui/imagetransparentcolordialog.h
|
||||
${QET_DIR}/sources/ui/importelementdialog.cpp
|
||||
${QET_DIR}/sources/ui/importelementdialog.h
|
||||
${QET_DIR}/sources/ui/importelementtextpatterndialog.cpp
|
||||
@@ -790,6 +798,8 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/undocommand/setautonumcontextcommand.h
|
||||
${QET_DIR}/sources/undocommand/rotateselectioncommand.cpp
|
||||
${QET_DIR}/sources/undocommand/rotateselectioncommand.h
|
||||
${QET_DIR}/sources/undocommand/promoteshapecommand.cpp
|
||||
${QET_DIR}/sources/undocommand/promoteshapecommand.h
|
||||
${QET_DIR}/sources/undocommand/rotatetextscommand.cpp
|
||||
${QET_DIR}/sources/undocommand/rotatetextscommand.h
|
||||
${QET_DIR}/sources/undocommand/movegraphicsitemcommand.cpp
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#232629;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g transform="translate(1,1)">
|
||||
<path style="fill:currentColor;fill-opacity:1;stroke:none" d="m18.5 4c-.65424 0-1.202197.418077-1.408203 1h-13.0918v1c3.601 0 6.5 2.899 6.5 6.5 0 1.905514-.822115 3.601777-2.121094 4.787109-.245727-.180239-.548776-.287109-.878906-.287109-.831 0-1.5.669-1.5 1.5 0 .831.669 1.5 1.5 1.5.65424 0 1.202197-.418077 1.408203-1h4.091797v1h3v-3h-3v1h-3.912109c1.479147-1.367931 2.412109-3.317097 2.412109-5.5 0-2.788667-1.510668-5.207469-3.757813-6.5h9.349609c.206006.581923.753963 1 1.408203 1 .831 0 1.5-.669 1.5-1.5 0-.831-.669-1.5-1.5-1.5m0 1c.277 0 .5.223.5.5 0 .277-.223.5-.5.5-.06925 0-.135453-.013828-.195312-.039063-.179579-.075703-.304688-.253188-.304688-.460938 0-.06925.013828-.135453.039062-.195313.075704-.179578.253188-.304688.460938-.304688m-11 13c.277 0 .5.223.5.5 0 .277-.223.5-.5.5-.277 0-.5-.223-.5-.5 0-.277.223-.5.5-.5m6.5 0h1v1h-1v-1z" transform="translate(-.99999-.99999)" class="ColorScheme-Text"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -548,6 +548,7 @@
|
||||
<file>ico/breeze-icons/scalable/mimetypes/small/48x48/application-x-qet-element.svgz</file>
|
||||
<file>ico/breeze-icons/scalable/mimetypes/small/48x48/application-x-qet-project.svgz</file>
|
||||
<file>ico/breeze-icons/scalable/mimetypes/small/48x48/application-x-qet-titleblock.svgz</file>
|
||||
<file>ico/breeze-icons/scalable/apps/hidef/draw-bezier-curves.svg</file>
|
||||
<file>ico/16x16/object-group.png</file>
|
||||
<file>ico/mac_icon/elmt.icns</file>
|
||||
<file>ico/mac_icon/qelectrotech.icns</file>
|
||||
|
||||
+37
-13
@@ -109,7 +109,16 @@ QString diagramStem(Diagram *diagram, int index)
|
||||
}
|
||||
|
||||
/// Render @p diagram into @p painter, fitting @p target to the page rect.
|
||||
void renderDiagram(Diagram *diagram, QPainter &painter, const QRectF &target)
|
||||
/// @p showTerminals: paint terminal markers (red stroke + blue docking
|
||||
/// dot) and terminal names, as the interactive editor does. Off by
|
||||
/// default — Terminal::paint() draws them whenever the diagram's
|
||||
/// drawTerminals()/drawTerminalNames() flags are set (default true, so
|
||||
/// headless export used to ship them as editor UI); the GUI export
|
||||
/// dialog already defaults to clearing them via
|
||||
/// Diagram::applyProperties(). Pass true (--show-terminals) to keep
|
||||
/// them, e.g. to visually debug an unconnected pin.
|
||||
void renderDiagram(Diagram *diagram, QPainter &painter, const QRectF &target,
|
||||
bool showTerminals = false)
|
||||
{
|
||||
const QRect source = diagramRect(diagram);
|
||||
// Export without the editor grid: drawBackground() only paints it when
|
||||
@@ -117,14 +126,21 @@ void renderDiagram(Diagram *diagram, QPainter &painter, const QRectF &target)
|
||||
// and restore it afterwards.
|
||||
const bool was_drawing_grid = diagram->displayGrid();
|
||||
const bool was_drawing_guides = diagram->displayGuides();
|
||||
const bool was_drawing_terminals = diagram->drawTerminals();
|
||||
const bool was_drawing_terminal_names = diagram->drawTerminalNames();
|
||||
diagram->setDisplayGrid(false);
|
||||
diagram->setDisplayGuides(false);
|
||||
diagram->setDrawTerminals(showTerminals);
|
||||
diagram->setDrawTerminalNames(showTerminals);
|
||||
diagram->render(&painter, target, source, Qt::KeepAspectRatio);
|
||||
diagram->setDisplayGrid(was_drawing_grid);
|
||||
diagram->setDisplayGuides(was_drawing_guides);
|
||||
diagram->setDrawTerminals(was_drawing_terminals);
|
||||
diagram->setDrawTerminalNames(was_drawing_terminal_names);
|
||||
}
|
||||
|
||||
int exportPdf(QETProject &project, const QString &output)
|
||||
int exportPdf(QETProject &project, const QString &output,
|
||||
bool showTerminals = false)
|
||||
{
|
||||
const QList<Diagram *> diagrams = project.diagrams();
|
||||
if (diagrams.isEmpty()) {
|
||||
@@ -164,7 +180,7 @@ int exportPdf(QETProject &project, const QString &output)
|
||||
}
|
||||
const QRectF target(0, 0,
|
||||
writer.width(), writer.height());
|
||||
renderDiagram(diagram, painter, target);
|
||||
renderDiagram(diagram, painter, target, showTerminals);
|
||||
|
||||
// Inject clickable cross-reference / folio-report hyperlinks for this
|
||||
// page. The geometry is rebuilt from the QPdfWriter (not a QPrinter):
|
||||
@@ -214,7 +230,7 @@ int exportPdf(QETProject &project, const QString &output)
|
||||
}
|
||||
|
||||
int exportImages(QETProject &project, const QString &format,
|
||||
const QString &out_dir)
|
||||
const QString &out_dir, bool showTerminals = false)
|
||||
{
|
||||
const QList<Diagram *> diagrams = project.diagrams();
|
||||
if (diagrams.isEmpty()) {
|
||||
@@ -237,14 +253,16 @@ int exportImages(QETProject &project, const QString &format,
|
||||
gen.setViewBox(QRect(0, 0, r.width(), r.height()));
|
||||
gen.setTitle(diagram->title());
|
||||
QPainter painter(&gen);
|
||||
renderDiagram(diagram, painter, QRectF(QPointF(0, 0), r.size()));
|
||||
renderDiagram(diagram, painter, QRectF(QPointF(0, 0), r.size()),
|
||||
showTerminals);
|
||||
painter.end();
|
||||
} else { // png
|
||||
QImage image(r.size(), QImage::Format_ARGB32);
|
||||
image.fill(Qt::white);
|
||||
QPainter painter(&image);
|
||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||
renderDiagram(diagram, painter, QRectF(QPointF(0, 0), r.size()));
|
||||
renderDiagram(diagram, painter, QRectF(QPointF(0, 0), r.size()),
|
||||
showTerminals);
|
||||
painter.end();
|
||||
if (!image.save(path)) {
|
||||
err << "Failed to write '" << path << "'.\n";
|
||||
@@ -768,13 +786,19 @@ bool isExportRequest(const QStringList &args)
|
||||
|
||||
int run(const QStringList &args)
|
||||
{
|
||||
// --show-terminals is a standalone switch (not tied to a position),
|
||||
// so pull it out before the positional project/output arguments are
|
||||
// collected below.
|
||||
QStringList filtered = args;
|
||||
const bool showTerminals = filtered.removeAll("--show-terminals") > 0;
|
||||
|
||||
QString flag;
|
||||
QStringList rest;
|
||||
for (int i = 0; i < args.size(); ++i) {
|
||||
if (exportFlags().contains(args.at(i))) {
|
||||
flag = args.at(i);
|
||||
for (int j = i + 1; j < args.size(); ++j)
|
||||
rest << args.at(j);
|
||||
for (int i = 0; i < filtered.size(); ++i) {
|
||||
if (exportFlags().contains(filtered.at(i))) {
|
||||
flag = filtered.at(i);
|
||||
for (int j = i + 1; j < filtered.size(); ++j)
|
||||
rest << filtered.at(j);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -818,7 +842,7 @@ int run(const QStringList &args)
|
||||
return 2;
|
||||
}
|
||||
if (format == "pdf")
|
||||
return exportPdf(project, output);
|
||||
return exportPdf(project, output, showTerminals);
|
||||
if (format == "cables" || format == "wires")
|
||||
return exportCsv(project, format, output);
|
||||
if (format == "bom")
|
||||
@@ -831,7 +855,7 @@ int run(const QStringList &args)
|
||||
return resaveProject(project, output);
|
||||
if (format == "settb")
|
||||
return setTitleBlock(project, output, rest.mid(2));
|
||||
return exportImages(project, format, output);
|
||||
return exportImages(project, format, output, showTerminals);
|
||||
}
|
||||
|
||||
} // namespace CLIExport
|
||||
|
||||
@@ -42,9 +42,9 @@ namespace CLIExport {
|
||||
@return process exit code (0 on success).
|
||||
|
||||
Usage:
|
||||
qelectrotech --export-pdf <project.qet> <output.pdf>
|
||||
qelectrotech --export-png <project.qet> <output_dir>
|
||||
qelectrotech --export-svg <project.qet> <output_dir>
|
||||
qelectrotech --export-pdf <project.qet> <output.pdf> [--show-terminals]
|
||||
qelectrotech --export-png <project.qet> <output_dir> [--show-terminals]
|
||||
qelectrotech --export-svg <project.qet> <output_dir> [--show-terminals]
|
||||
qelectrotech --export-cables <project.qet> <output.csv>
|
||||
qelectrotech --export-wires <project.qet> <output.csv>
|
||||
qelectrotech --export-bom <project.qet> <output.csv>
|
||||
@@ -57,6 +57,10 @@ namespace CLIExport {
|
||||
|
||||
PDF: one multi-page document (one diagram per page).
|
||||
PNG/SVG: one file per diagram, named <output_dir>/<NN>_<title>.<ext>.
|
||||
--show-terminals: also paint terminal markers (red stroke + blue
|
||||
docking dot) and terminal names, as the interactive editor
|
||||
does; off by default, matching the GUI export dialog's
|
||||
default. Has no effect on the non-image export modes.
|
||||
cables: wiring list (one row per conductor) as CSV.
|
||||
wires: list of distinct wire numbers as CSV.
|
||||
bom: bill of materials (one row per element) as CSV.
|
||||
|
||||
@@ -19,10 +19,14 @@
|
||||
#include "diagrameventaddimage.h"
|
||||
|
||||
#include "../qetapp.h"
|
||||
#include "../qetdiagrameditor.h"
|
||||
#include "../diagram.h"
|
||||
#include "../undocommand/addgraphicsobjectcommand.h"
|
||||
#include "../qetgraphicsitem/diagramimageitem.h"
|
||||
|
||||
#include <QStatusBar>
|
||||
#include <QTimer>
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddImage::DiagramEventAddImage
|
||||
Default constructor
|
||||
@@ -34,6 +38,16 @@ DiagramEventAddImage::DiagramEventAddImage(Diagram *diagram) :
|
||||
m_is_added (false)
|
||||
{
|
||||
openDialog();
|
||||
if (m_running)
|
||||
{
|
||||
// Deferred for the same reason as the shape tools' own
|
||||
// constructor-time hint: Diagram::setEventInterface() destroys
|
||||
// whatever tool was previously active *after* this constructor
|
||||
// returns, and that tool's own destructor clears the status bar
|
||||
// -- an immediate show here would just get wiped out moments
|
||||
// later by that cleanup.
|
||||
QTimer::singleShot(0, this, [this]() { showHint(); });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,33 +61,61 @@ DiagramEventAddImage::~DiagramEventAddImage()
|
||||
delete m_image;
|
||||
}
|
||||
|
||||
if (!m_diagram->views().isEmpty())
|
||||
{
|
||||
if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst()))
|
||||
editor->statusBar()->clearMessage();
|
||||
}
|
||||
|
||||
foreach (QGraphicsView *view, m_diagram->views())
|
||||
view->setContextMenuPolicy((Qt::DefaultContextMenu));
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddImage::showHint
|
||||
Re-asserted on every move (see mouseMoveEvent), not just once at
|
||||
activation: Qt's own built-in "show an action's statusTip on hover"
|
||||
has its own internal "restore whatever was there before" logic for
|
||||
when the hover ends. Since this message is first shown *during* that
|
||||
same hover session (the user is still over the toolbar icon when the
|
||||
deferred constructor-time call above fires), Qt's hover-tracking has
|
||||
no idea this code changed the status bar in the meantime -- the
|
||||
moment the mouse leaves the icon for the canvas, it silently
|
||||
restores whatever it remembers being there before its own tip
|
||||
started, overwriting this one. Re-showing it on every move within
|
||||
the canvas simply outlasts that one-time restore -- the exact same
|
||||
issue already found and fixed for the shape tools.
|
||||
*/
|
||||
void DiagramEventAddImage::showHint() const
|
||||
{
|
||||
if (m_diagram->views().isEmpty())
|
||||
return;
|
||||
if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst()))
|
||||
editor->statusBar()->showMessage(tr("Clic : positionner à la taille d'origine. "
|
||||
"Cliquer-glisser : positionner et redimensionner. "
|
||||
"Clic droit : pivoter de 90°. Ctrl+molette : ajuster la taille."));
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddImage::mousePressEvent
|
||||
Action when mouse is pressed
|
||||
Left button: starts a potential drag-to-resize, anchored here -- but
|
||||
doesn't commit to anything yet. A quick click-release (see
|
||||
mouseMoveEvent's threshold check) still places the image at its
|
||||
original size, matching the previous behavior exactly; only an
|
||||
actual drag switches to resizing. Right button still rotates in 90
|
||||
degree steps, unchanged, and only while not already left-dragging.
|
||||
@param event : event of mouse pressed
|
||||
*/
|
||||
void DiagramEventAddImage::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
||||
{
|
||||
if (m_image && event -> button() == Qt::LeftButton)
|
||||
if (m_image && event->button() == Qt::LeftButton)
|
||||
{
|
||||
QPointF pos = event->scenePos();
|
||||
pos.rx() -= m_image->boundingRect().width()/2;
|
||||
pos.ry() -= m_image->boundingRect().height()/2;
|
||||
m_diagram -> undoStack().push (new AddGraphicsObjectCommand(m_image, m_diagram, pos));
|
||||
|
||||
for (QGraphicsView *view : m_diagram->views()) {
|
||||
view->setContextMenuPolicy((Qt::DefaultContextMenu));
|
||||
}
|
||||
|
||||
m_running = false;
|
||||
emit finish();
|
||||
m_pressed = true;
|
||||
m_resize_engaged = false;
|
||||
m_press_pos = event->scenePos();
|
||||
event->setAccepted(true);
|
||||
}
|
||||
else if (m_image && event -> button() == Qt::RightButton)
|
||||
else if (m_image && !m_pressed && event->button() == Qt::RightButton)
|
||||
{
|
||||
m_image->setRotation(m_image->rotation() + 90);
|
||||
event->setAccepted(true);
|
||||
@@ -87,10 +129,12 @@ void DiagramEventAddImage::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
||||
*/
|
||||
void DiagramEventAddImage::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
|
||||
{
|
||||
if (!m_image || event->buttons() != Qt::NoButton) {
|
||||
if (!m_image) {
|
||||
return;
|
||||
}
|
||||
|
||||
showHint();
|
||||
|
||||
QPointF pos = event->scenePos();
|
||||
|
||||
if (!m_is_added)
|
||||
@@ -103,10 +147,77 @@ void DiagramEventAddImage::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
|
||||
m_is_added = true;
|
||||
}
|
||||
|
||||
m_image->setPos(pos - m_image->boundingRect().center());
|
||||
if (m_pressed)
|
||||
{
|
||||
// Anchored on m_press_pos, not the item's own current position:
|
||||
// dragging in any direction has to visibly grow the image from
|
||||
// where the click started, not from wherever the "no button
|
||||
// held" preview phase happened to leave it centered.
|
||||
const QPointF delta = pos - m_press_pos;
|
||||
if (!m_resize_engaged && QLineF(m_press_pos, pos).length() >= 4.0)
|
||||
m_resize_engaged = true; // latched: crossing back within the threshold afterward must not un-engage it
|
||||
|
||||
if (!m_resize_engaged)
|
||||
{
|
||||
// Still just a (so far) plain click -- keep behaving like
|
||||
// the pre-drag preview: original size, centered here, so
|
||||
// releasing right now reproduces the old click-to-place
|
||||
// behavior exactly.
|
||||
m_image->setPos(m_press_pos - m_image->boundingRect().center());
|
||||
}
|
||||
else
|
||||
{
|
||||
const QSizeF naturalSize = m_image->boundingRect().size();
|
||||
if (naturalSize.width() > 0 && naturalSize.height() > 0)
|
||||
{
|
||||
const qreal scaleX = qAbs(delta.x()) / naturalSize.width();
|
||||
const qreal scaleY = qAbs(delta.y()) / naturalSize.height();
|
||||
// The larger of the two, not a per-axis stretch: images
|
||||
// only support a single uniform scale today (see
|
||||
// boundingRect()/paint(), which never touch aspect
|
||||
// ratio), so this is a diagonal-drag size, not a
|
||||
// free-form one -- breaking aspect ratio on purpose is
|
||||
// its own, separate, larger piece of work.
|
||||
const qreal newScale = qBound(0.01, qMax(scaleX, scaleY), 50.0);
|
||||
m_image->setScale(newScale);
|
||||
}
|
||||
m_image->setPos(qMin(m_press_pos.x(), pos.x()), qMin(m_press_pos.y(), pos.y()));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_image->setPos(pos - m_image->boundingRect().center());
|
||||
}
|
||||
|
||||
event->setAccepted(true);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddImage::mouseReleaseEvent
|
||||
Left button release commits whatever mouseMoveEvent last set --
|
||||
original size and centered if the press never turned into a real
|
||||
drag, or the dragged-out size and position otherwise. Either way,
|
||||
this is the only place placement is actually finalized now; a plain
|
||||
click no longer finishes inside mousePressEvent itself, since it has
|
||||
to wait and see whether a drag follows.
|
||||
@param event : event of mouse release
|
||||
*/
|
||||
void DiagramEventAddImage::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
||||
{
|
||||
if (m_image && m_pressed && event->button() == Qt::LeftButton)
|
||||
{
|
||||
m_diagram->undoStack().push(new AddGraphicsObjectCommand(m_image, m_diagram, m_image->pos()));
|
||||
|
||||
for (QGraphicsView *view : m_diagram->views()) {
|
||||
view->setContextMenuPolicy((Qt::DefaultContextMenu));
|
||||
}
|
||||
|
||||
m_running = false;
|
||||
emit finish();
|
||||
event->setAccepted(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddImage::mouseDoubleClickEvent
|
||||
This method is used only to overwrite double click.
|
||||
@@ -124,7 +235,14 @@ void DiagramEventAddImage::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event
|
||||
*/
|
||||
void DiagramEventAddImage::wheelEvent(QGraphicsSceneWheelEvent *event)
|
||||
{
|
||||
if (!m_is_added || !m_image || event -> modifiers() != Qt::CTRL) {
|
||||
// !m_pressed added alongside the modifier fix: without it, wheel
|
||||
// scaling could fight with an active drag-resize, both trying to
|
||||
// set scale() from different sources in the same gesture.
|
||||
// event->modifiers() & Qt::ControlModifier, not != Qt::CTRL: the
|
||||
// same exact-equality bug already found and fixed several times
|
||||
// this session elsewhere -- Ctrl held together with any other
|
||||
// modifier would silently fail to register as Ctrl at all.
|
||||
if (!m_is_added || !m_image || m_pressed || !(event->modifiers() & Qt::ControlModifier)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
#include "diagrameventinterface.h"
|
||||
|
||||
#include <QPointF>
|
||||
|
||||
class Diagram;
|
||||
class DiagramImageItem;
|
||||
|
||||
@@ -37,15 +39,20 @@ class DiagramEventAddImage : public DiagramEventInterface
|
||||
|
||||
void mousePressEvent (QGraphicsSceneMouseEvent *event) override;
|
||||
void mouseMoveEvent (QGraphicsSceneMouseEvent *event) override;
|
||||
void mouseReleaseEvent (QGraphicsSceneMouseEvent *event) override;
|
||||
void mouseDoubleClickEvent (QGraphicsSceneMouseEvent *event) override;
|
||||
void wheelEvent (QGraphicsSceneWheelEvent *event) override;
|
||||
|
||||
bool isNull () const;
|
||||
private:
|
||||
void openDialog();
|
||||
void showHint() const;
|
||||
|
||||
DiagramImageItem *m_image;
|
||||
bool m_is_added;
|
||||
bool m_pressed = false; // left button held: dragging out a size, not just positioning
|
||||
bool m_resize_engaged = false; // latched once the drag threshold is crossed, matching the pen tool's own curve-drag threshold convention -- so dragging out and back near the start point doesn't "snap back" to original size before release
|
||||
QPointF m_press_pos; // scene position of the left-button press, the resize anchor
|
||||
};
|
||||
|
||||
#endif // DIAGRAMEVENTADDIMAGE_H
|
||||
|
||||
@@ -0,0 +1,427 @@
|
||||
/*
|
||||
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 "diagrameventaddpath.h"
|
||||
|
||||
#include "../diagram.h"
|
||||
#include "../lastusedstyle.h"
|
||||
#include "../qetapp.h"
|
||||
#include "../qetdiagrameditor.h"
|
||||
#include "../undocommand/addgraphicsobjectcommand.h"
|
||||
|
||||
#include <QGraphicsLineItem>
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
#include <QKeyEvent>
|
||||
#include <QLineF>
|
||||
#include <QStatusBar>
|
||||
#include <QTimer>
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddPath::DiagramEventAddPath
|
||||
@param diagram : the diagram where this event must operate
|
||||
*/
|
||||
DiagramEventAddPath::DiagramEventAddPath(Diagram *diagram) :
|
||||
DiagramEventInterface(diagram),
|
||||
m_shape_item (nullptr),
|
||||
m_help_horiz (nullptr),
|
||||
m_help_verti (nullptr)
|
||||
{
|
||||
m_running = true;
|
||||
init();
|
||||
// Deferred for the same reason as DiagramEventAddShape's own
|
||||
// constructor-time hint: Diagram::setEventInterface() destroys
|
||||
// whatever tool was previously active *after* this constructor
|
||||
// returns, and that tool's own destructor clears the status bar --
|
||||
// an immediate show here would just get wiped out moments later.
|
||||
QTimer::singleShot(0, this, [this]() { showHint(); });
|
||||
}
|
||||
|
||||
DiagramEventAddPath::~DiagramEventAddPath()
|
||||
{
|
||||
if ((m_running || m_abort) && m_shape_item)
|
||||
{
|
||||
m_diagram->removeItem(m_shape_item);
|
||||
delete m_shape_item;
|
||||
}
|
||||
delete m_help_horiz;
|
||||
delete m_help_verti;
|
||||
|
||||
if (m_diagram && !m_diagram->views().isEmpty())
|
||||
{
|
||||
if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst()))
|
||||
editor->statusBar()->clearMessage();
|
||||
}
|
||||
|
||||
foreach (QGraphicsView *v, m_diagram->views())
|
||||
v->setContextMenuPolicy(Qt::DefaultContextMenu);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddPath::showHint
|
||||
Re-asserted on every move within the canvas (see mouseMoveEvent), not
|
||||
just once at activation: Qt's own built-in "show an action's
|
||||
statusTip on hover" has its own internal "restore whatever was there
|
||||
before" logic for when the hover ends. Since this message is first
|
||||
shown *during* that same hover session (the user is still over the
|
||||
toolbar icon when the deferred constructor-time call above fires),
|
||||
Qt's hover-tracking has no idea this code changed the status bar in
|
||||
the meantime -- the moment the mouse leaves the icon for the canvas,
|
||||
it silently restores whatever it remembers being there before its
|
||||
own tip started, overwriting this one. Re-showing it on every move
|
||||
within the canvas simply outlasts that one-time restore.
|
||||
*/
|
||||
void DiagramEventAddPath::showHint() const
|
||||
{
|
||||
if (!m_diagram || m_diagram->views().isEmpty())
|
||||
return;
|
||||
if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst()))
|
||||
editor->statusBar()->showMessage(tr("Clic: point anguleux. Cliquer-glisser: point courbe. "
|
||||
"Clic sur le premier point: fermer. Échap/Entrée: terminer. "
|
||||
"Clic droit: annuler le dernier point."));
|
||||
}
|
||||
|
||||
void DiagramEventAddPath::init()
|
||||
{
|
||||
foreach (QGraphicsView *v, m_diagram->views())
|
||||
v->setContextMenuPolicy(Qt::NoContextMenu);
|
||||
}
|
||||
|
||||
QPointF DiagramEventAddPath::snapped(const QPointF &scenePos, Qt::KeyboardModifiers mods) const
|
||||
{
|
||||
return mods == Qt::ControlModifier ? scenePos : Diagram::snapToGrid(scenePos);
|
||||
}
|
||||
|
||||
int DiagramEventAddPath::confirmedNodeCount() const
|
||||
{
|
||||
// The trailing element is always the live preview while m_shape_item
|
||||
// exists; with no shape yet there are no nodes of any kind.
|
||||
return m_shape_item ? qMax(0, m_nodes.size() - 1) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddPath::mousePressEvent
|
||||
Left click: on the very first click, creates the shape with a real
|
||||
node *and* an immediate preview node at the same spot, so a segment
|
||||
exists (even if zero-length) from the start rather than requiring a
|
||||
second click before anything is visible. On later clicks: either
|
||||
confirms the live preview into a real point and appends a fresh one
|
||||
for the next segment, or -- if close enough to the first node --
|
||||
closes the path.
|
||||
*/
|
||||
void DiagramEventAddPath::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
||||
{
|
||||
if (Q_UNLIKELY(m_diagram->isReadOnly()))
|
||||
return;
|
||||
|
||||
if (event->button() != Qt::LeftButton)
|
||||
{
|
||||
// Accept every button while this tool is running, not just the
|
||||
// one it actually acts on -- Diagram::mousePressEvent falls
|
||||
// through to Qt's own default scene handling for anything left
|
||||
// unaccepted, which is exactly the kind of competing control
|
||||
// this tool can't afford while it's supposed to have exclusive
|
||||
// ownership of input.
|
||||
event->setAccepted(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const QPointF pos = snapped(event->scenePos(), event->modifiers());
|
||||
|
||||
if (!m_shape_item)
|
||||
{
|
||||
m_shape_item = new QetShapeItem(pos, pos, QetShapeItem::Path);
|
||||
if (LastUsedStyle::hasShapePen())
|
||||
m_shape_item->setPen(LastUsedStyle::shapePen());
|
||||
if (LastUsedStyle::hasShapeBrush())
|
||||
m_shape_item->setBrush(LastUsedStyle::shapeBrush());
|
||||
m_diagram->addItem(m_shape_item);
|
||||
// Handles only ever get built for a selected item.
|
||||
m_shape_item->setSelected(true);
|
||||
|
||||
QetShapeItem::PathNode node;
|
||||
node.anchor = pos;
|
||||
m_nodes << node;
|
||||
m_nodes << node; // live preview, tracks the mouse from here on
|
||||
m_shape_item->setPathNodes(m_nodes);
|
||||
m_shape_item->enableNodeEditMode();
|
||||
|
||||
m_dragging_node = 0;
|
||||
event->setAccepted(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirmedNodeCount() >= 2 && nearFirstNode(pos))
|
||||
{
|
||||
finishPath(true);
|
||||
event->setAccepted(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Confirm the preview node as a real point, then append a fresh
|
||||
// preview (a plain Corner, not a copy of the just-confirmed node's
|
||||
// kind/handles) for the segment after it.
|
||||
m_dragging_node = m_nodes.size() - 1;
|
||||
m_nodes[m_dragging_node].anchor = pos;
|
||||
|
||||
QetShapeItem::PathNode preview;
|
||||
preview.anchor = pos;
|
||||
m_nodes << preview;
|
||||
|
||||
m_shape_item->setPathNodes(m_nodes);
|
||||
m_shape_item->enableNodeEditMode();
|
||||
event->setAccepted(true);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddPath::mouseMoveEvent
|
||||
Two mutually exclusive behaviours, matching whether a button is held:
|
||||
with the left button down on a just-placed node, dragging shapes that
|
||||
node's handles (same convention as editing an existing node -- see
|
||||
QetShapeItem::dragPathControlHandle()). With no button held, the
|
||||
trailing preview node instead tracks the mouse, giving the live
|
||||
rubber-band segment.
|
||||
*/
|
||||
void DiagramEventAddPath::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
|
||||
{
|
||||
updateHelpCross(event->scenePos());
|
||||
showHint();
|
||||
|
||||
if (m_shape_item)
|
||||
{
|
||||
const QPointF pos = snapped(event->scenePos(), event->modifiers());
|
||||
|
||||
if (m_dragging_node >= 0 && (event->buttons() & Qt::LeftButton))
|
||||
{
|
||||
QetShapeItem::PathNode &node = m_nodes[m_dragging_node];
|
||||
const QPointF delta = pos - node.anchor;
|
||||
|
||||
// A small threshold so an accidental few-pixel wobble on
|
||||
// what was meant to be a plain click doesn't silently add
|
||||
// curve handles the user never intended.
|
||||
if (QLineF(QPointF(), delta).length() > 3.0)
|
||||
{
|
||||
node.kind = QetShapeItem::NodeKind::Smooth;
|
||||
node.outHandle = delta;
|
||||
node.inHandle = -delta;
|
||||
}
|
||||
else
|
||||
{
|
||||
node.kind = QetShapeItem::NodeKind::Corner;
|
||||
node.outHandle.reset();
|
||||
node.inHandle.reset();
|
||||
}
|
||||
m_shape_item->setPathNodes(m_nodes);
|
||||
}
|
||||
else if (!(event->buttons() & Qt::LeftButton) && !m_nodes.isEmpty())
|
||||
{
|
||||
m_nodes.last().anchor = pos;
|
||||
m_shape_item->setPathNodes(m_nodes);
|
||||
}
|
||||
}
|
||||
|
||||
// Ours unconditionally while running: a stray, unaccepted move event
|
||||
// falling through to Qt's default handling risks it dragging our
|
||||
// selected, movable in-progress shape out from under the tool.
|
||||
event->setAccepted(true);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddPath::mouseReleaseEvent
|
||||
Left release just ends the current node's drag phase (the trailing
|
||||
preview resumes tracking the mouse on the next move). Right release
|
||||
steps back one *confirmed* point (the preview is left alone), or
|
||||
cancels outright once only one remains, or exits the tool entirely if
|
||||
nothing is in progress at all.
|
||||
*/
|
||||
void DiagramEventAddPath::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
||||
{
|
||||
if (event->button() == Qt::LeftButton)
|
||||
{
|
||||
m_dragging_node = -1;
|
||||
}
|
||||
else if (event->button() == Qt::RightButton)
|
||||
{
|
||||
if (m_shape_item)
|
||||
{
|
||||
if (confirmedNodeCount() > 1)
|
||||
{
|
||||
m_nodes.remove(m_nodes.size() - 2); // the last *confirmed* node; keep the trailing preview
|
||||
m_shape_item->setPathNodes(m_nodes);
|
||||
}
|
||||
else
|
||||
{
|
||||
cancelPath();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_running = false;
|
||||
emit finish();
|
||||
}
|
||||
}
|
||||
event->setAccepted(true);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddPath::mouseDoubleClickEvent
|
||||
A double-click is a press, release, press, release, doubleclick
|
||||
sequence -- the second press already confirmed the preview into a
|
||||
duplicate point (mousePressEvent can't distinguish a double-click
|
||||
from two single clicks in the same place) and appended a fresh
|
||||
preview after it. Dropping the last node here removes that fresh
|
||||
preview; finishPath()'s own trailing-preview removal then removes the
|
||||
duplicate underneath it, leaving only the genuinely-placed points.
|
||||
*/
|
||||
void DiagramEventAddPath::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
|
||||
{
|
||||
if (m_shape_item && event->button() == Qt::LeftButton && !m_nodes.isEmpty())
|
||||
{
|
||||
m_nodes.removeLast();
|
||||
finishPath(false);
|
||||
}
|
||||
event->setAccepted(true);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddPath::keyPressEvent
|
||||
Escape or Enter finish the path open once at least two real points
|
||||
exist; Escape with fewer (or none placed at all) cancels/exits
|
||||
instead, since there's nothing meaningful to keep.
|
||||
*/
|
||||
void DiagramEventAddPath::keyPressEvent(QKeyEvent *event)
|
||||
{
|
||||
if (event->key() == Qt::Key_Escape)
|
||||
{
|
||||
if (m_shape_item && confirmedNodeCount() >= 2)
|
||||
finishPath(false);
|
||||
else if (m_shape_item)
|
||||
cancelPath();
|
||||
else
|
||||
{
|
||||
m_running = false;
|
||||
emit finish();
|
||||
}
|
||||
event->accept();
|
||||
}
|
||||
else if ((event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter)
|
||||
&& m_shape_item && confirmedNodeCount() >= 2)
|
||||
{
|
||||
finishPath(false);
|
||||
event->accept();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddPath::finishPath
|
||||
Strips the trailing live-preview node, commits the in-progress path
|
||||
onto the undo stack, and resets so the tool is ready to draw another
|
||||
one immediately -- matching every other shape tool's own behaviour
|
||||
after finishing a shape.
|
||||
*/
|
||||
void DiagramEventAddPath::finishPath(bool closed)
|
||||
{
|
||||
if (!m_shape_item)
|
||||
return;
|
||||
|
||||
if (!m_nodes.isEmpty())
|
||||
m_nodes.removeLast();
|
||||
|
||||
if (m_nodes.size() < 2)
|
||||
{
|
||||
cancelPath();
|
||||
return;
|
||||
}
|
||||
|
||||
if (closed)
|
||||
m_shape_item->setClosed(true);
|
||||
|
||||
m_shape_item->setPathNodes(m_nodes);
|
||||
m_diagram->undoStack().push(new AddGraphicsObjectCommand(m_shape_item, m_diagram));
|
||||
m_shape_item = nullptr;
|
||||
m_nodes.clear();
|
||||
m_dragging_node = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddPath::cancelPath
|
||||
Discards the in-progress path entirely -- nothing worth keeping (an
|
||||
empty or single-point path isn't a usable shape).
|
||||
*/
|
||||
void DiagramEventAddPath::cancelPath()
|
||||
{
|
||||
if (m_shape_item)
|
||||
{
|
||||
m_diagram->removeItem(m_shape_item);
|
||||
delete m_shape_item;
|
||||
m_shape_item = nullptr;
|
||||
}
|
||||
m_nodes.clear();
|
||||
m_dragging_node = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddPath::nearFirstNode
|
||||
m_shape_item's pos()/transform() stay at their identity defaults for
|
||||
its entire construction here -- nothing during drawing ever touches
|
||||
them -- so the first node's anchor, stored in local coordinates, is
|
||||
directly comparable to a scene position without any mapping.
|
||||
*/
|
||||
bool DiagramEventAddPath::nearFirstNode(const QPointF &scenePos) const
|
||||
{
|
||||
if (m_nodes.isEmpty())
|
||||
return false;
|
||||
return QLineF(m_nodes.first().anchor, scenePos).length() <= CLOSE_THRESHOLD;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddPath::updateHelpCross
|
||||
Same crosshair guide as every other shape tool (see
|
||||
DiagramEventAddShape::updateHelpCross) -- duplicated rather than
|
||||
shared, since the two classes don't otherwise share a common base
|
||||
beyond DiagramEventInterface.
|
||||
*/
|
||||
void DiagramEventAddPath::updateHelpCross(const QPointF &p)
|
||||
{
|
||||
if (!m_help_horiz || !m_help_verti)
|
||||
{
|
||||
QPen pen;
|
||||
pen.setWidthF(0.4);
|
||||
pen.setCosmetic(true);
|
||||
pen.setColor(Diagram::background_color == Qt::darkGray ? Qt::lightGray : Qt::darkGray);
|
||||
|
||||
QRectF rect = m_diagram->border_and_titleblock.insideBorderRect();
|
||||
|
||||
if (!m_help_horiz)
|
||||
{
|
||||
m_help_horiz = new QGraphicsLineItem(rect.topLeft().x(), 0, rect.topRight().x(), 0);
|
||||
m_help_horiz->setPen(pen);
|
||||
m_diagram->addItem(m_help_horiz);
|
||||
}
|
||||
|
||||
if (!m_help_verti)
|
||||
{
|
||||
m_help_verti = new QGraphicsLineItem(0, rect.topLeft().y(), 0, rect.bottomLeft().y());
|
||||
m_help_verti->setPen(pen);
|
||||
m_diagram->addItem(m_help_verti);
|
||||
}
|
||||
}
|
||||
|
||||
QPointF point = Diagram::snapToGrid(p);
|
||||
|
||||
m_help_horiz->setY(point.y());
|
||||
m_help_verti->setX(point.x());
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
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 DIAGRAMEVENTADDPATH_H
|
||||
#define DIAGRAMEVENTADDPATH_H
|
||||
|
||||
#include "../qetgraphicsitem/qetshapeitem.h"
|
||||
#include "diagrameventinterface.h"
|
||||
|
||||
class QGraphicsLineItem;
|
||||
|
||||
/**
|
||||
@brief The DiagramEventAddPath class
|
||||
Pen tool: interactively draw a new Path (Bezier) shape, following the
|
||||
same vocabulary every vector editor's pen tool uses:
|
||||
- click places a Corner node;
|
||||
- press-drag-release places a Smooth node, with the drag defining a
|
||||
pair of mirrored handles (same convention as
|
||||
QetShapeItem::dragPathControlHandle's "Smooth" mirroring);
|
||||
- clicking back on the first node closes the path;
|
||||
- double-click, Enter, or Escape (once 2+ nodes exist) finishes it
|
||||
open;
|
||||
- right-click steps back one node;
|
||||
- right-click or Escape with nothing placed yet cancels the tool.
|
||||
While running, m_nodes always carries one extra "preview" node at the
|
||||
end, tracking the mouse so a live rubber-band segment is always
|
||||
visible -- confirmedNodeCount() excludes it; every public gesture
|
||||
handler is responsible for stripping it before treating the list as
|
||||
"the path so far" (see finishPath(), which does this once for every
|
||||
finishing gesture).
|
||||
*/
|
||||
class DiagramEventAddPath : public DiagramEventInterface
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
DiagramEventAddPath(Diagram *diagram);
|
||||
~DiagramEventAddPath() override;
|
||||
|
||||
void mousePressEvent (QGraphicsSceneMouseEvent *event) override;
|
||||
void mouseMoveEvent (QGraphicsSceneMouseEvent *event) override;
|
||||
void mouseReleaseEvent (QGraphicsSceneMouseEvent *event) override;
|
||||
void mouseDoubleClickEvent (QGraphicsSceneMouseEvent *event) override;
|
||||
void keyPressEvent (QKeyEvent *event) override;
|
||||
void init() override;
|
||||
|
||||
private:
|
||||
void updateHelpCross (const QPointF &p);
|
||||
void showHint () const;
|
||||
void finishPath (bool closed);
|
||||
void cancelPath ();
|
||||
bool nearFirstNode (const QPointF &scenePos) const;
|
||||
int confirmedNodeCount () const; // m_nodes always carries one trailing "preview" node while running; this excludes it
|
||||
QPointF snapped (const QPointF &scenePos, Qt::KeyboardModifiers mods) const;
|
||||
|
||||
QetShapeItem *m_shape_item;
|
||||
QVector<QetShapeItem::PathNode> m_nodes;
|
||||
int m_dragging_node = -1;
|
||||
QGraphicsLineItem *m_help_horiz, *m_help_verti;
|
||||
|
||||
// Scene units within which a click on an existing path is
|
||||
// treated as "on the first node" and closes the shape, rather
|
||||
// than adding yet another node right next to it.
|
||||
static constexpr qreal CLOSE_THRESHOLD = 12.0;
|
||||
};
|
||||
|
||||
#endif // DIAGRAMEVENTADDPATH_H
|
||||
@@ -19,8 +19,16 @@
|
||||
|
||||
#include "../diagram.h"
|
||||
#include "../lastusedstyle.h"
|
||||
#include "../qetapp.h"
|
||||
#include "../qetdiagrameditor.h"
|
||||
#include "../undocommand/addgraphicsobjectcommand.h"
|
||||
|
||||
#include <QGraphicsEllipseItem>
|
||||
#include <QGuiApplication>
|
||||
#include <QKeyEvent>
|
||||
#include <QStatusBar>
|
||||
#include <QTimer>
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddShape::DiagramEventAddShape
|
||||
Default constructor
|
||||
@@ -36,6 +44,18 @@ DiagramEventAddShape::DiagramEventAddShape(Diagram *diagram, QetShapeItem::Shape
|
||||
{
|
||||
m_running = true;
|
||||
init();
|
||||
// Deferred to the next event-loop iteration, not shown immediately:
|
||||
// Diagram::setEventInterface() destroys whatever tool was
|
||||
// previously active *after* this constructor returns, and that
|
||||
// previous tool's own destructor clears the status bar (see
|
||||
// ~DiagramEventAddShape() below) -- an immediate show here would
|
||||
// just get wiped out moments later by that cleanup. Letting the old
|
||||
// tool's teardown finish first, then showing this one's message, is
|
||||
// the same fix already used for the tooltip-flicker issue in
|
||||
// QetShapeItem::refreshInteractionHints(), applied to the same
|
||||
// class of "something later in the same call chain undoes what I
|
||||
// just did" ordering problem.
|
||||
QTimer::singleShot(0, this, [this]() { updateCreationHint(); });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,11 +70,123 @@ DiagramEventAddShape::~DiagramEventAddShape()
|
||||
}
|
||||
delete m_help_horiz;
|
||||
delete m_help_verti;
|
||||
delete m_center_marker;
|
||||
|
||||
if (m_diagram && !m_diagram->views().isEmpty())
|
||||
{
|
||||
if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst()))
|
||||
editor->statusBar()->clearMessage();
|
||||
}
|
||||
|
||||
foreach (QGraphicsView *v, m_diagram->views())
|
||||
v->setContextMenuPolicy(Qt::DefaultContextMenu);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddShape::applyPosition
|
||||
Applies a drag/click position to the in-progress shape, honouring two
|
||||
modifiers that mirror how the very same shape can already be edited
|
||||
afterward, once placed:
|
||||
- Ctrl, for Rectangle/Ellipse only: the first click becomes the
|
||||
shape's *center* rather than a corner, growing symmetrically as
|
||||
the cursor moves away from it -- the same meaning Ctrl already
|
||||
has on a Resize handle (anchor at center). Deliberately not
|
||||
offered for Line: unlike the Rectangle/Ellipse case, there's no
|
||||
established convention for "a line grows symmetrically from its
|
||||
middle" to justify it by, so Ctrl for Line means only free
|
||||
positioning (see the plain grid-snap check above), nothing more.
|
||||
- Shift, for Rectangle/Ellipse only: forces the bounding box square
|
||||
(so Ellipse becomes a true circle), using whichever of the two
|
||||
dragged dimensions is currently larger and mirroring that onto
|
||||
the other, preserving the direction the user is actually
|
||||
dragging in.
|
||||
Both can combine (Ctrl+Shift: a centered square/circle). Whether or
|
||||
not Ctrl is currently held, the non-anchored branch always rebuilds
|
||||
from m_anchor_point rather than nudging the existing rect/line --
|
||||
otherwise, if Ctrl had been held earlier in the same drag (moving the
|
||||
shape's own first point to a mirrored position), releasing it would
|
||||
leave that point stuck there instead of actually restoring it.
|
||||
*/
|
||||
void DiagramEventAddShape::applyPosition(const QPointF &pos, Qt::KeyboardModifiers mods)
|
||||
{
|
||||
if (!m_shape_item)
|
||||
return;
|
||||
|
||||
if (m_shape_type == QetShapeItem::Polygon)
|
||||
{
|
||||
// setP2() has its own dedicated Polygon handling: it moves the
|
||||
// *last* vertex in place rather than setting a second point,
|
||||
// which is exactly the live "next segment follows the mouse"
|
||||
// preview -- the same idea DiagramEventAddPath's trailing
|
||||
// preview node gives the pen tool. No Ctrl/Shift modifiers apply
|
||||
// here (those are Rectangle/Ellipse/Line-specific below), so
|
||||
// this is a direct, unconditional call.
|
||||
m_shape_item->setP2(pos);
|
||||
return;
|
||||
}
|
||||
|
||||
// m_center_anchored is decided once, in mousePressEvent, not
|
||||
// re-checked here on every call -- re-checking it live meant
|
||||
// releasing Ctrl mid-drag (something you'd naturally do the moment
|
||||
// your hand gets tired holding it, long before you're done resizing)
|
||||
// silently snapped the shape back to corner-anchored, discarding
|
||||
// what felt like an already-made decision. Deciding it once at the
|
||||
// first click matches "I held Ctrl when I clicked, so this shape is
|
||||
// centered" -- a single, predictable rule instead of a live toggle.
|
||||
QPointF target = pos;
|
||||
|
||||
if ((mods & Qt::ShiftModifier)
|
||||
&& (m_shape_type == QetShapeItem::Rectangle || m_shape_type == QetShapeItem::Ellipse))
|
||||
{
|
||||
const QPointF ref = m_anchor_point;
|
||||
const qreal dx = target.x() - ref.x();
|
||||
const qreal dy = target.y() - ref.y();
|
||||
const qreal size = qMax(qAbs(dx), qAbs(dy));
|
||||
target.setX(ref.x() + (dx < 0 ? -size : size));
|
||||
target.setY(ref.y() + (dy < 0 ? -size : size));
|
||||
}
|
||||
|
||||
if (m_center_anchored)
|
||||
{
|
||||
const QPointF mirrored = 2 * m_anchor_point - target;
|
||||
m_shape_item->setRect(QRectF(mirrored, target));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (m_shape_type == QetShapeItem::Line)
|
||||
m_shape_item->setLine(QLineF(m_anchor_point, target));
|
||||
else
|
||||
m_shape_item->setRect(QRectF(m_anchor_point, target));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddShape::showCenterMarker
|
||||
Small, filled marker at the anchor point, shown only while Ctrl-
|
||||
anchoring is actually in effect right now (see applyPosition()) --
|
||||
doubling as live confirmation that it is, rather than leaving the
|
||||
user to infer it purely from how the shape happens to be growing.
|
||||
*/
|
||||
void DiagramEventAddShape::showCenterMarker(const QPointF &scenePos)
|
||||
{
|
||||
if (!m_center_marker)
|
||||
{
|
||||
m_center_marker = new QGraphicsEllipseItem(-4, -4, 8, 8);
|
||||
QPen pen(Qt::red);
|
||||
pen.setCosmetic(true);
|
||||
m_center_marker->setPen(pen);
|
||||
m_center_marker->setBrush(Qt::red);
|
||||
m_diagram->addItem(m_center_marker);
|
||||
}
|
||||
m_center_marker->setPos(scenePos);
|
||||
}
|
||||
|
||||
void DiagramEventAddShape::hideCenterMarker()
|
||||
{
|
||||
delete m_center_marker;
|
||||
m_center_marker = nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddShape::mousePressEvent
|
||||
Action when mouse is pressed
|
||||
@@ -67,7 +199,14 @@ void DiagramEventAddShape::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
||||
}
|
||||
|
||||
QPointF pos = event->scenePos();
|
||||
if (event->modifiers() != Qt::ControlModifier) {
|
||||
// A bitwise flag check, not exact equality: modifiers() == Ctrl
|
||||
// alone fails the moment any other key (Shift for the square/circle
|
||||
// lock, or an incidental platform flag) is also held, silently
|
||||
// falling through to snapToGrid() even though Ctrl is held --
|
||||
// exactly what made Ctrl+Shift together feel "frozen" (both grid-
|
||||
// snapped *and* square-locked, quantizing to whichever is coarser)
|
||||
// and made "Ctrl = free positioning" not actually hold up.
|
||||
if (!(event->modifiers() & Qt::ControlModifier)) {
|
||||
pos = Diagram::snapToGrid(pos);
|
||||
}
|
||||
|
||||
@@ -78,6 +217,15 @@ void DiagramEventAddShape::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
||||
if (!m_shape_item)
|
||||
{
|
||||
m_shape_item = new QetShapeItem(pos, pos, m_shape_type);
|
||||
m_anchor_point = pos;
|
||||
// Decided once, here, rather than re-checked on every mouse
|
||||
// move for the rest of the drag -- see applyPosition()'s doc
|
||||
// comment for why continuous re-checking made releasing Ctrl
|
||||
// mid-drag feel like a bug rather than a deliberate choice.
|
||||
m_center_anchored = (event->modifiers() & Qt::ControlModifier)
|
||||
&& (m_shape_type == QetShapeItem::Rectangle || m_shape_type == QetShapeItem::Ellipse);
|
||||
if (m_center_anchored)
|
||||
showCenterMarker(m_anchor_point);
|
||||
//Start from whatever pen/brush was last applied this
|
||||
//session, rather than always the hardcoded default.
|
||||
if (LastUsedStyle::hasShapePen()) {
|
||||
@@ -87,6 +235,7 @@ void DiagramEventAddShape::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
||||
m_shape_item->setBrush(LastUsedStyle::shapeBrush());
|
||||
}
|
||||
m_diagram->addItem (m_shape_item);
|
||||
updateCreationHint();
|
||||
event->setAccepted(true);
|
||||
return;
|
||||
}
|
||||
@@ -94,12 +243,14 @@ void DiagramEventAddShape::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
||||
//If current item isn't a polyline, add it with an undo command
|
||||
if (m_shape_type != QetShapeItem::Polygon)
|
||||
{
|
||||
m_shape_item->setP2 (pos);
|
||||
applyPosition(pos, event->modifiers());
|
||||
if (m_shape_item->shapeType() == QetShapeItem::Rectangle || m_shape_item->shapeType() == QetShapeItem::Ellipse) {
|
||||
m_shape_item->setRect(m_shape_item->rect().normalized());
|
||||
}
|
||||
m_diagram->undoStack().push (new AddGraphicsObjectCommand(m_shape_item, m_diagram));
|
||||
m_shape_item = nullptr; //< set to nullptr for create new shape at next left clic
|
||||
hideCenterMarker();
|
||||
updateCreationHint();
|
||||
}
|
||||
//Else add a new point to polyline
|
||||
else
|
||||
@@ -125,18 +276,76 @@ void DiagramEventAddShape::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
|
||||
{
|
||||
updateHelpCross(event->scenePos());
|
||||
|
||||
// Re-asserted on every move, not just once at activation: Qt's own
|
||||
// built-in "show an action's statusTip on hover" has its own
|
||||
// internal "restore whatever was there before" logic for when the
|
||||
// hover ends. Since our message is shown *during* that same hover
|
||||
// session (the user is still over the toolbar icon when the
|
||||
// deferred constructor-time call fires), Qt's hover-tracking has no
|
||||
// idea we changed the status bar in the meantime -- the moment the
|
||||
// mouse leaves the icon for the canvas, it "restores" to whatever it
|
||||
// remembers being there before its own tip started, which is stale
|
||||
// and empty, silently overwriting ours. Re-showing it here, on every
|
||||
// move within the canvas, simply outlasts that one-time restore.
|
||||
updateCreationHint();
|
||||
|
||||
if (m_shape_item && event->buttons() == Qt::NoButton)
|
||||
{
|
||||
m_last_mouse_scene_pos = event->scenePos(); // raw, before snapping -- see reapplyLastPosition()
|
||||
|
||||
QPointF pos = event->scenePos();
|
||||
if (event->modifiers() != Qt::ControlModifier) {
|
||||
if (!(event->modifiers() & Qt::ControlModifier)) {
|
||||
pos = Diagram::snapToGrid(pos);
|
||||
}
|
||||
|
||||
m_shape_item->setP2 (pos);
|
||||
applyPosition(pos, event->modifiers());
|
||||
event->setAccepted(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddShape::keyPressEvent / keyReleaseEvent
|
||||
Pressing or releasing Shift (the square/circle lock) does nothing
|
||||
visible on its own -- applyPosition() only ever runs from
|
||||
mouseMoveEvent, so without this, a keyboard-only change just sits
|
||||
there until the next, often incidental, pixel of mouse movement
|
||||
brings the shape in line with it. That's exactly what looked like a
|
||||
freeze: holding Shift while the mouse is genuinely still produces no
|
||||
visible change (correctly -- nothing has moved), and it only
|
||||
"unsticks" once the mouse moves again, which released keys tend to
|
||||
coincide with purely by hand tremor, not because releasing itself
|
||||
did anything. Re-running the last known mouse position through
|
||||
applyPosition() here makes the key press or release itself the
|
||||
trigger, giving immediate feedback instead of waiting on chance.
|
||||
*/
|
||||
void DiagramEventAddShape::keyPressEvent(QKeyEvent *event)
|
||||
{
|
||||
reapplyLastPosition(event);
|
||||
}
|
||||
|
||||
void DiagramEventAddShape::keyReleaseEvent(QKeyEvent *event)
|
||||
{
|
||||
reapplyLastPosition(event);
|
||||
}
|
||||
|
||||
void DiagramEventAddShape::reapplyLastPosition(QKeyEvent *event)
|
||||
{
|
||||
if (!m_shape_item || (event->key() != Qt::Key_Shift && event->key() != Qt::Key_Control))
|
||||
return;
|
||||
|
||||
// A fresh, global query, not event->modifiers(): for a press/release
|
||||
// of a modifier key itself, whether that key is already reflected in
|
||||
// the key event's own modifiers() is ambiguous and platform-
|
||||
// dependent -- the same reason Diagram::snapToGrid() queries this
|
||||
// directly rather than trusting a passed-in modifiers() value.
|
||||
const Qt::KeyboardModifiers mods = QGuiApplication::keyboardModifiers();
|
||||
QPointF pos = m_last_mouse_scene_pos;
|
||||
if (!(mods & Qt::ControlModifier))
|
||||
pos = Diagram::snapToGrid(pos);
|
||||
|
||||
applyPosition(pos, mods);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddShape::mouseReleaseEvent
|
||||
Action when mouse button is released
|
||||
@@ -155,7 +364,7 @@ void DiagramEventAddShape::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
||||
m_shape_item->removePoints();
|
||||
|
||||
QPointF pos = event->scenePos();
|
||||
if (event->modifiers() != Qt::ControlModifier)
|
||||
if (!(event->modifiers() & Qt::ControlModifier))
|
||||
pos = Diagram::snapToGrid(pos);
|
||||
|
||||
m_shape_item->setP2(pos); //Set the new last point under the cursor
|
||||
@@ -167,6 +376,8 @@ void DiagramEventAddShape::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
||||
m_diagram->removeItem(m_shape_item);
|
||||
delete m_shape_item;
|
||||
m_shape_item = nullptr;
|
||||
hideCenterMarker();
|
||||
updateCreationHint();
|
||||
event->setAccepted(true);
|
||||
return;
|
||||
}
|
||||
@@ -203,6 +414,8 @@ void DiagramEventAddShape::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event
|
||||
}
|
||||
m_diagram->undoStack().push (new AddGraphicsObjectCommand(m_shape_item, m_diagram));
|
||||
m_shape_item = nullptr; //< set to nullptr for create new shape at next left clic
|
||||
hideCenterMarker();
|
||||
updateCreationHint();
|
||||
event->setAccepted(true);
|
||||
}
|
||||
}
|
||||
@@ -213,6 +426,59 @@ void DiagramEventAddShape::init()
|
||||
v->setContextMenuPolicy(Qt::NoContextMenu);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddShape::updateCreationHint
|
||||
Shows whichever of beforeClickHint()/afterClickHint() matches the
|
||||
current phase -- there was previously either no message at all
|
||||
(Line/Rectangle/Ellipse) or a single static one that never changed
|
||||
regardless of progress (Polygon, set externally in
|
||||
QETDiagramEditor::addItemGroupTriggered()); this replaces both with
|
||||
one phase-aware message per shape type, managed by the tool itself.
|
||||
*/
|
||||
void DiagramEventAddShape::updateCreationHint() const
|
||||
{
|
||||
if (!m_diagram || m_diagram->views().isEmpty())
|
||||
return;
|
||||
if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst()))
|
||||
editor->statusBar()->showMessage(m_shape_item ? afterClickHint() : beforeClickHint());
|
||||
}
|
||||
|
||||
QString DiagramEventAddShape::beforeClickHint() const
|
||||
{
|
||||
switch (m_shape_type)
|
||||
{
|
||||
case QetShapeItem::Line:
|
||||
return tr("Clic gauche : positionner le point de départ (Ctrl = position libre)");
|
||||
case QetShapeItem::Rectangle:
|
||||
case QetShapeItem::Ellipse:
|
||||
return tr("Clic gauche : positionner le premier coin (Ctrl = point central, position libre)");
|
||||
case QetShapeItem::Polygon:
|
||||
return tr("Clic gauche : positionner le premier point (Ctrl = position libre)");
|
||||
default:
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
|
||||
QString DiagramEventAddShape::afterClickHint() const
|
||||
{
|
||||
switch (m_shape_type)
|
||||
{
|
||||
case QetShapeItem::Line:
|
||||
return tr("Clic gauche : positionner le point final (Ctrl = position libre) ; clic droit : annuler");
|
||||
case QetShapeItem::Rectangle:
|
||||
return tr("Clic gauche : positionner le coin opposé (Maj = carré, "
|
||||
"Ctrl = depuis le centre + position libre, Ctrl+Maj = carré centré) ; clic droit : annuler");
|
||||
case QetShapeItem::Ellipse:
|
||||
return tr("Clic gauche : positionner le coin opposé (Maj = cercle, "
|
||||
"Ctrl = depuis le centre + position libre, Ctrl+Maj = cercle centré) ; clic droit : annuler");
|
||||
case QetShapeItem::Polygon:
|
||||
return tr("Clic gauche : point suivant ; double-clic ou Entrée : terminer ; "
|
||||
"clic droit : annuler le dernier point");
|
||||
default:
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddShape::updateHelpCross
|
||||
Create and update the position of the cross to help user for draw new shape
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
#include "../qetgraphicsitem/qetshapeitem.h"
|
||||
#include "diagrameventinterface.h"
|
||||
|
||||
class QGraphicsEllipseItem;
|
||||
|
||||
/**
|
||||
@brief The DiagramEventAddShape class
|
||||
This event manage the creation of a shape.
|
||||
@@ -37,15 +39,28 @@ class DiagramEventAddShape : public DiagramEventInterface
|
||||
void mouseMoveEvent (QGraphicsSceneMouseEvent *event) override;
|
||||
void mouseReleaseEvent (QGraphicsSceneMouseEvent *event) override;
|
||||
void mouseDoubleClickEvent (QGraphicsSceneMouseEvent *event) override;
|
||||
void keyPressEvent (QKeyEvent *event) override;
|
||||
void keyReleaseEvent (QKeyEvent *event) override;
|
||||
void init() override;
|
||||
|
||||
private:
|
||||
void updateHelpCross (const QPointF &p);
|
||||
void applyPosition (const QPointF &pos, Qt::KeyboardModifiers mods);
|
||||
void updateCreationHint () const;
|
||||
QString beforeClickHint () const;
|
||||
QString afterClickHint () const;
|
||||
void showCenterMarker (const QPointF &scenePos);
|
||||
void hideCenterMarker ();
|
||||
void reapplyLastPosition (QKeyEvent *event);
|
||||
|
||||
protected:
|
||||
QetShapeItem::ShapeType m_shape_type;
|
||||
QetShapeItem *m_shape_item;
|
||||
QGraphicsLineItem *m_help_horiz, *m_help_verti;
|
||||
QPointF m_anchor_point; // the shape's first-click point -- meaningful once m_shape_item exists
|
||||
QGraphicsEllipseItem *m_center_marker = nullptr; // shown only while Ctrl-anchoring is actually in effect, so it doubles as confirmation that it is
|
||||
bool m_center_anchored = false; // decided once, at the first click -- see applyPosition()'s doc comment for why
|
||||
QPointF m_last_mouse_scene_pos; // raw, unsnapped -- lets a modifier-only change re-snap correctly when reapplied
|
||||
};
|
||||
|
||||
#endif // DIAGRAMEVENTADDSHAPE_H
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include "diagramevent/diagrameventaddpdf.h"
|
||||
#endif
|
||||
#include "diagramevent/diagrameventaddshape.h"
|
||||
#include "diagramevent/diagrameventaddpath.h"
|
||||
#include "diagramevent/diagrameventaddtext.h"
|
||||
#include "diagramview.h"
|
||||
#include "elementspanelwidget.h"
|
||||
@@ -60,6 +61,7 @@
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QTimer>
|
||||
#ifdef BUILD_WITHOUT_KF
|
||||
# include "ui/nokde/kautosavefile.h"
|
||||
#else
|
||||
@@ -718,6 +720,19 @@ void QETDiagramEditor::setUpActions()
|
||||
connect(&m_zoom_actions_group, &QActionGroup::triggered, this, &QETDiagramEditor::zoomGroupTriggered);
|
||||
|
||||
//Adding action (add text, image, shape...)
|
||||
// Exclusive (the default) prevents the active action from ever being
|
||||
// unchecked by clicking it again -- Qt only lets you switch to a
|
||||
// different one. That's exactly why clicking an already-active
|
||||
// tool's own icon never actually deactivated it: confirmed by
|
||||
// logging every addItemGroupTriggered() call and finding
|
||||
// isChecked()==true on *every* click, including the one meant to
|
||||
// turn the tool off -- the click-handling code's own "was this an
|
||||
// uncheck?" check was correct, its precondition just could never
|
||||
// occur. ExclusiveOptional allows exactly one more transition:
|
||||
// clicking the currently-checked action unchecks it, leaving none
|
||||
// checked, which is required for "click the active tool to turn it
|
||||
// off" to mean anything at the QAction level at all.
|
||||
m_add_item_actions_group.setExclusionPolicy(QActionGroup::ExclusionPolicy::ExclusiveOptional);
|
||||
QAction *add_text = m_add_item_actions_group.addAction(QET::Icons::PartTextField, tr("Ajouter un champ de texte"));
|
||||
QAction *add_image = m_add_item_actions_group.addAction(QET::Icons::adding_image, tr("Ajouter une image"));
|
||||
#ifdef QET_HAS_QTPDF
|
||||
@@ -727,6 +742,7 @@ void QETDiagramEditor::setUpActions()
|
||||
QAction *add_rectangle = m_add_item_actions_group.addAction(QET::Icons::PartRectangle, tr("Ajouter un rectangle"));
|
||||
QAction *add_ellipse = m_add_item_actions_group.addAction(QET::Icons::PartEllipse, tr("Ajouter une ellipse"));
|
||||
QAction *add_polyline = m_add_item_actions_group.addAction(QET::Icons::PartPolygon, tr("Ajouter une polyligne"));
|
||||
QAction *add_path = m_add_item_actions_group.addAction(QET::Icons::PartBezier, tr("Ajouter une courbe"));
|
||||
QAction *add_terminal_strip = m_add_item_actions_group.addAction(QET::Icons::TerminalStrip, tr("Ajouter un plan de bornes"));
|
||||
|
||||
add_text ->setStatusTip(tr("Ajoute un champ de texte sur le folio actuel"));
|
||||
@@ -738,6 +754,7 @@ void QETDiagramEditor::setUpActions()
|
||||
add_rectangle->setStatusTip(tr("Ajoute un rectangle sur le folio actuel"));
|
||||
add_ellipse ->setStatusTip(tr("Ajoute une ellipse sur le folio actuel"));
|
||||
add_polyline ->setStatusTip(tr("Ajoute une polyligne sur le folio actuel"));
|
||||
add_path ->setStatusTip(tr("Ajoute une courbe de Bézier sur le folio actuel"));
|
||||
add_terminal_strip->setStatusTip(tr("Ajoute un plan de bornier sur le folio actuel"));
|
||||
|
||||
add_text ->setData(QStringLiteral("text"));
|
||||
@@ -749,6 +766,7 @@ void QETDiagramEditor::setUpActions()
|
||||
add_rectangle->setData(QStringLiteral("rectangle"));
|
||||
add_ellipse ->setData(QStringLiteral("ellipse"));
|
||||
add_polyline ->setData(QStringLiteral("polyline"));
|
||||
add_path ->setData(QStringLiteral("path"));
|
||||
add_terminal_strip->setData(QStringLiteral("terminal_strip"));
|
||||
|
||||
add_text->setCheckable(true);
|
||||
@@ -756,6 +774,7 @@ void QETDiagramEditor::setUpActions()
|
||||
add_rectangle->setCheckable(true);
|
||||
add_ellipse->setCheckable(true);
|
||||
add_polyline->setCheckable(true);
|
||||
add_path->setCheckable(true);
|
||||
|
||||
connect(&m_add_item_actions_group, &QActionGroup::triggered, this, &QETDiagramEditor::addItemGroupTriggered);
|
||||
|
||||
@@ -1555,6 +1574,27 @@ void QETDiagramEditor::addItemGroupTriggered(QAction *action)
|
||||
if (Q_UNLIKELY (!currentDiagramView() || !currentDiagramView()->diagram() || value.isEmpty())) return;
|
||||
|
||||
Diagram *d = currentDiagramView()->diagram();
|
||||
|
||||
// This action group allows deselecting the currently-active tool by
|
||||
// clicking its own icon again, not just switching between tools --
|
||||
// that click still fires this slot (QActionGroup::triggered fires on
|
||||
// every click in the group, checked-state-changing or not), so
|
||||
// without this check it would unconditionally construct *another*
|
||||
// instance of the same tool and reactivate it, leaving the tool
|
||||
// fully active in the canvas while its own toolbar button shows
|
||||
// unchecked -- exactly backwards from what the click was for.
|
||||
// Scoped to checkable actions specifically: image/pdf/terminal_strip
|
||||
// are one-shot actions (pick a file, open a dialog) with no
|
||||
// persistent "active tool" state at all, and aren't even checkable
|
||||
// -- isChecked() on them is always false, so without this guard
|
||||
// they'd hit the branch above on every single click and never
|
||||
// reach their own handling below.
|
||||
if (action->isCheckable() && !action->isChecked())
|
||||
{
|
||||
d->clearEventInterface();
|
||||
return;
|
||||
}
|
||||
|
||||
DiagramEventInterface *diagram_event = nullptr;
|
||||
|
||||
if (value == "line")
|
||||
@@ -1564,12 +1604,10 @@ void QETDiagramEditor::addItemGroupTriggered(QAction *action)
|
||||
else if (value == "ellipse")
|
||||
diagram_event = new DiagramEventAddShape (d, QetShapeItem::Ellipse);
|
||||
else if (value == "polyline")
|
||||
{
|
||||
diagram_event = new DiagramEventAddShape (d, QetShapeItem::Polygon);
|
||||
statusBar()-> showMessage(tr("Double-click pour terminer la forme, Click droit pour annuler le dernier point"));
|
||||
connect(diagram_event, &DiagramEventInterface::destroyed, [this]() {
|
||||
statusBar()->clearMessage();
|
||||
});
|
||||
else if (value == "path")
|
||||
{
|
||||
diagram_event = new DiagramEventAddPath (d);
|
||||
}
|
||||
else if (value == "image")
|
||||
{
|
||||
@@ -1612,6 +1650,22 @@ void QETDiagramEditor::addItemGroupTriggered(QAction *action)
|
||||
{
|
||||
d->setEventInterface(diagram_event);
|
||||
connect(diagram_event, &DiagramEventInterface::destroyed, [action]() {action->setChecked(false);});
|
||||
// Defensive: on this style/theme, the toolbar button bound to an
|
||||
// exclusive-group action doesn't reliably repaint its checked
|
||||
// appearance after the group briefly had *no* action checked at
|
||||
// all (the gap between unchecking one tool and checking one
|
||||
// again, even the same one) -- confirmed by tracing a real
|
||||
// recording frame by frame: the status bar correctly showed this
|
||||
// tool's "before first click" hint (which can only appear from
|
||||
// inside a freshly-constructed tool's own constructor, so the
|
||||
// action's checked state and the tool's activation were both
|
||||
// genuinely correct) while the button itself stayed visually
|
||||
// unchecked for a sustained period. Forcing an explicit repaint
|
||||
// here makes the button's appearance match its actual state
|
||||
// regardless of whether Qt's own change notification fired
|
||||
// correctly.
|
||||
if (QWidget *button = m_add_item_tool_bar->widgetForAction(action))
|
||||
button->update();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,9 +19,16 @@
|
||||
#define DIAGRAM_IMAGE_ITEM_H
|
||||
|
||||
#include "qetgraphicsitem.h"
|
||||
#include "shapetransform.h"
|
||||
|
||||
#include <QColor>
|
||||
#include <QList>
|
||||
#include <QVector>
|
||||
|
||||
class QDomElement;
|
||||
class QDomDocument;
|
||||
class QGraphicsSceneContextMenuEvent;
|
||||
class QetGraphicsHandlerItem;
|
||||
|
||||
/**
|
||||
This class represents a selectable, movable and editable image on a
|
||||
@@ -30,6 +37,23 @@ class QDomDocument;
|
||||
*/
|
||||
class DiagramImageItem : public QetGraphicsItem {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QPixmap pixmap READ pixmap WRITE setPixmap NOTIFY pixmapChanged)
|
||||
Q_PROPERTY(qreal scaleFactorX READ scaleFactorX WRITE setScaleFactorX NOTIFY transformChanged)
|
||||
Q_PROPERTY(qreal scaleFactorY READ scaleFactorY WRITE setScaleFactorY NOTIFY transformChanged)
|
||||
Q_PROPERTY(qreal rotationAngle READ rotationAngle WRITE setRotationAngle NOTIFY transformChanged)
|
||||
Q_PROPERTY(qreal skewX READ skewX WRITE setSkewX NOTIFY transformChanged)
|
||||
Q_PROPERTY(qreal skewY READ skewY WRITE setSkewY NOTIFY transformChanged)
|
||||
Q_PROPERTY(QPointF pivot READ pivot WRITE setPivot NOTIFY transformChanged)
|
||||
// A second, deliberately non-compensating property on the SAME
|
||||
// underlying value -- setPivot() (above) intentionally adjusts
|
||||
// pos() to keep the image visually in place, which is exactly
|
||||
// wrong for crop()'s own undo chain: crop() already computes the
|
||||
// correct final pos() itself (accounting for the crop, not just
|
||||
// the pivot move) and pushes it as its own separate "pos" command,
|
||||
// so a chained "pivot" step going through the compensating setter
|
||||
// would silently overwrite that already-correct pos() a second
|
||||
// time. rawPivot exists solely for that one caller.
|
||||
Q_PROPERTY(QPointF rawPivot READ pivot WRITE setPivotRaw NOTIFY transformChanged)
|
||||
|
||||
// constructors, destructor
|
||||
public:
|
||||
@@ -41,6 +65,19 @@ class DiagramImageItem : public QetGraphicsItem {
|
||||
public:
|
||||
enum { Type = UserType + 1007 };
|
||||
|
||||
// A deliberately smaller, image-specific set than QetShapeItem's own
|
||||
// HandleMode/HandleRole: images have no path/polygon/arc concepts at
|
||||
// all, so reusing those enums directly would pull in a great deal of
|
||||
// irrelevant baggage for no benefit. RotateSkew's math (rotate,
|
||||
// skew, and their shared scaleAndShearOffset()/scaleOnlyOffset()
|
||||
// helpers) mirrors QetShapeItem's own identical formulas -- adapted
|
||||
// to this class's own, simpler 4-corner/4-edge/1-pivot handle set,
|
||||
// not shared code, since the two classes' broader handle roles
|
||||
// differ too much (no path/polygon/arc concepts here) for sharing
|
||||
// the dispatcher itself to be worth an indirect abstraction.
|
||||
enum class HandleMode { Size, RotateSkew };
|
||||
enum class HandleRole { Resize, Rotate, SkewEdge, Pivot };
|
||||
|
||||
// methods
|
||||
public:
|
||||
/**
|
||||
@@ -54,13 +91,108 @@ class DiagramImageItem : public QetGraphicsItem {
|
||||
virtual QDomElement toXml(QDomDocument &) const;
|
||||
void editProperty() override;
|
||||
void setPixmap(const QPixmap &pixmap);
|
||||
QPixmap pixmap() const { return pixmap_; }
|
||||
QRectF boundingRect() const override;
|
||||
QString name() const override;
|
||||
|
||||
qreal scaleFactorX() const { return m_transform.scaleX; }
|
||||
qreal scaleFactorY() const { return m_transform.scaleY; }
|
||||
void setScaleFactorX(qreal factor);
|
||||
void setScaleFactorY(qreal factor);
|
||||
qreal rotationAngle() const { return m_transform.rotation; }
|
||||
void setRotationAngle(qreal angle);
|
||||
qreal skewX() const { return m_transform.skewX; }
|
||||
void setSkewX(qreal skew);
|
||||
qreal skewY() const { return m_transform.skewY; }
|
||||
void setSkewY(qreal skew);
|
||||
QPointF pivot() const { return m_transform.pivot; }
|
||||
void setPivot(const QPointF &pivot);
|
||||
void setPivotRaw(const QPointF &pivot);
|
||||
|
||||
signals:
|
||||
void pixmapChanged();
|
||||
void transformChanged();
|
||||
|
||||
protected:
|
||||
void paint(QPainter *, const QStyleOptionGraphicsItem *, QWidget *) override;
|
||||
void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override;
|
||||
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
|
||||
void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override;
|
||||
void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override;
|
||||
bool sceneEventFilter(QGraphicsItem *watched, QEvent *event) override;
|
||||
QVariant itemChange(GraphicsItemChange change, const QVariant &value) override;
|
||||
|
||||
private:
|
||||
void replaceImage();
|
||||
void mirror(bool horizontal);
|
||||
void setTransparentColor();
|
||||
void crop();
|
||||
void restoreAspectRatio();
|
||||
static QPixmap computeDisplayPixmap(const QPixmap &base, const QRect &cropRect, const QList<QColor> &colors, int tolerance);
|
||||
|
||||
void toggleHandleMode();
|
||||
HandleMode nextHandleMode() const;
|
||||
static QString handleModeLabel(HandleMode mode);
|
||||
void updateModeHint();
|
||||
void refreshInteractionHints();
|
||||
QString currentModeStatusHint() const;
|
||||
void rebuildHandles();
|
||||
void repositionHandles();
|
||||
void clearHandles();
|
||||
void resetPivotToBoundingRectCenter();
|
||||
void handlerMousePressEvent(int index, Qt::KeyboardModifiers mods);
|
||||
void handlerMouseMoveEvent(int index, QGraphicsSceneMouseEvent *event);
|
||||
void handlerMouseReleaseEvent(int index);
|
||||
void dragResize(int index, const QPointF &localPos, Qt::KeyboardModifiers mods);
|
||||
void dragRotateHandle(int cornerIndex, const QPointF &scenePos, Qt::KeyboardModifiers mods);
|
||||
void dragSkewHandle(int edgeIndex, const QPointF &scenePos, Qt::KeyboardModifiers mods);
|
||||
void dragPivot(const QPointF &localPos);
|
||||
QPointF scaleOnlyOffset(const QPointF &localPoint) const;
|
||||
QPointF scaleAndShearOffset(const QPointF &localPoint) const;
|
||||
QPointF handlePosition(int index) const;
|
||||
static QPointF cornerPosition(int cornerIndex, qreal w, qreal h);
|
||||
static QPointF edgeMidpointPosition(int edgeIndex, qreal w, qreal h);
|
||||
static QColor colorForHandleRole(HandleRole role);
|
||||
static QString hintForHandleRole(HandleRole role);
|
||||
void showStatusHint(const QString &text) const;
|
||||
void clearStatusHint() const;
|
||||
|
||||
protected:
|
||||
QPixmap pixmap_;
|
||||
// The true, pristine original -- never itself cropped or colour-
|
||||
// keyed. pixmap_ (the displayed result) is always re-derived from
|
||||
// this plus m_crop_rect and m_transparent_colors/tolerance, via
|
||||
// computeDisplayPixmap(). Without keeping this separate, re-opening
|
||||
// either the crop or transparency dialog after using the other
|
||||
// would show an already-modified image as if it were the source --
|
||||
// areas already cropped away or coloured out would be gone for
|
||||
// good, with no way to recover or adjust them, only start over.
|
||||
// Updated by whatever genuinely replaces or reorients the image's
|
||||
// actual content (construction, replaceImage(), and mirror(), which
|
||||
// also mirrors m_crop_rect to keep referring to the same region of
|
||||
// the now-flipped base) -- never by crop() or setTransparentColor()
|
||||
// themselves, which only ever change which subset of this base is
|
||||
// shown.
|
||||
QPixmap m_base_pixmap;
|
||||
QRect m_crop_rect; // relative to m_base_pixmap; equals m_base_pixmap.rect() when nothing has been cropped
|
||||
QList<QColor> m_transparent_colors;
|
||||
int m_transparent_tolerance = 10;
|
||||
|
||||
// Independent scaleX/scaleY here is the actual point of this whole
|
||||
// member: QGraphicsItem::scale() is a single, uniform float, which
|
||||
// is exactly why an image could never break its own aspect ratio
|
||||
// before this. skewX/skewY exist in the struct but are never
|
||||
// written by anything below -- deliberately deferred, not an
|
||||
// oversight (see the .cpp for why).
|
||||
ShapeTransform m_transform;
|
||||
bool m_pivotIsCustom = false;
|
||||
HandleMode m_handleMode = HandleMode::Size;
|
||||
QVector<QetGraphicsHandlerItem *> m_handler_vector;
|
||||
QVector<HandleRole> m_handleRoles;
|
||||
int m_vector_index = -1;
|
||||
QPointF m_original_pos; // scene position at the start of a resize/rotate/pivot drag, for Escape-to-cancel
|
||||
ShapeTransform m_original_transform;
|
||||
bool m_deferHandleReposition = false; // see setPivot()'s comment
|
||||
bool m_resizeCenterAnchored = false; // decided once, at press time -- see handlerMousePressEvent()'s comment for why, mirroring the identical fix already made for shape creation
|
||||
};
|
||||
#endif
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,8 +20,11 @@
|
||||
|
||||
#include "../QetGraphicsItemModeler/qetgraphicshandleritem.h"
|
||||
#include "qetgraphicsitem.h"
|
||||
#include "shapetransform.h"
|
||||
|
||||
#include <QPen>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
class QDomElement;
|
||||
class QDomDocument;
|
||||
@@ -30,8 +33,10 @@ class QAction;
|
||||
|
||||
/**
|
||||
@brief The QetShapeItem class
|
||||
this class is used to draw a basic shape (line, rectangle, ellipse)
|
||||
into a diagram, that can be saved to .qet file.
|
||||
this class is used to draw a basic shape (line, rectangle, ellipse,
|
||||
polygon or free-form path) into a diagram, that can be saved to a
|
||||
.qet file. Beyond its local geometry, a shape may also carry a
|
||||
rotation/skew/scale (see ShapeTransform) around an arbitrary pivot.
|
||||
*/
|
||||
class QetShapeItem : public QetGraphicsItem
|
||||
{
|
||||
@@ -46,21 +51,86 @@ class QetShapeItem : public QetGraphicsItem
|
||||
Q_PROPERTY(qreal xRadius READ XRadius WRITE setXRadius NOTIFY XRadiusChanged)
|
||||
Q_PROPERTY(qreal yRadius READ YRadius WRITE setYRadius NOTIFY YRadiusChanged)
|
||||
|
||||
// One property per ShapeTransform scalar -- this is what lets a plain
|
||||
// QPropertyUndoCommand(this, "rotation", oldValue, newValue) work for
|
||||
// every handle, exactly like xRadius/yRadius already do for corner
|
||||
// rounding.
|
||||
Q_PROPERTY(qreal rotation READ rotation WRITE setRotation NOTIFY transformChanged)
|
||||
Q_PROPERTY(qreal skewX READ skewX WRITE setSkewX NOTIFY transformChanged)
|
||||
Q_PROPERTY(qreal skewY READ skewY WRITE setSkewY NOTIFY transformChanged)
|
||||
Q_PROPERTY(qreal scaleFactorX READ scaleFactorX WRITE setScaleFactorX NOTIFY transformChanged)
|
||||
Q_PROPERTY(qreal scaleFactorY READ scaleFactorY WRITE setScaleFactorY NOTIFY transformChanged)
|
||||
Q_PROPERTY(QPointF pivot READ pivot WRITE setPivot NOTIFY transformChanged)
|
||||
|
||||
Q_PROPERTY(qreal startAngle READ startAngle WRITE setStartAngle NOTIFY arcChanged)
|
||||
Q_PROPERTY(qreal endAngle READ endAngle WRITE setEndAngle NOTIFY arcChanged)
|
||||
|
||||
signals:
|
||||
void penChanged();
|
||||
void brushChanged();
|
||||
void closeChanged();
|
||||
void XRadiusChanged();
|
||||
void YRadiusChanged();
|
||||
|
||||
void transformChanged();
|
||||
void arcChanged();
|
||||
void geometryChanged(); // P1/P2, polygon points, or path nodes changed -- lets the properties panel stay in sync while a handle is dragged, not just when it's typed into
|
||||
|
||||
public:
|
||||
enum ShapeType {Line =1,
|
||||
Rectangle =2,
|
||||
Ellipse =4,
|
||||
Polygon =8 };
|
||||
Polygon =8,
|
||||
Path =16 };
|
||||
Q_ENUM (ShapeType)
|
||||
|
||||
enum ArcClosure {NoClosure = 0, Chord = 1, Pie = 2};
|
||||
Q_ENUM (ArcClosure)
|
||||
|
||||
// Point of a Path shape. Anchors are in the same local coordinate
|
||||
// frame as an ordinary Polygon's points; handle offsets are stored
|
||||
// *relative to the anchor*, so moving a node never has to rewrite
|
||||
// its own handle coordinates.
|
||||
enum class NodeKind {Corner, Smooth, Symmetric};
|
||||
struct PathNode {
|
||||
QPointF anchor;
|
||||
NodeKind kind = NodeKind::Corner;
|
||||
std::optional<QPointF> inHandle;
|
||||
std::optional<QPointF> outHandle;
|
||||
|
||||
bool operator==(const PathNode &other) const {
|
||||
return anchor == other.anchor && kind == other.kind
|
||||
&& inHandle == other.inHandle && outHandle == other.outHandle;
|
||||
}
|
||||
};
|
||||
|
||||
// Orthogonal to ShapeType: which handle set is currently shown.
|
||||
// Cycled by clicking an already-selected shape without dragging:
|
||||
// Size -> Corner -> RotateSkew -> Size for Rectangle (the only
|
||||
// type with a corner-radius concept); Size -> NodeEdit ->
|
||||
// RotateSkew -> Size for Path (reveals control handles for
|
||||
// every node that has any); Size -> RotateSkew -> Size for
|
||||
// everything else. One unified click-cycle for every shape
|
||||
// type, rather than a separate, less discoverable gesture
|
||||
// (e.g. double-click) for any one shape's extra mode.
|
||||
enum class HandleMode {Size, Corner, NodeEdit, RotateSkew};
|
||||
|
||||
// index conventions, deliberately matched to what already exists
|
||||
// rather than invented fresh:
|
||||
// Resize 0..7, same order as QetGraphicsHandlerUtility::pointsForRect
|
||||
// (this is exactly today's Rectangle/Ellipse handle set --
|
||||
// only the Ctrl/Shift dispatch around it is new)
|
||||
// Rotate 0..3, corners: NW, NE, SE, SW
|
||||
// SkewEdge 0..3, edges: N, E, S, W
|
||||
// CornerRadius 0..1, same order as QetGraphicsHandlerUtility::pointForRadiusRect
|
||||
// ArcEndpoint 0 = start angle, 1 = end angle
|
||||
enum class HandleRole {
|
||||
Resize, // Size mode
|
||||
Rotate, SkewEdge, Pivot, // RotateSkew mode
|
||||
CornerRadius, // Rectangle, always shown alongside Size handles
|
||||
ArcEndpoint, // Ellipse, always shown
|
||||
PathAnchor, PathControlIn, PathControlOut // Polygon/Path, node-edit mode (see setPathNodes())
|
||||
};
|
||||
|
||||
enum { Type = UserType + 1008 };
|
||||
|
||||
QetShapeItem(
|
||||
@@ -102,6 +172,41 @@ class QetShapeItem : public QetGraphicsItem
|
||||
qreal YRadius() const {return m_yRadius;}
|
||||
void setYRadius(qreal Y);
|
||||
|
||||
//Transform: one accessor pair per ShapeTransform scalar (see
|
||||
//the Q_PROPERTY block above for why they are not grouped into
|
||||
//a single property).
|
||||
const ShapeTransform &shapeTransform() const {return m_transform;}
|
||||
qreal rotation() const {return m_transform.rotation;}
|
||||
void setRotation(qreal degrees);
|
||||
qreal skewX() const {return m_transform.skewX;}
|
||||
void setSkewX(qreal degrees);
|
||||
qreal skewY() const {return m_transform.skewY;}
|
||||
void setSkewY(qreal degrees);
|
||||
qreal scaleFactorX() const {return m_transform.scaleX;}
|
||||
void setScaleFactorX(qreal factor);
|
||||
qreal scaleFactorY() const {return m_transform.scaleY;}
|
||||
void setScaleFactorY(qreal factor);
|
||||
QPointF pivot() const {return m_transform.pivot;}
|
||||
void setPivot(const QPointF &pivot); // moves the pivot handle: compensates pos() so the shape does not jump
|
||||
void resetPivotToBoundingRectCenter();
|
||||
void enableNodeEditMode(); // Path only: switches to NodeEdit mode, so every node's control handles become visible
|
||||
|
||||
//Arc: only meaningful when shapeType() == Ellipse. A full
|
||||
//ellipse is just an arc with span 360 -- there is no separate
|
||||
//Arc shape type.
|
||||
qreal startAngle() const {return m_startAngle;}
|
||||
void setStartAngle(qreal degrees);
|
||||
qreal endAngle() const {return m_endAngle;}
|
||||
void setEndAngle(qreal degrees);
|
||||
qreal spanAngle() const {return m_endAngle - m_startAngle;}
|
||||
bool isFullEllipse() const {return qFuzzyCompare(qAbs(spanAngle()), qreal(360));}
|
||||
ArcClosure arcClosure() const {return m_arcClosure;}
|
||||
void setArcClosure(ArcClosure closure);
|
||||
|
||||
//Path (Bezier): only meaningful when shapeType() == Path.
|
||||
const QVector<PathNode> &pathNodes() const {return m_nodes;}
|
||||
void setPathNodes(const QVector<PathNode> &nodes);
|
||||
|
||||
//Methods available for polygon shape
|
||||
int pointsCount () const;
|
||||
void setNextPoint (QPointF P);
|
||||
@@ -118,6 +223,8 @@ class QetShapeItem : public QetGraphicsItem
|
||||
void hoverEnterEvent (QGraphicsSceneHoverEvent *event) override;
|
||||
void hoverLeaveEvent (QGraphicsSceneHoverEvent *event) override;
|
||||
void mousePressEvent (QGraphicsSceneMouseEvent *event) override;
|
||||
void mouseMoveEvent (QGraphicsSceneMouseEvent *event) override;
|
||||
void mouseReleaseEvent (QGraphicsSceneMouseEvent *event) override;
|
||||
QVariant itemChange(
|
||||
GraphicsItemChange change,
|
||||
const QVariant &value) override;
|
||||
@@ -128,17 +235,63 @@ class QetShapeItem : public QetGraphicsItem
|
||||
QGraphicsSceneContextMenuEvent *event) override;
|
||||
|
||||
private:
|
||||
void switchResizeMode();
|
||||
void addHandler();
|
||||
void adjustHandlerPos();
|
||||
void toggleHandleMode();
|
||||
HandleMode nextHandleMode() const; // what a click would switch to from here -- shared by toggleHandleMode() and the tooltip
|
||||
void updateModeHint(); // keeps the tooltip in sync with nextHandleMode()
|
||||
void refreshInteractionHints(); // updateModeHint(), plus an immediate re-show of tooltip/status bar if currently hovered
|
||||
void showStatusHint(const QString &text) const;
|
||||
void clearStatusHint() const;
|
||||
QString currentModeStatusHint() const; // richer, one-line gesture/modifier reference for the status bar, shown when hovering the shape body
|
||||
QString handleRoleTooltip(HandleRole role, int slot) const; // shown natively by Qt when hovering that specific handle, and pushed to the status bar too
|
||||
static QString handleModeLabel(HandleMode mode);
|
||||
static bool isResizeCornerSlot(int slot); // true for the 4 corner slots (of 8) in QetGraphicsHandlerUtility::pointsForRect's ordering
|
||||
void rebuildHandles(); // (re)creates handler items -- only when the *set* of handles changes
|
||||
void repositionHandles(); // moves existing handler items -- safe to call every frame of a live drag
|
||||
void insertPoint();
|
||||
void removePoint();
|
||||
void convertToPathExplicitly(); // context-menu action; see promoteRectangleOrEllipseToPolygon()
|
||||
void mirror(bool horizontal); // context-menu action: flips scaleFactorX (horizontal) or scaleFactorY (vertical) around the current pivot
|
||||
void setNodeKind(int nodeIndex, NodeKind kind); // context-menu action on a Path node
|
||||
|
||||
void handlerMousePressEvent();
|
||||
void handlerMouseMoveEvent(QGraphicsSceneMouseEvent *event);
|
||||
void handlerMouseReleaseEvent();
|
||||
void handlerMousePressEvent(int handlerIndex);
|
||||
void handlerMouseMoveEvent(int handlerIndex, QGraphicsSceneMouseEvent *event);
|
||||
void handlerMouseReleaseEvent(int handlerIndex);
|
||||
|
||||
///ATTRIBUTES
|
||||
// One dispatch function per handle role -- called from
|
||||
// handlerMouseMoveEvent() with the mouse position already
|
||||
// mapped to local coordinates and any grid snap applied.
|
||||
// dragResize() covers both the plain and Ctrl/Shift/Alt-modified
|
||||
// interpretations of the whole Size handle set.
|
||||
void dragResize (int index, const QPointF &localPos, Qt::KeyboardModifiers mods);
|
||||
void dragRotateHandle(int cornerIndex, const QPointF &scenePos, Qt::KeyboardModifiers mods);
|
||||
void dragSkewHandle (int edgeIndex, const QPointF &scenePos, Qt::KeyboardModifiers mods);
|
||||
void dragPivotHandle (const QPointF &localPos);
|
||||
void dragArcEndpoint (int which, const QPointF &localPos, Qt::KeyboardModifiers mods);
|
||||
void dragCornerRadius(int which, const QPointF &localPos);
|
||||
void dragPathAnchor (int which, const QPointF &localPos, Qt::KeyboardModifiers mods);
|
||||
void dragPathControlHandle(bool isOutHandle, int nodeIndex, const QPointF &localPos, Qt::KeyboardModifiers mods);
|
||||
void mirrorOppositeHandle(PathNode &node, bool justChangedIsOut); // shared by dragPathControlHandle() and dragCurveSegment()
|
||||
void dragCurveSegment(int segmentIndex, qreal t, const QPointF &localPos); // Inkscape-style "grab the curve itself"
|
||||
|
||||
void promoteRectangleOrEllipseToPolygon(int detachedResizeIndex, const QPointF &newLocalPos);
|
||||
std::pair<int, qreal> nearestPathSegment(const QPointF &localPos) const; // {segmentIndex, t}, -1 if fewer than 2 nodes
|
||||
void insertPathPoint(int segmentIndex, qreal t); // De Casteljau split -- see .cpp for why
|
||||
void removePathPoint(int nodeIndex);
|
||||
QDomElement snapshotXml() const; // helper for PromoteShapeCommand: toXml() into a throwaway document
|
||||
|
||||
QRectF localRect() const;
|
||||
QPainterPath outline() const; // raw, unstroked path for the current type; shared by shape() and paint()
|
||||
QVector<QPointF> currentHandlePositions() const; // in m_handleRoles/m_handleSlot order, local coordinates
|
||||
QPointF handlePositionFor(HandleRole role, int slot) const;
|
||||
QPointF rotateHandleReference(int slot) const; // the point a Rotate handle tracks, before rotation is applied
|
||||
QPointF scaleOnlyOffset(const QPointF &localPoint) const; // (localPoint - pivot), scaled, in the pre-shear frame
|
||||
QPointF scaleAndShearOffset(const QPointF &localPoint) const; // same, with current shear also applied -- the pre-rotation frame
|
||||
static QColor colorForHandleRole(HandleRole role);
|
||||
static QRectF lockAspectRatio(const QRectF &oldRect, QRectF newRect, int resizeIndex, bool mirrored);
|
||||
static QPointF cornerPoint(const QRectF &rect, int cornerIndex); // 0=NW,1=NE,2=SE,3=SW
|
||||
static QPointF edgeMidpoint(const QRectF &rect, int edgeIndex); // 0=N, 1=E, 2=S, 3=W
|
||||
|
||||
///ATTRIBUTES
|
||||
private:
|
||||
ShapeType m_shapeType;
|
||||
QPen m_pen;
|
||||
@@ -153,7 +306,6 @@ class QetShapeItem : public QetGraphicsItem
|
||||
int m_vector_index;
|
||||
bool m_closed = false,
|
||||
m_modifie_radius_equaly = false;
|
||||
int m_resize_mode = 1;
|
||||
QVector<QetGraphicsHandlerItem *> m_handler_vector;
|
||||
QAction *m_insert_point,
|
||||
*m_remove_point;
|
||||
@@ -161,5 +313,26 @@ class QetShapeItem : public QetGraphicsItem
|
||||
m_yRadius = 0,
|
||||
m_old_xRadius,
|
||||
m_old_yRadius;
|
||||
|
||||
ShapeTransform m_transform;
|
||||
ShapeTransform m_old_transform;
|
||||
HandleMode m_handleMode = HandleMode::Size;
|
||||
QVector<HandleRole> m_handleRoles; // parallel to m_handler_vector, one role per handle
|
||||
QVector<int> m_handleSlot; // parallel to m_handler_vector, meaning depends on role (see HandleRole)
|
||||
QPointF m_old_pos;
|
||||
|
||||
qreal m_startAngle = 0;
|
||||
qreal m_endAngle = 360;
|
||||
ArcClosure m_arcClosure = NoClosure;
|
||||
bool m_pivotIsCustom = false; // false: pivot auto-follows the bounding-rect center on every geometry edit
|
||||
bool m_deferHandleReposition = false; // true while setPivot() is applying its two related updates together
|
||||
|
||||
QVector<PathNode> m_nodes;
|
||||
QVector<PathNode> m_old_nodes; // saved at handle press, for undo
|
||||
int m_curveDragSegment = -1; // >=0 while dragging the curve itself (not a handle) between two nodes
|
||||
qreal m_curveDragT = 0; // parameter along that segment where the drag started
|
||||
QPointF m_curveDragOriginalP1, m_curveDragOriginalP2; // absolute control points at drag start, fixed for the whole drag
|
||||
QPointF m_curveDragPressPos; // local position at press, to tell a plain click from a real drag
|
||||
bool m_curveDragEngaged = false; // false until the drag actually exceeds a small threshold
|
||||
};
|
||||
#endif // QETSHAPEITEM_H
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
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 "shapetransform.h"
|
||||
|
||||
#include <QtCore/qmath.h>
|
||||
|
||||
bool ShapeTransform::isIdentity() const
|
||||
{
|
||||
return qFuzzyIsNull(rotation) && qFuzzyIsNull(skewX) && qFuzzyIsNull(skewY)
|
||||
&& qFuzzyCompare(scaleX, qreal(1)) && qFuzzyCompare(scaleY, qreal(1));
|
||||
}
|
||||
|
||||
QTransform ShapeTransform::linearPart() const
|
||||
{
|
||||
QTransform t;
|
||||
t.rotate(rotation);
|
||||
t.shear(qTan(qDegreesToRadians(skewX)), qTan(qDegreesToRadians(skewY)));
|
||||
t.scale(scaleX, scaleY);
|
||||
return t;
|
||||
}
|
||||
|
||||
QTransform ShapeTransform::toMatrix() const
|
||||
{
|
||||
QTransform t;
|
||||
t.translate(pivot.x(), pivot.y());
|
||||
t.rotate(rotation);
|
||||
t.shear(qTan(qDegreesToRadians(skewX)), qTan(qDegreesToRadians(skewY)));
|
||||
t.scale(scaleX, scaleY);
|
||||
t.translate(-pivot.x(), -pivot.y());
|
||||
return t;
|
||||
}
|
||||
|
||||
bool ShapeTransform::operator==(const ShapeTransform &other) const
|
||||
{
|
||||
return qFuzzyCompare(rotation, other.rotation)
|
||||
&& qFuzzyCompare(skewX, other.skewX)
|
||||
&& qFuzzyCompare(skewY, other.skewY)
|
||||
&& qFuzzyCompare(scaleX, other.scaleX)
|
||||
&& qFuzzyCompare(scaleY, other.scaleY)
|
||||
&& pivot == other.pivot;
|
||||
}
|
||||
|
||||
ShapeTransform decomposeLinear(const QTransform &m)
|
||||
{
|
||||
const qreal scaleX = qSqrt(m.m11() * m.m11() + m.m12() * m.m12());
|
||||
const qreal rotation = qRadiansToDegrees(qAtan2(m.m12(), m.m11()));
|
||||
const qreal rad = qDegreesToRadians(rotation);
|
||||
const qreal c = qCos(rad), s = qSin(rad);
|
||||
|
||||
// v * R(-rotation): undo the rotation, leaving the pure shear+scale
|
||||
// contribution the local y-axis picked up from Shear(sh,0)*Scale(sx,sy).
|
||||
const qreal vx = m.m21() * c + m.m22() * s;
|
||||
const qreal scaleY = -m.m21() * s + m.m22() * c;
|
||||
const qreal skewX = qFuzzyIsNull(scaleY) ? 0.0 : qRadiansToDegrees(qAtan(vx / scaleY));
|
||||
|
||||
ShapeTransform result;
|
||||
result.rotation = rotation;
|
||||
result.skewX = skewX;
|
||||
result.skewY = 0;
|
||||
result.scaleX = scaleX;
|
||||
result.scaleY = scaleY;
|
||||
return result;
|
||||
}
|
||||
|
||||
QPointF compensatedPositionForNewPivot(
|
||||
const QPointF &position,
|
||||
const QPointF &oldPivot,
|
||||
const QPointF &newPivot,
|
||||
const QTransform &linearPart)
|
||||
{
|
||||
return position + (oldPivot - linearPart.map(oldPivot))
|
||||
- (newPivot - linearPart.map(newPivot));
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
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 SHAPETRANSFORM_H
|
||||
#define SHAPETRANSFORM_H
|
||||
|
||||
#include <QPointF>
|
||||
#include <QTransform>
|
||||
|
||||
/**
|
||||
@brief The ShapeTransform struct
|
||||
Rotation, skew and scale applied to a shape's local geometry around an
|
||||
arbitrary local pivot point.
|
||||
|
||||
These five scalars plus a pivot are a convenient, directly-editable
|
||||
basis for 2D affine transforms: every handle in the UI changes exactly
|
||||
one field, which is what makes per-handle drag math simple. Note that
|
||||
a plain 2x2 linear map only has 4 true degrees of freedom, one fewer
|
||||
than (rotation, skewX, skewY, scaleX, scaleY) -- so this is not a
|
||||
*unique* representation of a matrix, only a convenient one. That
|
||||
redundancy is harmless for editing (nothing here ever needs to invert
|
||||
the forward direction) and only matters to decomposeLinear() below,
|
||||
which exists for later consumers (flattening a group transform onto
|
||||
its children, importing a foreign matrix) rather than everyday use.
|
||||
|
||||
The resulting matrix is meant to be handed directly to
|
||||
QGraphicsItem::setTransform(). QGraphicsItem::transformOriginPoint()
|
||||
is deliberately NOT used anywhere in this design: it only centers
|
||||
QGraphicsItem's own rotation()/scale() convenience properties, and has
|
||||
no effect on a custom transform() matrix -- the pivot has to be baked
|
||||
into the matrix itself, as toMatrix() does. QGraphicsItem::pos()
|
||||
supplies the translation on top, unchanged from how shapes are
|
||||
already positioned and moved today.
|
||||
*/
|
||||
struct ShapeTransform
|
||||
{
|
||||
qreal rotation = 0; // degrees
|
||||
qreal skewX = 0; // degrees
|
||||
qreal skewY = 0; // degrees
|
||||
qreal scaleX = 1;
|
||||
qreal scaleY = 1;
|
||||
QPointF pivot; // local coordinates; caller decides the default (usually local bbox center)
|
||||
|
||||
bool isIdentity() const;
|
||||
|
||||
// Rotate+shear+scale about the origin, ignoring pivot -- this is the
|
||||
// "linear part" used by compensatedPositionForNewPivot() and by
|
||||
// anything that needs to map a direction/offset rather than a point.
|
||||
QTransform linearPart() const;
|
||||
|
||||
// The pivot-centered matrix to pass to QGraphicsItem::setTransform().
|
||||
QTransform toMatrix() const;
|
||||
|
||||
bool operator==(const ShapeTransform &other) const;
|
||||
bool operator!=(const ShapeTransform &other) const { return !(*this == other); }
|
||||
};
|
||||
|
||||
// Canonical decomposition of an arbitrary 2x2 linear map into the five
|
||||
// scalars above, always returning skewY == 0 (all shear folded into
|
||||
// skewX -- matching how most authoring tools represent shear on export).
|
||||
// Rebuilding via ShapeTransform::linearPart() from the result reproduces
|
||||
// the input matrix exactly; the individual scalar values are not
|
||||
// guaranteed to match whatever scalars (if any) originally produced that
|
||||
// matrix, only the matrix itself is guaranteed to match.
|
||||
ShapeTransform decomposeLinear(const QTransform &linear);
|
||||
|
||||
// Position adjustment needed when the pivot moves, so the shape does not
|
||||
// visibly jump on screen: call once, at the end of a pivot-handle drag,
|
||||
// alongside setting the new pivot.
|
||||
QPointF compensatedPositionForNewPivot(
|
||||
const QPointF &position,
|
||||
const QPointF &oldPivot,
|
||||
const QPointF &newPivot,
|
||||
const QTransform &linearPart);
|
||||
|
||||
#endif // SHAPETRANSFORM_H
|
||||
@@ -128,6 +128,7 @@ namespace QET {
|
||||
QIcon ObjectUnlocked;
|
||||
QIcon Orientations;
|
||||
QIcon PartArc;
|
||||
QIcon PartBezier;
|
||||
QIcon PartCircle;
|
||||
QIcon PartEllipse;
|
||||
QIcon PartLine;
|
||||
@@ -570,6 +571,7 @@ void QET::Icons::initIcons()
|
||||
ObjectUnlocked .addFile(":/ico/22x22/object-unlocked.png");
|
||||
Orientations .addFile(":/ico/16x16/orientations.png");
|
||||
PartArc .addFile(":/ico/22x22/arc.png");
|
||||
PartBezier .addFile(":/ico/breeze-icons/scalable/apps/hidef/draw-bezier-curves.svg");
|
||||
PartCircle .addFile(":/ico/16x16/circle.png");
|
||||
PartEllipse .addFile(":/ico/22x22/ellipse.png");
|
||||
PartLine .addFile(":/ico/22x22/line.png");
|
||||
|
||||
@@ -136,6 +136,7 @@ namespace QET {
|
||||
extern QIcon ObjectUnlocked;
|
||||
extern QIcon Orientations;
|
||||
extern QIcon PartArc;
|
||||
extern QIcon PartBezier;
|
||||
extern QIcon PartCircle;
|
||||
extern QIcon PartEllipse;
|
||||
extern QIcon PartLine;
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
/*
|
||||
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 "imagecropdialog.h"
|
||||
|
||||
#include <QDialogButtonBox>
|
||||
#include <QLabel>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace {
|
||||
// Fits the dialog comfortably on a normal screen regardless of the
|
||||
// source image's own resolution -- same reasoning, and same value,
|
||||
// as ImageTransparentColorDialog's identical constant.
|
||||
constexpr int MAX_DISPLAY_SIZE = 350;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief CropAreaWidget::CropAreaWidget
|
||||
@param sourceImage the full-resolution image to crop
|
||||
@param initialCropRect a previously-chosen crop rect (in
|
||||
sourceImage's own coordinates), or an empty QRect to start covering
|
||||
the whole image
|
||||
@param parent
|
||||
*/
|
||||
CropAreaWidget::CropAreaWidget(const QImage &sourceImage, const QRect &initialCropRect, QWidget *parent) :
|
||||
QWidget(parent),
|
||||
m_source(sourceImage)
|
||||
{
|
||||
const qreal scaleW = qreal(MAX_DISPLAY_SIZE) / m_source.width();
|
||||
const qreal scaleH = qreal(MAX_DISPLAY_SIZE) / m_source.height();
|
||||
m_displayScale = qMin(qreal(1.0), qMin(scaleW, scaleH)); // never upscale a small image, only ever shrink a large one
|
||||
|
||||
const QSize displaySize = m_source.size() * m_displayScale;
|
||||
m_displayImageRect = QRect(QPoint(0, 0), displaySize);
|
||||
setFixedSize(displaySize);
|
||||
|
||||
if (initialCropRect.isEmpty())
|
||||
{
|
||||
// No previous crop to start from -- covers the whole image, so
|
||||
// the user shrinks it from there rather than having to first
|
||||
// drag out a rectangle from nothing.
|
||||
m_cropRect = m_displayImageRect;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Converting an existing, original-space crop rect back into
|
||||
// display-space -- the reverse of what cropRect() does going
|
||||
// the other way.
|
||||
m_cropRect = QRect(
|
||||
qRound(initialCropRect.left() * m_displayScale),
|
||||
qRound(initialCropRect.top() * m_displayScale),
|
||||
qRound(initialCropRect.width() * m_displayScale),
|
||||
qRound(initialCropRect.height() * m_displayScale))
|
||||
.intersected(m_displayImageRect);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief CropAreaWidget::resetToFullImage
|
||||
*/
|
||||
void CropAreaWidget::resetToFullImage()
|
||||
{
|
||||
m_cropRect = m_displayImageRect;
|
||||
update();
|
||||
emit cropRectChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief CropAreaWidget::cropRect
|
||||
@return the crop rectangle in the original, full-resolution image's
|
||||
own coordinates -- m_cropRect itself is display-space only.
|
||||
*/
|
||||
QRect CropAreaWidget::cropRect() const
|
||||
{
|
||||
QRect r(
|
||||
qRound(m_cropRect.left() / m_displayScale),
|
||||
qRound(m_cropRect.top() / m_displayScale),
|
||||
qRound(m_cropRect.width() / m_displayScale),
|
||||
qRound(m_cropRect.height() / m_displayScale));
|
||||
return r.intersected(m_source.rect());
|
||||
}
|
||||
|
||||
/**
|
||||
@brief CropAreaWidget::handleAt
|
||||
@param pos widget-space position to test
|
||||
@return whichever handle (or the rect's own interior, for moving)
|
||||
pos is close enough to, or DragMode::None if it's not near anything
|
||||
*/
|
||||
CropAreaWidget::DragMode CropAreaWidget::handleAt(const QPoint &pos) const
|
||||
{
|
||||
const int tol = HANDLE_SIZE; // a generous hit area, larger than the drawn handle itself, since a hit-test exactly matching a small handle's visible size is needlessly hard to land
|
||||
auto near = [&](const QPoint &p) { return (pos - p).manhattanLength() <= tol; };
|
||||
|
||||
if (near(m_cropRect.topLeft())) return DragMode::ResizeNW;
|
||||
if (near(m_cropRect.topRight())) return DragMode::ResizeNE;
|
||||
if (near(m_cropRect.bottomLeft())) return DragMode::ResizeSW;
|
||||
if (near(m_cropRect.bottomRight())) return DragMode::ResizeSE;
|
||||
if (near(QPoint(m_cropRect.center().x(), m_cropRect.top()))) return DragMode::ResizeN;
|
||||
if (near(QPoint(m_cropRect.center().x(), m_cropRect.bottom()))) return DragMode::ResizeS;
|
||||
if (near(QPoint(m_cropRect.left(), m_cropRect.center().y()))) return DragMode::ResizeW;
|
||||
if (near(QPoint(m_cropRect.right(), m_cropRect.center().y()))) return DragMode::ResizeE;
|
||||
|
||||
if (m_cropRect.contains(pos))
|
||||
return DragMode::Move;
|
||||
|
||||
return DragMode::None;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief CropAreaWidget::applyDrag
|
||||
Recomputes m_cropRect from m_dragStartRect plus however far pos has
|
||||
moved from m_dragStartPos, according to m_dragMode -- always
|
||||
clamped to stay within the displayed image and never smaller than
|
||||
MIN_CROP_SIZE, so the rect can never escape its bounds or collapse
|
||||
to nothing while being dragged.
|
||||
@param pos current widget-space mouse position
|
||||
*/
|
||||
void CropAreaWidget::applyDrag(const QPoint &pos)
|
||||
{
|
||||
const QPoint delta = pos - m_dragStartPos;
|
||||
QRect r = m_dragStartRect;
|
||||
|
||||
auto clampLeft = [&](int v) { r.setLeft(qBound(m_displayImageRect.left(), v, r.right() - MIN_CROP_SIZE)); };
|
||||
auto clampRight = [&](int v) { r.setRight(qBound(r.left() + MIN_CROP_SIZE, v, m_displayImageRect.right())); };
|
||||
auto clampTop = [&](int v) { r.setTop(qBound(m_displayImageRect.top(), v, r.bottom() - MIN_CROP_SIZE)); };
|
||||
auto clampBottom = [&](int v) { r.setBottom(qBound(r.top() + MIN_CROP_SIZE, v, m_displayImageRect.bottom())); };
|
||||
|
||||
switch (m_dragMode)
|
||||
{
|
||||
case DragMode::Move:
|
||||
{
|
||||
// Clamping the translation itself, rather than each edge of
|
||||
// the resulting rect independently -- clamping edges one at
|
||||
// a time would shrink or distort the rect the instant it
|
||||
// hit a boundary, instead of just stopping its movement.
|
||||
QRect moved = m_dragStartRect.translated(delta);
|
||||
if (moved.left() < m_displayImageRect.left())
|
||||
moved.translate(m_displayImageRect.left() - moved.left(), 0);
|
||||
if (moved.right() > m_displayImageRect.right())
|
||||
moved.translate(m_displayImageRect.right() - moved.right(), 0);
|
||||
if (moved.top() < m_displayImageRect.top())
|
||||
moved.translate(0, m_displayImageRect.top() - moved.top());
|
||||
if (moved.bottom() > m_displayImageRect.bottom())
|
||||
moved.translate(0, m_displayImageRect.bottom() - moved.bottom());
|
||||
r = moved;
|
||||
break;
|
||||
}
|
||||
case DragMode::ResizeN: clampTop(m_dragStartRect.top() + delta.y()); break;
|
||||
case DragMode::ResizeS: clampBottom(m_dragStartRect.bottom() + delta.y()); break;
|
||||
case DragMode::ResizeE: clampRight(m_dragStartRect.right() + delta.x()); break;
|
||||
case DragMode::ResizeW: clampLeft(m_dragStartRect.left() + delta.x()); break;
|
||||
case DragMode::ResizeNE: clampTop(m_dragStartRect.top() + delta.y()); clampRight(m_dragStartRect.right() + delta.x()); break;
|
||||
case DragMode::ResizeNW: clampTop(m_dragStartRect.top() + delta.y()); clampLeft(m_dragStartRect.left() + delta.x()); break;
|
||||
case DragMode::ResizeSE: clampBottom(m_dragStartRect.bottom() + delta.y()); clampRight(m_dragStartRect.right() + delta.x()); break;
|
||||
case DragMode::ResizeSW: clampBottom(m_dragStartRect.bottom() + delta.y()); clampLeft(m_dragStartRect.left() + delta.x()); break;
|
||||
case DragMode::None: return;
|
||||
}
|
||||
|
||||
m_cropRect = r;
|
||||
}
|
||||
|
||||
void CropAreaWidget::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->button() != Qt::LeftButton)
|
||||
return;
|
||||
m_dragMode = handleAt(event->pos());
|
||||
m_dragStartPos = event->pos();
|
||||
m_dragStartRect = m_cropRect;
|
||||
}
|
||||
|
||||
void CropAreaWidget::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
if (m_dragMode == DragMode::None)
|
||||
return;
|
||||
applyDrag(event->pos());
|
||||
update();
|
||||
emit cropRectChanged();
|
||||
}
|
||||
|
||||
void CropAreaWidget::mouseReleaseEvent(QMouseEvent *event)
|
||||
{
|
||||
Q_UNUSED(event);
|
||||
m_dragMode = DragMode::None;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief CropAreaWidget::paintEvent
|
||||
Draws the image, dims everything outside the crop rect (four
|
||||
strips around it, rather than one overlay with a hole cut out of
|
||||
it, since that would need a more involved clip/composition setup
|
||||
for no real benefit here), then the rect's own dashed border and
|
||||
its eight handles.
|
||||
*/
|
||||
void CropAreaWidget::paintEvent(QPaintEvent *)
|
||||
{
|
||||
QPainter painter(this);
|
||||
painter.drawImage(m_displayImageRect, m_source);
|
||||
|
||||
const QColor dim(0, 0, 0, 140);
|
||||
painter.fillRect(QRect(m_displayImageRect.left(), m_displayImageRect.top(),
|
||||
m_displayImageRect.width(), m_cropRect.top() - m_displayImageRect.top()), dim);
|
||||
painter.fillRect(QRect(m_displayImageRect.left(), m_cropRect.bottom() + 1,
|
||||
m_displayImageRect.width(), m_displayImageRect.bottom() - m_cropRect.bottom()), dim);
|
||||
painter.fillRect(QRect(m_displayImageRect.left(), m_cropRect.top(),
|
||||
m_cropRect.left() - m_displayImageRect.left(), m_cropRect.height()), dim);
|
||||
painter.fillRect(QRect(m_cropRect.right() + 1, m_cropRect.top(),
|
||||
m_displayImageRect.right() - m_cropRect.right(), m_cropRect.height()), dim);
|
||||
|
||||
QPen borderPen(Qt::white);
|
||||
borderPen.setStyle(Qt::DashLine);
|
||||
painter.setPen(borderPen);
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.drawRect(m_cropRect);
|
||||
|
||||
painter.setPen(Qt::black);
|
||||
painter.setBrush(Qt::white);
|
||||
auto drawHandle = [&](const QPoint ¢er) {
|
||||
painter.drawRect(QRect(center.x() - HANDLE_SIZE / 2, center.y() - HANDLE_SIZE / 2, HANDLE_SIZE, HANDLE_SIZE));
|
||||
};
|
||||
drawHandle(m_cropRect.topLeft());
|
||||
drawHandle(m_cropRect.topRight());
|
||||
drawHandle(m_cropRect.bottomLeft());
|
||||
drawHandle(m_cropRect.bottomRight());
|
||||
drawHandle(QPoint(m_cropRect.center().x(), m_cropRect.top()));
|
||||
drawHandle(QPoint(m_cropRect.center().x(), m_cropRect.bottom()));
|
||||
drawHandle(QPoint(m_cropRect.left(), m_cropRect.center().y()));
|
||||
drawHandle(QPoint(m_cropRect.right(), m_cropRect.center().y()));
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ImageCropDialog::ImageCropDialog
|
||||
@param pixmap the image to crop
|
||||
@param existingCropRect a crop rectangle remembered from a previous
|
||||
session, or an empty QRect if there isn't one yet
|
||||
@param parent
|
||||
*/
|
||||
ImageCropDialog::ImageCropDialog(const QPixmap &pixmap, const QRect &existingCropRect, QWidget *parent) :
|
||||
QDialog(parent)
|
||||
{
|
||||
setWindowTitle(tr("Rogner l'image"));
|
||||
|
||||
m_cropArea = new CropAreaWidget(pixmap.toImage(), existingCropRect, this);
|
||||
|
||||
auto *hint = new QLabel(tr("Faites glisser les poignées, ou l'intérieur du cadre, pour ajuster la zone à conserver."), this);
|
||||
hint->setWordWrap(true);
|
||||
|
||||
auto *resetButton = new QPushButton(tr("Réinitialiser"), this);
|
||||
resetButton->setToolTip(tr("Revenir à l'image complète, sans rognage"));
|
||||
connect(resetButton, &QPushButton::clicked, m_cropArea, &CropAreaWidget::resetToFullImage);
|
||||
|
||||
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||
buttons->addButton(resetButton, QDialogButtonBox::ResetRole);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->addWidget(hint);
|
||||
layout->addWidget(m_cropArea, 0, Qt::AlignHCenter);
|
||||
layout->addWidget(buttons);
|
||||
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ImageCropDialog::cropRect
|
||||
@return the chosen crop rectangle, in the original pixmap's own coordinates
|
||||
*/
|
||||
QRect ImageCropDialog::cropRect() const
|
||||
{
|
||||
return m_cropArea->cropRect();
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
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 IMAGE_CROP_DIALOG_H
|
||||
#define IMAGE_CROP_DIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QImage>
|
||||
#include <QRect>
|
||||
#include <QWidget>
|
||||
|
||||
class QMouseEvent;
|
||||
class QPaintEvent;
|
||||
class QPushButton;
|
||||
|
||||
/**
|
||||
@brief The CropAreaWidget class
|
||||
Displays the image scaled to fit a reasonable dialog size, with a
|
||||
draggable, resizable crop rectangle overlaid on top -- the area
|
||||
outside it dimmed, the usual visual convention for "this part goes
|
||||
away" during a crop, so it's never ambiguous which region survives.
|
||||
All internal geometry (the crop rect, drag handling, hit-testing)
|
||||
works in display-space pixel coordinates; only cropRect() converts
|
||||
back to the original, full-resolution image's own coordinates for
|
||||
the caller, the same original-vs-display distinction already used
|
||||
for colour sampling in ImageTransparentColorDialog.
|
||||
*/
|
||||
class CropAreaWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/// @param sourceImage the full, uncropped image to crop from
|
||||
/// @param initialCropRect a previously-chosen crop rectangle, in
|
||||
/// sourceImage's own coordinates, to start from -- an empty
|
||||
/// (default-constructed) QRect means "no previous crop", i.e.
|
||||
/// start covering the whole image.
|
||||
explicit CropAreaWidget(const QImage &sourceImage, const QRect &initialCropRect = QRect(), QWidget *parent = nullptr);
|
||||
|
||||
/// The current crop rectangle, in the original image's own
|
||||
/// coordinates (not display-space) -- always clamped to the
|
||||
/// image's own bounds and never degenerate (zero width/height).
|
||||
QRect cropRect() const;
|
||||
|
||||
public slots:
|
||||
/// Resets the crop rectangle back to covering the entire image --
|
||||
/// the direct answer to "I want to undo the crop, not just
|
||||
/// adjust it", since dragging every handle back out by hand to
|
||||
/// exactly the original extent is needlessly fiddly.
|
||||
void resetToFullImage();
|
||||
|
||||
signals:
|
||||
void cropRectChanged();
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
void mouseMoveEvent(QMouseEvent *event) override;
|
||||
void mouseReleaseEvent(QMouseEvent *event) override;
|
||||
|
||||
private:
|
||||
enum class DragMode { None, Move, ResizeN, ResizeS, ResizeE, ResizeW, ResizeNE, ResizeNW, ResizeSE, ResizeSW };
|
||||
|
||||
DragMode handleAt(const QPoint &pos) const;
|
||||
void applyDrag(const QPoint &pos);
|
||||
|
||||
QImage m_source;
|
||||
qreal m_displayScale = 1.0;
|
||||
QRect m_displayImageRect; // where the (possibly scaled) pixmap actually sits within this widget
|
||||
QRect m_cropRect; // in display-space, relative to m_displayImageRect's own origin
|
||||
|
||||
DragMode m_dragMode = DragMode::None;
|
||||
QPoint m_dragStartPos;
|
||||
QRect m_dragStartRect;
|
||||
|
||||
static constexpr int HANDLE_SIZE = 8;
|
||||
static constexpr int MIN_CROP_SIZE = 16;
|
||||
};
|
||||
|
||||
/**
|
||||
@brief The ImageCropDialog class
|
||||
Self-contained modal dialog wrapping CropAreaWidget with OK/Cancel --
|
||||
the same reasoning as ImageTransparentColorDialog: this needs
|
||||
neither undo-during-drag nor coexistence with other tools, only a
|
||||
rectangle the user drags out against a pixmap already in hand.
|
||||
*/
|
||||
class ImageCropDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/// @param pixmap the full, uncropped image to crop from
|
||||
/// @param existingCropRect a crop rectangle remembered from a
|
||||
/// previous session, shown from the start rather than resetting
|
||||
/// to the full image every time the dialog is reopened -- an
|
||||
/// empty QRect means there isn't one yet.
|
||||
explicit ImageCropDialog(const QPixmap &pixmap, const QRect &existingCropRect = QRect(), QWidget *parent = nullptr);
|
||||
|
||||
/// The chosen crop rectangle, in the original pixmap's own
|
||||
/// coordinates. Only meaningful after the dialog was accepted.
|
||||
QRect cropRect() const;
|
||||
|
||||
private:
|
||||
CropAreaWidget *m_cropArea;
|
||||
};
|
||||
|
||||
#endif // IMAGE_CROP_DIALOG_H
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "../QPropertyUndoCommand/qpropertyundocommand.h"
|
||||
#include "../diagram.h"
|
||||
#include "../qetgraphicsitem/diagramimageitem.h"
|
||||
#include "../qeticons.h"
|
||||
#include "../ui_imagepropertieswidget.h"
|
||||
|
||||
/**
|
||||
@@ -34,6 +35,11 @@ ImagePropertiesWidget::ImagePropertiesWidget(DiagramImageItem *image, QWidget *p
|
||||
m_image(nullptr)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
// Matches the .ui file's own initial checked="true" -- toggled()
|
||||
// only fires on a subsequent CHANGE, not for the state a widget
|
||||
// already starts in, so the icon needs setting explicitly here or
|
||||
// the button would show blank until first clicked.
|
||||
ui->m_lock_ratio_tb->setIcon(QET::Icons::ObjectLocked);
|
||||
this->setDisabled(true);
|
||||
setImageItem(image);
|
||||
}
|
||||
@@ -58,12 +64,16 @@ void ImagePropertiesWidget::setImageItem(DiagramImageItem *image)
|
||||
this->setEnabled(true);
|
||||
if (m_image == image) return;
|
||||
if (m_image)
|
||||
disconnect(m_image, &QGraphicsObject::scaleChanged, this, &ImagePropertiesWidget::updateUi);
|
||||
disconnect(m_image, &DiagramImageItem::transformChanged, this, &ImagePropertiesWidget::updateUi);
|
||||
|
||||
m_image = image;
|
||||
connect(m_image, &QGraphicsObject::scaleChanged, this, &ImagePropertiesWidget::updateUi);
|
||||
connect(m_image, &DiagramImageItem::transformChanged, this, &ImagePropertiesWidget::updateUi);
|
||||
m_movable = image->isMovable();
|
||||
m_scale = m_image->scale();
|
||||
m_scaleX = m_image->scaleFactorX();
|
||||
m_scaleY = m_image->scaleFactorY();
|
||||
m_rotation = m_image->rotationAngle();
|
||||
m_skewX = m_image->skewX();
|
||||
m_skewY = m_image->skewY();
|
||||
updateUi();
|
||||
}
|
||||
|
||||
@@ -77,16 +87,20 @@ void ImagePropertiesWidget::apply()
|
||||
|
||||
if (m_image->diagram())
|
||||
{
|
||||
if (m_live_edit) disconnect(m_image, &QGraphicsObject::scaleChanged, this, &ImagePropertiesWidget::updateUi);
|
||||
if (m_live_edit) disconnect(m_image, &DiagramImageItem::transformChanged, this, &ImagePropertiesWidget::updateUi);
|
||||
|
||||
QUndoCommand *undo = associatedUndo();
|
||||
if (undo)
|
||||
m_image->diagram()->undoStack().push(undo);
|
||||
|
||||
if (m_live_edit) connect(m_image, &QGraphicsObject::scaleChanged, this, &ImagePropertiesWidget::updateUi);
|
||||
if (m_live_edit) connect(m_image, &DiagramImageItem::transformChanged, this, &ImagePropertiesWidget::updateUi);
|
||||
}
|
||||
|
||||
m_scale = m_image->scale();
|
||||
m_scaleX = m_image->scaleFactorX();
|
||||
m_scaleY = m_image->scaleFactorY();
|
||||
m_rotation = m_image->rotationAngle();
|
||||
m_skewX = m_image->skewX();
|
||||
m_skewY = m_image->skewY();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,7 +111,11 @@ void ImagePropertiesWidget::reset()
|
||||
{
|
||||
if(!m_image) return;
|
||||
|
||||
m_image->setScale(m_scale);
|
||||
m_image->setScaleFactorX(m_scaleX);
|
||||
m_image->setScaleFactorY(m_scaleY);
|
||||
m_image->setRotationAngle(m_rotation);
|
||||
m_image->setSkewX(m_skewX);
|
||||
m_image->setSkewY(m_skewY);
|
||||
m_image->setMovable(m_movable);
|
||||
updateUi();
|
||||
}
|
||||
@@ -115,13 +133,19 @@ bool ImagePropertiesWidget::setLiveEdit(bool live_edit)
|
||||
|
||||
if (m_live_edit)
|
||||
{
|
||||
connect (ui->m_scale_slider, &QSlider::sliderReleased, this, &ImagePropertiesWidget::apply);
|
||||
connect (ui->m_scale_sb, &QSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
|
||||
connect (ui->m_width_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
|
||||
connect (ui->m_height_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
|
||||
connect (ui->m_angle_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
|
||||
connect (ui->m_skew_x_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
|
||||
connect (ui->m_skew_y_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
|
||||
}
|
||||
else
|
||||
{
|
||||
disconnect (ui->m_scale_slider, &QSlider::sliderReleased, this, &ImagePropertiesWidget::apply);
|
||||
disconnect (ui->m_scale_sb, &QSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
|
||||
disconnect (ui->m_width_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
|
||||
disconnect (ui->m_height_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
|
||||
disconnect (ui->m_angle_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
|
||||
disconnect (ui->m_skew_x_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
|
||||
disconnect (ui->m_skew_y_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -129,18 +153,44 @@ bool ImagePropertiesWidget::setLiveEdit(bool live_edit)
|
||||
|
||||
/**
|
||||
@brief ImagePropertiesWidget::associatedUndo
|
||||
@return the change in an undo command (ItemResizerCommand).
|
||||
If there is no change return nullptr
|
||||
@return the change in an undo command. If there is no change return
|
||||
nullptr. Chains scaleFactorX, scaleFactorY, rotationAngle, skewX and
|
||||
skewY as separate QPropertyUndoCommands, matching the established
|
||||
"check each field, chain if changed" pattern used elsewhere -- each
|
||||
of these is genuinely independent (the resize/rotate/skew handles
|
||||
can set any of them without touching the others), so a single
|
||||
combined command wouldn't correctly capture "only two of five
|
||||
fields actually changed".
|
||||
*/
|
||||
QUndoCommand* ImagePropertiesWidget::associatedUndo() const
|
||||
{
|
||||
const qreal newScaleX = ui->m_width_sb->value() / 100.0;
|
||||
const qreal newScaleY = ui->m_height_sb->value() / 100.0;
|
||||
const qreal newRotation = ui->m_angle_sb->value();
|
||||
const qreal newSkewX = ui->m_skew_x_sb->value();
|
||||
const qreal newSkewY = ui->m_skew_y_sb->value();
|
||||
|
||||
QPropertyUndoCommand *undo = nullptr;
|
||||
auto chain = [&](const char *property, qreal oldValue, qreal newValue, const QString &text)
|
||||
{
|
||||
if (qFuzzyCompare(oldValue, newValue))
|
||||
return;
|
||||
if (undo)
|
||||
new QPropertyUndoCommand(m_image, property, oldValue, newValue, undo);
|
||||
else
|
||||
{
|
||||
undo = new QPropertyUndoCommand(m_image, property, oldValue, newValue);
|
||||
undo->enableAnimation();
|
||||
undo->setText(text);
|
||||
}
|
||||
};
|
||||
|
||||
chain("scaleFactorX", m_scaleX, newScaleX, tr("Modifier la largeur d'une image"));
|
||||
chain("scaleFactorY", m_scaleY, newScaleY, tr("Modifier la hauteur d'une image"));
|
||||
chain("rotationAngle", m_rotation, newRotation, tr("Modifier l'angle d'une image"));
|
||||
chain("skewX", m_skewX, newSkewX, tr("Modifier l'inclinaison d'une image"));
|
||||
chain("skewY", m_skewY, newSkewY, tr("Modifier l'inclinaison d'une image"));
|
||||
|
||||
qreal value = ui->m_scale_slider->value();
|
||||
value /= 100;
|
||||
if (m_scale == value) return nullptr;
|
||||
QPropertyUndoCommand *undo = new QPropertyUndoCommand(m_image, "scale", m_scale, value);
|
||||
undo->enableAnimation();
|
||||
undo->setText(tr("Modifier la taille d'une image"));
|
||||
return undo;
|
||||
}
|
||||
|
||||
@@ -150,20 +200,167 @@ QUndoCommand* ImagePropertiesWidget::associatedUndo() const
|
||||
*/
|
||||
void ImagePropertiesWidget::updateUi()
|
||||
{
|
||||
if (!m_image) return;
|
||||
ui->m_scale_slider->setValue(m_image->scale() * 100);
|
||||
// Two different uses of the same flag: an entry guard here (skip
|
||||
// entirely if a locked width/height operation is already in
|
||||
// progress elsewhere -- its own explicit updateUi() call once
|
||||
// finished is what actually refreshes things), and, immediately
|
||||
// below, a re-entrancy guard around this function's OWN setValue()
|
||||
// calls, so they can't recurse back into
|
||||
// on_m_width_sb_valueChanged()/on_m_height_sb_valueChanged() when
|
||||
// this runs normally (e.g. from setImageItem(), or a transformChanged
|
||||
// signal from a resize handle used directly on the canvas while
|
||||
// this dialog is open).
|
||||
if (!m_image || m_updating_ratio) return;
|
||||
|
||||
m_updating_ratio = true;
|
||||
ui->m_width_sb->setValue(m_image->scaleFactorX() * 100.0);
|
||||
ui->m_height_sb->setValue(m_image->scaleFactorY() * 100.0);
|
||||
m_updating_ratio = false;
|
||||
|
||||
ui->m_angle_sb->setValue(m_image->rotationAngle());
|
||||
ui->m_skew_x_sb->setValue(m_image->skewX());
|
||||
ui->m_skew_y_sb->setValue(m_image->skewY());
|
||||
ui->m_lock_pos_cb->setChecked(!m_image->isMovable());
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ImagePropertiesWidget::on_m_scale_slider_valueChanged
|
||||
Update the size of image when move slider.
|
||||
@param value
|
||||
@brief ImagePropertiesWidget::on_m_lock_ratio_tb_toggled
|
||||
Keeps the button's own icon and tooltip in sync with its checked
|
||||
state -- Qt shows the checked/unchecked state visually (a sunken
|
||||
look) regardless, but the lock/unlock glyph makes the meaning
|
||||
obvious at a glance rather than needing that convention noticed.
|
||||
@param checked
|
||||
*/
|
||||
void ImagePropertiesWidget::on_m_scale_slider_valueChanged(int value)
|
||||
void ImagePropertiesWidget::on_m_lock_ratio_tb_toggled(bool checked)
|
||||
{
|
||||
qreal scale = value;
|
||||
m_image->setScale(scale / 100);
|
||||
ui->m_lock_ratio_tb->setIcon(checked ? QET::Icons::ObjectLocked : QET::Icons::ObjectUnlocked);
|
||||
ui->m_lock_ratio_tb->setToolTip(checked
|
||||
? tr("Verrouillé : modifier la largeur ou la hauteur ajuste l'autre pour conserver les proportions. Cliquer pour déverrouiller.")
|
||||
: tr("Déverrouillé : largeur et hauteur peuvent être modifiées indépendamment. Cliquer pour verrouiller."));
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ImagePropertiesWidget::on_m_restore_ratio_pb_clicked
|
||||
The same action as the item's own "Restaurer les proportions"
|
||||
context-menu entry, offered here too since this dialog is exactly
|
||||
where someone would notice the proportions are off in the first
|
||||
place. Deliberately bypasses on_m_height_sb_valueChanged()'s own
|
||||
lock-following logic (guarded by m_updating_ratio, then applied
|
||||
directly rather than through the spinbox's own signal) rather than
|
||||
going through it normally: if the lock happened to be engaged,
|
||||
that logic would treat this as an ordinary edit and scale width by
|
||||
the same ratio the height change just applied -- compounding into
|
||||
some other, unrelated value instead of the plain "make height
|
||||
match width" this button promises regardless of lock state.
|
||||
*/
|
||||
void ImagePropertiesWidget::on_m_restore_ratio_pb_clicked()
|
||||
{
|
||||
if (!m_image) return;
|
||||
const qreal newScaleY = m_image->scaleFactorX();
|
||||
m_updating_ratio = true;
|
||||
ui->m_height_sb->setValue(newScaleY * 100.0);
|
||||
m_updating_ratio = false;
|
||||
m_image->setScaleFactorY(newScaleY);
|
||||
apply();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ImagePropertiesWidget::on_m_width_sb_valueChanged
|
||||
Applies the new width live, and -- if "Conserver les proportions" is
|
||||
checked -- scales height by the same ratio this change just applied
|
||||
to width, so the two stay locked together. Guarded by
|
||||
m_updating_ratio: the setValue() call below would otherwise
|
||||
re-trigger on_m_height_sb_valueChanged(), which would then try to
|
||||
scale width again in response, recursing indefinitely.
|
||||
@param value the new width, as a percentage
|
||||
*/
|
||||
void ImagePropertiesWidget::on_m_width_sb_valueChanged(double value)
|
||||
{
|
||||
if (!m_image || m_updating_ratio) return;
|
||||
|
||||
const qreal oldScaleX = m_image->scaleFactorX();
|
||||
const qreal newScaleX = value / 100.0;
|
||||
const bool wasLocked = ui->m_lock_ratio_tb->isChecked();
|
||||
|
||||
// Guards updateUi() itself here, not just the mutual spinbox
|
||||
// updates below: setScaleFactorX() emits transformChanged()
|
||||
// immediately, synchronously calling updateUi() before this
|
||||
// function gets any further -- at that exact moment scaleX has
|
||||
// already changed but scaleY hasn't caught up yet, so updateUi()'s
|
||||
// "auto-unlock if currently mismatched" check would fire and
|
||||
// uncheck the lock before the compensating height change below
|
||||
// ever runs, defeating the lock on every single use. Released
|
||||
// before the explicit updateUi() call at the end, once both axes
|
||||
// genuinely agree again (if locked) and that check is meaningful.
|
||||
m_updating_ratio = true;
|
||||
m_image->setScaleFactorX(newScaleX);
|
||||
|
||||
if (wasLocked && !qFuzzyIsNull(oldScaleX))
|
||||
{
|
||||
const qreal ratio = newScaleX / oldScaleX;
|
||||
const qreal newScaleY = m_image->scaleFactorY() * ratio;
|
||||
ui->m_height_sb->setValue(newScaleY * 100.0);
|
||||
m_image->setScaleFactorY(newScaleY);
|
||||
}
|
||||
m_updating_ratio = false;
|
||||
updateUi();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ImagePropertiesWidget::on_m_height_sb_valueChanged
|
||||
Mirror of on_m_width_sb_valueChanged() -- see its comment.
|
||||
@param value the new height, as a percentage
|
||||
*/
|
||||
void ImagePropertiesWidget::on_m_height_sb_valueChanged(double value)
|
||||
{
|
||||
if (!m_image || m_updating_ratio) return;
|
||||
|
||||
const qreal oldScaleY = m_image->scaleFactorY();
|
||||
const qreal newScaleY = value / 100.0;
|
||||
const bool wasLocked = ui->m_lock_ratio_tb->isChecked();
|
||||
|
||||
m_updating_ratio = true; // see on_m_width_sb_valueChanged()'s identical comment
|
||||
m_image->setScaleFactorY(newScaleY);
|
||||
|
||||
if (wasLocked && !qFuzzyIsNull(oldScaleY))
|
||||
{
|
||||
const qreal ratio = newScaleY / oldScaleY;
|
||||
const qreal newScaleX = m_image->scaleFactorX() * ratio;
|
||||
ui->m_width_sb->setValue(newScaleX * 100.0);
|
||||
m_image->setScaleFactorX(newScaleX);
|
||||
}
|
||||
m_updating_ratio = false;
|
||||
updateUi();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ImagePropertiesWidget::on_m_angle_sb_valueChanged
|
||||
@param value the new rotation, in degrees
|
||||
*/
|
||||
void ImagePropertiesWidget::on_m_angle_sb_valueChanged(double value)
|
||||
{
|
||||
if (!m_image) return;
|
||||
m_image->setRotationAngle(value);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ImagePropertiesWidget::on_m_skew_x_sb_valueChanged
|
||||
@param value the new X skew, in degrees
|
||||
*/
|
||||
void ImagePropertiesWidget::on_m_skew_x_sb_valueChanged(double value)
|
||||
{
|
||||
if (!m_image) return;
|
||||
m_image->setSkewX(value);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ImagePropertiesWidget::on_m_skew_y_sb_valueChanged
|
||||
@param value the new Y skew, in degrees
|
||||
*/
|
||||
void ImagePropertiesWidget::on_m_skew_y_sb_valueChanged(double value)
|
||||
{
|
||||
if (!m_image) return;
|
||||
m_image->setSkewY(value);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -48,14 +48,33 @@ class ImagePropertiesWidget : public PropertiesEditorWidget
|
||||
void updateUi() override;
|
||||
|
||||
private slots:
|
||||
void on_m_scale_slider_valueChanged(int value);
|
||||
void on_m_width_sb_valueChanged(double value);
|
||||
void on_m_height_sb_valueChanged(double value);
|
||||
void on_m_angle_sb_valueChanged(double value);
|
||||
void on_m_skew_x_sb_valueChanged(double value);
|
||||
void on_m_skew_y_sb_valueChanged(double value);
|
||||
void on_m_lock_ratio_tb_toggled(bool checked);
|
||||
void on_m_restore_ratio_pb_clicked();
|
||||
void on_m_lock_pos_cb_clicked();
|
||||
|
||||
private:
|
||||
Ui::ImagePropertiesWidget *ui;
|
||||
DiagramImageItem *m_image;
|
||||
bool m_movable;
|
||||
qreal m_scale;
|
||||
// All tracked independently -- the image may already be
|
||||
// non-uniformly scaled, rotated, and/or skewed via the resize/
|
||||
// rotate/skew handles before this dialog is even opened, and
|
||||
// undo/reset have to restore each to whatever it actually was.
|
||||
qreal m_scaleX;
|
||||
qreal m_scaleY;
|
||||
qreal m_rotation;
|
||||
qreal m_skewX;
|
||||
qreal m_skewY;
|
||||
// Guards the width/height spinboxes' mutual updates when
|
||||
// "Conserver les proportions" is checked, so setting one
|
||||
// programmatically in response to the other doesn't re-trigger
|
||||
// its own valueChanged and recurse.
|
||||
bool m_updating_ratio = false;
|
||||
};
|
||||
|
||||
#endif // IMAGEPROPERTIESWIDGET_H
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>244</width>
|
||||
<height>105</height>
|
||||
<width>280</width>
|
||||
<height>230</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@@ -17,47 +17,149 @@
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetMinimumSize</enum>
|
||||
</property>
|
||||
<item row="2" column="0">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_width">
|
||||
<property name="text">
|
||||
<string>Largeur</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QDoubleSpinBox" name="m_width_sb">
|
||||
<property name="suffix">
|
||||
<string> %</string>
|
||||
</property>
|
||||
<property name="decimals">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>1.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>1000.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2" rowspan="2">
|
||||
<widget class="QToolButton" name="m_lock_ratio_tb">
|
||||
<property name="toolTip">
|
||||
<string>Verrouillé : modifier la largeur ou la hauteur ajuste l'autre pour conserver les proportions. Cliquer pour déverrouiller.</string>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_height">
|
||||
<property name="text">
|
||||
<string>Hauteur</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QDoubleSpinBox" name="m_height_sb">
|
||||
<property name="suffix">
|
||||
<string> %</string>
|
||||
</property>
|
||||
<property name="decimals">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>1.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>1000.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="3">
|
||||
<widget class="QPushButton" name="m_restore_ratio_pb">
|
||||
<property name="text">
|
||||
<string>Restaurer les proportions</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_angle">
|
||||
<property name="text">
|
||||
<string>Angle</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QDoubleSpinBox" name="m_angle_sb">
|
||||
<property name="suffix">
|
||||
<string> °</string>
|
||||
</property>
|
||||
<property name="decimals">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-360.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>360.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="label_skew_x">
|
||||
<property name="text">
|
||||
<string>Inclinaison X</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QDoubleSpinBox" name="m_skew_x_sb">
|
||||
<property name="suffix">
|
||||
<string> °</string>
|
||||
</property>
|
||||
<property name="decimals">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-85.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>85.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="label_skew_y">
|
||||
<property name="text">
|
||||
<string>Inclinaison Y</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<widget class="QDoubleSpinBox" name="m_skew_y_sb">
|
||||
<property name="suffix">
|
||||
<string> °</string>
|
||||
</property>
|
||||
<property name="decimals">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-85.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>85.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="m_lock_pos_cb">
|
||||
<property name="text">
|
||||
<string>Verrouiller la position</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Dimension de l'image</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QSlider" name="m_scale_slider">
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>400</number>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QSpinBox" name="m_scale_sb">
|
||||
<property name="suffix">
|
||||
<string>%</string>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>400</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<item row="7" column="0">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
@@ -73,38 +175,5 @@
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>m_scale_slider</sender>
|
||||
<signal>valueChanged(int)</signal>
|
||||
<receiver>m_scale_sb</receiver>
|
||||
<slot>setValue(int)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>81</x>
|
||||
<y>40</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>190</x>
|
||||
<y>40</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>m_scale_sb</sender>
|
||||
<signal>valueChanged(int)</signal>
|
||||
<receiver>m_scale_slider</receiver>
|
||||
<slot>setValue(int)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>190</x>
|
||||
<y>40</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>81</x>
|
||||
<y>40</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
<connections/>
|
||||
</ui>
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
/*
|
||||
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 "imagetransparentcolordialog.h"
|
||||
|
||||
#include <QDialogButtonBox>
|
||||
#include <QGridLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QPushButton>
|
||||
#include <QSlider>
|
||||
#include <QVBoxLayout>
|
||||
#include <QVector>
|
||||
|
||||
namespace {
|
||||
// Fits the dialog comfortably on a normal screen regardless of the
|
||||
// source image's own resolution -- a photo straight off a phone
|
||||
// would otherwise make this dialog enormous.
|
||||
constexpr int MAX_DISPLAY_SIZE = 350;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ClickableImageLabel::ClickableImageLabel
|
||||
@param sourceImage the full-resolution image to display and sample from
|
||||
@param parent
|
||||
*/
|
||||
ClickableImageLabel::ClickableImageLabel(const QImage &sourceImage, QWidget *parent) :
|
||||
QLabel(parent),
|
||||
m_source(sourceImage)
|
||||
{
|
||||
const qreal scaleW = qreal(MAX_DISPLAY_SIZE) / m_source.width();
|
||||
const qreal scaleH = qreal(MAX_DISPLAY_SIZE) / m_source.height();
|
||||
m_displayScale = qMin(qreal(1.0), qMin(scaleW, scaleH)); // never upscale a small image, only ever shrink a large one
|
||||
|
||||
const QImage displayImage = (m_displayScale < 1.0)
|
||||
? m_source.scaled(m_source.size() * m_displayScale, Qt::KeepAspectRatio, Qt::SmoothTransformation)
|
||||
: m_source;
|
||||
setPixmap(QPixmap::fromImage(displayImage));
|
||||
setCursor(Qt::CrossCursor);
|
||||
setToolTip(tr("Cliquez pour choisir une couleur"));
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ClickableImageLabel::mousePressEvent
|
||||
Maps the click back to the original, full-resolution image before
|
||||
sampling -- see the class-level comment for why.
|
||||
@param event
|
||||
*/
|
||||
void ClickableImageLabel::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->button() != Qt::LeftButton || pixmap().isNull())
|
||||
return;
|
||||
|
||||
// The label may be larger than its pixmap (layout stretching); the
|
||||
// pixmap itself is always drawn centered by Qt's default alignment,
|
||||
// so the click has to be re-based against the pixmap's own rect
|
||||
// within the label, not the label's own top-left.
|
||||
const QRect pixmapRect(
|
||||
(width() - pixmap().width()) / 2,
|
||||
(height() - pixmap().height()) / 2,
|
||||
pixmap().width(), pixmap().height());
|
||||
if (!pixmapRect.contains(event->pos()))
|
||||
return;
|
||||
|
||||
const QPoint withinPixmap = event->pos() - pixmapRect.topLeft();
|
||||
QPoint originalPos(qRound(withinPixmap.x() / m_displayScale), qRound(withinPixmap.y() / m_displayScale));
|
||||
originalPos.setX(qBound(0, originalPos.x(), m_source.width() - 1));
|
||||
originalPos.setY(qBound(0, originalPos.y(), m_source.height() - 1));
|
||||
|
||||
emit colorPicked(m_source.pixelColor(originalPos));
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ImageTransparentColorDialog::ImageTransparentColorDialog
|
||||
@param pixmap the image to pick a transparent colour from
|
||||
@param parent
|
||||
*/
|
||||
ImageTransparentColorDialog::ImageTransparentColorDialog(const QPixmap &basePixmap, const QList<QColor> &existingColors,
|
||||
int existingTolerance, QWidget *parent) :
|
||||
QDialog(parent),
|
||||
m_sourceImage(basePixmap.toImage()),
|
||||
m_pickedColors(existingColors),
|
||||
m_tolerance(existingTolerance)
|
||||
{
|
||||
setWindowTitle(tr("Couleur transparente"));
|
||||
|
||||
m_sourceLabel = new ClickableImageLabel(m_sourceImage, this);
|
||||
m_previewLabel = new QLabel(this);
|
||||
|
||||
m_hintLabel = new QLabel(this);
|
||||
|
||||
m_toleranceSlider = new QSlider(Qt::Horizontal, this);
|
||||
m_toleranceSlider->setRange(0, 100);
|
||||
m_toleranceSlider->setValue(m_tolerance);
|
||||
|
||||
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||
m_okButton = buttons->button(QDialogButtonBox::Ok);
|
||||
m_okButton->setEnabled(!m_pickedColors.isEmpty()); // nothing to apply until at least one colour has been picked
|
||||
|
||||
auto *grid = new QGridLayout;
|
||||
grid->addWidget(new QLabel(tr("Image source")), 0, 0);
|
||||
grid->addWidget(new QLabel(tr("Aperçu")), 0, 1);
|
||||
grid->addWidget(m_sourceLabel, 1, 0);
|
||||
grid->addWidget(m_previewLabel, 1, 1);
|
||||
|
||||
// An empty row to start with if existingColors is empty --
|
||||
// rebuildSwatches() below populates it either way (including from
|
||||
// existingColors on the first call), and again as colours get
|
||||
// added or removed.
|
||||
m_swatchesLayout = new QHBoxLayout;
|
||||
|
||||
auto *colorRow = new QHBoxLayout;
|
||||
colorRow->addWidget(m_hintLabel);
|
||||
colorRow->addStretch();
|
||||
colorRow->addLayout(m_swatchesLayout);
|
||||
|
||||
auto *toleranceRow = new QHBoxLayout;
|
||||
toleranceRow->addWidget(new QLabel(tr("Tolérance")));
|
||||
toleranceRow->addWidget(m_toleranceSlider);
|
||||
|
||||
auto *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->addLayout(grid);
|
||||
mainLayout->addLayout(colorRow);
|
||||
mainLayout->addLayout(toleranceRow);
|
||||
mainLayout->addWidget(buttons);
|
||||
|
||||
connect(m_sourceLabel, &ClickableImageLabel::colorPicked, this, &ImageTransparentColorDialog::onColorPicked);
|
||||
connect(m_toleranceSlider, &QSlider::valueChanged, this, &ImageTransparentColorDialog::onToleranceChanged);
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
|
||||
rebuildSwatches(); // shows existingColors immediately, if any
|
||||
updatePreview(); // and the preview already reflects them too, rather than only appearing after the next pick
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ImageTransparentColorDialog::onColorPicked
|
||||
Adds to the set of picked colours rather than replacing the previous
|
||||
pick -- clicking a second, different pixel used to silently discard
|
||||
the first choice, with no way to work with more than one colour (a
|
||||
white background *and* a grey border, say) in the same pass.
|
||||
Skips an exact duplicate rather than adding a second, indistinguishable
|
||||
swatch for it.
|
||||
@param color the colour sampled from the source image
|
||||
*/
|
||||
void ImageTransparentColorDialog::onColorPicked(const QColor &color)
|
||||
{
|
||||
if (m_pickedColors.contains(color))
|
||||
return;
|
||||
|
||||
m_pickedColors.append(color);
|
||||
m_okButton->setEnabled(true);
|
||||
rebuildSwatches();
|
||||
updatePreview();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ImageTransparentColorDialog::removeColor
|
||||
Removes one colour from the set -- the counterpart onColorPicked()
|
||||
was missing entirely before: picking the wrong pixel by mistake had
|
||||
no way to undo except cancelling the whole dialog and starting over.
|
||||
@param color the colour to remove
|
||||
*/
|
||||
void ImageTransparentColorDialog::removeColor(const QColor &color)
|
||||
{
|
||||
m_pickedColors.removeAll(color);
|
||||
m_okButton->setEnabled(!m_pickedColors.isEmpty());
|
||||
rebuildSwatches();
|
||||
updatePreview();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ImageTransparentColorDialog::rebuildSwatches
|
||||
Rebuilds the row of picked-colour swatches from scratch against the
|
||||
current m_pickedColors -- simpler and safer than trying to
|
||||
incrementally add/remove individual widgets in sync with the list,
|
||||
given the list only ever changes one colour at a time and is never
|
||||
large enough for a full rebuild to be a real cost.
|
||||
*/
|
||||
void ImageTransparentColorDialog::rebuildSwatches()
|
||||
{
|
||||
QLayoutItem *item;
|
||||
while ((item = m_swatchesLayout->takeAt(0)) != nullptr)
|
||||
{
|
||||
delete item->widget();
|
||||
delete item;
|
||||
}
|
||||
|
||||
for (const QColor &color : std::as_const(m_pickedColors))
|
||||
{
|
||||
auto *swatch = new QPushButton(this);
|
||||
swatch->setFixedSize(24, 24);
|
||||
swatch->setStyleSheet(QStringLiteral("background-color: rgb(%1,%2,%3); border: 1px solid palette(mid);")
|
||||
.arg(color.red()).arg(color.green()).arg(color.blue()));
|
||||
swatch->setToolTip(tr("rgb(%1, %2, %3) -- cliquer pour retirer").arg(color.red()).arg(color.green()).arg(color.blue()));
|
||||
connect(swatch, &QPushButton::clicked, this, [this, color]() { removeColor(color); });
|
||||
m_swatchesLayout->addWidget(swatch);
|
||||
}
|
||||
|
||||
m_hintLabel->setText(m_pickedColors.isEmpty()
|
||||
? tr("Cliquez sur l'image pour choisir une couleur")
|
||||
: tr("Cliquez sur l'image pour ajouter une couleur, ou sur une pastille pour la retirer"));
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ImageTransparentColorDialog::onToleranceChanged
|
||||
@param value the new tolerance, 0-100
|
||||
*/
|
||||
void ImageTransparentColorDialog::onToleranceChanged(int value)
|
||||
{
|
||||
m_tolerance = value;
|
||||
if (!m_pickedColors.isEmpty())
|
||||
updatePreview();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ImageTransparentColorDialog::updatePreview
|
||||
Recomputes the checkerboard-backed preview against the current set
|
||||
of picked colours and the shared tolerance. Always runs against
|
||||
m_sourceImage (the original, full-resolution image), not any
|
||||
already-keyed result -- so adjusting the tolerance, or adding or
|
||||
removing a colour, re-evaluates every picked colour from scratch
|
||||
each time rather than compounding successive passes.
|
||||
*/
|
||||
void ImageTransparentColorDialog::updatePreview()
|
||||
{
|
||||
const QImage keyed = applyColorKey(m_sourceImage, m_pickedColors, m_tolerance);
|
||||
m_previewLabel->setPixmap(onCheckerboard(keyed));
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ImageTransparentColorDialog::resultPixmap
|
||||
@return the colour-keyed pixmap against every picked colour, or the
|
||||
original pixmap unchanged if none were ever picked (the Ok button
|
||||
stays disabled until at least one is, so this is mostly a defensive
|
||||
fallback).
|
||||
*/
|
||||
QPixmap ImageTransparentColorDialog::resultPixmap() const
|
||||
{
|
||||
if (m_pickedColors.isEmpty())
|
||||
return QPixmap::fromImage(m_sourceImage);
|
||||
return QPixmap::fromImage(applyColorKey(m_sourceImage, m_pickedColors, m_tolerance));
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ImageTransparentColorDialog::applyColorKey
|
||||
Binary transparency within tolerance, not a smooth falloff: every
|
||||
pixel within `tolerance` (0-100, mapped onto the maximum possible
|
||||
RGB distance) of *any* of keyColors becomes fully transparent,
|
||||
everything else keeps its existing alpha untouched. Squared distance
|
||||
throughout, avoiding a sqrt per pixel; breaks out of the inner loop
|
||||
on the first matching colour, since further matches wouldn't change
|
||||
the outcome.
|
||||
@param source the image to key
|
||||
@param keyColors the colours to make transparent
|
||||
@param tolerance 0 (exact match only) to 100 (everything)
|
||||
@return the resulting image, always in Format_ARGB32
|
||||
*/
|
||||
QImage ImageTransparentColorDialog::applyColorKey(const QImage &source, const QList<QColor> &keyColors, int tolerance)
|
||||
{
|
||||
QImage result = source.convertToFormat(QImage::Format_ARGB32);
|
||||
if (keyColors.isEmpty())
|
||||
return result;
|
||||
|
||||
QVector<QRgb> keys;
|
||||
keys.reserve(keyColors.size());
|
||||
for (const QColor &c : keyColors)
|
||||
keys.append(c.rgb());
|
||||
|
||||
const qint64 threshold = qint64(tolerance) * tolerance * 3 * 255 * 255 / (100 * 100);
|
||||
|
||||
for (int y = 0; y < result.height(); ++y)
|
||||
{
|
||||
QRgb *line = reinterpret_cast<QRgb *>(result.scanLine(y));
|
||||
for (int x = 0; x < result.width(); ++x)
|
||||
{
|
||||
const QRgb px = line[x];
|
||||
for (const QRgb &key : keys)
|
||||
{
|
||||
const int dr = qRed(px) - qRed(key), dg = qGreen(px) - qGreen(key), db = qBlue(px) - qBlue(key);
|
||||
const qint64 distSq = qint64(dr) * dr + qint64(dg) * dg + qint64(db) * db;
|
||||
if (distSq <= threshold)
|
||||
{
|
||||
line[x] = qRgba(qRed(px), qGreen(px), qBlue(px), 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ImageTransparentColorDialog::onCheckerboard
|
||||
Composites `image` over a light/dark checkerboard, the standard
|
||||
visual convention for showing where an image is transparent --
|
||||
without this, a fully keyed-out region would just show whatever
|
||||
widget background happens to be behind it, easy to misread as "still
|
||||
opaque, just white" rather than "correctly transparent".
|
||||
@param image the (possibly partially transparent) image to composite
|
||||
@return a checkerboard-backed pixmap ready to display
|
||||
*/
|
||||
QPixmap ImageTransparentColorDialog::onCheckerboard(const QImage &image)
|
||||
{
|
||||
const int cell = 8;
|
||||
QPixmap board(image.size());
|
||||
QPainter painter(&board);
|
||||
for (int y = 0; y < image.height(); y += cell)
|
||||
{
|
||||
for (int x = 0; x < image.width(); x += cell)
|
||||
{
|
||||
const bool dark = ((x / cell) + (y / cell)) % 2 == 0;
|
||||
painter.fillRect(x, y, cell, cell, dark ? QColor(200, 200, 200) : QColor(255, 255, 255));
|
||||
}
|
||||
}
|
||||
painter.drawImage(0, 0, image);
|
||||
painter.end();
|
||||
return board;
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
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 IMAGE_TRANSPARENT_COLOR_DIALOG_H
|
||||
#define IMAGE_TRANSPARENT_COLOR_DIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QImage>
|
||||
#include <QColor>
|
||||
#include <QLabel>
|
||||
#include <QList>
|
||||
|
||||
class QSlider;
|
||||
class QPushButton;
|
||||
class QHBoxLayout;
|
||||
class QMouseEvent;
|
||||
|
||||
/**
|
||||
@brief The ClickableImageLabel class
|
||||
Displays an image scaled to fit a reasonable dialog size, but always
|
||||
samples the picked color from the original, full-resolution image at
|
||||
the corresponding coordinate -- never from the (possibly smoothly
|
||||
interpolated, and therefore color-blended) scaled-down pixmap being
|
||||
displayed, which would make the picked color subtly wrong for
|
||||
exactly the pixels near an edge where it matters most.
|
||||
*/
|
||||
class ClickableImageLabel : public QLabel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ClickableImageLabel(const QImage &sourceImage, QWidget *parent = nullptr);
|
||||
|
||||
signals:
|
||||
void colorPicked(const QColor &color);
|
||||
|
||||
protected:
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
|
||||
private:
|
||||
QImage m_source;
|
||||
qreal m_displayScale = 1.0;
|
||||
};
|
||||
|
||||
/**
|
||||
@brief The ImageTransparentColorDialog class
|
||||
Lets the user click directly on a preview of the image to sample one
|
||||
or more colors -- each click adds to the set rather than replacing
|
||||
the previous pick, shown as a row of removable swatches -- adjust a
|
||||
shared tolerance, and see a live checkerboard-backed preview of the
|
||||
result before committing. A self-contained modal dialog rather than
|
||||
a diagram-level "click the canvas to pick" interaction mode, since
|
||||
this needs neither undo-during-drag nor coexistence with other
|
||||
tools; it only ever needs a handful of clicks, evaluated against a
|
||||
pixmap the caller already has in hand.
|
||||
*/
|
||||
class ImageTransparentColorDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/// @param basePixmap the pristine source to pick colours from --
|
||||
/// the caller's responsibility to pass the true original, not
|
||||
/// an already colour-keyed result, or previously-transparent
|
||||
/// areas would show as plain background rather than a pickable
|
||||
/// surface, and re-picking the same colour would be a no-op.
|
||||
/// @param existingColors colours already keyed out of basePixmap
|
||||
/// in a previous session, shown as swatches from the start
|
||||
/// rather than forcing them to be re-picked from scratch.
|
||||
/// @param existingTolerance the tolerance from that previous
|
||||
/// session, if any.
|
||||
explicit ImageTransparentColorDialog(const QPixmap &basePixmap, const QList<QColor> &existingColors = {},
|
||||
int existingTolerance = 10, QWidget *parent = nullptr);
|
||||
|
||||
/// The resulting pixmap: basePixmap unchanged if no colour is
|
||||
/// picked, colour-keyed against every picked colour otherwise.
|
||||
QPixmap resultPixmap() const;
|
||||
/// The final set of picked colours, for the caller to remember
|
||||
/// across dialog sessions -- may differ from existingColors if
|
||||
/// any were added or removed.
|
||||
QList<QColor> pickedColors() const { return m_pickedColors; }
|
||||
/// The final tolerance, for the same reason.
|
||||
int tolerance() const { return m_tolerance; }
|
||||
|
||||
/// Public so DiagramImageItem can re-derive its display pixmap
|
||||
/// directly (base + crop + these colours) without needing to
|
||||
/// re-open this dialog every time the crop region changes --
|
||||
/// binary transparency within tolerance, not a smooth falloff:
|
||||
/// every pixel within `tolerance` (0-100, mapped onto the
|
||||
/// maximum possible RGB distance) of *any* of keyColors becomes
|
||||
/// fully transparent, everything else keeps its existing alpha
|
||||
/// untouched. Squared distance throughout, avoiding a sqrt per
|
||||
/// pixel; breaks out of the inner loop on the first matching
|
||||
/// colour, since further matches wouldn't change the outcome.
|
||||
static QImage applyColorKey(const QImage &source, const QList<QColor> &keyColors, int tolerance);
|
||||
|
||||
private slots:
|
||||
void onColorPicked(const QColor &color);
|
||||
void onToleranceChanged(int value);
|
||||
|
||||
private:
|
||||
void removeColor(const QColor &color);
|
||||
void rebuildSwatches();
|
||||
void updatePreview();
|
||||
static QPixmap onCheckerboard(const QImage &image);
|
||||
|
||||
QImage m_sourceImage;
|
||||
QList<QColor> m_pickedColors;
|
||||
int m_tolerance = 10;
|
||||
|
||||
ClickableImageLabel *m_sourceLabel;
|
||||
QLabel *m_previewLabel;
|
||||
QHBoxLayout *m_swatchesLayout;
|
||||
QLabel *m_hintLabel;
|
||||
QSlider *m_toleranceSlider;
|
||||
QPushButton *m_okButton;
|
||||
};
|
||||
|
||||
#endif // IMAGE_TRANSPARENT_COLOR_DIALOG_H
|
||||
@@ -73,7 +73,8 @@ void ShapeGraphicsItemPropertiesWidget::setItem(QetShapeItem *shape)
|
||||
}
|
||||
|
||||
m_shape = shape;
|
||||
ui->m_close_polygon->setVisible(m_shape->shapeType() == QetShapeItem::Polygon);
|
||||
ui->m_close_polygon->setVisible(m_shape->shapeType() == QetShapeItem::Polygon
|
||||
|| m_shape->shapeType() == QetShapeItem::Path);
|
||||
ui->m_filling_gb->setHidden(m_shape->shapeType() == QetShapeItem::Line);
|
||||
|
||||
updateUi();
|
||||
@@ -164,6 +165,58 @@ QUndoCommand* ShapeGraphicsItemPropertiesWidget::associatedUndo() const
|
||||
{
|
||||
QPropertyUndoCommand *undo = nullptr;
|
||||
|
||||
//Geometry
|
||||
const bool hasGeometry = (m_shape->shapeType() == QetShapeItem::Line
|
||||
|| m_shape->shapeType() == QetShapeItem::Rectangle
|
||||
|| m_shape->shapeType() == QetShapeItem::Ellipse);
|
||||
if (hasGeometry)
|
||||
{
|
||||
if (m_shape->shapeType() == QetShapeItem::Line)
|
||||
{
|
||||
const QLineF old_line = m_shape->line();
|
||||
QLineF new_line = old_line;
|
||||
new_line.setLength(ui->m_geom_dim1_dsb->value());
|
||||
|
||||
if (new_line != old_line)
|
||||
{
|
||||
undo = new QPropertyUndoCommand(m_shape, "line", old_line, new_line);
|
||||
undo->setText(tr("Modifier la longueur d'une ligne"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const QRectF old_rect = m_shape->rect().normalized();
|
||||
const bool isEllipse = (m_shape->shapeType() == QetShapeItem::Ellipse);
|
||||
const qreal new_width = isEllipse ? ui->m_geom_dim1_dsb->value() * 2.0 : ui->m_geom_dim1_dsb->value();
|
||||
const qreal new_height = isEllipse ? ui->m_geom_dim2_dsb->value() * 2.0 : ui->m_geom_dim2_dsb->value();
|
||||
// Anchored on the shape's own current top-left, not
|
||||
// re-centered -- matches how the resize handle itself
|
||||
// behaves (the opposite corner stays put while the
|
||||
// dragged one moves), so typing a width/height here
|
||||
// and dragging a handle agree on what "resizing" means.
|
||||
const QRectF new_rect(old_rect.topLeft(), QSizeF(new_width, new_height));
|
||||
|
||||
if (new_rect != old_rect)
|
||||
{
|
||||
undo = new QPropertyUndoCommand(m_shape, "rect", old_rect, new_rect);
|
||||
undo->setText(tr("Modifier la taille d'une forme"));
|
||||
}
|
||||
}
|
||||
|
||||
const qreal old_angle = m_shape->rotation();
|
||||
const qreal new_angle = ui->m_geom_angle_dsb->value();
|
||||
if (qAbs(old_angle - new_angle) > 0.05)
|
||||
{
|
||||
if (undo)
|
||||
new QPropertyUndoCommand(m_shape, "rotation", old_angle, new_angle, undo);
|
||||
else
|
||||
{
|
||||
undo = new QPropertyUndoCommand(m_shape, "rotation", old_angle, new_angle);
|
||||
undo->setText(tr("Modifier l'angle d'une forme"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QPen old_pen = m_shape->pen();
|
||||
QPen new_pen = old_pen;
|
||||
|
||||
@@ -366,6 +419,35 @@ void ShapeGraphicsItemPropertiesWidget::updateUi()
|
||||
|
||||
if (m_shape)
|
||||
{
|
||||
//Geometry
|
||||
const bool hasGeometry = (m_shape->shapeType() == QetShapeItem::Line
|
||||
|| m_shape->shapeType() == QetShapeItem::Rectangle
|
||||
|| m_shape->shapeType() == QetShapeItem::Ellipse);
|
||||
ui->m_geometry_gb->setVisible(hasGeometry);
|
||||
if (hasGeometry)
|
||||
{
|
||||
ui->m_geom_angle_dsb->setValue(m_shape->rotation());
|
||||
|
||||
if (m_shape->shapeType() == QetShapeItem::Line)
|
||||
{
|
||||
ui->m_geom_dim1_label->setText(tr("Longueur"));
|
||||
ui->m_geom_dim1_dsb->setValue(m_shape->line().length());
|
||||
ui->m_geom_dim2_label->setVisible(false);
|
||||
ui->m_geom_dim2_dsb->setVisible(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
const QRectF r = m_shape->rect().normalized();
|
||||
const bool isEllipse = (m_shape->shapeType() == QetShapeItem::Ellipse);
|
||||
ui->m_geom_dim1_label->setText(isEllipse ? tr("Rayon X") : tr("Largeur"));
|
||||
ui->m_geom_dim2_label->setText(isEllipse ? tr("Rayon Y") : tr("Hauteur"));
|
||||
ui->m_geom_dim1_dsb->setValue(isEllipse ? r.width() / 2.0 : r.width());
|
||||
ui->m_geom_dim2_dsb->setValue(isEllipse ? r.height() / 2.0 : r.height());
|
||||
ui->m_geom_dim2_label->setVisible(true);
|
||||
ui->m_geom_dim2_dsb->setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
//Pen
|
||||
ui->m_style_cb->setCurrentIndex(static_cast<int>(m_shape->pen().style()) - 1);
|
||||
ui->m_size_dsb ->setValue(m_shape->pen().widthF());
|
||||
@@ -373,7 +455,7 @@ void ShapeGraphicsItemPropertiesWidget::updateUi()
|
||||
ui->m_color_kpb->setColor(m_shape->pen().color());
|
||||
|
||||
//Brush
|
||||
if (m_shape->shapeType() == QetShapeItem::Polygon)
|
||||
if (m_shape->shapeType() == QetShapeItem::Polygon || m_shape->shapeType() == QetShapeItem::Path)
|
||||
ui->m_filling_gb->setVisible(m_shape->isClosed());
|
||||
|
||||
ui->m_brush_style_cb->setCurrentIndex(static_cast<int>(m_shape->brush().style()));
|
||||
@@ -384,6 +466,7 @@ void ShapeGraphicsItemPropertiesWidget::updateUi()
|
||||
}
|
||||
else if (m_shapes_list.size() >= 2)
|
||||
{
|
||||
ui->m_geometry_gb->setVisible(false);
|
||||
ui->m_close_polygon->setHidden(true);
|
||||
bool same = true;
|
||||
//Pen
|
||||
@@ -477,6 +560,15 @@ void ShapeGraphicsItemPropertiesWidget::setUpEditConnection()
|
||||
|
||||
if (m_shape || !m_shapes_list.isEmpty())
|
||||
{
|
||||
m_edit_connection << connect (ui->m_geom_dim1_dsb, QOverload<double>::of(&QDoubleSpinBox::valueChanged),
|
||||
this, &ShapeGraphicsItemPropertiesWidget::apply);
|
||||
|
||||
m_edit_connection << connect (ui->m_geom_dim2_dsb, QOverload<double>::of(&QDoubleSpinBox::valueChanged),
|
||||
this, &ShapeGraphicsItemPropertiesWidget::apply);
|
||||
|
||||
m_edit_connection << connect (ui->m_geom_angle_dsb, QOverload<double>::of(&QDoubleSpinBox::valueChanged),
|
||||
this, &ShapeGraphicsItemPropertiesWidget::apply);
|
||||
|
||||
m_edit_connection << connect (ui->m_style_cb, QOverload<int>::of(&QComboBox::activated),
|
||||
this, &ShapeGraphicsItemPropertiesWidget::apply);
|
||||
|
||||
@@ -495,14 +587,23 @@ void ShapeGraphicsItemPropertiesWidget::setUpEditConnection()
|
||||
m_edit_connection << connect (ui->m_close_polygon, &QCheckBox::clicked,
|
||||
this, &ShapeGraphicsItemPropertiesWidget::apply);
|
||||
|
||||
m_edit_connection << connect (m_shape, &QetShapeItem::penChanged,
|
||||
this, &ShapeGraphicsItemPropertiesWidget::updateUi);
|
||||
if (m_shape)
|
||||
{
|
||||
m_edit_connection << connect (m_shape, &QetShapeItem::penChanged,
|
||||
this, &ShapeGraphicsItemPropertiesWidget::updateUi);
|
||||
|
||||
m_edit_connection << connect (m_shape, &QetShapeItem::closeChanged,
|
||||
this, &ShapeGraphicsItemPropertiesWidget::updateUi);
|
||||
m_edit_connection << connect (m_shape, &QetShapeItem::closeChanged,
|
||||
this, &ShapeGraphicsItemPropertiesWidget::updateUi);
|
||||
|
||||
m_edit_connection << connect (m_shape, &QetShapeItem::brushChanged,
|
||||
this, &ShapeGraphicsItemPropertiesWidget::updateUi);
|
||||
m_edit_connection << connect (m_shape, &QetShapeItem::brushChanged,
|
||||
this, &ShapeGraphicsItemPropertiesWidget::updateUi);
|
||||
|
||||
m_edit_connection << connect (m_shape, &QetShapeItem::geometryChanged,
|
||||
this, &ShapeGraphicsItemPropertiesWidget::updateUi);
|
||||
|
||||
m_edit_connection << connect (m_shape, &QetShapeItem::transformChanged,
|
||||
this, &ShapeGraphicsItemPropertiesWidget::updateUi);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,78 @@
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="m_geometry_gb">
|
||||
<property name="title">
|
||||
<string>Géométrie</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="m_geom_dim1_label">
|
||||
<property name="text">
|
||||
<string>Largeur</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QDoubleSpinBox" name="m_geom_dim1_dsb">
|
||||
<property name="decimals">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>0.010000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>100000.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="m_geom_dim2_label">
|
||||
<property name="text">
|
||||
<string>Hauteur</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QDoubleSpinBox" name="m_geom_dim2_dsb">
|
||||
<property name="decimals">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>0.010000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>100000.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="m_geom_angle_label">
|
||||
<property name="text">
|
||||
<string>Angle</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QDoubleSpinBox" name="m_geom_angle_dsb">
|
||||
<property name="decimals">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-360.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>360.000000000000000</double>
|
||||
</property>
|
||||
<property name="suffix">
|
||||
<string> °</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
@@ -230,7 +302,7 @@
|
||||
<item>
|
||||
<widget class="QCheckBox" name="m_close_polygon">
|
||||
<property name="text">
|
||||
<string>Polygone fermé</string>
|
||||
<string>Forme fermée</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -257,6 +329,9 @@
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<tabstops>
|
||||
<tabstop>m_geom_dim1_dsb</tabstop>
|
||||
<tabstop>m_geom_dim2_dsb</tabstop>
|
||||
<tabstop>m_geom_angle_dsb</tabstop>
|
||||
<tabstop>m_style_cb</tabstop>
|
||||
<tabstop>m_size_dsb</tabstop>
|
||||
<tabstop>m_color_kpb</tabstop>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
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 "promoteshapecommand.h"
|
||||
#include "../qetgraphicsitem/qetshapeitem.h"
|
||||
|
||||
PromoteShapeCommand::PromoteShapeCommand(
|
||||
QetShapeItem *shape,
|
||||
const QDomElement &priorStateXml,
|
||||
const QDomElement &newStateXml,
|
||||
QUndoCommand *parent) :
|
||||
QUndoCommand(QObject::tr("Transformer %1").arg(shape ? shape->name() : QString()), parent),
|
||||
m_shape(shape)
|
||||
{
|
||||
m_priorDoc.appendChild(m_priorDoc.importNode(priorStateXml, true));
|
||||
m_newDoc.appendChild(m_newDoc.importNode(newStateXml, true));
|
||||
}
|
||||
|
||||
void PromoteShapeCommand::undo()
|
||||
{
|
||||
if (m_shape)
|
||||
m_shape->fromXml(m_priorDoc.documentElement());
|
||||
}
|
||||
|
||||
void PromoteShapeCommand::redo()
|
||||
{
|
||||
if (m_shape)
|
||||
m_shape->fromXml(m_newDoc.documentElement());
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
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 PROMOTESHAPECOMMAND_H
|
||||
#define PROMOTESHAPECOMMAND_H
|
||||
|
||||
#include <QUndoCommand>
|
||||
#include <QPointer>
|
||||
#include <QDomElement>
|
||||
|
||||
class QetShapeItem;
|
||||
|
||||
/**
|
||||
@brief The PromoteShapeCommand class
|
||||
Used whenever a shape's *identity* changes -- a Rectangle's corner is
|
||||
dragged independently and it becomes a Polygon, an Arc is explicitly
|
||||
converted to a free-form Path, etc.
|
||||
|
||||
The saved .qet file never records what a shape used to be; that fact
|
||||
only needs to survive as long as the rest of the edit history does, so
|
||||
it lives entirely here, as a full XML snapshot on each side. Undo
|
||||
restores the exact prior shape -- type, geometry and transform alike --
|
||||
by round-tripping through the same fromXml() every other load uses.
|
||||
Once the file is saved and reopened, the promotion is permanent, same
|
||||
as any other undo history.
|
||||
*/
|
||||
class PromoteShapeCommand : public QUndoCommand
|
||||
{
|
||||
public:
|
||||
PromoteShapeCommand(
|
||||
QetShapeItem *shape,
|
||||
const QDomElement &priorStateXml,
|
||||
const QDomElement &newStateXml,
|
||||
QUndoCommand *parent = nullptr);
|
||||
|
||||
void undo() override;
|
||||
void redo() override;
|
||||
|
||||
private:
|
||||
QPointer<QetShapeItem> m_shape;
|
||||
// Deep-cloned into private documents at construction time: a
|
||||
// QDomElement is only a reference into whatever QDomDocument it
|
||||
// came from, and that document (typically a short-lived one built
|
||||
// just to call toXml()) is not guaranteed to outlive this command.
|
||||
QDomDocument m_priorDoc, m_newDoc;
|
||||
};
|
||||
|
||||
#endif // PROMOTESHAPECOMMAND_H
|
||||
Reference in New Issue
Block a user