diff --git a/sources/diagram.cpp b/sources/diagram.cpp index 01a6e63d2..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 @@ -2684,6 +2685,29 @@ QPointF Diagram::snapToGrid(const QPointF &p) return (QPointF(p_x, p_y)); } +/** + @brief Diagram::snapToTextGrid + 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 = + QApplication::keyboardModifiers().testFlag(Qt::ControlModifier) + ? 0 + : settings.value(TextGrid::settings_key, 1).toReal(); + + 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/diagram.h b/sources/diagram.h index 198323bbc..244299a04 100644 --- a/sources/diagram.h +++ b/sources/diagram.h @@ -233,6 +233,7 @@ class Diagram : public QGraphicsScene BorderOptions borderOptions(); DiagramPosition convertPosition(const QPointF &); static QPointF snapToGrid(const QPointF &p); + static QPointF snapToTextGrid(const QPointF &p); bool drawTerminals() const; void setDrawTerminals(bool); diff --git a/sources/elementtextsmover.cpp b/sources/elementtextsmover.cpp index ddf2f235c..8835dc9a8 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(); } @@ -101,7 +117,7 @@ void ElementTextsMover::continueMovement(QGraphicsSceneMouseEvent *event) button_down_parent_pos = qgi->mapToParent(qgi->mapFromScene(event->buttonDownScenePos(Qt::LeftButton))); QPointF new_pos = m_items_hash.value(qgi) + current_parent_pos - button_down_parent_pos; - event->modifiers() == Qt::ControlModifier ? qgi->setPos(new_pos) : qgi->setPos(Diagram::snapToGrid(new_pos)); + event->modifiers() == Qt::ControlModifier ? qgi->setPos(new_pos) : qgi->setPos(Diagram::snapToTextGrid(new_pos)); } } @@ -112,13 +128,19 @@ 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; //Movement is null QGraphicsItem *qgi = m_items_hash.keys().first(); if(qgi->pos() == m_items_hash.value(qgi)) + { + m_movement_running = false; return; + } QUndoCommand *undo = new QUndoCommand(undoText()); 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 f1fdcec7a..7c5f2ddb2 100644 --- a/sources/qetdiagrameditor.cpp +++ b/sources/qetdiagrameditor.cpp @@ -20,6 +20,7 @@ #include "scripting/qetscripting.h" #endif #include +#include #include "ElementsCollection/elementscollectionwidget.h" #include "QWidgetAnimation/qwidgetanimation.h" #include "autoNum/ui/autonumberingdockwidget.h" @@ -48,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" @@ -481,6 +483,33 @@ void QETDiagramEditor::setUpActions() } }); + //Snap step for dragged texts, as a fraction of the folio grid + m_text_grid_menu = new QMenu(tr("Grille des textes"), this); + m_text_grid_menu->setIcon(QET::Icons::Grid); + m_text_grid_menu->setToolTipsVisible(true); + m_text_grid_button = new QToolButton(this); + m_text_grid_button->setMenu(m_text_grid_menu); + m_text_grid_button->setPopupMode(QToolButton::InstantPopup); + m_text_grid_button->setToolButtonStyle(Qt::ToolButtonTextOnly); + 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); + for (const qreal divisor : TextGrid::divisors) + { + QAction *action = m_text_grid_menu->addAction( + divisor > 0 ? TextGrid::ratioLabel(divisor) : tr("Désactivée")); + action->setCheckable(true); + action->setData(divisor); + text_grid_group->addAction(action); + } + 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); m_draw_guides->setStatusTip(tr("Affiche ou masque les guides")); @@ -956,6 +985,7 @@ void QETDiagramEditor::setUpToolBar() view_tool_bar -> addWidget(new DiagramEditorHandlerSizeWidget(this)); view_tool_bar -> addSeparator(); view_tool_bar -> addAction(m_draw_grid); + view_tool_bar -> addWidget(m_text_grid_button); view_tool_bar -> addAction(m_draw_guides); view_tool_bar -> addWidget(m_background_color_button); view_tool_bar -> addSeparator(); @@ -1115,6 +1145,7 @@ void QETDiagramEditor::setUpMenu() menu_affichage -> addAction(m_mode_visualise); menu_affichage -> addSeparator(); menu_affichage -> addAction(m_draw_grid); + menu_affichage -> addMenu(m_text_grid_menu); menu_affichage -> addAction(m_draw_guides); menu_affichage -> addMenu(m_background_color_button->menu()); menu_affichage -> addSeparator(); @@ -3230,3 +3261,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 c0495b948..fdcc6a88c 100644 --- a/sources/qetdiagrameditor.h +++ b/sources/qetdiagrameditor.h @@ -28,6 +28,8 @@ #include #include +class QToolButton; + class QMdiSubWindow; class QETProject; class QETResult; @@ -164,6 +166,7 @@ class QETDiagramEditor : public QETMainWindow void subWindowActivated(QMdiSubWindow *subWindows); private slots: + void updateTextGridButton(); void selectionChanged(); public: @@ -248,6 +251,8 @@ class QETDiagramEditor : public QETMainWindow ConductorColorToolButton *m_conductor_color_button = nullptr; ///< Diagram background color picker, in the "Affichage" toolbar DiagramBgColorToolButton *m_background_color_button = nullptr; + QMenu *m_text_grid_menu = nullptr; ///< Snap step used when dragging texts + QToolButton *m_text_grid_button = nullptr; QList m_zoom_action_toolBar; ///Only zoom action must displayed in the toolbar diff --git a/sources/qetgraphicsitem/conductortextitem.cpp b/sources/qetgraphicsitem/conductortextitem.cpp index e5e2201ac..624b5ea77 100644 --- a/sources/qetgraphicsitem/conductortextitem.cpp +++ b/sources/qetgraphicsitem/conductortextitem.cpp @@ -185,7 +185,7 @@ void ConductorTextItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { if (parent_conductor_) { if (parent_conductor_->nearShape().contains(intended_pos)) { - event->modifiers() == Qt::ControlModifier ? setPos(intended_pos) : setPos(Diagram::snapToGrid(intended_pos)); + event->modifiers() == Qt::ControlModifier ? setPos(intended_pos) : setPos(Diagram::snapToTextGrid(intended_pos)); parent_conductor_ -> setHighlighted(Conductor::Normal); } else { parent_conductor_ -> setHighlighted(Conductor::Alert); diff --git a/sources/qetgraphicsitem/diagramtextitem.cpp b/sources/qetgraphicsitem/diagramtextitem.cpp index 4587ab647..771a3d42d 100644 --- a/sources/qetgraphicsitem/diagramtextitem.cpp +++ b/sources/qetgraphicsitem/diagramtextitem.cpp @@ -370,7 +370,7 @@ void DiagramTextItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { //Set the actual pos QPointF new_pos = event->scenePos() + m_mouse_to_origin_movement; - event->modifiers() == Qt::ControlModifier ? setPos(new_pos) : setPos(Diagram::snapToGrid(new_pos)); + event->modifiers() == Qt::ControlModifier ? setPos(new_pos) : setPos(Diagram::snapToTextGrid(new_pos)); //Update the actual movement for other selected item diff --git a/sources/qetgraphicsitem/dynamicelementtextitem.cpp b/sources/qetgraphicsitem/dynamicelementtextitem.cpp index 1316d992b..fda123da9 100644 --- a/sources/qetgraphicsitem/dynamicelementtextitem.cpp +++ b/sources/qetgraphicsitem/dynamicelementtextitem.cpp @@ -639,7 +639,7 @@ void DynamicElementTextItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) //DiagramTextItem::mouseMoveEvent() for independent texts. //Without it this was the only text move in the editor that //ignored the grid. - event->modifiers() == Qt::ControlModifier ? setPos(new_pos) : setPos(Diagram::snapToGrid(new_pos)); + event->modifiers() == Qt::ControlModifier ? setPos(new_pos) : setPos(Diagram::snapToTextGrid(new_pos)); if(diagram()) diagram()->elementTextsMover().continueMovement(event); diff --git a/sources/qetgraphicsitem/elementtextitemgroup.cpp b/sources/qetgraphicsitem/elementtextitemgroup.cpp index 253c10146..9e4016fcf 100644 --- a/sources/qetgraphicsitem/elementtextitemgroup.cpp +++ b/sources/qetgraphicsitem/elementtextitemgroup.cpp @@ -688,7 +688,7 @@ void ElementTextItemGroup::mouseMoveEvent(QGraphicsSceneMouseEvent *event) button_down_parent_pos = mapToParent(mapFromScene(event->buttonDownScenePos(Qt::LeftButton))); QPointF new_pos = m_initial_position + current_parent_pos - button_down_parent_pos; - event->modifiers() == Qt::ControlModifier ? setPos(new_pos) : setPos(Diagram::snapToGrid(new_pos)); + event->modifiers() == Qt::ControlModifier ? setPos(new_pos) : setPos(Diagram::snapToTextGrid(new_pos)); if(diagram()) diagram()->elementTextsMover().continueMovement(event); 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"