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"