From 95f3ea2f6d32099296eaf4b1ac13086e7f2d2a83 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sat, 26 Sep 2026 10:11:18 +1200 Subject: [PATCH] Share the text grid choices; add a preference, a status hint and a unit test Drop 1:2.5: on a grid of 10 it steps by 4, which misses 10, so texts on two elements 10 apart could never line up. Every remaining divisor is a whole number, which tst_textgrid checks. The divisor list and the snapping arithmetic move to the header-only textgrid.h so the toolbar menu, the preferences page and the test share them. The preferences page gets the same choice under Grille + Clavier; QETApp::textGridChanged keeps every editor's toolbar button in step. While element texts are dragged, the status bar names the text grid and says to release Shift and hold Ctrl for free placement -- Ctrl+Shift together is the pan shortcut. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG --- sources/diagram.cpp | 32 +++----- sources/elementtextsmover.cpp | 19 +++++ sources/elementtextsmover.h | 3 + sources/qetapp.cpp | 1 + sources/qetapp.h | 4 + sources/qetdiagrameditor.cpp | 49 +++++++----- sources/qetdiagrameditor.h | 1 + sources/textgrid.h | 62 ++++++++++++++ .../configpage/generalconfigurationpage.cpp | 11 +++ .../ui/configpage/generalconfigurationpage.ui | 20 +++++ tests/qttest/CMakeLists.txt | 7 ++ tests/qttest/tst_textgrid.cpp | 80 +++++++++++++++++++ 12 files changed, 252 insertions(+), 37 deletions(-) create mode 100644 sources/textgrid.h create mode 100644 tests/qttest/tst_textgrid.cpp diff --git a/sources/diagram.cpp b/sources/diagram.cpp index 022b0a9d2..e8c6b17f9 100644 --- a/sources/diagram.cpp +++ b/sources/diagram.cpp @@ -42,6 +42,7 @@ #include "qetinformation.h" #include "qetproject.h" #include "diagramsortkeys.h" +#include "textgrid.h" #include #include #include @@ -2686,32 +2687,25 @@ QPointF Diagram::snapToGrid(const QPointF &p) /** @brief Diagram::snapToTextGrid - Return the nearest point of p on the text grid. - The text grid is the folio grid divided by the - "diagrameditor/text_grid_divisor" setting (1 = same as the folio grid, - 0 = no grid). Because it divides the folio grid, texts snapped to it - still line up with every element and with each other. - Ctrl held, or a divisor of 0, rounds to the nearest pixel instead. + Return the nearest point of p on the text grid, see TextGrid. + Ctrl held rounds to the nearest pixel instead, as snapToGrid() does. @param p point to find the nearest snapped point @return */ QPointF Diagram::snapToTextGrid(const QPointF &p) { QSettings settings; - const qreal divisor = settings.value( - QStringLiteral("diagrameditor/text_grid_divisor"), - 1).toReal(); + const qreal divisor = + QApplication::keyboardModifiers().testFlag(Qt::ControlModifier) + ? 0 + : settings.value(TextGrid::settings_key, 1).toReal(); - if (divisor <= 0 - || QApplication::keyboardModifiers().testFlag(Qt::ControlModifier)) - return QPointF(qRound(p.x()), qRound(p.y())); - - const qreal x_step = settings.value(QStringLiteral("diagrameditor/Xgrid"), - Diagram::xGrid).toInt() / divisor; - const qreal y_step = settings.value(QStringLiteral("diagrameditor/Ygrid"), - Diagram::yGrid).toInt() / divisor; - return QPointF(qRound(p.x() / x_step) * x_step, - qRound(p.y() / y_step) * y_step); + return TextGrid::snap(p, + settings.value(QStringLiteral("diagrameditor/Xgrid"), + Diagram::xGrid).toInt(), + settings.value(QStringLiteral("diagrameditor/Ygrid"), + Diagram::yGrid).toInt(), + divisor); } diff --git a/sources/elementtextsmover.cpp b/sources/elementtextsmover.cpp index f0b61139a..f5eaad4c3 100644 --- a/sources/elementtextsmover.cpp +++ b/sources/elementtextsmover.cpp @@ -22,8 +22,11 @@ #include "qetapp.h" #include "qetgraphicsitem/dynamicelementtextitem.h" #include "qetgraphicsitem/elementtextitemgroup.h" +#include "qetdiagrameditor.h" +#include "textgrid.h" #include +#include /** @brief ElementTextsMover::ElementTextsMover @@ -80,6 +83,19 @@ int ElementTextsMover::beginMovement(Diagram *diagram, QGraphicsItem *driver_ite return -1; m_movement_running = true; + + m_status_bar.clear(); + if (!diagram->views().isEmpty()) + if (const auto qde = QETApp::diagramEditorAncestorOf(diagram->views().at(0))) + m_status_bar = qde->statusBar(); + if (m_status_bar) + { + const qreal divisor = QSettings().value(TextGrid::settings_key, 1).toReal(); + m_status_bar->showMessage(divisor > 0 + ? QObject::tr("Grille des textes %1. Relâcher Maj et maintenir Ctrl pour placer librement.") + .arg(TextGrid::ratioLabel(divisor)) + : QObject::tr("Grille des textes désactivée.")); + } return m_items_hash.size(); } @@ -112,6 +128,9 @@ void ElementTextsMover::continueMovement(QGraphicsSceneMouseEvent *event) void ElementTextsMover::endMovement() { //No movement or no items to move + if (m_status_bar) + m_status_bar->clearMessage(); + if(!m_movement_running || m_items_hash.isEmpty()) return; diff --git a/sources/elementtextsmover.h b/sources/elementtextsmover.h index fd4ad61af..a1b793a43 100644 --- a/sources/elementtextsmover.h +++ b/sources/elementtextsmover.h @@ -21,6 +21,8 @@ #include #include #include +#include +#include class QGraphicsItem; class DiagramTextItem; @@ -55,6 +57,7 @@ class ElementTextsMover QHash m_texts_hash; QHash m_grps_hash; QHash m_items_hash; + QPointer m_status_bar; int m_text_count = 0, m_group_count = 0; }; diff --git a/sources/qetapp.cpp b/sources/qetapp.cpp index c57a907cd..48d9001b6 100644 --- a/sources/qetapp.cpp +++ b/sources/qetapp.cpp @@ -2231,6 +2231,7 @@ void QETApp::configureQET() // affiche le dialogue puis evite de le lier a un quelconque widget parent cd.exec(); cd.setParent(nullptr, cd.windowFlags()); + emit textGridChanged(); #ifdef QET_SPACEMOUSE_SUPPORT if (m_space_mouse_listener) { diff --git a/sources/qetapp.h b/sources/qetapp.h index ad3850c79..5c0b15106 100644 --- a/sources/qetapp.h +++ b/sources/qetapp.h @@ -260,6 +260,10 @@ class QETApp : public QObject static QString m_interface_language; + signals: + /// The text grid setting changed, see TextGrid. + void textGridChanged(); + public slots: void systray(QSystemTrayIcon::ActivationReason); void reduceEveryEditor(); diff --git a/sources/qetdiagrameditor.cpp b/sources/qetdiagrameditor.cpp index 1c2d1c410..f90525a58 100644 --- a/sources/qetdiagrameditor.cpp +++ b/sources/qetdiagrameditor.cpp @@ -49,6 +49,7 @@ #include "qeticons.h" #include "qetmessagebox.h" #include "recentfiles.h" +#include "textgrid.h" #include "shortcutmanager.h" #include "ui/bomexportdialog.h" #include "ui/conductorcolortoolbutton.h" @@ -493,29 +494,21 @@ void QETDiagramEditor::setUpActions() m_text_grid_button->setToolTip(tr("Grille d'accrochage des textes déplacés à la souris.\n" "Maintenir Ctrl pendant le déplacement pour placer librement.")); auto text_grid_group = new QActionGroup(this); - const qreal current_divisor = settings.value("diagrameditor/text_grid_divisor", 1).toReal(); - const QList> text_grids { - {tr("Désactivée"), 0}, - {QStringLiteral("1:1"), 1}, - {QStringLiteral("1:2"), 2}, - {QStringLiteral("1:2.5"), 2.5}, - {QStringLiteral("1:5"), 5}, - {QStringLiteral("1:10"), 10}}; - for (const auto &grid : text_grids) + for (const qreal divisor : TextGrid::divisors) { - QAction *action = m_text_grid_menu->addAction(grid.first); + QAction *action = m_text_grid_menu->addAction( + divisor > 0 ? TextGrid::ratioLabel(divisor) : tr("Désactivée")); action->setCheckable(true); - action->setData(grid.second); + action->setData(divisor); text_grid_group->addAction(action); - if (qFuzzyCompare(grid.second + 1, current_divisor + 1)) { - action->setChecked(true); - m_text_grid_button->setText(tr("Textes %1").arg(grid.first)); - } } - connect(text_grid_group, &QActionGroup::triggered, this, [this](QAction *action) { - QSettings().setValue("diagrameditor/text_grid_divisor", action->data()); - m_text_grid_button->setText(tr("Textes %1").arg(action->text())); + connect(text_grid_group, &QActionGroup::triggered, this, [](QAction *action) { + QSettings().setValue(TextGrid::settings_key, action->data()); + emit QETApp::instance()->textGridChanged(); }); + connect(QETApp::instance(), &QETApp::textGridChanged, + this, &QETDiagramEditor::updateTextGridButton); + updateTextGridButton(); // Draw or not the custom guides m_draw_guides = new QAction ( QIcon::fromTheme("guides"), tr("Afficher les guides"), this); @@ -3262,3 +3255,23 @@ void QETDiagramEditor::slot_runScript() { QetScripting::runOnProject(script_path, project, currentDiagramView()); } #endif + +/** + @brief QETDiagramEditor::updateTextGridButton + Show the current text grid on its toolbar button and check it in its menu. +*/ +void QETDiagramEditor::updateTextGridButton() +{ + const qreal divisor = QSettings().value(TextGrid::settings_key, 1).toReal(); + for (QAction *action : m_text_grid_menu->actions()) + { + if (qFuzzyCompare(action->data().toReal() + 1, divisor + 1)) + { + action->setChecked(true); + m_text_grid_button->setText(tr("Textes %1").arg(action->text())); + return; + } + } + //A divisor the menu does not offer, set by hand in the config file + m_text_grid_button->setText(tr("Textes %1").arg(TextGrid::ratioLabel(divisor))); +} diff --git a/sources/qetdiagrameditor.h b/sources/qetdiagrameditor.h index bf6de32b5..487ee3e35 100644 --- a/sources/qetdiagrameditor.h +++ b/sources/qetdiagrameditor.h @@ -166,6 +166,7 @@ class QETDiagramEditor : public QETMainWindow void subWindowActivated(QMdiSubWindow *subWindows); private slots: + void updateTextGridButton(); void selectionChanged(); public: diff --git a/sources/textgrid.h b/sources/textgrid.h new file mode 100644 index 000000000..10e77fa76 --- /dev/null +++ b/sources/textgrid.h @@ -0,0 +1,62 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#ifndef TEXTGRID_H +#define TEXTGRID_H + +#include +#include +#include +#include + +/** + The text grid: the step texts snap to when dragged with the mouse, + a fraction of the folio grid. A divisor of 1 is the folio grid itself, + 0 means no grid. Because every step divides the folio grid, a text + snapped to it still lines up with every element and with the texts + of other elements. +*/ +namespace TextGrid +{ + /// The choices offered in the menu and the preferences, 0 first. + inline const QList divisors{0, 1, 2, 5, 10}; + + /// QSettings key holding the divisor. + inline const QString settings_key{QStringLiteral("diagrameditor/text_grid_divisor")}; + + /// "1:5" for 5. Not meaningful for 0. + inline QString ratioLabel(qreal divisor) { + return QStringLiteral("1:") + QString::number(divisor); + } + + /** + @return p snapped to a grid of x_grid / divisor by y_grid / divisor, + or rounded to the nearest pixel when divisor is 0 or less. + */ + inline QPointF snap(const QPointF &p, int x_grid, int y_grid, qreal divisor) + { + if (divisor <= 0 || x_grid <= 0 || y_grid <= 0) + return QPointF(qRound(p.x()), qRound(p.y())); + + const qreal x_step = x_grid / divisor; + const qreal y_step = y_grid / divisor; + return QPointF(qRound(p.x() / x_step) * x_step, + qRound(p.y() / y_step) * y_step); + } +} + +#endif // TEXTGRID_H diff --git a/sources/ui/configpage/generalconfigurationpage.cpp b/sources/ui/configpage/generalconfigurationpage.cpp index f5689046c..06816e113 100644 --- a/sources/ui/configpage/generalconfigurationpage.cpp +++ b/sources/ui/configpage/generalconfigurationpage.cpp @@ -23,6 +23,7 @@ #include "../../utils/qetsettings.h" #include "../../utils/qetutils.h" #include "../../qetmessagebox.h" +#include "../../textgrid.h" #include "../nokde/kcolorbutton.h" #include #include @@ -68,6 +69,15 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) : ui->guides_startup_cb->setChecked(settings.value("diagrameditor/guides_display_startup", false).toBool()); ui->DiagramEditor_xGrid_sb->setValue(settings.value("diagrameditor/Xgrid", 10).toInt()); ui->DiagramEditor_yGrid_sb->setValue(settings.value("diagrameditor/Ygrid", 10).toInt()); + for (const qreal divisor : TextGrid::divisors) + ui->DiagramEditor_textGrid_cb->addItem( + divisor > 0 ? TextGrid::ratioLabel(divisor) : tr("Désactivée"), + divisor); + int text_grid_index = ui->DiagramEditor_textGrid_cb->findData( + settings.value(TextGrid::settings_key, 1).toReal()); + if (text_grid_index < 0) + text_grid_index = ui->DiagramEditor_textGrid_cb->findData(qreal(1)); + ui->DiagramEditor_textGrid_cb->setCurrentIndex(text_grid_index); ui->DiagramEditor_xKeyGrid_sb->setValue(settings.value("diagrameditor/key_Xgrid", 10).toInt()); ui->DiagramEditor_yKeyGrid_sb->setValue(settings.value("diagrameditor/key_Ygrid", 10).toInt()); ui->DiagramEditor_xKeyGridFine_sb->setValue(settings.value("diagrameditor/key_fine_Xgrid", 1).toInt()); @@ -287,6 +297,7 @@ void GeneralConfigurationPage::applyConf() //Grid step and key navigation settings.setValue("diagrameditor/Xgrid", ui->DiagramEditor_xGrid_sb->value()); settings.setValue("diagrameditor/Ygrid", ui->DiagramEditor_yGrid_sb->value()); + settings.setValue(TextGrid::settings_key, ui->DiagramEditor_textGrid_cb->currentData()); settings.setValue("diagrameditor/key_Xgrid", ui->DiagramEditor_xKeyGrid_sb->value()); settings.setValue("diagrameditor/key_Ygrid", ui->DiagramEditor_yKeyGrid_sb->value()); settings.setValue("diagrameditor/key_fine_Xgrid", ui->DiagramEditor_xKeyGridFine_sb->value()); diff --git a/sources/ui/configpage/generalconfigurationpage.ui b/sources/ui/configpage/generalconfigurationpage.ui index 030b66877..3bdd12356 100644 --- a/sources/ui/configpage/generalconfigurationpage.ui +++ b/sources/ui/configpage/generalconfigurationpage.ui @@ -784,6 +784,26 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments + + + + Grille des textes déplacés à la souris + + + Fraction de la grille des folios. Maintenir Ctrl pendant le déplacement pour placer librement. + + + + + + + + 80 + 0 + + + + diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index 2fec5922f..63766e0a1 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -91,6 +91,13 @@ add_test(NAME tst_contactusage COMMAND tst_contactusage) target_include_directories(tst_contactusage PRIVATE ${QET_DIR}/sources) target_link_libraries(tst_contactusage PRIVATE Qt::Test) +# textgrid.h is a header-only helper holding the text snap rules, so this +# test builds independently of the rest of the QET sources. +add_executable(tst_textgrid tst_textgrid.cpp) +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) + add_executable( tst_qetpalette tst_qetpalette.cpp diff --git a/tests/qttest/tst_textgrid.cpp b/tests/qttest/tst_textgrid.cpp new file mode 100644 index 000000000..a58251c3f --- /dev/null +++ b/tests/qttest/tst_textgrid.cpp @@ -0,0 +1,80 @@ +#include + +#include "textgrid.h" + +class tst_textgrid : public QObject +{ + Q_OBJECT + +private slots: + // A label starting off the grid at (-13.3, 13.7), the case from + // discussion #1020: the sideways jump shrinks with a finer text grid. + void snapsToStep_data() + { + QTest::addColumn("grid"); + QTest::addColumn("divisor"); + QTest::addColumn("expected"); + + QTest::newRow("off rounds to pixel") << 10 << 0.0 << QPointF(-13, 14); + QTest::newRow("1:1 is the folio grid") << 10 << 1.0 << QPointF(-10, 10); + QTest::newRow("1:2") << 10 << 2.0 << QPointF(-15, 15); + QTest::newRow("1:5") << 10 << 5.0 << QPointF(-14, 14); + QTest::newRow("1:10") << 10 << 10.0 << QPointF(-13, 14); + QTest::newRow("grid 7, 1:2 steps 3.5") << 7 << 2.0 << QPointF(-14, 14); + } + + void snapsToStep() + { + QFETCH(int, grid); + QFETCH(qreal, divisor); + QFETCH(QPointF, expected); + + const QPointF snapped = TextGrid::snap(QPointF(-13.3, 13.7), grid, grid, divisor); + QCOMPARE(snapped.x(), expected.x()); + QCOMPARE(snapped.y(), expected.y()); + } + + // Every folio grid point is also a text grid point, so a text can + // always sit exactly where an element or a wire does, and texts of + // different elements can line up. This is why every divisor offered + // is a whole number: 1:2.5 on a grid of 10 steps by 4, which misses 10. + void folioGridPointsAreKept_data() + { + QTest::addColumn("grid"); + QTest::addColumn("divisor"); + + for (int grid : {10, 7, 5}) + for (qreal divisor : TextGrid::divisors) + if (divisor > 0) + QTest::newRow(qPrintable(QStringLiteral("grid %1, %2") + .arg(grid).arg(TextGrid::ratioLabel(divisor)))) + << grid << divisor; + } + + void folioGridPointsAreKept() + { + QFETCH(int, grid); + QFETCH(qreal, divisor); + + for (int k = -20; k <= 20; ++k) { + const QPointF on_grid(k * grid, -k * grid); + QCOMPARE(TextGrid::snap(on_grid, grid, grid, divisor), on_grid); + } + } + + // Separate X and Y grid sizes are honoured independently. + void usesEachAxisGrid() + { + QCOMPARE(TextGrid::snap(QPointF(13, 13), 10, 20, 2), QPointF(15, 10)); + } + + void ratioLabel() + { + QCOMPARE(TextGrid::ratioLabel(2), QStringLiteral("1:2")); + QCOMPARE(TextGrid::ratioLabel(10), QStringLiteral("1:10")); + } +}; + +QTEST_GUILESS_MAIN(tst_textgrid) + +#include "tst_textgrid.moc"