mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-27 12:34:14 +02:00
Merge pull request #1073 from ispyisail/feat/1069-snap-to-grid
Add "Snap to grid" for selected symbols, pictures and texts
This commit is contained in:
@@ -836,6 +836,8 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/undocommand/removediagramcommand.h
|
||||
${QET_DIR}/sources/undocommand/setautonumcontextcommand.cpp
|
||||
${QET_DIR}/sources/undocommand/setautonumcontextcommand.h
|
||||
${QET_DIR}/sources/undocommand/alignselectioncommand.cpp
|
||||
${QET_DIR}/sources/undocommand/alignselectioncommand.h
|
||||
${QET_DIR}/sources/undocommand/rotateselectioncommand.cpp
|
||||
${QET_DIR}/sources/undocommand/rotateselectioncommand.h
|
||||
${QET_DIR}/sources/undocommand/promoteshapecommand.cpp
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
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 ALIGNMENT_H
|
||||
#define ALIGNMENT_H
|
||||
|
||||
#include "textgrid.h"
|
||||
|
||||
#include <QPointF>
|
||||
|
||||
/**
|
||||
The geometry behind the align commands, kept free of any scene so it
|
||||
can be tested on its own.
|
||||
*/
|
||||
namespace Alignment
|
||||
{
|
||||
/**
|
||||
@return the movement that puts p on a grid of x_grid by y_grid,
|
||||
divided by divisor as TextGrid::snap() does, or a null point when
|
||||
p is already on it. Unlike Diagram::snapToGrid(), this never looks
|
||||
at the keyboard: a command run from a shortcut with Ctrl in it must
|
||||
not quietly round to the pixel instead.
|
||||
Less than a millionth of a pixel counts as on the grid: positions
|
||||
that went through arithmetic carry residues of that size, and
|
||||
qFuzzyIsNull() (1e-12) is too strict to absorb them.
|
||||
*/
|
||||
inline QPointF gridOffset(const QPointF &p, int x_grid, int y_grid, qreal divisor = 1)
|
||||
{
|
||||
const QPointF offset = TextGrid::snap(p, x_grid, y_grid, divisor) - p;
|
||||
auto clean = [](qreal v) { return qAbs(v) < 1e-6 ? 0.0 : v; };
|
||||
return QPointF(clean(offset.x()), clean(offset.y()));
|
||||
}
|
||||
}
|
||||
|
||||
#endif // ALIGNMENT_H
|
||||
@@ -1782,6 +1782,7 @@ QList<QAction *> DiagramView::contextMenuActions() const
|
||||
list << qde->m_conductor_reset;
|
||||
list << m_separators.at(1);
|
||||
list << qde->m_selection_actions_group.actions();
|
||||
list << qde->m_align_menu->menuAction();
|
||||
list << m_separators.at(2);
|
||||
list << qde->m_depth_action_group->actions();
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
#include "undocommand/addelementtextcommand.h"
|
||||
#include "utils/qetsettings.h"
|
||||
#include "utils/qetutils.h"
|
||||
#include "undocommand/alignselectioncommand.h"
|
||||
#include "undocommand/rotateselectioncommand.h"
|
||||
#include "undocommand/rotatetextscommand.h"
|
||||
#include "diagram.h"
|
||||
@@ -96,6 +97,7 @@ QETDiagramEditor::QETDiagramEditor(const QStringList &files, QWidget *parent) :
|
||||
QETMainWindow(parent),
|
||||
m_row_column_actions_group (this),
|
||||
m_selection_actions_group (this),
|
||||
m_align_actions_group (this),
|
||||
m_add_item_actions_group (this),
|
||||
m_zoom_actions_group (this),
|
||||
m_select_actions_group (this),
|
||||
@@ -921,6 +923,15 @@ void QETDiagramEditor::setUpActions()
|
||||
|
||||
connect(&m_selection_actions_group, &QActionGroup::triggered, this, &QETDiagramEditor::selectionGroupTriggered);
|
||||
|
||||
//Align actions. No default shortcut: they are reached from the
|
||||
//Edit menu, the selection's context menu and the command search,
|
||||
//and a user can bind one if they want.
|
||||
QAction *snap_to_grid = m_align_actions_group.addAction(tr("Aligner sur la grille"));
|
||||
ShortcutManager::instance().registerAction(snap_to_grid, "diagrameditor.snap_selection_to_grid", tr("Éditeur de schémas"), QKeySequence());
|
||||
snap_to_grid->setStatusTip(tr("Remet les éléments, images et textes sélectionnés sur la grille", "status bar tip"));
|
||||
snap_to_grid->setData("snap_selection_to_grid");
|
||||
connect(&m_align_actions_group, &QActionGroup::triggered, this, &QETDiagramEditor::alignGroupTriggered);
|
||||
|
||||
//Select Action
|
||||
QAction *select_all = m_select_actions_group.addAction( QET::Icons::EditSelectAll, tr("Tout sélectionner") );
|
||||
QAction *select_nothing = m_select_actions_group.addAction( QET::Icons::EditSelectNone, tr("Désélectionner tout") );
|
||||
@@ -1215,6 +1226,8 @@ void QETDiagramEditor::setUpMenu()
|
||||
menu_edition -> addActions(m_select_actions_group.actions());
|
||||
menu_edition -> addSeparator();
|
||||
menu_edition -> addActions(m_selection_actions_group.actions());
|
||||
m_align_menu = menu_edition -> addMenu(tr("Aligner"));
|
||||
m_align_menu -> addActions(m_align_actions_group.actions());
|
||||
menu_edition -> addSeparator();
|
||||
menu_edition -> addAction(m_conductor_reset);
|
||||
menu_edition -> addSeparator();
|
||||
@@ -2065,6 +2078,45 @@ void QETDiagramEditor::selectionGroupTriggered(QAction *action)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETDiagramEditor::alignGroupTriggered
|
||||
Run the align action @a action on the selection of the current diagram,
|
||||
and say in the status bar what it did, including when there was nothing
|
||||
to do or when locked items were left in place.
|
||||
@param action
|
||||
*/
|
||||
void QETDiagramEditor::alignGroupTriggered(QAction *action)
|
||||
{
|
||||
DiagramView *dv = currentDiagramView();
|
||||
if (!dv || action->data().toString() != QLatin1String("snap_selection_to_grid"))
|
||||
return;
|
||||
|
||||
Diagram *diagram = dv->diagram();
|
||||
auto *command = new AlignSelectionCommand(diagram, AlignSelectionCommand::SnapToGrid);
|
||||
const int locked = command->lockedCount();
|
||||
|
||||
QString message;
|
||||
if (command->isValid())
|
||||
{
|
||||
message = tr("%n objet(s) remis sur la grille", "", command->movedCount());
|
||||
diagram->undoStack().push(command);
|
||||
}
|
||||
else
|
||||
{
|
||||
message = tr("La sélection est déjà sur la grille");
|
||||
delete command;
|
||||
}
|
||||
if (locked)
|
||||
message += QLatin1Char(' ') + tr("(%n objet(s) verrouillé(s) laissé(s) en place)", "", locked);
|
||||
|
||||
//Queued, not shown at once: when the action comes from a menu or the
|
||||
//command search, closing it queues events that would clear a message
|
||||
//shown right now.
|
||||
QTimer::singleShot(0, this, [this, message]() {
|
||||
statusBar()->showMessage(message, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
void QETDiagramEditor::rowColumnGroupTriggered(QAction *action)
|
||||
{
|
||||
QString value = action->data().toString();
|
||||
@@ -2201,6 +2253,7 @@ void QETDiagramEditor::slot_updateComplexActions()
|
||||
<< m_group_selected_texts;
|
||||
for(QAction *action : action_list)
|
||||
action->setEnabled(false);
|
||||
m_align_actions_group.setEnabled(false);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -2322,6 +2375,11 @@ void QETDiagramEditor::slot_updateComplexActions()
|
||||
| DiagramContent::Shapes
|
||||
| DiagramContent::Images);
|
||||
m_depth_action_group->setEnabled(list.isEmpty()? false : true);
|
||||
|
||||
//Align actions: symbols, pictures and free texts take part
|
||||
m_align_actions_group.setEnabled(!ro && (selected_elements_count
|
||||
|| selected_image
|
||||
|| dc.count(DiagramContent::TextFields)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -122,6 +122,7 @@ class QETDiagramEditor : public QETMainWindow
|
||||
void selectGroupTriggered (QAction *action);
|
||||
void addItemGroupTriggered (QAction *action);
|
||||
void selectionGroupTriggered (QAction *action);
|
||||
void alignGroupTriggered (QAction *action);
|
||||
void rowColumnGroupTriggered (QAction *action);
|
||||
void slot_updateActions();
|
||||
void slot_updateUndoStack();
|
||||
@@ -189,10 +190,12 @@ class QETDiagramEditor : public QETMainWindow
|
||||
QActionGroup
|
||||
m_row_column_actions_group, /// Action related to add/remove rows/column in diagram
|
||||
m_selection_actions_group, ///Action related to edit a selected item
|
||||
m_align_actions_group, ///Action related to align the selected items
|
||||
*m_depth_action_group = nullptr;
|
||||
|
||||
QMenu
|
||||
*m_add_item_menu = nullptr, ///< Submenu of m_add_item_actions_group
|
||||
*m_align_menu = nullptr, ///< Submenu of m_align_actions_group
|
||||
*m_row_column_menu = nullptr; ///< Submenu of m_row_column_actions_group
|
||||
|
||||
private:
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
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 "alignselectioncommand.h"
|
||||
|
||||
#include "../QPropertyUndoCommand/qpropertyundocommand.h"
|
||||
#include "../alignment.h"
|
||||
#include "../diagram.h"
|
||||
#include "../diagramcontent.h"
|
||||
#include "../qetgraphicsitem/diagramimageitem.h"
|
||||
#include "../qetgraphicsitem/element.h"
|
||||
#include "../qetgraphicsitem/independenttextitem.h"
|
||||
|
||||
#include <QSettings>
|
||||
|
||||
/**
|
||||
@brief AlignSelectionCommand::AlignSelectionCommand
|
||||
Works out the movement of every selected item for @a mode. Nothing
|
||||
moves until the command is pushed.
|
||||
Elements redraw their conductors themselves when their position
|
||||
changes (see Element's constructor), so a plain "pos" property undo
|
||||
per item is enough.
|
||||
@param diagram : diagram whose selection is aligned
|
||||
@param mode : what to align the selection to
|
||||
@param parent : parent undo command
|
||||
*/
|
||||
AlignSelectionCommand::AlignSelectionCommand(Diagram *diagram, Mode mode, QUndoCommand *parent) :
|
||||
QUndoCommand(parent),
|
||||
m_diagram(diagram)
|
||||
{
|
||||
Q_UNUSED(mode)
|
||||
|
||||
DiagramContent dc(diagram);
|
||||
m_locked_count = dc.removeNonMovableItems();
|
||||
|
||||
QSettings settings;
|
||||
const int x_grid = settings.value(QStringLiteral("diagrameditor/Xgrid"), Diagram::xGrid).toInt();
|
||||
const int y_grid = settings.value(QStringLiteral("diagrameditor/Ygrid"), Diagram::yGrid).toInt();
|
||||
const qreal text_divisor = settings.value(TextGrid::settings_key, 1).toReal();
|
||||
|
||||
auto move = [this](QGraphicsObject *item, const QPointF &offset)
|
||||
{
|
||||
if (!offset.isNull())
|
||||
new QPropertyUndoCommand(item, "pos", item->pos(), item->pos() + offset, this);
|
||||
};
|
||||
|
||||
//Each kind goes where dragging it would have left it: symbols and
|
||||
//pictures on the folio grid, free texts on the text grid.
|
||||
//Shapes are left out: they are made of several points and no single
|
||||
//one of them is the obvious one to snap.
|
||||
for (Element *element : std::as_const(dc.m_elements))
|
||||
move(element, Alignment::gridOffset(element->pos(), x_grid, y_grid));
|
||||
for (DiagramImageItem *image : std::as_const(dc.m_images))
|
||||
move(image, Alignment::gridOffset(image->pos(), x_grid, y_grid));
|
||||
for (IndependentTextItem *text : std::as_const(dc.m_text_fields))
|
||||
move(text, Alignment::gridOffset(text->pos(), x_grid, y_grid, text_divisor));
|
||||
|
||||
setText(QObject::tr("Aligner %n objet(s) sur la grille", "", childCount()));
|
||||
}
|
||||
|
||||
/**
|
||||
@brief AlignSelectionCommand::undo
|
||||
*/
|
||||
void AlignSelectionCommand::undo()
|
||||
{
|
||||
if (m_diagram)
|
||||
m_diagram->showMe();
|
||||
QUndoCommand::undo();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief AlignSelectionCommand::redo
|
||||
*/
|
||||
void AlignSelectionCommand::redo()
|
||||
{
|
||||
if (m_diagram)
|
||||
m_diagram->showMe();
|
||||
QUndoCommand::redo();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief AlignSelectionCommand::isValid
|
||||
@return true if this command moves at least one item.
|
||||
*/
|
||||
bool AlignSelectionCommand::isValid() const
|
||||
{
|
||||
return childCount() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@return the number of items this command moves.
|
||||
*/
|
||||
int AlignSelectionCommand::movedCount() const
|
||||
{
|
||||
return childCount();
|
||||
}
|
||||
|
||||
/**
|
||||
@return the number of selected items left in place because their
|
||||
position is locked.
|
||||
*/
|
||||
int AlignSelectionCommand::lockedCount() const
|
||||
{
|
||||
return m_locked_count;
|
||||
}
|
||||
@@ -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 ALIGNSELECTIONCOMMAND_H
|
||||
#define ALIGNSELECTIONCOMMAND_H
|
||||
|
||||
#include <QPointer>
|
||||
#include <QUndoCommand>
|
||||
|
||||
class Diagram;
|
||||
|
||||
/**
|
||||
@brief The AlignSelectionCommand class
|
||||
Moves each selected item by its own amount, as one undo step.
|
||||
Symbols, pictures and free texts take part; locked items are left
|
||||
where they are and counted, so the caller can say so.
|
||||
*/
|
||||
class AlignSelectionCommand : public QUndoCommand
|
||||
{
|
||||
public:
|
||||
enum Mode {
|
||||
SnapToGrid ///< put each item where a drag would have left it
|
||||
};
|
||||
|
||||
AlignSelectionCommand(Diagram *diagram, Mode mode, QUndoCommand *parent = nullptr);
|
||||
|
||||
void undo() override;
|
||||
void redo() override;
|
||||
|
||||
bool isValid() const;
|
||||
int movedCount() const;
|
||||
int lockedCount() const;
|
||||
|
||||
private:
|
||||
QPointer<Diagram> m_diagram;
|
||||
int m_locked_count = 0;
|
||||
};
|
||||
|
||||
#endif // ALIGNSELECTIONCOMMAND_H
|
||||
@@ -104,6 +104,13 @@ add_test(NAME tst_textgrid COMMAND tst_textgrid)
|
||||
target_include_directories(tst_textgrid PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_textgrid PRIVATE Qt::Test)
|
||||
|
||||
# alignment.h is header-only too (it builds on textgrid.h): the geometry
|
||||
# behind the align commands, tested without a scene.
|
||||
add_executable(tst_alignment tst_alignment.cpp)
|
||||
add_test(NAME tst_alignment COMMAND tst_alignment)
|
||||
target_include_directories(tst_alignment PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_alignment PRIVATE Qt::Test)
|
||||
|
||||
add_executable(
|
||||
tst_qetpalette
|
||||
tst_qetpalette.cpp
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
#include <QtTest>
|
||||
|
||||
#include "alignment.h"
|
||||
|
||||
class tst_alignment : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void gridOffset_data()
|
||||
{
|
||||
QTest::addColumn<QPointF>("pos");
|
||||
QTest::addColumn<int>("x_grid");
|
||||
QTest::addColumn<int>("y_grid");
|
||||
QTest::addColumn<qreal>("divisor");
|
||||
QTest::addColumn<QPointF>("expected");
|
||||
|
||||
QTest::newRow("on the grid") << QPointF(100, 50) << 10 << 10 << 1.0 << QPointF(0, 0);
|
||||
QTest::newRow("5 px right, fine nudge") << QPointF(105, 50) << 10 << 10 << 1.0 << QPointF(5, 0);
|
||||
QTest::newRow("either side") << QPointF(103, 97) << 10 << 10 << 1.0 << QPointF(-3, 3);
|
||||
QTest::newRow("negative, fraction") << QPointF(-13.3, 13.7) << 10 << 10 << 1.0 << QPointF(3.3, -3.7);
|
||||
QTest::newRow("uneven grid") << QPointF(12, 13) << 10 << 5 << 1.0 << QPointF(-2, 2);
|
||||
QTest::newRow("text grid 1:2") << QPointF(103, 97) << 10 << 10 << 2.0 << QPointF(2, -2);
|
||||
QTest::newRow("text grid off") << QPointF(103.4, 96.6) << 10 << 10 << 0.0 << QPointF(-0.4, 0.4);
|
||||
}
|
||||
|
||||
void gridOffset()
|
||||
{
|
||||
QFETCH(QPointF, pos);
|
||||
QFETCH(int, x_grid);
|
||||
QFETCH(int, y_grid);
|
||||
QFETCH(qreal, divisor);
|
||||
QFETCH(QPointF, expected);
|
||||
|
||||
const QPointF offset = Alignment::gridOffset(pos, x_grid, y_grid, divisor);
|
||||
QVERIFY2(qAbs(offset.x() - expected.x()) < 1e-9, qPrintable(QString::number(offset.x())));
|
||||
QVERIFY2(qAbs(offset.y() - expected.y()) < 1e-9, qPrintable(QString::number(offset.y())));
|
||||
QVERIFY(qAbs(pos.x() + offset.x() - TextGrid::snap(pos, x_grid, y_grid, divisor).x()) < 1e-9);
|
||||
}
|
||||
|
||||
// An item already on the grid must give an exactly null offset, not a
|
||||
// rounding residue: the command pushes no undo step only when every
|
||||
// offset is null.
|
||||
void onGridIsExactlyNull()
|
||||
{
|
||||
for (int k = -50; k <= 50; ++k) {
|
||||
const QPointF on_grid(k * 10, -k * 10);
|
||||
QVERIFY(Alignment::gridOffset(on_grid, 10, 10).isNull());
|
||||
QVERIFY(Alignment::gridOffset(on_grid + QPointF(1e-10, -1e-10), 10, 10).isNull());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_APPLESS_MAIN(tst_alignment)
|
||||
#include "tst_alignment.moc"
|
||||
Reference in New Issue
Block a user