Merge master to resolve conflicts with snap-to-grid (#1073)

Both branches added an undo command, an include and a test target at
the same spots; kept both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ispyisail
2026-09-28 05:40:05 +13:00
46 changed files with 96071 additions and 52051 deletions
@@ -13,7 +13,7 @@
"finish-args": [
"--socket=wayland",
"--socket=fallback-x11",
"--device=dri",
"--device=all",
"--share=ipc",
"--share=network",
"--socket=cups",
@@ -33,6 +33,23 @@
"modules": [
"tkinter.json",
"pypi-dependencies.json",
{
"name": "hidapi",
"buildsystem": "cmake-ninja",
"config-opts": [
"-DCMAKE_INSTALL_PREFIX=/app",
"-DCMAKE_BUILD_TYPE=Release",
"-DHIDAPI_BUILD_HIDTEST=OFF",
"-DHIDAPI_WITH_TESTS=OFF"
],
"sources": [
{
"type": "git",
"url": "https://github.com/libusb/hidapi.git",
"tag": "hidapi-0.15.0"
}
]
},
{
"name": "qelectrotech",
"buildsystem": "cmake",
@@ -45,7 +62,9 @@
"-DBUILD_PUGIXML=ON",
"-DFETCHCONTENT_FULLY_DISCONNECTED=ON",
"-DFETCHCONTENT_SOURCE_DIR_PUGIXML=/run/build/qelectrotech/pugixml",
"-DQET_EXPORT_PROJECT_DB=ON"
"-DQET_EXPORT_PROJECT_DB=ON",
"-DQET_ENABLE_SPACEMOUSE=ON",
"-DQET_SPACEMOUSE_BACKEND=hid"
],
"post-install": [
"mv ${FLATPAK_DEST}/share/mime/packages/qelectrotech.xml ${FLATPAK_DEST}/share/mime/packages/org.qelectrotech.QElectroTech.xml"
+2
View File
@@ -840,6 +840,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/undocommand/setautonumcontextcommand.h
${QET_DIR}/sources/undocommand/groupitemscommand.cpp
${QET_DIR}/sources/undocommand/groupitemscommand.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
+2968 -1648
View File
File diff suppressed because it is too large Load Diff
+3200 -1489
View File
File diff suppressed because it is too large Load Diff
+2954 -1527
View File
File diff suppressed because it is too large Load Diff
+2915 -1522
View File
File diff suppressed because it is too large Load Diff
+2801 -1380
View File
File diff suppressed because it is too large Load Diff
+2915 -1522
View File
File diff suppressed because it is too large Load Diff
+3075 -1669
View File
File diff suppressed because it is too large Load Diff
+2896 -1571
View File
File diff suppressed because it is too large Load Diff
+2813 -1735
View File
File diff suppressed because it is too large Load Diff
+2860 -1714
View File
File diff suppressed because it is too large Load Diff
+2867 -1706
View File
File diff suppressed because it is too large Load Diff
+2773 -1385
View File
File diff suppressed because it is too large Load Diff
+2903 -1566
View File
File diff suppressed because it is too large Load Diff
+2873 -1584
View File
File diff suppressed because it is too large Load Diff
+2900 -1529
View File
File diff suppressed because it is too large Load Diff
+2879 -1562
View File
File diff suppressed because it is too large Load Diff
+2900 -1579
View File
File diff suppressed because it is too large Load Diff
+2926 -1525
View File
File diff suppressed because it is too large Load Diff
+2798 -1389
View File
File diff suppressed because it is too large Load Diff
+2777 -1699
View File
File diff suppressed because it is too large Load Diff
+3228 -1489
View File
File diff suppressed because it is too large Load Diff
+2842 -1685
View File
File diff suppressed because it is too large Load Diff
+2799 -1386
View File
File diff suppressed because it is too large Load Diff
+2878 -1678
View File
File diff suppressed because it is too large Load Diff
+2862 -1766
View File
File diff suppressed because it is too large Load Diff
+2943 -1526
View File
File diff suppressed because it is too large Load Diff
+2838 -1742
View File
File diff suppressed because it is too large Load Diff
+2859 -1745
View File
File diff suppressed because it is too large Load Diff
+2838 -1742
View File
File diff suppressed because it is too large Load Diff
+2927 -1526
View File
File diff suppressed because it is too large Load Diff
+2897 -1527
View File
File diff suppressed because it is too large Load Diff
+2943 -1526
View File
File diff suppressed because it is too large Load Diff
+2773 -1385
View File
File diff suppressed because it is too large Load Diff
+49
View File
@@ -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
+16 -2
View File
@@ -27,6 +27,20 @@ const double Createdxf::sheetWidth = 4000;
const double Createdxf::sheetHeight = 2700;
double Createdxf::xScale = 1;
namespace {
/// @p text on one line. A DXF value ends at the line break, so a break
/// inside a text would shift every group code after it by one line and
/// a reader would take values for codes (FINDINGS F052). Callers that
/// mean several lines split them first; this is the safety net.
QString singleLine(QString text)
{
text.replace(QLatin1String("\r\n"), QLatin1String(" "));
text.replace(QLatin1Char('\r'), QLatin1Char(' '));
text.replace(QLatin1Char('\n'), QLatin1Char(' '));
return text;
}
}
double Createdxf::yScale = 1;
Createdxf::Createdxf()
@@ -727,7 +741,7 @@ void Createdxf::drawText(
To_Dxf << 41 << "\r\n";
To_Dxf << xScaleW << "\r\n"; // X Scale
To_Dxf << 1 << "\r\n";
To_Dxf << text << "\r\n"; // Text Value
To_Dxf << singleLine(text) << "\r\n"; // Text Value
To_Dxf << 50 << "\r\n";
To_Dxf << rotation << "\r\n"; // Text Rotation
file.close();
@@ -780,7 +794,7 @@ void Createdxf::drawTextAligned(
To_Dxf << 41 << "\r\n";
To_Dxf << xScaleW << "\r\n"; // X Scale
To_Dxf << 1 << "\r\n";
To_Dxf << text << "\r\n"; // Text Value
To_Dxf << singleLine(text) << "\r\n"; // Text Value
To_Dxf << 50 << "\r\n";
To_Dxf << rotation << "\r\n"; // Text Rotation
#if 0
+1
View File
@@ -1789,6 +1789,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();
}
+19 -1
View File
@@ -37,6 +37,7 @@
#include "qetmessagebox.h"
#include <QGraphicsSimpleTextItem>
#include <QSet>
#include <QSvgGenerator>
#include <QtXml>
#include <cmath>
@@ -483,7 +484,24 @@ void ExportDialog::generateDxf(
// QList <Terminal *> list_terminals;
// Determine les elements a "XMLiser"
foreach(QGraphicsItem *qgi, diagram -> items()) {
// In stacking order (z, then insertion order, which a load makes the
// file's order), not items() order: that follows memory addresses, so
// the same folio came out in a different order on every export, as
// saving did before bugtracker #343. A rect query gives stacking order
// even with NoIndex; anything it misses keeps its items() place after
// the rest.
QList<QGraphicsItem *> stacked_items = diagram -> items(
QRectF(-1e9, -1e9, 2e9, 2e9), Qt::IntersectsItemBoundingRect,
Qt::AscendingOrder);
{
const QSet<QGraphicsItem *> ranked(stacked_items.cbegin(), stacked_items.cend());
for (QGraphicsItem *qgi : diagram -> items()) {
if (!ranked.contains(qgi)) {
stacked_items << qgi;
}
}
}
for (QGraphicsItem *qgi : std::as_const(stacked_items)) {
if (Element *elmt = qgraphicsitem_cast<Element *>(qgi)) {
list_elements << elmt;
} else if (Conductor *f = qgraphicsitem_cast<Conductor *>(qgi)) {
+58
View File
@@ -67,6 +67,7 @@
#include "utils/qetsettings.h"
#include "utils/qetutils.h"
#include "undocommand/groupitemscommand.h"
#include "undocommand/alignselectioncommand.h"
#include "undocommand/rotateselectioncommand.h"
#include "undocommand/rotatetextscommand.h"
#include "diagram.h"
@@ -97,6 +98,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),
@@ -932,6 +934,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") );
@@ -1226,6 +1237,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();
@@ -2085,6 +2098,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();
@@ -2223,6 +2275,7 @@ void QETDiagramEditor::slot_updateComplexActions()
<< m_ungroup_selection;
for(QAction *action : action_list)
action->setEnabled(false);
m_align_actions_group.setEnabled(false);
return;
}
@@ -2347,6 +2400,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)));
}
/**
+3
View File
@@ -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:
+48 -22
View File
@@ -1993,32 +1993,58 @@ void TitleBlockTemplate::renderTextCellDxf(
}
//painter.setFont(text_font);
qreal ratio = 1.0;
if (cell.hadjust)
//A cell can hold several lines, which the screen shows one under the
//other (QPainter::drawText()). A DXF TEXT is a single line, and a line
//break inside one breaks the file (FINDINGS F052), so each line gets
//its own, stacked as the cell's vertical alignment says: the first at
//y1 when aligned to the top, the last at y1 when aligned to the
//bottom, the block centred on y1 otherwise. Spacing as the diagram
//texts' DXF export: 1.6 times the text height. A single line is
//written exactly as before.
const QStringList lines = text.split(QRegularExpression(QStringLiteral("\r\n|\r|\n")));
const qreal line_spacing = textHeight * Createdxf::yScale * 1.6;
const int last = lines.size() - 1;
for (int i = 0 ; i < lines.size() ; ++i)
{
// Scale font width to fit string in cell width w
// As DXF font aspect ratio is implementation dependent we add a fudge-factor based on tests with AutoCAD
int len = text.length() * textHeight * Createdxf::xScale * 1.2;
const QString &line = lines.at(i);
if (line.isEmpty()) continue;
if(len > w)
ratio = (w/len);
qreal offset; // upward, from y1
if (vAlign == 3)
offset = -i * line_spacing;
else if (vAlign == 2)
offset = (last / 2.0 - i) * line_spacing;
else
offset = (last - i) * line_spacing;
qreal ratio = 1.0;
if (cell.hadjust)
{
// Scale font width to fit string in cell width w
// As DXF font aspect ratio is implementation dependent we add a fudge-factor based on tests with AutoCAD
int len = line.length() * textHeight * Createdxf::xScale * 1.2;
if(len > w)
ratio = (w/len);
}
// x offset value below currently set heuristically based on appearance...
Createdxf::drawTextAligned(
file_path,
line,
x - 2*Createdxf::xScale,
y1 + offset,
textHeight*Createdxf::yScale,
0,
0,
hAlign,
vAlign,
x2,
ratio,
color);
}
// x offset value below currently set heuristically based on appearance...
Createdxf::drawTextAligned(
file_path,
text,
x - 2*Createdxf::xScale,
y1,
textHeight*Createdxf::yScale,
0,
0,
hAlign,
vAlign,
x2,
ratio,
color);
}
/**
@@ -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
+7
View File
@@ -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)
# itemgroups.cpp keeps group membership and the selection rule apart from
# Diagram, so it is tested here on a plain scene of rectangles.
add_executable(tst_itemgroups tst_itemgroups.cpp ${QET_DIR}/sources/itemgroups.cpp)
+55
View File
@@ -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"