mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-28 04:54:13 +02:00
Merge pull request #1087 from ispyisail/feat/1069-align
Add Align left/centre/right/top/middle/bottom for the selection
This commit is contained in:
@@ -20,7 +20,11 @@
|
||||
|
||||
#include "textgrid.h"
|
||||
|
||||
#include <QList>
|
||||
#include <QPointF>
|
||||
#include <QRectF>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
/**
|
||||
The geometry behind the align commands, kept free of any scene so it
|
||||
@@ -28,6 +32,84 @@
|
||||
*/
|
||||
namespace Alignment
|
||||
{
|
||||
/**
|
||||
What the items of a selection are lined up on.
|
||||
*/
|
||||
enum Edge {
|
||||
Left, ///< left edges, on the left-most one
|
||||
HCenter, ///< reference points, on the mean of their x
|
||||
Right, ///< right edges, on the right-most one
|
||||
Top, ///< top edges, on the top-most one
|
||||
VCenter, ///< reference points, on the mean of their y
|
||||
Bottom ///< bottom edges, on the bottom-most one
|
||||
};
|
||||
|
||||
/**
|
||||
One item to align: its edges, and the point that counts as its
|
||||
centre. For a symbol that point is its origin, where its wires
|
||||
usually leave, not the middle of its drawn shape.
|
||||
*/
|
||||
struct Item {
|
||||
QRectF edges;
|
||||
QPointF ref;
|
||||
};
|
||||
|
||||
/**
|
||||
@return true if aligning on @a edge moves items along x
|
||||
*/
|
||||
inline bool isHorizontal(Edge edge)
|
||||
{
|
||||
return edge == Left || edge == HCenter || edge == Right;
|
||||
}
|
||||
|
||||
/**
|
||||
@return the movement of each of @a items that lines them up on
|
||||
@a edge, in the same order. Items only move across the line they
|
||||
are aligned on: aligning left never moves anything up or down.
|
||||
*/
|
||||
inline QList<QPointF> alignOffsets(const QList<Item> &items, Edge edge)
|
||||
{
|
||||
QList<qreal> values;
|
||||
for (const Item &item : items)
|
||||
{
|
||||
switch (edge) {
|
||||
case Left: values << item.edges.left(); break;
|
||||
case HCenter: values << item.ref.x(); break;
|
||||
case Right: values << item.edges.right(); break;
|
||||
case Top: values << item.edges.top(); break;
|
||||
case VCenter: values << item.ref.y(); break;
|
||||
case Bottom: values << item.edges.bottom(); break;
|
||||
}
|
||||
}
|
||||
|
||||
QList<QPointF> offsets;
|
||||
if (values.isEmpty())
|
||||
return offsets;
|
||||
|
||||
qreal target = 0;
|
||||
switch (edge) {
|
||||
case Left:
|
||||
case Top:
|
||||
target = *std::min_element(values.cbegin(), values.cend());
|
||||
break;
|
||||
case Right:
|
||||
case Bottom:
|
||||
target = *std::max_element(values.cbegin(), values.cend());
|
||||
break;
|
||||
case HCenter:
|
||||
case VCenter:
|
||||
for (qreal v : std::as_const(values))
|
||||
target += v;
|
||||
target /= values.size();
|
||||
break;
|
||||
}
|
||||
|
||||
for (qreal v : std::as_const(values))
|
||||
offsets << (isHorizontal(edge) ? QPointF(target - v, 0)
|
||||
: QPointF(0, target - v));
|
||||
return offsets;
|
||||
}
|
||||
|
||||
/**
|
||||
@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
|
||||
@@ -44,6 +126,25 @@ namespace Alignment
|
||||
auto clean = [](qreal v) { return qAbs(v) < 1e-6 ? 0.0 : v; };
|
||||
return QPointF(clean(offset.x()), clean(offset.y()));
|
||||
}
|
||||
|
||||
/**
|
||||
@return @a offset, the movement alignOffsets() gave an item at
|
||||
@a pos, rounded so the item lands on the grid along the line it is
|
||||
aligned on. The other coordinate is left as it is, even off the
|
||||
grid: aligning left must not also move items up or down.
|
||||
Edges of items whose width is not a whole number of grid steps
|
||||
apart from their origin cannot all land on one line; they end up as
|
||||
close to it as the grid allows.
|
||||
*/
|
||||
inline QPointF snappedOffset(const QPointF &pos, const QPointF &offset, Edge edge,
|
||||
int x_grid, int y_grid, qreal divisor = 1)
|
||||
{
|
||||
const QPointF snap = gridOffset(pos + offset, x_grid, y_grid, divisor);
|
||||
QPointF result = offset + (isHorizontal(edge) ? QPointF(snap.x(), 0)
|
||||
: QPointF(0, snap.y()));
|
||||
auto clean = [](qreal v) { return qAbs(v) < 1e-6 ? 0.0 : v; };
|
||||
return QPointF(clean(result.x()), clean(result.y()));
|
||||
}
|
||||
}
|
||||
|
||||
#endif // ALIGNMENT_H
|
||||
|
||||
@@ -941,6 +941,26 @@ void QETDiagramEditor::setUpActions()
|
||||
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");
|
||||
|
||||
const struct {
|
||||
const char *id;
|
||||
QString text;
|
||||
QString tip;
|
||||
} align_actions[] = {
|
||||
{"align_left", tr("Aligner à gauche"), tr("Aligne les bords gauches des objets sélectionnés", "status bar tip")},
|
||||
{"align_hcenter", tr("Centrer horizontalement"), tr("Aligne les objets sélectionnés sur une même verticale, par leur point d'origine pour les éléments", "status bar tip")},
|
||||
{"align_right", tr("Aligner à droite"), tr("Aligne les bords droits des objets sélectionnés", "status bar tip")},
|
||||
{"align_top", tr("Aligner en haut"), tr("Aligne les bords supérieurs des objets sélectionnés", "status bar tip")},
|
||||
{"align_vcenter", tr("Centrer verticalement"), tr("Aligne les objets sélectionnés sur une même horizontale, par leur point d'origine pour les éléments", "status bar tip")},
|
||||
{"align_bottom", tr("Aligner en bas"), tr("Aligne les bords inférieurs des objets sélectionnés", "status bar tip")}
|
||||
};
|
||||
for (const auto &a : align_actions)
|
||||
{
|
||||
QAction *action = m_align_actions_group.addAction(a.text);
|
||||
ShortcutManager::instance().registerAction(action, QStringLiteral("diagrameditor.") + QLatin1String(a.id), tr("Éditeur de schémas"), QKeySequence());
|
||||
action->setStatusTip(a.tip);
|
||||
action->setData(QString::fromLatin1(a.id));
|
||||
}
|
||||
connect(&m_align_actions_group, &QActionGroup::triggered, this, &QETDiagramEditor::alignGroupTriggered);
|
||||
|
||||
//Select Action
|
||||
@@ -1238,7 +1258,13 @@ void QETDiagramEditor::setUpMenu()
|
||||
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());
|
||||
//Snap to grid, then the horizontal three, then the vertical three,
|
||||
//in the order setUpActions() adds them
|
||||
m_align_menu -> addAction(m_align_actions_group.actions().first());
|
||||
m_align_menu -> addSeparator();
|
||||
m_align_menu -> addActions(m_align_actions_group.actions().mid(1, 3));
|
||||
m_align_menu -> addSeparator();
|
||||
m_align_menu -> addActions(m_align_actions_group.actions().mid(4));
|
||||
menu_edition -> addSeparator();
|
||||
menu_edition -> addAction(m_conductor_reset);
|
||||
menu_edition -> addSeparator();
|
||||
@@ -2108,22 +2134,43 @@ void QETDiagramEditor::selectionGroupTriggered(QAction *action)
|
||||
void QETDiagramEditor::alignGroupTriggered(QAction *action)
|
||||
{
|
||||
DiagramView *dv = currentDiagramView();
|
||||
if (!dv || action->data().toString() != QLatin1String("snap_selection_to_grid"))
|
||||
if (!dv)
|
||||
return;
|
||||
|
||||
static const QHash<QString, AlignSelectionCommand::Mode> modes {
|
||||
{QStringLiteral("snap_selection_to_grid"), AlignSelectionCommand::SnapToGrid},
|
||||
{QStringLiteral("align_left"), AlignSelectionCommand::AlignLeft},
|
||||
{QStringLiteral("align_hcenter"), AlignSelectionCommand::AlignHCenter},
|
||||
{QStringLiteral("align_right"), AlignSelectionCommand::AlignRight},
|
||||
{QStringLiteral("align_top"), AlignSelectionCommand::AlignTop},
|
||||
{QStringLiteral("align_vcenter"), AlignSelectionCommand::AlignVCenter},
|
||||
{QStringLiteral("align_bottom"), AlignSelectionCommand::AlignBottom}
|
||||
};
|
||||
const QString id = action->data().toString();
|
||||
if (!modes.contains(id))
|
||||
return;
|
||||
const AlignSelectionCommand::Mode mode = modes.value(id);
|
||||
|
||||
Diagram *diagram = dv->diagram();
|
||||
auto *command = new AlignSelectionCommand(diagram, AlignSelectionCommand::SnapToGrid);
|
||||
auto *command = new AlignSelectionCommand(diagram, mode);
|
||||
const int locked = command->lockedCount();
|
||||
|
||||
QString message;
|
||||
if (command->isValid())
|
||||
{
|
||||
message = tr("%n objet(s) remis sur la grille", "", command->movedCount());
|
||||
message = mode == AlignSelectionCommand::SnapToGrid
|
||||
? tr("%n objet(s) remis sur la grille", "", command->movedCount())
|
||||
: tr("%n objet(s) aligné(s)", "", command->movedCount());
|
||||
diagram->undoStack().push(command);
|
||||
}
|
||||
else
|
||||
{
|
||||
message = tr("La sélection est déjà sur la grille");
|
||||
if (mode == AlignSelectionCommand::SnapToGrid)
|
||||
message = tr("La sélection est déjà sur la grille");
|
||||
else if (command->itemCount() < 2)
|
||||
message = tr("Sélectionnez au moins deux éléments, images, textes ou groupes non verrouillés");
|
||||
else
|
||||
message = tr("La sélection est déjà alignée, à la grille près");
|
||||
delete command;
|
||||
}
|
||||
if (locked)
|
||||
@@ -2401,10 +2448,15 @@ void QETDiagramEditor::slot_updateComplexActions()
|
||||
| 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)));
|
||||
//Align actions: symbols, pictures and free texts take part.
|
||||
//Snapping needs one of them, lining them up needs two.
|
||||
const int alignable = selected_elements_count
|
||||
+ selected_image
|
||||
+ dc.count(DiagramContent::TextFields);
|
||||
m_align_actions_group.setEnabled(!ro && alignable);
|
||||
const QList<QAction *> align_actions = m_align_actions_group.actions();
|
||||
for (QAction *action : align_actions.mid(1))
|
||||
action->setEnabled(!ro && alignable >= 2);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -97,6 +97,7 @@ class DiagramImageItem : public QetGraphicsItem {
|
||||
QUuid uuid() const {return m_uuid;}
|
||||
void setUuid(const QUuid &uuid) {m_uuid = uuid; emit uuidChanged();}
|
||||
void newUuid() {setUuid(QUuid::createUuid());} //create new uuid for this item
|
||||
QRectF imageRect() const;
|
||||
void editProperty() override;
|
||||
void setPixmap(const QPixmap &pixmap);
|
||||
QPixmap pixmap() const { return pixmap_; }
|
||||
@@ -186,7 +187,6 @@ class DiagramImageItem : public QetGraphicsItem {
|
||||
static QString hintForHandleRole(HandleRole role);
|
||||
void showStatusHint(const QString &text) const;
|
||||
void clearStatusHint() const;
|
||||
QRectF imageRect() const;
|
||||
QRectF labelRect() const;
|
||||
void updateLabelScale();
|
||||
|
||||
|
||||
@@ -21,10 +21,13 @@
|
||||
#include "../alignment.h"
|
||||
#include "../diagram.h"
|
||||
#include "../diagramcontent.h"
|
||||
#include "../itemgroups.h"
|
||||
#include "../qetgraphicsitem/diagramimageitem.h"
|
||||
#include "../qetgraphicsitem/element.h"
|
||||
#include "../qetgraphicsitem/independenttextitem.h"
|
||||
#include "../qetgraphicsitem/qetshapeitem.h"
|
||||
|
||||
#include <QHash>
|
||||
#include <QSettings>
|
||||
|
||||
/**
|
||||
@@ -42,8 +45,6 @@ AlignSelectionCommand::AlignSelectionCommand(Diagram *diagram, Mode mode, QUndoC
|
||||
QUndoCommand(parent),
|
||||
m_diagram(diagram)
|
||||
{
|
||||
Q_UNUSED(mode)
|
||||
|
||||
DiagramContent dc(diagram);
|
||||
m_locked_count = dc.removeNonMovableItems();
|
||||
|
||||
@@ -52,24 +53,130 @@ AlignSelectionCommand::AlignSelectionCommand(Diagram *diagram, Mode mode, QUndoC
|
||||
const int y_grid = settings.value(QStringLiteral("diagrameditor/Ygrid"), Diagram::yGrid).toInt();
|
||||
const qreal text_divisor = settings.value(TextGrid::settings_key, 1).toReal();
|
||||
|
||||
//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.
|
||||
struct Entry {
|
||||
QGraphicsObject *item;
|
||||
Alignment::Item geometry;
|
||||
qreal divisor;
|
||||
};
|
||||
QList<Entry> entries;
|
||||
//A symbol's edges are its own outline, without its texts, and its
|
||||
//centre is its origin point: that is where its wires leave.
|
||||
for (Element *element : std::as_const(dc.m_elements))
|
||||
entries << Entry{element, {element->sceneBoundingRect(), element->pos()}, 1};
|
||||
//A picture's edges are the picture's, without its caption
|
||||
for (DiagramImageItem *image : std::as_const(dc.m_images))
|
||||
{
|
||||
const QRectF rect = image->mapRectToScene(image->imageRect());
|
||||
entries << Entry{image, {rect, rect.center()}, 1};
|
||||
}
|
||||
for (IndependentTextItem *text : std::as_const(dc.m_text_fields))
|
||||
entries << Entry{text, {text->sceneBoundingRect(), text->sceneBoundingRect().center()}, text_divisor};
|
||||
m_item_count = entries.size();
|
||||
|
||||
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));
|
||||
if (mode == SnapToGrid)
|
||||
{
|
||||
for (const Entry &entry : std::as_const(entries))
|
||||
move(entry.item, Alignment::gridOffset(entry.item->pos(), x_grid, y_grid, entry.divisor));
|
||||
setText(QObject::tr("Aligner %n objet(s) sur la grille", "", childCount()));
|
||||
return;
|
||||
}
|
||||
|
||||
setText(QObject::tr("Aligner %n objet(s) sur la grille", "", childCount()));
|
||||
//A group (#1070) lines up as one piece: its edges are its members'
|
||||
//together, its centre the middle of that box, and all its members
|
||||
//move by the same amount, so the group keeps its shape. Shapes in a
|
||||
//group come along; shapes outside one are left out, as above.
|
||||
struct Unit {
|
||||
QList<QGraphicsObject *> members;
|
||||
Alignment::Item geometry;
|
||||
QGraphicsObject *snap_item = nullptr; ///< lands on its grid
|
||||
qreal divisor = 1;
|
||||
};
|
||||
QList<Unit> units;
|
||||
QHash<QUuid, int> group_units;
|
||||
auto unitFor = [&](QGraphicsObject *item) -> Unit &
|
||||
{
|
||||
const QUuid group = ItemGroups::groupOf(item);
|
||||
if (group.isNull()) {
|
||||
units << Unit();
|
||||
return units.last();
|
||||
}
|
||||
if (!group_units.contains(group)) {
|
||||
group_units.insert(group, units.size());
|
||||
units << Unit();
|
||||
}
|
||||
return units[group_units.value(group)];
|
||||
};
|
||||
for (const Entry &entry : std::as_const(entries))
|
||||
{
|
||||
Unit &unit = unitFor(entry.item);
|
||||
unit.members << entry.item;
|
||||
unit.geometry.edges = unit.geometry.edges.isNull()
|
||||
? entry.geometry.edges
|
||||
: unit.geometry.edges.united(entry.geometry.edges);
|
||||
unit.geometry.ref = entry.geometry.ref;
|
||||
//Symbols come first in entries, so a group with one snaps on it
|
||||
if (!unit.snap_item) {
|
||||
unit.snap_item = entry.item;
|
||||
unit.divisor = entry.divisor;
|
||||
}
|
||||
}
|
||||
for (QetShapeItem *shape : std::as_const(dc.m_shapes))
|
||||
{
|
||||
if (ItemGroups::groupOf(shape).isNull())
|
||||
continue;
|
||||
Unit &unit = unitFor(shape);
|
||||
unit.members << shape;
|
||||
unit.geometry.edges = unit.geometry.edges.isNull()
|
||||
? shape->sceneBoundingRect()
|
||||
: unit.geometry.edges.united(shape->sceneBoundingRect());
|
||||
if (!unit.snap_item)
|
||||
unit.snap_item = shape;
|
||||
}
|
||||
for (Unit &unit : units) {
|
||||
if (unit.members.size() > 1)
|
||||
unit.geometry.ref = unit.geometry.edges.center();
|
||||
}
|
||||
m_item_count = units.size();
|
||||
|
||||
//Lining up a single item on itself would only snap it
|
||||
if (units.size() < 2)
|
||||
return;
|
||||
|
||||
Alignment::Edge edge = Alignment::Left;
|
||||
switch (mode) {
|
||||
case AlignLeft: edge = Alignment::Left; break;
|
||||
case AlignHCenter: edge = Alignment::HCenter; break;
|
||||
case AlignRight: edge = Alignment::Right; break;
|
||||
case AlignTop: edge = Alignment::Top; break;
|
||||
case AlignVCenter: edge = Alignment::VCenter; break;
|
||||
case AlignBottom: edge = Alignment::Bottom; break;
|
||||
case SnapToGrid: break;
|
||||
}
|
||||
|
||||
QList<Alignment::Item> geometry;
|
||||
for (const Unit &unit : std::as_const(units))
|
||||
geometry << unit.geometry;
|
||||
const QList<QPointF> offsets = Alignment::alignOffsets(geometry, edge);
|
||||
|
||||
for (int i = 0 ; i < units.size() ; ++i)
|
||||
{
|
||||
const Unit &unit = units.at(i);
|
||||
const QPointF offset = Alignment::snappedOffset(unit.snap_item->pos(), offsets.at(i), edge,
|
||||
x_grid, y_grid, unit.divisor);
|
||||
for (QGraphicsObject *member : unit.members)
|
||||
move(member, offset);
|
||||
}
|
||||
setText(QObject::tr("Aligner %n objet(s)", "", childCount()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,3 +224,12 @@ int AlignSelectionCommand::lockedCount() const
|
||||
{
|
||||
return m_locked_count;
|
||||
}
|
||||
|
||||
/**
|
||||
@return the number of things that took part: symbols, pictures and
|
||||
free texts whose position is not locked, a group counting as one.
|
||||
*/
|
||||
int AlignSelectionCommand::itemCount() const
|
||||
{
|
||||
return m_item_count;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,13 @@ class AlignSelectionCommand : public QUndoCommand
|
||||
{
|
||||
public:
|
||||
enum Mode {
|
||||
SnapToGrid ///< put each item where a drag would have left it
|
||||
SnapToGrid, ///< put each item where a drag would have left it
|
||||
AlignLeft,
|
||||
AlignHCenter,
|
||||
AlignRight,
|
||||
AlignTop,
|
||||
AlignVCenter,
|
||||
AlignBottom
|
||||
};
|
||||
|
||||
AlignSelectionCommand(Diagram *diagram, Mode mode, QUndoCommand *parent = nullptr);
|
||||
@@ -44,10 +50,12 @@ class AlignSelectionCommand : public QUndoCommand
|
||||
bool isValid() const;
|
||||
int movedCount() const;
|
||||
int lockedCount() const;
|
||||
int itemCount() const;
|
||||
|
||||
private:
|
||||
QPointer<Diagram> m_diagram;
|
||||
int m_locked_count = 0;
|
||||
int m_item_count = 0;
|
||||
};
|
||||
|
||||
#endif // ALIGNSELECTIONCOMMAND_H
|
||||
|
||||
@@ -49,6 +49,116 @@ private slots:
|
||||
QVERIFY(Alignment::gridOffset(on_grid + QPointF(1e-10, -1e-10), 10, 10).isNull());
|
||||
}
|
||||
}
|
||||
|
||||
// Three items: a 40x20 symbol with its origin 10 px in from its left
|
||||
// edge, a 20x40 one with its origin at its centre, and a 60x10 picture
|
||||
// whose reference point is its centre.
|
||||
static QList<Alignment::Item> threeItems()
|
||||
{
|
||||
return {
|
||||
{QRectF(100, 200, 40, 20), QPointF(110, 210)},
|
||||
{QRectF(150, 100, 20, 40), QPointF(160, 120)},
|
||||
{QRectF( 70, 300, 60, 10), QPointF(100, 305)}
|
||||
};
|
||||
}
|
||||
|
||||
void alignOffsets_data()
|
||||
{
|
||||
QTest::addColumn<int>("edge");
|
||||
QTest::addColumn<QList<QPointF>>("expected");
|
||||
|
||||
QTest::newRow("left, on the left-most edge")
|
||||
<< int(Alignment::Left) << QList<QPointF>{{-30, 0}, {-80, 0}, {0, 0}};
|
||||
QTest::newRow("centre, on the mean origin x (123.33)")
|
||||
<< int(Alignment::HCenter) << QList<QPointF>{{370.0/3 - 110, 0}, {370.0/3 - 160, 0}, {370.0/3 - 100, 0}};
|
||||
QTest::newRow("right, on the right-most edge")
|
||||
<< int(Alignment::Right) << QList<QPointF>{{30, 0}, {0, 0}, {40, 0}};
|
||||
QTest::newRow("top, on the top-most edge")
|
||||
<< int(Alignment::Top) << QList<QPointF>{{0, -100}, {0, 0}, {0, -200}};
|
||||
QTest::newRow("middle, on the mean origin y (211.67)")
|
||||
<< int(Alignment::VCenter) << QList<QPointF>{{0, 635.0/3 - 210}, {0, 635.0/3 - 120}, {0, 635.0/3 - 305}};
|
||||
QTest::newRow("bottom, on the bottom-most edge")
|
||||
<< int(Alignment::Bottom) << QList<QPointF>{{0, 90}, {0, 170}, {0, 0}};
|
||||
}
|
||||
|
||||
void alignOffsets()
|
||||
{
|
||||
QFETCH(int, edge);
|
||||
QFETCH(QList<QPointF>, expected);
|
||||
|
||||
const QList<QPointF> offsets = Alignment::alignOffsets(threeItems(), Alignment::Edge(edge));
|
||||
QCOMPARE(offsets.size(), expected.size());
|
||||
for (int i = 0 ; i < offsets.size() ; ++i) {
|
||||
QVERIFY2(qAbs(offsets.at(i).x() - expected.at(i).x()) < 1e-9, qPrintable(QString("item %1 x %2").arg(i).arg(offsets.at(i).x())));
|
||||
QVERIFY2(qAbs(offsets.at(i).y() - expected.at(i).y()) < 1e-9, qPrintable(QString("item %1 y %2").arg(i).arg(offsets.at(i).y())));
|
||||
}
|
||||
}
|
||||
|
||||
// Aligning items that are already lined up, and on the grid, moves
|
||||
// nothing: the command then pushes no undo step.
|
||||
void alreadyAligned()
|
||||
{
|
||||
const QList<Alignment::Item> items {
|
||||
{QRectF(100, 200, 40, 20), QPointF(110, 210)},
|
||||
{QRectF(100, 300, 40, 20), QPointF(110, 310)}
|
||||
};
|
||||
for (int e = Alignment::Left ; e <= Alignment::Bottom ; ++e)
|
||||
{
|
||||
const auto edge = Alignment::Edge(e);
|
||||
if (edge == Alignment::Top || edge == Alignment::VCenter || edge == Alignment::Bottom)
|
||||
continue; // they sit one above the other, so these do move
|
||||
const QList<QPointF> offsets = Alignment::alignOffsets(items, edge);
|
||||
for (int i = 0 ; i < items.size() ; ++i)
|
||||
QVERIFY(Alignment::snappedOffset(items.at(i).ref, offsets.at(i), edge, 10, 10).isNull());
|
||||
}
|
||||
}
|
||||
|
||||
// The result lands on the grid along the aligned line, and the other
|
||||
// coordinate is left alone even when it is off the grid.
|
||||
void snapsOnlyAlongTheLine()
|
||||
{
|
||||
// raw offset puts x at 123.33: rounded to 120; y 207 stays 207
|
||||
const QPointF pos(110, 207);
|
||||
const QPointF offset = Alignment::snappedOffset(pos, QPointF(370.0/3 - 110, 0), Alignment::HCenter, 10, 10);
|
||||
QVERIFY(qAbs(offset.x() - 10) < 1e-9);
|
||||
QCOMPARE(offset.y(), 0.0);
|
||||
|
||||
const QPointF v = Alignment::snappedOffset(QPointF(113, 207), QPointF(0, 7), Alignment::Bottom, 10, 10);
|
||||
QCOMPARE(v.x(), 0.0);
|
||||
QVERIFY(qAbs(v.y() - 3) < 1e-9); // 207 + 7 = 214, rounded to 210
|
||||
}
|
||||
|
||||
// Two symbols on the grid whose left edges are 5 px apart relative to
|
||||
// their origins cannot both have their origin on the grid and their
|
||||
// left edges on one line. The origins win: nothing leaves the grid.
|
||||
void mixedWidthsStayOnTheGrid()
|
||||
{
|
||||
const QList<Alignment::Item> items {
|
||||
{QRectF(95, 200, 30, 20), QPointF(110, 210)}, // origin 15 px in
|
||||
{QRectF(200, 300, 20, 20), QPointF(210, 310)} // origin 10 px in
|
||||
};
|
||||
const QList<QPointF> offsets = Alignment::alignOffsets(items, Alignment::Left);
|
||||
for (int i = 0 ; i < items.size() ; ++i)
|
||||
{
|
||||
const QPointF moved = items.at(i).ref + Alignment::snappedOffset(items.at(i).ref, offsets.at(i), Alignment::Left, 10, 10);
|
||||
QVERIFY(Alignment::gridOffset(moved, 10, 10).isNull());
|
||||
}
|
||||
// the second one moves 105 px left to 105, a tie, rounded up to 110
|
||||
QVERIFY(qAbs(Alignment::snappedOffset(items.at(1).ref, offsets.at(1), Alignment::Left, 10, 10).x() + 100) < 1e-9);
|
||||
}
|
||||
|
||||
// Free texts go to the text grid, which can be finer than the folio's.
|
||||
void textGridDivisor()
|
||||
{
|
||||
const QPointF offset = Alignment::snappedOffset(QPointF(103, 50), QPointF(0, 0), Alignment::Left, 10, 10, 2.0);
|
||||
QVERIFY(qAbs(offset.x() - 2) < 1e-9); // 103 -> 105 on a 5 px text grid
|
||||
QCOMPARE(offset.y(), 0.0);
|
||||
}
|
||||
|
||||
void emptySelection()
|
||||
{
|
||||
QVERIFY(Alignment::alignOffsets({}, Alignment::Left).isEmpty());
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_APPLESS_MAIN(tst_alignment)
|
||||
|
||||
Reference in New Issue
Block a user