Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1793dcb9a8 | |||
| cc7aaf3538 | |||
| a6285c7ef7 | |||
| f87381a910 | |||
| bd5cff4211 | |||
| 296f142036 | |||
| 4d991d0419 | |||
| a85bd6a49b | |||
| ef8e41a5c3 | |||
| 6f80da66d5 | |||
| a065867502 | |||
| b98d41357a | |||
| b2f4ef5d25 | |||
| bd8470afa8 | |||
| 7c5f8c6a4b | |||
| 6b27043f33 | |||
| ed5a510592 | |||
| 3abf187688 | |||
| 825a1a594d | |||
| 38e1b3fe49 | |||
| fd3e213222 | |||
| ef4e186e42 | |||
| 975ab31cc1 | |||
| 2699a0aec8 | |||
| 76ff5c4dd9 | |||
| b395527915 | |||
| 7741bb33b2 | |||
| f02b576cb8 | |||
| e0ef24fe68 | |||
| 70c665199f | |||
| 6b577ee757 | |||
| 74e878207a | |||
| 1e1039dd32 | |||
| e410896f5d | |||
| 10bf0411bb | |||
| e3cb994e0e | |||
| 0d08a4e265 | |||
| ae6b992ae4 | |||
| b72794bea1 | |||
| 11357eb504 | |||
| 85346305e5 |
@@ -3,6 +3,8 @@
|
||||
"runtime": "org.kde.Platform",
|
||||
"runtime-version": "6.11",
|
||||
"sdk": "org.kde.Sdk",
|
||||
"base": "io.qt.qtwebengine.BaseApp",
|
||||
"base-version": "6.11",
|
||||
"command": "qelectrotech",
|
||||
"rename-desktop-file": "org.qelectrotech.qelectrotech.desktop",
|
||||
"rename-appdata-file": "qelectrotech.appdata.xml",
|
||||
@@ -25,13 +27,14 @@
|
||||
"*.la",
|
||||
"*.a"
|
||||
],
|
||||
"cleanup-commands": [
|
||||
"/app/cleanup-BaseApp.sh"
|
||||
],
|
||||
"modules": [
|
||||
"tkinter.json",
|
||||
"pypi-dependencies.json",
|
||||
{
|
||||
"name": "qelectrotech",
|
||||
"//qt6-private-headers-note": "qt6-base-private-dev has no Flatpak build-depends equivalent — private Qt6 headers ship inside org.kde.Sdk itself. If the cmake build fails looking for QtCore/private/*.h, the SDK/runtime branch is mismatched with the source tree, not a missing package.",
|
||||
"//sqlite-driver-note": "libqt6sql6-sqlite / libsqlite3-dev have no Flatpak build-depends equivalent either. QET_EXPORT_PROJECT_DB=ON assumes the KDE runtime ships the Qt6 SQLite plugin (libqsqlite.so) already built into QtSql — verified at Debian packaging time this needed an explicit runtime dep there (see debian/control). Test the in-app database export after building; if it silently fails, the runtime's QtSql plugin set needs checking, not a package to add here.",
|
||||
"buildsystem": "cmake",
|
||||
"config-opts": [
|
||||
"-DCMAKE_INSTALL_PREFIX=/app",
|
||||
|
||||
@@ -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 |
@@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#232629;
|
||||
}
|
||||
</style>
|
||||
<g transform="translate(1,1)">
|
||||
<g class="ColorScheme-Text" fill="currentColor">
|
||||
<path d="m6 14c0 2.77 2.23 5 5 5s5-2.23 5-5z" fill-rule="evenodd" opacity=".66"/>
|
||||
<path d="m7.482 10c-.34.586-.65 1.148-.886 1.676a4.958 4.958 0 0 0 -.596 2.324h10c0-.845-.228-1.627-.596-2.324a18.518 18.518 0 0 0 -.886-1.676z" fill-rule="evenodd" opacity=".33"/>
|
||||
<path d="m11 3c-.973 3.405-3.354 6.332-4.404 8.676a4.958 4.958 0 0 0 -.596 2.324c0 2.77 2.23 5 5 5s5-2.23 5-5c0-.845-.228-1.627-.596-2.324-1.05-2.344-3.431-5.271-4.404-8.676zm0 2.393c1.194 2.668 2.747 5.027 3.492 6.691l.012.03.016.029a3.94 3.94 0 0 1 .48 1.857c0 2.233-1.767 4-4 4a3.97 3.97 0 0 1 -4-4c0-.665.177-1.283.48-1.857l.016-.03.012-.029c.745-1.664 2.298-4.023 3.492-6.691z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 983 B |
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#232629;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor;fill-opacity:1;stroke:none"
|
||||
d="M 4 2 L 4 3 L 5 3 L 5 2 L 4 2 z M 11 2 L 11 3 L 12 3 L 12 2 L 11 2 z M 2 4 L 2 12 L 7 12 L 7 10 L 9 10 L 9 12 L 14 12 L 14 4 L 9 4 L 9 6 L 7 6 L 7 4 L 2 4 z M 3 5 L 6 5 L 6 11 L 3 11 L 3 5 z M 10 5 L 13 5 L 13 11 L 10 11 L 10 5 z M 11 6 L 11 10 L 12 10 L 12 6 L 11 6 z M 7 7 L 9 7 L 9 9 L 7 9 L 7 7 z M 4 13 L 4 14 L 5 14 L 5 13 L 4 13 z M 11 13 L 11 14 L 12 14 L 12 13 L 11 13 z "
|
||||
class="ColorScheme-Text"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 716 B |
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#232629;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:currentColor;fill-opacity:1;stroke:none"
|
||||
d="M 4 2 L 4 7 L 6 7 L 6 9 L 4 9 L 4 14 L 12 14 L 12 9 L 10 9 L 10 7 L 12 7 L 12 2 L 4 2 z M 5 3 L 11 3 L 11 6 L 5 6 L 5 3 z M 2 4 L 2 5 L 3 5 L 3 4 L 2 4 z M 13 4 L 13 5 L 14 5 L 14 4 L 13 4 z M 7 7 L 9 7 L 9 9 L 7 9 L 7 7 z M 5 10 L 11 10 L 11 13 L 5 13 L 5 10 z M 2 11 L 2 12 L 3 12 L 3 11 L 2 11 z M 6 11 L 6 12 L 10 12 L 10 11 L 6 11 z M 13 11 L 13 12 L 14 12 L 14 11 L 13 11 z "
|
||||
class="ColorScheme-Text"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 716 B |
@@ -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="M 3 3 L 3 5 L 3 6 L 6 6 L 6 5 L 6 3 L 4 3 L 3 3 z M 16 3 L 16 4 L 16 5 L 16 6 L 17 6 L 18 6 L 19 6 L 19 5 L 19 4 L 19 3 L 16 3 z M 4 4 L 5 4 L 5 5 L 4 5 L 4 4 z M 17 4 L 18 4 L 18 5 L 17 5 L 17 4 z M 7 5 L 7 6 L 9 6 L 9 5 L 7 5 z M 10 5 L 10 6 L 12 6 L 12 5 L 10 5 z M 13 5 L 13 6 L 15 6 L 15 5 L 13 5 z M 5 7 L 5 9 L 6 9 L 6 7 L 5 7 z M 16 7 L 16 9 L 17 9 L 17 7 L 16 7 z M 5 10 L 5 12 L 6 12 L 6 10 L 5 10 z M 16 10 L 16 12 L 17 12 L 17 10 L 16 10 z M 5 13 L 5 15 L 6 15 L 6 13 L 5 13 z M 16 13 L 16 15 L 17 15 L 17 13 L 16 13 z M 3 16 L 3 17 L 3 19 L 4 19 L 5 19 L 6 19 L 6 16 L 3 16 z M 7 16 L 7 17 L 9 17 L 9 16 L 7 16 z M 10 16 L 10 17 L 12 17 L 12 16 L 10 16 z M 13 16 L 13 17 L 15 17 L 15 16 L 13 16 z M 16 16 L 16 17 L 16 18 L 16 19 L 17 19 L 18 19 L 19 19 L 19 18 L 19 17 L 19 16 L 17 16 L 16 16 z M 4 17 L 5 17 L 5 18 L 4 18 L 4 17 z M 17 17 L 18 17 L 18 18 L 17 18 L 17 17 z " class="ColorScheme-Text"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
@@ -548,6 +548,14 @@
|
||||
<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/breeze-icons/scalable/apps/hidef/transform-crop.svg</file>
|
||||
<file>ico/breeze-icons/scalable/apps/hidef/image-flip-horizontal-symbolic.svg</file>
|
||||
<file>ico/breeze-icons/scalable/apps/hidef/image-flip-vertical-symbolic.svg</file>
|
||||
<file>ico/breeze-icons/scalable/apps/hidef/edit-opacity.svg</file>
|
||||
<file>ico/generated/rect-to-polyline.svg</file>
|
||||
<file>ico/generated/rect-to-bezier.svg</file>
|
||||
<file>ico/generated/ellipse-to-bezier.svg</file>
|
||||
<file>ico/16x16/object-group.png</file>
|
||||
<file>ico/mac_icon/elmt.icns</file>
|
||||
<file>ico/mac_icon/qelectrotech.icns</file>
|
||||
|
||||
@@ -72,6 +72,7 @@ QPropertyUndoCommand::QPropertyUndoCommand(const QPropertyUndoCommand *other)
|
||||
m_new_value = other->m_new_value;
|
||||
m_animate = other->m_animate;
|
||||
m_first_time = other->m_first_time;
|
||||
m_undo_first_time = other->m_undo_first_time;
|
||||
setText(other->text());
|
||||
}
|
||||
|
||||
@@ -99,11 +100,20 @@ void QPropertyUndoCommand::enableAnimation (bool animate) {
|
||||
@param first_time = if true,
|
||||
the first animation is done at the first call of redo if false,
|
||||
the first animation is done at the second call of redo.
|
||||
The same rule applies to undo, tracked independently: redo() always
|
||||
runs before the first undo() (QUndoStack::push() calls redo()
|
||||
immediately), so by the time undo() can run at all, redo()'s own
|
||||
m_first_time has already flipped true. Sharing that flag would leave
|
||||
undo() with no instant path ever reachable in practice -- reusing it
|
||||
is not actually symmetric, it just looks like it is. m_undo_first_time
|
||||
gives undo() the same one-time grace period redo() has, on its own
|
||||
first call instead of redo's.
|
||||
*/
|
||||
void QPropertyUndoCommand::setAnimated(bool animate, bool first_time)
|
||||
{
|
||||
m_animate = animate;
|
||||
m_first_time = first_time;
|
||||
m_undo_first_time = first_time;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,6 +128,18 @@ bool QPropertyUndoCommand::mergeWith(const QUndoCommand *other)
|
||||
QPropertyUndoCommand const *undo = static_cast<const QPropertyUndoCommand *>(other);
|
||||
if (m_object != undo->m_object
|
||||
|| m_property_name != undo->m_property_name) return false;
|
||||
// Same object and property name alone isn't enough: two entirely
|
||||
// separate, deliberate actions (say, cropping an image and then
|
||||
// mirroring it) both go through the same "pixmap" property and
|
||||
// would otherwise silently coalesce into one undo entry, carrying
|
||||
// only the first action's label -- the second vanishes from the
|
||||
// undo list with no way to undo just it. Legitimate merging (a
|
||||
// slider or spinbox pushing one command per tick while being
|
||||
// dragged, e.g. ArcEditor's angle editors) always reuses the exact
|
||||
// same text() across the whole sequence, so requiring a match here
|
||||
// keeps that working unchanged while refusing to merge anything
|
||||
// that isn't actually a continuation of the same action.
|
||||
if (text() != other->text()) return false;
|
||||
m_new_value = undo->m_new_value;
|
||||
return true;
|
||||
}
|
||||
@@ -155,7 +177,7 @@ void QPropertyUndoCommand::undo()
|
||||
{
|
||||
if (m_object->property(m_property_name) != m_old_value)
|
||||
{
|
||||
if (m_animate)
|
||||
if (m_animate && m_undo_first_time)
|
||||
{
|
||||
QPropertyAnimation *animation = new QPropertyAnimation(m_object, m_property_name);
|
||||
animation->setStartValue(m_new_value);
|
||||
@@ -163,7 +185,10 @@ void QPropertyUndoCommand::undo()
|
||||
animation->start(QAbstractAnimation::DeleteWhenStopped);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_object->setProperty(m_property_name, m_old_value);
|
||||
m_undo_first_time = true;
|
||||
}
|
||||
}
|
||||
|
||||
QUndoCommand::undo();
|
||||
|
||||
@@ -58,7 +58,8 @@ class QPropertyUndoCommand : public QUndoCommand
|
||||
const char *m_property_name;
|
||||
QVariant m_old_value, m_new_value;
|
||||
bool m_animate = false,
|
||||
m_first_time = true;
|
||||
m_first_time = true,
|
||||
m_undo_first_time = true;
|
||||
};
|
||||
|
||||
#endif // QPROPERTYUNDOCOMMAND_H
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -225,12 +225,57 @@ void projectDataBase::addDiagram(Diagram *diagram)
|
||||
|
||||
void projectDataBase::removeDiagram(Diagram *diagram)
|
||||
{
|
||||
m_remove_diagram_query.bindValue(":uuid", diagram->uuid().toString());
|
||||
const QString uuid_str = diagram->uuid().toString();
|
||||
|
||||
//Order matters: element_info and terminal are scoped through a
|
||||
//subquery on element, so they must run before element itself is
|
||||
//deleted below. The whole cascade runs in one transaction and is
|
||||
//rolled back on the first error, so a mid-cascade failure (e.g. a
|
||||
//locked DB) can't leave the diagram row deleted while its
|
||||
//element/terminal/element_info/conductor rows survive.
|
||||
m_data_base.transaction();
|
||||
|
||||
m_cascade_remove_element_info_query.bindValue(":uuid", uuid_str);
|
||||
if (!m_cascade_remove_element_info_query.exec()) {
|
||||
qDebug() << "projectDataBase::removeDiagram element_info cascade error : "
|
||||
<< m_cascade_remove_element_info_query.lastError();
|
||||
m_data_base.rollback();
|
||||
return;
|
||||
}
|
||||
|
||||
m_cascade_remove_terminal_query.bindValue(":uuid", uuid_str);
|
||||
if (!m_cascade_remove_terminal_query.exec()) {
|
||||
qDebug() << "projectDataBase::removeDiagram terminal cascade error : "
|
||||
<< m_cascade_remove_terminal_query.lastError();
|
||||
m_data_base.rollback();
|
||||
return;
|
||||
}
|
||||
|
||||
m_cascade_remove_conductor_query.bindValue(":uuid", uuid_str);
|
||||
if (!m_cascade_remove_conductor_query.exec()) {
|
||||
qDebug() << "projectDataBase::removeDiagram conductor cascade error : "
|
||||
<< m_cascade_remove_conductor_query.lastError();
|
||||
m_data_base.rollback();
|
||||
return;
|
||||
}
|
||||
|
||||
m_cascade_remove_element_query.bindValue(":uuid", uuid_str);
|
||||
if (!m_cascade_remove_element_query.exec()) {
|
||||
qDebug() << "projectDataBase::removeDiagram element cascade error : "
|
||||
<< m_cascade_remove_element_query.lastError();
|
||||
m_data_base.rollback();
|
||||
return;
|
||||
}
|
||||
|
||||
m_remove_diagram_query.bindValue(":uuid", uuid_str);
|
||||
if (!m_remove_diagram_query.exec()) {
|
||||
qDebug() << "projectDataBase::removeDiagram delete error : " << m_remove_diagram_query.lastError();
|
||||
} else {
|
||||
emit dataBaseUpdated();
|
||||
m_data_base.rollback();
|
||||
return;
|
||||
}
|
||||
|
||||
m_data_base.commit();
|
||||
emit dataBaseUpdated();
|
||||
}
|
||||
|
||||
void projectDataBase::diagramInfoChanged(Diagram *diagram)
|
||||
@@ -772,7 +817,27 @@ void projectDataBase::prepareQuery()
|
||||
m_insert_diagram_query = QSqlQuery(m_data_base);
|
||||
m_insert_diagram_query.prepare("INSERT INTO diagram (uuid, pos) VALUES (:uuid, :pos)");
|
||||
|
||||
//REMOVE DIAGRAM
|
||||
//REMOVE DIAGRAM (cascade first: element_info and terminal have no
|
||||
//diagram_uuid column of their own, so both are scoped through
|
||||
//element while the element rows for this diagram still exist).
|
||||
m_cascade_remove_element_info_query = QSqlQuery(m_data_base);
|
||||
m_cascade_remove_element_info_query.prepare(
|
||||
"DELETE FROM element_info WHERE element_uuid IN "
|
||||
"(SELECT uuid FROM element WHERE diagram_uuid = :uuid)");
|
||||
|
||||
m_cascade_remove_terminal_query = QSqlQuery(m_data_base);
|
||||
m_cascade_remove_terminal_query.prepare(
|
||||
"DELETE FROM terminal WHERE element_uuid IN "
|
||||
"(SELECT uuid FROM element WHERE diagram_uuid = :uuid)");
|
||||
|
||||
m_cascade_remove_conductor_query = QSqlQuery(m_data_base);
|
||||
m_cascade_remove_conductor_query.prepare(
|
||||
"DELETE FROM conductor WHERE diagram_uuid = :uuid");
|
||||
|
||||
m_cascade_remove_element_query = QSqlQuery(m_data_base);
|
||||
m_cascade_remove_element_query.prepare(
|
||||
"DELETE FROM element WHERE diagram_uuid = :uuid");
|
||||
|
||||
m_remove_diagram_query = QSqlQuery(m_data_base);
|
||||
m_remove_diagram_query.prepare("DELETE FROM diagram WHERE uuid=:uuid");
|
||||
|
||||
|
||||
@@ -106,7 +106,11 @@ class projectDataBase : public QObject
|
||||
m_insert_terminal_query,
|
||||
m_insert_conductor_query,
|
||||
m_update_conductor_query,
|
||||
m_remove_conductor_query;
|
||||
m_remove_conductor_query,
|
||||
m_cascade_remove_element_info_query,
|
||||
m_cascade_remove_terminal_query,
|
||||
m_cascade_remove_conductor_query,
|
||||
m_cascade_remove_element_query;
|
||||
|
||||
#ifdef QET_EXPORT_PROJECT_DB
|
||||
public:
|
||||
|
||||
@@ -40,9 +40,53 @@
|
||||
#include "undocommand/addelementtextcommand.h"
|
||||
#include "qetinformation.h"
|
||||
#include "qetproject.h"
|
||||
#include "diagramsortkeys.h"
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <math.h>
|
||||
|
||||
namespace {
|
||||
using DiagramSortKeys::positionKey;
|
||||
|
||||
/// Sort key for Diagram::toXml()'s <elements> block: the element's own
|
||||
/// diagram-local position, exactly what it's already saved as (x/y),
|
||||
/// never invented or regenerated. uuid() is deliberately NOT used here:
|
||||
/// for an element with no persisted uuid attribute, fromXml() invents a
|
||||
/// fresh random one on every load, so sorting by uuid would still be
|
||||
/// non-deterministic across process runs for any legacy file.
|
||||
QString elementSortKey(Element *elmt)
|
||||
{
|
||||
return positionKey(elmt->pos());
|
||||
}
|
||||
|
||||
/// Sort key for a terminal: its parent element's position, then the
|
||||
/// terminal's own position local to that element (from the .elmt
|
||||
/// definition, fixed regardless of where the element is placed).
|
||||
QString terminalSortKey(Terminal *terminal)
|
||||
{
|
||||
if (!terminal)
|
||||
return QString();
|
||||
Element *parent = terminal->parentElement();
|
||||
return (parent ? positionKey(parent->pos()) : QStringLiteral("?"))
|
||||
+ QLatin1Char(':') + positionKey(terminal->pos());
|
||||
}
|
||||
|
||||
/// Sort key for Diagram::toXml()'s <conductors> block. Built from both
|
||||
/// endpoints' terminalSortKey(), not Conductor::uuid(): in every example
|
||||
/// project checked, conductors have no persisted uuid attribute at all,
|
||||
/// so uuid() is a freshly-minted random value on every load -- exactly
|
||||
/// as unusable for cross-run determinism as the element case above, just
|
||||
/// with no persisted fallback to reach for instead. Canonicalised
|
||||
/// (smaller key first) since a conductor's two ends are unordered for
|
||||
/// this purpose.
|
||||
QString conductorSortKey(Conductor *cond)
|
||||
{
|
||||
QString a = terminalSortKey(cond->terminal1);
|
||||
QString b = terminalSortKey(cond->terminal2);
|
||||
return (a <= b) ? (a + QLatin1Char('>') + b) : (b + QLatin1Char('>') + a);
|
||||
}
|
||||
}
|
||||
|
||||
int Diagram::xGrid = 10;
|
||||
int Diagram::yGrid = 10;
|
||||
int Diagram::xKeyGrid = 10;
|
||||
@@ -1014,6 +1058,20 @@ QDomDocument Diagram::toXml(bool whole_content, bool is_copy_command) {
|
||||
}
|
||||
}
|
||||
|
||||
// items() returns items in stacking order, which is not guaranteed
|
||||
// reproducible across processes (ties between same-Z items follow
|
||||
// the scene's internal index, not any content-derived order) -- so
|
||||
// without this, saving an unmodified project produces a different
|
||||
// byte stream on every run. Elements and conductors are the two
|
||||
// blocks observed to actually churn across the example corpus;
|
||||
// sort them into a deterministic, content-derived order before
|
||||
// serializing. This also fixes the legacy terminal-id churn below,
|
||||
// since those ids are assigned sequentially in element order.
|
||||
std::stable_sort(list_elements.begin(), list_elements.end(),
|
||||
[](Element *a, Element *b) { return elementSortKey(a) < elementSortKey(b); });
|
||||
std::stable_sort(list_conductors.begin(), list_conductors.end(),
|
||||
[](Conductor *a, Conductor *b) { return conductorSortKey(a) < conductorSortKey(b); });
|
||||
|
||||
// correspondence table between the addresses of the terminals and their ids
|
||||
// table de correspondance entre les adresses des bornes et leurs ids
|
||||
QHash<Terminal *, int> table_adr_id;
|
||||
|
||||
@@ -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,26 +129,95 @@ 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)
|
||||
{
|
||||
for (QGraphicsView *view : m_diagram->views()) {
|
||||
view->setContextMenuPolicy((Qt::NoContextMenu));
|
||||
}
|
||||
|
||||
|
||||
m_diagram->addItem(m_image);
|
||||
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
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
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 DIAGRAMSORTKEYS_H
|
||||
#define DIAGRAMSORTKEYS_H
|
||||
|
||||
#include <QPointF>
|
||||
#include <QString>
|
||||
#include <QtGlobal>
|
||||
|
||||
/// Sort-key helpers used by Diagram::toXml() to serialize elements and
|
||||
/// conductors in a deterministic order. Pulled into their own header
|
||||
/// (instead of an anonymous namespace in diagram.cpp) so they can be
|
||||
/// unit-tested directly -- see tests/qttest/tst_diagramsortkeys.cpp.
|
||||
namespace DiagramSortKeys {
|
||||
|
||||
/// Format a coordinate as a string that sorts the same way the number
|
||||
/// does. Plain fixed-precision formatting ("%.4f") does NOT do this --
|
||||
/// e.g. "15.0000" sorts before "5.0000" as text even though 15 > 5 --
|
||||
/// so shift into a non-negative range and zero-pad to a fixed width
|
||||
/// before comparing.
|
||||
inline QString coordinateKey(double v)
|
||||
{
|
||||
//Diagram coordinates are nowhere near this range; the offset and
|
||||
//width just need to be big enough that shifted values are always
|
||||
//non-negative and always the same digit count.
|
||||
constexpr double offset = 1e9;
|
||||
qint64 scaled = qint64(qRound64((v + offset) * 10000.0));
|
||||
return QStringLiteral("%1").arg(scaled, 20, 10, QLatin1Char('0'));
|
||||
}
|
||||
|
||||
inline QString positionKey(const QPointF &pos)
|
||||
{
|
||||
return coordinateKey(pos.x()) + QLatin1Char('|') + coordinateKey(pos.y());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -200,18 +200,35 @@ void DxfPaintEngine::drawTextItem(const QPointF &p, const QTextItem &textItem)
|
||||
|
||||
/**
|
||||
@brief DxfPaintEngine::drawPixmap
|
||||
Not implemented in v1 - out of scope per the design note in the
|
||||
header (CrossRefItem, the only item exported through this engine so
|
||||
far, never draws a pixmap). qWarning() rather than a hard failure, so
|
||||
an item that does call this in the future degrades to "one entity
|
||||
missing" instead of crashing the whole export.
|
||||
No DXF dialect this old (AC1006, AutoCAD R10 from 1988) has any
|
||||
raster image representation at all -- IMAGE/IMAGEDEF wasn't
|
||||
introduced until R2000, over a decade later, and even there the
|
||||
pixels are never embedded, only referenced by external file path.
|
||||
Actually supporting images means upgrading the DXF version target
|
||||
and managing a second file alongside the DXF; until then, this
|
||||
draws a placeholder rectangle outline (the item's own destination
|
||||
rect, mapped through m_world_transform exactly like drawRects()
|
||||
does) instead of silently dropping the item -- so its position,
|
||||
size, rotation, and skew all survive the export even though the
|
||||
picture itself can't yet. A fixed, plain pen, not m_pen: an image
|
||||
item has no meaningful pen of its own for this engine to have
|
||||
picked up from a prior updateState().
|
||||
*/
|
||||
void DxfPaintEngine::drawPixmap(const QRectF &r, const QPixmap &pm, const QRectF &sr)
|
||||
{
|
||||
Q_UNUSED(r)
|
||||
Q_UNUSED(pm)
|
||||
Q_UNUSED(sr)
|
||||
qWarning() << "DxfPaintEngine::drawPixmap: not supported, entity skipped";
|
||||
|
||||
QPolygonF corners;
|
||||
corners << r.topLeft() << r.topRight() << r.bottomRight() << r.bottomLeft();
|
||||
|
||||
QPolygonF mapped;
|
||||
mapped.reserve(corners.size() + 1);
|
||||
for (const QPointF &p : corners)
|
||||
mapped << toDxf(p);
|
||||
mapped << mapped.first();
|
||||
|
||||
Createdxf::drawPolyline(m_filepath, mapped, Createdxf::dxfColor(QPen(Qt::black)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -261,8 +278,21 @@ int DxfPaintDevice::metric(PaintDeviceMetric metric) const
|
||||
case PdmDepth:
|
||||
return 24;
|
||||
case PdmDevicePixelRatio:
|
||||
case PdmDevicePixelRatioScaled:
|
||||
return 1;
|
||||
case PdmDevicePixelRatioScaled:
|
||||
// Not just 1: Qt's own convention (see QPaintDevice's docs)
|
||||
// is that this metric equals PdmDevicePixelRatio scaled by
|
||||
// devicePixelRatioFScale() -- a large constant (65536)
|
||||
// used internally for sub-integer precision. Returning the
|
||||
// plain, unscaled ratio here (as this used to) meant
|
||||
// QPainter's own transform bookkeeping divided by it
|
||||
// expecting the scaled value, silently shrinking every
|
||||
// drawPixmap() destination rect by a factor of 65536 --
|
||||
// this is what was actually behind the placeholder
|
||||
// rectangle rendering at a barely-visible fraction of its
|
||||
// real size instead of the size the item's own
|
||||
// sceneTransform() correctly specified.
|
||||
return int(1 * QPaintDevice::devicePixelRatioFScale());
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -56,11 +56,18 @@
|
||||
- fillPath -> same outline-only handling as drawRects; no HATCH
|
||||
support in v1 (see design note in the PR)
|
||||
|
||||
Anything outside this list (images, gradients, etc.) is intentionally
|
||||
unimplemented and asserts in debug builds rather than silently
|
||||
producing an incomplete drawing - callers should know immediately if
|
||||
an item they're exporting uses something this engine doesn't cover
|
||||
yet, rather than getting a DXF file quietly missing content.
|
||||
- drawPixmap -> no raster image entity exists in this DXF
|
||||
dialect at all (see drawPixmap()'s own
|
||||
comment) -- draws a placeholder rectangle
|
||||
outline instead, preserving the item's
|
||||
position/size/rotation/skew even though the
|
||||
picture itself can't be included yet
|
||||
|
||||
Anything else outside this list (gradients, etc.) is intentionally
|
||||
unimplemented and logs a warning rather than silently producing an
|
||||
incomplete drawing - callers should know immediately if an item
|
||||
they're exporting uses something this engine doesn't cover yet,
|
||||
rather than getting a DXF file quietly missing content.
|
||||
*/
|
||||
class DxfPaintEngine : public QPaintEngine
|
||||
{
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "editorcommands.h"
|
||||
#include "../diagram.h"
|
||||
|
||||
/**
|
||||
@brief ElementEditionCommand::ElementEditionCommand
|
||||
@@ -666,10 +667,32 @@ void RotateFineElementsCommand::redo()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@brief selectionCenter
|
||||
@param items
|
||||
@return the center of the united scene bounding rect of items,
|
||||
snapped to the nearest half of the diagram grid. Mirroring across a
|
||||
half-grid line keeps points that were on the grid on the grid.
|
||||
*/
|
||||
static QPointF selectionCenter(const QList<QGraphicsItem *> &items)
|
||||
{
|
||||
QRectF bounding;
|
||||
for (auto *item : items) {
|
||||
bounding = bounding.united(item->sceneBoundingRect());
|
||||
}
|
||||
QPointF center = bounding.center();
|
||||
const qreal half_x = Diagram::xGrid / 2.0;
|
||||
const qreal half_y = Diagram::yGrid / 2.0;
|
||||
center.setX(qRound(center.x() / half_x) * half_x);
|
||||
center.setY(qRound(center.y() / half_y) * half_y);
|
||||
return center;
|
||||
}
|
||||
|
||||
MirrorElementsCommand::MirrorElementsCommand(ElementScene *scene, QUndoCommand *parent) :
|
||||
ElementEditionCommand(QObject::tr("Miroir de sélection", "undo caption"), scene, nullptr, parent)
|
||||
{
|
||||
m_items = scene->selectedItems();
|
||||
m_axis_x = selectionCenter(m_items).x();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -680,28 +703,28 @@ void MirrorElementsCommand::redo()
|
||||
foreach (auto *item, m_items) {
|
||||
if (item->type() == PartText::Type) {
|
||||
PartText* staticText = qgraphicsitem_cast<PartText*>(item);
|
||||
staticText->mirror();
|
||||
staticText->mirror(m_axis_x);
|
||||
} else if (item->type() == PartDynamicTextField::Type) {
|
||||
PartDynamicTextField* dyntext = qgraphicsitem_cast<PartDynamicTextField*>(item);
|
||||
dyntext->mirror();
|
||||
dyntext->mirror(m_axis_x);
|
||||
} else if (item->type() == PartArc::Type) {
|
||||
PartArc* arc = qgraphicsitem_cast<PartArc*>(item);
|
||||
arc->mirror();
|
||||
arc->mirror(m_axis_x);
|
||||
} else if (item->type() == PartEllipse::Type) {
|
||||
PartEllipse* ellipse = qgraphicsitem_cast<PartEllipse*>(item);
|
||||
ellipse->mirror();
|
||||
ellipse->mirror(m_axis_x);
|
||||
} else if (item->type() == PartLine::Type) {
|
||||
PartLine* line = qgraphicsitem_cast<PartLine*>(item);
|
||||
line->mirror();
|
||||
line->mirror(m_axis_x);
|
||||
} else if (item->type() == PartPolygon::Type) {
|
||||
PartPolygon* poly = qgraphicsitem_cast<PartPolygon*>(item);
|
||||
poly->mirror();
|
||||
poly->mirror(m_axis_x);
|
||||
} else if (item->type() == PartRectangle::Type) {
|
||||
PartRectangle* rect = qgraphicsitem_cast<PartRectangle*>(item);
|
||||
rect->mirror();
|
||||
rect->mirror(m_axis_x);
|
||||
} else if (item->type() == PartTerminal::Type) {
|
||||
PartTerminal* term = qgraphicsitem_cast<PartTerminal*>(item);
|
||||
term->mirror();
|
||||
term->mirror(m_axis_x);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -718,6 +741,7 @@ FlipElementsCommand::FlipElementsCommand(ElementScene *scene, QUndoCommand *pare
|
||||
ElementEditionCommand(QObject::tr("Retourner la sélection", "undo caption"), scene, nullptr, parent)
|
||||
{
|
||||
m_items = scene->selectedItems();
|
||||
m_axis_y = selectionCenter(m_items).y();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -728,28 +752,28 @@ void FlipElementsCommand::redo()
|
||||
foreach (auto *item, m_items) {
|
||||
if (item->type() == PartText::Type) {
|
||||
PartText* staticText = qgraphicsitem_cast<PartText*>(item);
|
||||
staticText->flip();
|
||||
staticText->flip(m_axis_y);
|
||||
} else if (item->type() == PartDynamicTextField::Type) {
|
||||
PartDynamicTextField* dyntext = qgraphicsitem_cast<PartDynamicTextField*>(item);
|
||||
dyntext->flip();
|
||||
dyntext->flip(m_axis_y);
|
||||
} else if (item->type() == PartArc::Type) {
|
||||
PartArc* arc = qgraphicsitem_cast<PartArc*>(item);
|
||||
arc->flip();
|
||||
arc->flip(m_axis_y);
|
||||
} else if (item->type() == PartEllipse::Type) {
|
||||
PartEllipse* ellipse = qgraphicsitem_cast<PartEllipse*>(item);
|
||||
ellipse->flip();
|
||||
ellipse->flip(m_axis_y);
|
||||
} else if (item->type() == PartLine::Type) {
|
||||
PartLine* line = qgraphicsitem_cast<PartLine*>(item);
|
||||
line->flip();
|
||||
line->flip(m_axis_y);
|
||||
} else if (item->type() == PartPolygon::Type) {
|
||||
PartPolygon* poly = qgraphicsitem_cast<PartPolygon*>(item);
|
||||
poly->flip();
|
||||
poly->flip(m_axis_y);
|
||||
} else if (item->type() == PartRectangle::Type) {
|
||||
PartRectangle* rect = qgraphicsitem_cast<PartRectangle*>(item);
|
||||
rect->flip();
|
||||
rect->flip(m_axis_y);
|
||||
} else if (item->type() == PartTerminal::Type) {
|
||||
PartTerminal* term = qgraphicsitem_cast<PartTerminal*>(item);
|
||||
term->flip();
|
||||
term->flip(m_axis_y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,6 +279,12 @@ private:
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
@brief The MirrorElementsCommand class
|
||||
Mirror the selected parts horizontally (left <-> right) across the
|
||||
vertical line through the center of the selection, so the selection
|
||||
keeps its place in the scene.
|
||||
*/
|
||||
class MirrorElementsCommand : public ElementEditionCommand
|
||||
{
|
||||
public:
|
||||
@@ -288,8 +294,15 @@ public:
|
||||
private:
|
||||
ElementScene *m_scene =nullptr;
|
||||
QList<QGraphicsItem*> m_items;
|
||||
qreal m_axis_x = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
@brief The FlipElementsCommand class
|
||||
Flip the selected parts vertically (top <-> bottom) across the
|
||||
horizontal line through the center of the selection, so the selection
|
||||
keeps its place in the scene.
|
||||
*/
|
||||
class FlipElementsCommand : public ElementEditionCommand
|
||||
{
|
||||
public:
|
||||
@@ -299,6 +312,7 @@ public:
|
||||
private:
|
||||
ElementScene *m_scene =nullptr;
|
||||
QList<QGraphicsItem*> m_items;
|
||||
qreal m_axis_y = 0;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -197,13 +197,13 @@ qreal PartArc::rotation() const {
|
||||
return qRound(m_rot * 100.0) / 100.0;
|
||||
}
|
||||
|
||||
void PartArc::flip() {
|
||||
void PartArc::flip(qreal axis_y) {
|
||||
m_start_angle = (-1) * m_start_angle;
|
||||
m_span_angle = (-1) * m_span_angle;
|
||||
while (m_start_angle < 0) { m_start_angle += (360*16); }
|
||||
while (m_start_angle >= (360*16)) { m_start_angle -= (360*16); }
|
||||
auto p1 = mapToScene(m_rect.x(),m_rect.y());
|
||||
p1.setY(((-1.0) * p1.y()) - m_rect.height());
|
||||
p1.setY(2 * axis_y - p1.y() - m_rect.height());
|
||||
p1 = mapFromScene(p1.x(),p1.y());
|
||||
m_rect = QRectF(m_rect.x(), p1.y(), m_rect.width(), m_rect.height());
|
||||
prepareGeometryChange();
|
||||
@@ -211,13 +211,13 @@ void PartArc::flip() {
|
||||
emit rectChanged();
|
||||
}
|
||||
|
||||
void PartArc::mirror() {
|
||||
void PartArc::mirror(qreal axis_x) {
|
||||
m_start_angle = (180.0 * 16) - m_start_angle;
|
||||
m_span_angle = (-1) * m_span_angle;
|
||||
while (m_start_angle < 0) { m_start_angle += (360*16); }
|
||||
while (m_start_angle >= (360*16)) { m_start_angle -= (360*16); }
|
||||
auto p1 = mapToScene(m_rect.x(),m_rect.y());
|
||||
p1.setX(((-1.0) * p1.x()) - m_rect.width());
|
||||
p1.setX(2 * axis_x - p1.x() - m_rect.width());
|
||||
p1 = mapFromScene(p1.x(), p1.y());
|
||||
m_rect = QRectF(p1.x(), m_rect.y(), m_rect.width(), m_rect.height());
|
||||
prepareGeometryChange();
|
||||
|
||||
@@ -62,8 +62,8 @@ class PartArc : public AbstractPartEllipse
|
||||
QRectF sceneGeometricRect() const override;
|
||||
void setRotation(qreal angle);
|
||||
qreal rotation() const;
|
||||
void flip();
|
||||
void mirror();
|
||||
void flip(qreal axis_y = 0);
|
||||
void mirror(qreal axis_x = 0);
|
||||
|
||||
void addHandler() override;
|
||||
void removeHandler() override;
|
||||
|
||||
@@ -74,19 +74,19 @@ void PartDynamicTextField::setRotation(qreal angle) {
|
||||
setPos(QTransform().rotate(diffAngle).map(pos()));
|
||||
}
|
||||
|
||||
void PartDynamicTextField::mirror() {
|
||||
void PartDynamicTextField::mirror(qreal axis_x) {
|
||||
// at first: rotate the text:
|
||||
QGraphicsObject::setRotation(QET::correctAngle(360-rotation(), true));
|
||||
// then see, where we need to re-position depending on the angle!
|
||||
qreal rot = qRound(QET::correctAngle(rotation(), true));
|
||||
qreal c = qCos(qDegreesToRadians(rot));
|
||||
qreal s = qSin(qDegreesToRadians(rot));
|
||||
qreal x = (-1) * pos().x() - c * boundingRect().width();
|
||||
qreal x = 2 * axis_x - pos().x() - c * boundingRect().width();
|
||||
qreal y = pos().y() - s * boundingRect().width();
|
||||
setPos(x, y);
|
||||
}
|
||||
|
||||
void PartDynamicTextField::flip() {
|
||||
void PartDynamicTextField::flip(qreal axis_y) {
|
||||
// at first: rotate the text:
|
||||
QGraphicsObject::setRotation(QET::correctAngle(360-rotation(), true));
|
||||
// then see, where we need to re-position depending on the angle!
|
||||
@@ -94,7 +94,7 @@ void PartDynamicTextField::flip() {
|
||||
qreal c = qCos(qDegreesToRadians(rot));
|
||||
qreal s = qSin(qDegreesToRadians(rot));
|
||||
qreal x = pos().x() + s * boundingRect().height();
|
||||
qreal y = (-1) * pos().y() - c * boundingRect().height();
|
||||
qreal y = 2 * axis_y - pos().y() - c * boundingRect().height();
|
||||
setPos(x, y);
|
||||
}
|
||||
|
||||
|
||||
@@ -106,8 +106,8 @@ class PartDynamicTextField : public QGraphicsTextItem, public CustomElementPart
|
||||
bool rotationPointCenter() const;
|
||||
|
||||
void setRotation(qreal angle);
|
||||
void mirror();
|
||||
void flip();
|
||||
void mirror(qreal axis_x = 0);
|
||||
void flip(qreal axis_y = 0);
|
||||
|
||||
|
||||
protected:
|
||||
|
||||
@@ -256,9 +256,9 @@ qreal PartEllipse::rotation() const {
|
||||
return qRound(m_rot * 100.0) / 100.0;
|
||||
}
|
||||
|
||||
void PartEllipse::flip() {
|
||||
void PartEllipse::flip(qreal axis_y) {
|
||||
auto p1 = mapToScene(m_rect.x(), m_rect.y());
|
||||
p1.setY(((-1.0) * p1.y()) - m_rect.height());
|
||||
p1.setY(2 * axis_y - p1.y() - m_rect.height());
|
||||
p1 = mapFromScene(p1.x(), p1.y());
|
||||
m_rect = QRectF(p1.x(), p1.y(), m_rect.width(), m_rect.height());
|
||||
prepareGeometryChange();
|
||||
@@ -266,9 +266,9 @@ void PartEllipse::flip() {
|
||||
emit rectChanged();
|
||||
}
|
||||
|
||||
void PartEllipse::mirror() {
|
||||
void PartEllipse::mirror(qreal axis_x) {
|
||||
auto p1 = mapToScene(m_rect.x(), m_rect.y());
|
||||
p1.setX(((-1.0) * p1.x()) - m_rect.width());
|
||||
p1.setX(2 * axis_x - p1.x() - m_rect.width());
|
||||
p1 = mapFromScene(p1.x(), p1.y());
|
||||
m_rect = QRectF(p1.x(), p1.y(), m_rect.width(), m_rect.height());
|
||||
prepareGeometryChange();
|
||||
|
||||
@@ -62,8 +62,8 @@ class PartEllipse : public AbstractPartEllipse
|
||||
void setRect(const QRectF &rect) override {AbstractPartEllipse::setRect(rect); adjustHandlerPos();}
|
||||
void setRotation(qreal angle);
|
||||
qreal rotation() const;
|
||||
void flip();
|
||||
void mirror();
|
||||
void flip(qreal axis_y = 0);
|
||||
void mirror(qreal axis_x = 0);
|
||||
|
||||
void addHandler() override;
|
||||
void removeHandler() override;
|
||||
|
||||
@@ -590,11 +590,11 @@ qreal PartLine::rotation() const {
|
||||
return qRound(m_rot * 100.0) / 100.0;
|
||||
}
|
||||
|
||||
void PartLine::flip() {
|
||||
void PartLine::flip(qreal axis_y) {
|
||||
auto p1 = mapToScene(m_line.p1());
|
||||
auto p2 = mapToScene(m_line.p2());
|
||||
p1 = QPointF(p1.x(), (-1) * p1.y());
|
||||
p2 = QPointF(p2.x(), (-1) * p2.y());
|
||||
p1 = QPointF(p1.x(), 2 * axis_y - p1.y());
|
||||
p2 = QPointF(p2.x(), 2 * axis_y - p2.y());
|
||||
m_line.setP1(mapFromScene(p1));
|
||||
m_line.setP2(mapFromScene(p2));
|
||||
setLine(m_line);
|
||||
@@ -603,11 +603,11 @@ void PartLine::flip() {
|
||||
emit lineChanged();
|
||||
}
|
||||
|
||||
void PartLine::mirror() {
|
||||
void PartLine::mirror(qreal axis_x) {
|
||||
auto p1 = mapToScene(m_line.p1());
|
||||
auto p2 = mapToScene(m_line.p2());
|
||||
p1 = QPointF((-1) * p1.x(), p1.y());
|
||||
p2 = QPointF((-1) * p2.x(), p2.y());
|
||||
p1 = QPointF(2 * axis_x - p1.x(), p1.y());
|
||||
p2 = QPointF(2 * axis_x - p2.x(), p2.y());
|
||||
m_line.setP1(mapFromScene(p1));
|
||||
m_line.setP2(mapFromScene(p2));
|
||||
setLine(m_line);
|
||||
|
||||
@@ -96,8 +96,8 @@ class PartLine : public CustomElementGraphicPart
|
||||
void setSecondEndLength(const qreal &l);
|
||||
void setRotation(qreal angle);
|
||||
qreal rotation() const;
|
||||
void flip();
|
||||
void mirror();
|
||||
void flip(qreal axis_y = 0);
|
||||
void mirror(qreal axis_x = 0);
|
||||
|
||||
void addHandler() override;
|
||||
void removeHandler() override;
|
||||
|
||||
@@ -307,10 +307,10 @@ qreal PartPolygon::rotation() const {
|
||||
return qRound(m_rot * 100.0) / 100.0;
|
||||
}
|
||||
|
||||
void PartPolygon::flip() {
|
||||
void PartPolygon::flip(qreal axis_y) {
|
||||
for (auto &pt : m_polygon) {
|
||||
pt = mapToScene(pt.x(), pt.y());
|
||||
pt = QPointF(pt.x(), (-1) * pt.y());
|
||||
pt = QPointF(pt.x(), 2 * axis_y - pt.y());
|
||||
pt = mapFromScene(pt.x(), pt.y());
|
||||
}
|
||||
setPolygon(m_polygon);
|
||||
@@ -319,10 +319,10 @@ void PartPolygon::flip() {
|
||||
emit polygonChanged();
|
||||
}
|
||||
|
||||
void PartPolygon::mirror() {
|
||||
void PartPolygon::mirror(qreal axis_x) {
|
||||
for (auto &pt : m_polygon) {
|
||||
pt = mapToScene(pt.x(), pt.y());
|
||||
pt = QPointF((-1) * pt.x(), pt.y());
|
||||
pt = QPointF(2 * axis_x - pt.x(), pt.y());
|
||||
pt = mapFromScene(pt.x(), pt.y());
|
||||
}
|
||||
setPolygon(m_polygon);
|
||||
|
||||
@@ -89,8 +89,8 @@ class PartPolygon : public CustomElementGraphicPart
|
||||
|
||||
void setRotation (qreal angle);
|
||||
qreal rotation () const;
|
||||
void flip();
|
||||
void mirror();
|
||||
void flip(qreal axis_y = 0);
|
||||
void mirror(qreal axis_x = 0);
|
||||
|
||||
void addHandler() override;
|
||||
void removeHandler() override;
|
||||
|
||||
@@ -187,11 +187,11 @@ qreal PartRectangle::rotation() const {
|
||||
return qRound(m_rot * 100.0) / 100.0;
|
||||
}
|
||||
|
||||
void PartRectangle::flip() {
|
||||
void PartRectangle::flip(qreal axis_y) {
|
||||
auto height = m_rect.height();
|
||||
auto p1 = mapToScene(m_rect.x(),m_rect.y());
|
||||
qreal x = p1.x();
|
||||
qreal y = ((-1.0) * p1.y()) - height;
|
||||
qreal y = 2 * axis_y - p1.y() - height;
|
||||
p1 = mapFromScene(x, y);
|
||||
m_rect.setX(p1.x());
|
||||
m_rect.setY(p1.y());
|
||||
@@ -201,10 +201,10 @@ void PartRectangle::flip() {
|
||||
emit rectChanged();
|
||||
}
|
||||
|
||||
void PartRectangle::mirror() {
|
||||
void PartRectangle::mirror(qreal axis_x) {
|
||||
auto width = m_rect.width();
|
||||
auto p1 = mapToScene(m_rect.x(),m_rect.y());
|
||||
qreal x = ((-1.0) * p1.x()) - width;
|
||||
qreal x = 2 * axis_x - p1.x() - width;
|
||||
qreal y = p1.y();
|
||||
p1 = mapFromScene(x, y);
|
||||
m_rect.setX(p1.x());
|
||||
|
||||
@@ -73,8 +73,8 @@ class PartRectangle : public CustomElementGraphicPart
|
||||
void setYRadius(qreal Y);
|
||||
void setRotation(qreal angle);
|
||||
qreal rotation() const;
|
||||
void flip();
|
||||
void mirror();
|
||||
void flip(qreal axis_y = 0);
|
||||
void mirror(qreal axis_x = 0);
|
||||
|
||||
QRectF sceneGeometricRect() const override;
|
||||
virtual QPointF sceneTopLeft() const;
|
||||
|
||||
@@ -293,9 +293,9 @@ qreal PartTerminal::rotation() const {
|
||||
@brief PartTerminal::flip
|
||||
turn part upside down
|
||||
*/
|
||||
void PartTerminal::flip() {
|
||||
void PartTerminal::flip(qreal axis_y) {
|
||||
d->m_pos.setX( pos().x());
|
||||
d->m_pos.setY((-1.0) * pos().y());
|
||||
d->m_pos.setY(2 * axis_y - pos().y());
|
||||
switch (d->m_orientation) {
|
||||
case Qet::North : setOrientation(Qet::South);
|
||||
break;
|
||||
@@ -314,8 +314,8 @@ void PartTerminal::flip() {
|
||||
@brief PartTerminal::mirror
|
||||
turn part from left to right
|
||||
*/
|
||||
void PartTerminal::mirror() {
|
||||
d->m_pos.setX((-1.0) * pos().x());
|
||||
void PartTerminal::mirror(qreal axis_x) {
|
||||
d->m_pos.setX(2 * axis_x - pos().x());
|
||||
d->m_pos.setY( pos().y());
|
||||
switch (d->m_orientation) {
|
||||
case Qet::North : break;
|
||||
|
||||
@@ -96,8 +96,8 @@ class PartTerminal : public CustomElementGraphicPart
|
||||
|
||||
void setRotation(qreal angle);
|
||||
qreal rotation() const;
|
||||
void flip();
|
||||
void mirror();
|
||||
void flip(qreal axis_y = 0);
|
||||
void mirror(qreal axis_x = 0);
|
||||
void nextOrientation();
|
||||
void previousOrientation();
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ void PartText::setRotation(qreal angle) {
|
||||
setPos(QTransform().rotate(diffAngle).map(pos()));
|
||||
}
|
||||
|
||||
void PartText::mirror() {
|
||||
void PartText::mirror(qreal axis_x) {
|
||||
// at first: rotate the text:
|
||||
QGraphicsObject::setRotation(QET::correctAngle((360-rotation()), true));
|
||||
// then see, where we need to re-position depending on text, font ...
|
||||
@@ -86,12 +86,12 @@ void PartText::mirror() {
|
||||
qreal c = qCos(qDegreesToRadians(rot));
|
||||
qreal s = qSin(qDegreesToRadians(rot));
|
||||
// Now: Move!
|
||||
qreal x = (-1) * pos().x() - c * (textwidth);
|
||||
qreal x = 2 * axis_x - pos().x() - c * (textwidth);
|
||||
qreal y = pos().y() - s * (textwidth);
|
||||
setPos(x, y);
|
||||
}
|
||||
|
||||
void PartText::flip() {
|
||||
void PartText::flip(qreal axis_y) {
|
||||
// at first: rotate the text:
|
||||
QGraphicsObject::setRotation(QET::correctAngle((360-rotation()), true));
|
||||
// then see, where we need to re-position depending on text, font ...
|
||||
@@ -103,7 +103,7 @@ void PartText::flip() {
|
||||
qreal s = qSin(qDegreesToRadians(rot));
|
||||
// Now: Move!
|
||||
qreal x = pos().x() - s * (textheight);
|
||||
qreal y = (-1) * pos().y() + c * (textheight);
|
||||
qreal y = 2 * axis_y - pos().y() + c * (textheight);
|
||||
setPos(x, y);
|
||||
}
|
||||
|
||||
|
||||
@@ -64,8 +64,8 @@ class PartText : public QGraphicsTextItem, public CustomElementPart {
|
||||
void fromXml(const QDomElement &) override;
|
||||
const QDomElement toXml(QDomDocument &) const override;
|
||||
void setRotation(qreal angle);
|
||||
void mirror();
|
||||
void flip();
|
||||
void mirror(qreal axis_x = 0);
|
||||
void flip(qreal axis_y = 0);
|
||||
bool isUseless() const override;
|
||||
QRectF sceneGeometricRect() const override;
|
||||
void startUserTransformation(const QRectF &) override;
|
||||
|
||||
@@ -744,6 +744,25 @@ void ExportDialog::generateDxf(
|
||||
painter.end();
|
||||
}
|
||||
|
||||
//Draw images -- collected above (list_images) but never actually
|
||||
//drawn until now, an existing gap this reuses the same paint()
|
||||
//-replay approach to fix: DiagramImageItem::paint() has no
|
||||
//viewport-dependent logic (unlike CrossRefItem, which needs its own
|
||||
//paintForExport() for that reason), so it's called directly with a
|
||||
//default QStyleOptionGraphicsItem rather than needing an export-
|
||||
//specific variant of its own. DxfPaintEngine::drawPixmap() is what
|
||||
//actually turns the drawPixmap() call inside paint() into a
|
||||
//placeholder outline, since this DXF dialect has no raster image
|
||||
//entity to draw instead.
|
||||
for (DiagramImageItem *image : std::as_const(list_images))
|
||||
{
|
||||
DxfPaintDevice dxf_device(file_path);
|
||||
QPainter painter(&dxf_device);
|
||||
painter.setWorldTransform(image->sceneTransform());
|
||||
image->paintForExport(&painter);
|
||||
painter.end();
|
||||
}
|
||||
|
||||
Createdxf::dxfEnd(file_path);
|
||||
|
||||
saveReloadDiagramParameters(diagram, false);
|
||||
@@ -818,6 +837,42 @@ void ExportDialog::slot_export()
|
||||
return;
|
||||
}
|
||||
|
||||
// Warn once, up front, rather than per-diagram: this DXF dialect
|
||||
// (AC1006, AutoCAD R10) has no raster image representation at all
|
||||
// (IMAGE/IMAGEDEF wasn't introduced until R2000, over a decade
|
||||
// later, and even there the picture is never embedded, only
|
||||
// referenced by external file path) -- so any image ends up as a
|
||||
// placeholder rectangle outline instead (see DxfPaintEngine::
|
||||
// drawPixmap()), with its position/size/rotation/skew preserved but
|
||||
// not its actual content.
|
||||
if (epw -> exportProperties().format.compare(QLatin1String("dxf"), Qt::CaseInsensitive) == 0)
|
||||
{
|
||||
bool any_images = false;
|
||||
for (ExportDiagramLine *diagram_line : std::as_const(diagrams_to_export))
|
||||
{
|
||||
for (QGraphicsItem *item : diagram_line->diagram->items())
|
||||
{
|
||||
if (qgraphicsitem_cast<DiagramImageItem *>(item)) {
|
||||
any_images = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (any_images) break;
|
||||
}
|
||||
|
||||
if (any_images)
|
||||
{
|
||||
QET::QetMessageBox::warning(
|
||||
this,
|
||||
tr("Images non incluses dans l'export DXF", "message box title"),
|
||||
tr("Le format DXF utilisé ici (AC1006) ne permet pas d'inclure d'image. "
|
||||
"Les images seront représentées uniquement par un rectangle de contour "
|
||||
"(position, taille, rotation et inclinaison conservées), sans le contenu de l'image.",
|
||||
"message box content")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// exporte chaque schema a exporter
|
||||
foreach(ExportDiagramLine *diagram_line, diagrams_to_export) {
|
||||
exportDiagram(diagram_line);
|
||||
|
||||
@@ -297,11 +297,19 @@ QString QET::ElementsAndConductorsSentence(
|
||||
|
||||
if (images_count) {
|
||||
if (!text.isEmpty()) text += ", ";
|
||||
text += QObject::tr(
|
||||
"%n image(s)",
|
||||
"part of a sentence listing the content of a diagram",
|
||||
images_count
|
||||
);
|
||||
// Qt's %n only selects a grammatical singular/plural form (the
|
||||
// "(s)" convention used by every other count here) -- it never
|
||||
// spells the number out as a word, so getting "une image"
|
||||
// instead of the literal "1 image" for the single-item case
|
||||
// means handling that count outside %n entirely, with its own
|
||||
// fixed string.
|
||||
text += images_count == 1
|
||||
? QObject::tr("une image", "part of a sentence listing the content of a diagram")
|
||||
: QObject::tr(
|
||||
"%n images",
|
||||
"part of a sentence listing the content of a diagram",
|
||||
images_count
|
||||
);
|
||||
}
|
||||
|
||||
if (shapes_count) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1011,10 +1011,14 @@ bool Conductor::fromXml(QDomElement &dom_element)
|
||||
//that field was introduced (see terminal1/terminal2 handling in
|
||||
//toXml() below).
|
||||
m_uuid = QUuid(dom_element.attribute(QStringLiteral("uuid")));
|
||||
m_persist_uuid = !m_uuid.isNull();
|
||||
if (m_uuid.isNull()) {
|
||||
//Absent, empty or malformed: mint one. A null uuid is not a usable
|
||||
//identity -- every conductor carrying one would collide with every
|
||||
//other on the conductor table's primary key.
|
||||
//other on the conductor table's primary key. It's runtime-only,
|
||||
//though: toXml() must not write it back out, or a legacy file
|
||||
//with no conductor uuids gets a freshly different one on every
|
||||
//single load-and-resave (see #754).
|
||||
m_uuid = QUuid::createUuid();
|
||||
}
|
||||
|
||||
@@ -1056,7 +1060,8 @@ QDomElement Conductor::toXml(QDomDocument &dom_document,
|
||||
{
|
||||
QDomElement dom_element = dom_document.createElement("conductor");
|
||||
|
||||
dom_element.setAttribute("uuid", m_uuid.toString());
|
||||
if (m_persist_uuid)
|
||||
dom_element.setAttribute("uuid", m_uuid.toString());
|
||||
dom_element.setAttribute("x", QString::number(pos().x()));
|
||||
dom_element.setAttribute("y", QString::number(pos().y()));
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ class Conductor : public QGraphicsObject
|
||||
Diagram *diagram() const;
|
||||
ConductorTextItem *textItem() const;
|
||||
QUuid uuid() const {return m_uuid;}
|
||||
void newUuid() {m_uuid = QUuid::createUuid();} //create new uuid for this conductor
|
||||
void newUuid() {m_uuid = QUuid::createUuid(); m_persist_uuid = true;} //create new uuid for this conductor
|
||||
void updatePath(const QRectF & = QRectF());
|
||||
|
||||
//This method do nothing, it's only made to be used with Q_PROPERTY
|
||||
@@ -209,6 +209,10 @@ class Conductor : public QGraphicsObject
|
||||
bool m_valid;
|
||||
bool m_freeze_label = false;
|
||||
QUuid m_uuid;
|
||||
/// false when m_uuid was synthesized by fromXml() because the
|
||||
/// file had none -- toXml() must not persist that value, or
|
||||
/// every reload mints and saves a new random one (see #754).
|
||||
bool m_persist_uuid = true;
|
||||
|
||||
/// QPen et QBrush objects used to draw conductors
|
||||
static QPen conductor_pen;
|
||||
|
||||
@@ -19,9 +19,18 @@
|
||||
#define DIAGRAM_IMAGE_ITEM_H
|
||||
|
||||
#include "qetgraphicsitem.h"
|
||||
#include "shapetransform.h"
|
||||
#include "../ui/imagetransparentcolordialog.h"
|
||||
|
||||
#include <QColor>
|
||||
#include <QList>
|
||||
#include <QStyleOptionGraphicsItem>
|
||||
#include <QVector>
|
||||
|
||||
class QDomElement;
|
||||
class QDomDocument;
|
||||
class QGraphicsSceneContextMenuEvent;
|
||||
class QetGraphicsHandlerItem;
|
||||
|
||||
/**
|
||||
This class represents a selectable, movable and editable image on a
|
||||
@@ -30,6 +39,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:
|
||||
@@ -40,6 +66,19 @@ class DiagramImageItem : public QetGraphicsItem {
|
||||
// attributes
|
||||
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 +93,125 @@ 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;
|
||||
|
||||
|
||||
/// DXF export: replay this item's paint() on an arbitrary QPainter
|
||||
/// (e.g. one targeting DxfPaintDevice). paint() itself stays
|
||||
/// protected, as it should for the normal
|
||||
/// QGraphicsScene/QGraphicsView paint contract - this is a
|
||||
/// deliberate, narrow escape hatch for exporters, not a general
|
||||
/// relaxation of that contract. Matches CrossRefItem::paintForExport()
|
||||
/// exactly, for the identical reason.
|
||||
void paintForExport(QPainter *painter)
|
||||
{
|
||||
QStyleOptionGraphicsItem option;
|
||||
paint(painter, &option, nullptr);
|
||||
}
|
||||
|
||||
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();
|
||||
void saveImageAs();
|
||||
void saveOriginalImageAs();
|
||||
void saveImagePixmapAs(const QPixmap &pixmap, const QString &dialogTitle, bool hasTransparency);
|
||||
static bool writeRasterAsSvg(const QPixmap &pixmap, const QString &path);
|
||||
static QPixmap computeDisplayPixmap(const QPixmap &base, const QRect &cropRect, const QList<ImageTransparentColorDialog::PickedColor> &colors);
|
||||
|
||||
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 (each colour
|
||||
// carrying its own 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<ImageTransparentColorDialog::PickedColor> m_transparent_colors;
|
||||
|
||||
// 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
|
||||
|
||||
@@ -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,65 @@ 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 handlerMousePressEvent();
|
||||
void handlerMouseMoveEvent(QGraphicsSceneMouseEvent *event);
|
||||
void handlerMouseReleaseEvent();
|
||||
void convertToPathOrPolygon(); // context-menu action; see promoteRectangleOrEllipseToPolygon() for the separate Alt+drag mechanism
|
||||
static QVector<PathNode> bezierNodesForArc(const QRectF &rect, qreal startAngleDeg, qreal spanAngleDeg);
|
||||
static QVector<PathNode> bezierNodesForRoundedRect(const QRectF &rect, qreal xRadius, qreal yRadius);
|
||||
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
|
||||
|
||||
///ATTRIBUTES
|
||||
void handlerMousePressEvent(int handlerIndex);
|
||||
void handlerMouseMoveEvent(int handlerIndex, QGraphicsSceneMouseEvent *event);
|
||||
void handlerMouseReleaseEvent(int handlerIndex);
|
||||
|
||||
// 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,13 +308,35 @@ 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;
|
||||
qreal m_xRadius = 0,
|
||||
m_yRadius = 0,
|
||||
m_old_xRadius,
|
||||
m_old_yRadius;
|
||||
m_old_yRadius,
|
||||
m_old_startAngle,
|
||||
m_old_endAngle;
|
||||
|
||||
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
|
||||
@@ -65,6 +65,7 @@ namespace QET {
|
||||
QIcon EditCopy;
|
||||
QIcon EditCut;
|
||||
QIcon EditDelete;
|
||||
QIcon EditOpacity;
|
||||
QIcon EditPaste;
|
||||
QIcon EditRedo;
|
||||
QIcon EditRename;
|
||||
@@ -86,6 +87,7 @@ namespace QET {
|
||||
QIcon ElementEdit;
|
||||
QIcon ElementNew;
|
||||
QIcon ElementSmall;
|
||||
QIcon EllipseToBezier;
|
||||
QIcon EndLineCircle;
|
||||
QIcon EndLineDiamond;
|
||||
QIcon EndLineNone;
|
||||
@@ -115,6 +117,8 @@ namespace QET {
|
||||
QIcon Hide;
|
||||
QIcon Home;
|
||||
QIcon HotSpot;
|
||||
QIcon ImageFlipHorizontal;
|
||||
QIcon ImageFlipVertical;
|
||||
QIcon InsertImage;
|
||||
QIcon Lower;
|
||||
QIcon IC_MoveFile;
|
||||
@@ -128,6 +132,7 @@ namespace QET {
|
||||
QIcon ObjectUnlocked;
|
||||
QIcon Orientations;
|
||||
QIcon PartArc;
|
||||
QIcon PartBezier;
|
||||
QIcon PartCircle;
|
||||
QIcon PartEllipse;
|
||||
QIcon PartLine;
|
||||
@@ -155,6 +160,8 @@ namespace QET {
|
||||
QIcon QETOxygenLogo;
|
||||
QIcon QtLogo;
|
||||
QIcon Raise;
|
||||
QIcon RectToBezier;
|
||||
QIcon RectToPolyline;
|
||||
QIcon Remove;
|
||||
QIcon Restore;
|
||||
QIcon RunDxf;
|
||||
@@ -168,6 +175,7 @@ namespace QET {
|
||||
QIcon TitleBlock;
|
||||
QIcon TitleBlockBottom;
|
||||
QIcon TitleBlockRight;
|
||||
QIcon TransformCrop;
|
||||
QIcon TransformRotate;
|
||||
QIcon UserInformations;
|
||||
QIcon ViewFitWidth;
|
||||
@@ -469,6 +477,7 @@ void QET::Icons::initIcons()
|
||||
EditCut .addFile(":/ico/22x22/edit-cut.png");
|
||||
EditDelete .addFile(":/ico/16x16/edit-delete.png");
|
||||
EditDelete .addFile(":/ico/22x22/edit-delete.png");
|
||||
EditOpacity .addFile(":/ico/breeze-icons/scalable/apps/hidef/edit-opacity.svg");
|
||||
EditPaste .addFile(":/ico/22x22/edit-paste.png");
|
||||
EditPaste .addFile(":/ico/16x16/edit-paste.png");
|
||||
if (rtl) {
|
||||
@@ -517,6 +526,7 @@ void QET::Icons::initIcons()
|
||||
ElementNew .addFile(":/ico/16x16/element-new.png");
|
||||
ElementNew .addFile(":/ico/22x22/element-new.png");
|
||||
ElementSmall .addFile(":/ico/16x16/element.png");
|
||||
EllipseToBezier .addFile(":/ico/generated/ellipse-to-bezier.svg");
|
||||
EndLineCircle .addFile(":/ico/16x16/endline-circle.png");
|
||||
EndLineDiamond .addFile(":/ico/16x16/endline-diamond.png");
|
||||
EndLineNone .addFile(":/ico/16x16/endline-none.png");
|
||||
@@ -556,6 +566,8 @@ void QET::Icons::initIcons()
|
||||
Home .addFile(":/ico/16x16/go-home.png");
|
||||
Home .addFile(":/ico/22x22/go-home.png");
|
||||
HotSpot .addFile(":/ico/22x22/hotspot.png");
|
||||
ImageFlipHorizontal .addFile(":/ico/breeze-icons/scalable/apps/hidef/image-flip-horizontal-symbolic.svg");
|
||||
ImageFlipVertical .addFile(":/ico/breeze-icons/scalable/apps/hidef/image-flip-vertical-symbolic.svg");
|
||||
InsertImage .addFile(":/ico/22x22/insert-image.png");
|
||||
Lower .addFile(":/ico/22x22/lower.png");
|
||||
IC_MoveFile .addFile(":/ico/16x16/item-move.png");
|
||||
@@ -570,6 +582,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");
|
||||
@@ -613,6 +626,8 @@ void QET::Icons::initIcons()
|
||||
QETVideo .addFile(":/ico/16x16/kdenlive-show-video.png");
|
||||
QtLogo .addFile(":/ico/16x16/qt.png");
|
||||
Raise .addFile(":/ico/22x22/raise.png");
|
||||
RectToBezier .addFile(":/ico/generated/rect-to-bezier.svg");
|
||||
RectToPolyline .addFile(":/ico/generated/rect-to-polyline.svg");
|
||||
Remove .addFile(":/ico/16x16/list-remove.png");
|
||||
Remove .addFile(":/ico/22x22/list-remove.png");
|
||||
resize_image .addFile(":/ico/22x22/transform-scale.png");
|
||||
@@ -631,6 +646,7 @@ void QET::Icons::initIcons()
|
||||
TitleBlock .addFile(":/ico/22x22/label.png");
|
||||
TitleBlockBottom .addFile(":/ico/22x22/titleblock-bottom.png");
|
||||
TitleBlockRight .addFile(":/ico/22x22/titleblock-right.png");
|
||||
TransformCrop .addFile(":/ico/breeze-icons/scalable/apps/hidef/transform-crop.svg");
|
||||
TransformRotate .addFile(":/ico/16x16/transform-rotate.png");
|
||||
UserInformations .addFile(":/ico/16x16/preferences-desktop-user.png");
|
||||
UserInformations .addFile(":/ico/22x22/preferences-desktop-user.png");
|
||||
|
||||
@@ -72,6 +72,7 @@ namespace QET {
|
||||
extern QIcon EditCopy;
|
||||
extern QIcon EditCut;
|
||||
extern QIcon EditDelete;
|
||||
extern QIcon EditOpacity;
|
||||
extern QIcon EditPaste;
|
||||
extern QIcon EditRedo;
|
||||
extern QIcon EditRename;
|
||||
@@ -92,6 +93,7 @@ namespace QET {
|
||||
extern QIcon ElementDelete;
|
||||
extern QIcon ElementEdit;
|
||||
extern QIcon ElementNew;
|
||||
extern QIcon EllipseToBezier;
|
||||
extern QIcon EndLineCircle;
|
||||
extern QIcon EndLineDiamond;
|
||||
extern QIcon EndLineNone;
|
||||
@@ -121,6 +123,8 @@ namespace QET {
|
||||
extern QIcon Hide;
|
||||
extern QIcon Home;
|
||||
extern QIcon HotSpot;
|
||||
extern QIcon ImageFlipHorizontal;
|
||||
extern QIcon ImageFlipVertical;
|
||||
extern QIcon InsertImage;
|
||||
extern QIcon Lower;
|
||||
extern QIcon IC_MoveFile;
|
||||
@@ -136,6 +140,7 @@ namespace QET {
|
||||
extern QIcon ObjectUnlocked;
|
||||
extern QIcon Orientations;
|
||||
extern QIcon PartArc;
|
||||
extern QIcon PartBezier;
|
||||
extern QIcon PartCircle;
|
||||
extern QIcon PartEllipse;
|
||||
extern QIcon PartLine;
|
||||
@@ -163,6 +168,8 @@ namespace QET {
|
||||
extern QIcon QETOxygenLogo;
|
||||
extern QIcon QtLogo;
|
||||
extern QIcon Raise;
|
||||
extern QIcon RectToBezier;
|
||||
extern QIcon RectToPolyline;
|
||||
extern QIcon Remove;
|
||||
extern QIcon Restore;
|
||||
extern QIcon RunDxf;
|
||||
@@ -176,6 +183,7 @@ namespace QET {
|
||||
extern QIcon TitleBlock;
|
||||
extern QIcon TitleBlockBottom;
|
||||
extern QIcon TitleBlockRight;
|
||||
extern QIcon TransformCrop;
|
||||
extern QIcon TransformRotate;
|
||||
extern QIcon UserInformations;
|
||||
extern QIcon ViewFitWidth;
|
||||
|
||||
@@ -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,394 @@
|
||||
/*
|
||||
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 <QToolButton>
|
||||
#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
|
||||
|
||||
m_displayImage = (m_displayScale < 1.0)
|
||||
? m_source.scaled(m_source.size() * m_displayScale, Qt::KeepAspectRatio, Qt::SmoothTransformation)
|
||||
: m_source;
|
||||
setPixmap(QPixmap::fromImage(m_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<PickedColor> &existingColors, QWidget *parent) :
|
||||
QDialog(parent),
|
||||
m_sourceImage(basePixmap.toImage()),
|
||||
m_pickedColors(existingColors)
|
||||
{
|
||||
setWindowTitle(tr("Couleur transparente"));
|
||||
|
||||
if (!m_pickedColors.isEmpty())
|
||||
m_lastToleranceUsed = m_pickedColors.last().tolerance;
|
||||
|
||||
m_sourceLabel = new ClickableImageLabel(m_sourceImage, this);
|
||||
m_previewSourceImage = m_sourceLabel->displayImage();
|
||||
m_previewLabel = new QLabel(this);
|
||||
|
||||
m_hintLabel = new QLabel(this);
|
||||
m_hintLabel->setWordWrap(true);
|
||||
|
||||
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||
m_okButton = buttons->button(QDialogButtonBox::Ok);
|
||||
m_startedWithColors = !m_pickedColors.isEmpty();
|
||||
updateOkEnabled();
|
||||
|
||||
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);
|
||||
|
||||
// One row per picked colour, appended/removed as rebuildSwatches()
|
||||
// runs -- starts empty either way, populated by the very first
|
||||
// rebuildSwatches() call below (including from existingColors, if
|
||||
// any were passed in).
|
||||
m_swatchesLayout = new QVBoxLayout;
|
||||
|
||||
auto *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->addLayout(grid);
|
||||
mainLayout->addWidget(m_hintLabel);
|
||||
mainLayout->addLayout(m_swatchesLayout);
|
||||
mainLayout->addWidget(buttons);
|
||||
|
||||
connect(m_sourceLabel, &ClickableImageLabel::colorPicked, this, &ImageTransparentColorDialog::onColorPicked);
|
||||
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
|
||||
row for it. The new colour is seeded with whatever tolerance was
|
||||
last used (not some fixed default), so successive picks in one
|
||||
session feel consistent.
|
||||
@param color the colour sampled from the source image
|
||||
*/
|
||||
void ImageTransparentColorDialog::onColorPicked(const QColor &color)
|
||||
{
|
||||
for (const PickedColor &existing : std::as_const(m_pickedColors))
|
||||
if (existing.color == color)
|
||||
return;
|
||||
|
||||
m_pickedColors.append({color, m_lastToleranceUsed});
|
||||
updateOkEnabled();
|
||||
rebuildSwatches();
|
||||
updatePreview();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ImageTransparentColorDialog::setToleranceForIndex
|
||||
Updates one colour's own tolerance in place -- deliberately doesn't
|
||||
call rebuildSwatches(): only a number changed, not which rows exist
|
||||
or what order they're in, so recreating every row's widgets (and,
|
||||
worse, the very slider currently being dragged) on each tick would
|
||||
be both wasteful and liable to interrupt the drag itself.
|
||||
@param index which entry in m_pickedColors changed
|
||||
@param value its new tolerance, 0-100
|
||||
*/
|
||||
void ImageTransparentColorDialog::setToleranceForIndex(int index, int value)
|
||||
{
|
||||
if (index < 0 || index >= m_pickedColors.size())
|
||||
return;
|
||||
m_pickedColors[index].tolerance = value;
|
||||
m_lastToleranceUsed = value;
|
||||
updatePreview();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ImageTransparentColorDialog::updateOkEnabled
|
||||
OK is disabled only for the one case where accepting would genuinely
|
||||
do nothing: a dialog that started with no colours and still has
|
||||
none. Removing every colour from a dialog that DID start with some
|
||||
is a different, meaningful action -- clearing all transparency back
|
||||
to a plain, opaque image -- and has to stay confirmable, not silently
|
||||
blocked the same way. Re-run after every add or remove, since either
|
||||
can cross the "any colours at all, ever" threshold in either
|
||||
direction.
|
||||
*/
|
||||
void ImageTransparentColorDialog::updateOkEnabled()
|
||||
{
|
||||
m_okButton->setEnabled(!m_pickedColors.isEmpty() || m_startedWithColors);
|
||||
}
|
||||
|
||||
/**
|
||||
@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 index position in m_pickedColors to remove
|
||||
*/
|
||||
void ImageTransparentColorDialog::removeColor(int index)
|
||||
{
|
||||
if (index < 0 || index >= m_pickedColors.size())
|
||||
return;
|
||||
|
||||
m_pickedColors.removeAt(index);
|
||||
updateOkEnabled();
|
||||
rebuildSwatches();
|
||||
updatePreview();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ImageTransparentColorDialog::rebuildSwatches
|
||||
Rebuilds the list of picked-colour rows from scratch against the
|
||||
current m_pickedColors -- simpler and safer than trying to
|
||||
incrementally add/remove individual rows 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. Each row is a
|
||||
colour swatch (a plain, round, non-interactive indicator -- there's
|
||||
nothing left to click it FOR, now that each row carries its own
|
||||
slider instead of one shared slider needing a row selected first),
|
||||
that row's own tolerance slider, and a small "x" button that
|
||||
removes it.
|
||||
*/
|
||||
void ImageTransparentColorDialog::rebuildSwatches()
|
||||
{
|
||||
QLayoutItem *item;
|
||||
while ((item = m_swatchesLayout->takeAt(0)) != nullptr)
|
||||
{
|
||||
delete item->widget();
|
||||
delete item;
|
||||
}
|
||||
|
||||
for (int i = 0; i < m_pickedColors.size(); ++i)
|
||||
{
|
||||
const PickedColor &pc = m_pickedColors.at(i);
|
||||
|
||||
auto *row = new QWidget(this);
|
||||
auto *rowLayout = new QHBoxLayout(row);
|
||||
rowLayout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
auto *swatch = new QLabel(row);
|
||||
swatch->setFixedSize(24, 24);
|
||||
swatch->setStyleSheet(QStringLiteral(
|
||||
"background-color: rgb(%1,%2,%3); border: 1px solid palette(mid); border-radius: 12px;")
|
||||
.arg(pc.color.red()).arg(pc.color.green()).arg(pc.color.blue()));
|
||||
swatch->setToolTip(tr("rgb(%1, %2, %3)").arg(pc.color.red()).arg(pc.color.green()).arg(pc.color.blue()));
|
||||
|
||||
auto *slider = new QSlider(Qt::Horizontal, row);
|
||||
slider->setRange(0, 100);
|
||||
slider->setValue(pc.tolerance);
|
||||
slider->setToolTip(tr("Tolérance pour cette couleur"));
|
||||
connect(slider, &QSlider::valueChanged, this, [this, i](int value) { setToleranceForIndex(i, value); });
|
||||
|
||||
auto *removeButton = new QToolButton(row);
|
||||
removeButton->setText(QStringLiteral("×"));
|
||||
removeButton->setToolTip(tr("Retirer cette couleur"));
|
||||
connect(removeButton, &QToolButton::clicked, this, [this, i]() { removeColor(i); });
|
||||
|
||||
rowLayout->addWidget(swatch);
|
||||
rowLayout->addWidget(slider);
|
||||
rowLayout->addWidget(removeButton);
|
||||
m_swatchesLayout->addWidget(row);
|
||||
}
|
||||
|
||||
m_hintLabel->setText(m_pickedColors.isEmpty()
|
||||
? tr("Cliquez sur l'image pour choisir une couleur")
|
||||
: tr("Cliquez sur l'image pour ajouter une couleur. Ajustez la tolérance de chaque couleur avec son curseur, ou cliquez sur × pour la retirer."));
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ImageTransparentColorDialog::updatePreview
|
||||
Recomputes the checkerboard-backed preview against the current set
|
||||
of picked colours, each at its own tolerance. Runs against
|
||||
m_previewSourceImage (the same downsampled copy ClickableImageLabel
|
||||
already computed for its own display, not the full-resolution
|
||||
m_sourceImage) -- this fires on every tolerance slider tick, not
|
||||
just on release, and a full-resolution O(width*height*colours) pass
|
||||
per tick was visibly laggy on a large source image. resultPixmap(),
|
||||
below, still computes the final, committed result at full
|
||||
resolution -- only this live preview is downsampled. Always
|
||||
re-evaluates every picked colour from scratch against
|
||||
m_previewSourceImage, not any already-keyed result, so adjusting a
|
||||
tolerance or adding/removing a colour never compounds successive
|
||||
passes.
|
||||
*/
|
||||
void ImageTransparentColorDialog::updatePreview()
|
||||
{
|
||||
const QImage keyed = applyColorKey(m_previewSourceImage, m_pickedColors);
|
||||
m_previewLabel->setPixmap(onCheckerboard(keyed));
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ImageTransparentColorDialog::resultPixmap
|
||||
@return the colour-keyed pixmap against every picked colour, each at
|
||||
its own tolerance, 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));
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ImageTransparentColorDialog::applyColorKey
|
||||
Binary transparency within tolerance, not a smooth falloff: every
|
||||
pixel within a colour's own `tolerance` (0-100, mapped onto the
|
||||
maximum possible RGB distance) of that colour becomes fully
|
||||
transparent, everything else keeps its existing alpha untouched --
|
||||
each of keyColors is checked against its own threshold, not one
|
||||
shared across all of them, since a background colour and a border
|
||||
colour rarely call for the same looseness of match. 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 (each with its own tolerance) to make transparent
|
||||
@return the resulting image, always in Format_ARGB32
|
||||
*/
|
||||
QImage ImageTransparentColorDialog::applyColorKey(const QImage &source, const QList<PickedColor> &keyColors)
|
||||
{
|
||||
QImage result = source.convertToFormat(QImage::Format_ARGB32);
|
||||
if (keyColors.isEmpty())
|
||||
return result;
|
||||
|
||||
struct KeyEntry { QRgb rgb; qint64 threshold; };
|
||||
QVector<KeyEntry> keys;
|
||||
keys.reserve(keyColors.size());
|
||||
for (const PickedColor &pc : keyColors)
|
||||
{
|
||||
const qint64 threshold = qint64(pc.tolerance) * pc.tolerance * 3 * 255 * 255 / (100 * 100);
|
||||
keys.append({pc.color.rgb(), threshold});
|
||||
}
|
||||
|
||||
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 KeyEntry &key : std::as_const(keys))
|
||||
{
|
||||
const int dr = qRed(px) - qRed(key.rgb), dg = qGreen(px) - qGreen(key.rgb), db = qBlue(px) - qBlue(key.rgb);
|
||||
const qint64 distSq = qint64(dr) * dr + qint64(dg) * dg + qint64(db) * db;
|
||||
if (distSq <= key.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,161 @@
|
||||
/*
|
||||
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 QVBoxLayout;
|
||||
class QPushButton;
|
||||
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);
|
||||
|
||||
/// The same downsampled image already computed for display --
|
||||
/// reused as the live-preview source so a tolerance drag runs
|
||||
/// its per-tick colour-key pass against a small image instead
|
||||
/// of the full-resolution one, which on a large source (a
|
||||
/// scanned schematic background, several Mpx) made every
|
||||
/// intermediate slider tick visibly lag. resultPixmap() still
|
||||
/// computes the final, committed result from the true
|
||||
/// full-resolution source -- only the live preview is
|
||||
/// downsampled.
|
||||
QImage displayImage() const { return m_displayImage; }
|
||||
|
||||
signals:
|
||||
void colorPicked(const QColor &color);
|
||||
|
||||
protected:
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
|
||||
private:
|
||||
QImage m_source;
|
||||
QImage m_displayImage;
|
||||
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 -- each shown as its own row: a colour swatch, a
|
||||
slider for that colour's own tolerance right next to it, and a
|
||||
remove button. Each colour keeps its own tolerance rather than
|
||||
sharing one: a white background and a grey border rarely need the
|
||||
same looseness of match, and forcing one tolerance onto both meant
|
||||
either the background left ragged edges or the border ate into
|
||||
content near it. A live checkerboard-backed preview of the combined
|
||||
result updates as any slider moves. 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:
|
||||
/// One picked colour and the tolerance it's individually keyed
|
||||
/// with -- the whole point of this being a struct rather than
|
||||
/// two parallel lists is that the two can never drift out of
|
||||
/// index alignment with each other.
|
||||
struct PickedColor
|
||||
{
|
||||
QColor color;
|
||||
int tolerance;
|
||||
};
|
||||
|
||||
/// @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 (with their individual
|
||||
/// tolerances) already keyed out of basePixmap in a previous
|
||||
/// session, shown as rows from the start rather than forcing
|
||||
/// them to be re-picked from scratch.
|
||||
explicit ImageTransparentColorDialog(const QPixmap &basePixmap,
|
||||
const QList<PickedColor> &existingColors = {}, QWidget *parent = nullptr);
|
||||
|
||||
/// The resulting pixmap: basePixmap unchanged if no colour is
|
||||
/// picked, colour-keyed against every picked colour (each at
|
||||
/// its own tolerance) otherwise.
|
||||
QPixmap resultPixmap() const;
|
||||
/// The final set of picked colours and their individual
|
||||
/// tolerances, for the caller to remember across dialog
|
||||
/// sessions -- may differ from existingColors if any were
|
||||
/// added, removed, or had their tolerance adjusted.
|
||||
QList<PickedColor> pickedColors() const { return m_pickedColors; }
|
||||
|
||||
/// 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 a colour's own `tolerance` (0-100, mapped
|
||||
/// onto the maximum possible RGB distance) of that colour
|
||||
/// 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<PickedColor> &keyColors);
|
||||
|
||||
private slots:
|
||||
void onColorPicked(const QColor &color);
|
||||
|
||||
private:
|
||||
void setToleranceForIndex(int index, int value);
|
||||
void removeColor(int index);
|
||||
void rebuildSwatches();
|
||||
void updatePreview();
|
||||
void updateOkEnabled();
|
||||
static QPixmap onCheckerboard(const QImage &image);
|
||||
|
||||
QImage m_sourceImage;
|
||||
QImage m_previewSourceImage; // downsampled -- see ClickableImageLabel::displayImage()'s comment for why
|
||||
QList<PickedColor> m_pickedColors;
|
||||
bool m_startedWithColors = false; // whether existingColors was non-empty -- see updateOkEnabled()'s comment for why this matters
|
||||
int m_lastToleranceUsed = 10; // seeds a newly-picked colour's own tolerance, so successive picks in one session feel consistent rather than each resetting to some fixed default
|
||||
|
||||
ClickableImageLabel *m_sourceLabel;
|
||||
QLabel *m_previewLabel;
|
||||
QVBoxLayout *m_swatchesLayout; // one row per picked colour, stacked vertically -- each row now carries its own slider, too wide to lay out side by side the way plain swatches once were
|
||||
QLabel *m_hintLabel;
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||