Merge remote-tracking branch 'upstream/master' into feature/customise-toolbar-options

# Conflicts:
#	tests/qttest/CMakeLists.txt
This commit is contained in:
ispyisail
2026-10-07 07:33:20 +13:00
38 changed files with 2241 additions and 102 deletions
+2 -2
View File
@@ -43,8 +43,8 @@ Here are the technical choices made for the software development:
* Libraries: Qt 6.x
* [KF6 Framework](https://github.com/KDE)
[Cmake](https://cmake.org/install/)
[kcoreaddons](https://github.com/KDE/kcoreaddons/tree/kf6)
[kwidgetsaddons](https://github.com/KDE/kwidgetsaddons/tree/kf6).
[kcoreaddons](https://invent.kde.org/frameworks/kcoreaddons)
[kwidgetsaddons](https://invent.kde.org/frameworks/kwidgetsaddons).
* Coding language: [C++](https://en.wikipedia.org/wiki/C%2B%2B)
* GUI translations: [Qt Linguist](http://doc.qt.io/qt-6/qtlinguist-index.html)
* Version control: [GIT](https://github.com/qelectrotech/qelectrotech-source-mirror.git)
+2
View File
@@ -616,6 +616,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/QetGraphicsItemModeler/qetgraphicshandleritem.h
${QET_DIR}/sources/QetGraphicsItemModeler/qetgraphicshandlerutility.cpp
${QET_DIR}/sources/QetGraphicsItemModeler/qetgraphicshandlerutility.h
${QET_DIR}/sources/QetGraphicsItemModeler/textresizehandles.cpp
${QET_DIR}/sources/QetGraphicsItemModeler/textresizehandles.h
${QET_DIR}/sources/QPropertyUndoCommand/qpropertyundocommand.cpp
${QET_DIR}/sources/QPropertyUndoCommand/qpropertyundocommand.h
@@ -0,0 +1,252 @@
/*
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 "textresizehandles.h"
#include "qetgraphicshandleritem.h"
#include <QAbstractTextDocumentLayout>
#include <QCursor>
#include <QGraphicsSceneMouseEvent>
#include <QGraphicsTextItem>
#include <QTextDocument>
#include <cmath>
#include <utility>
/**
@brief TextResizeHandles::TextResizeHandles
@param text : the text to resize, becomes the parent of the handles.
The text may be in a scene or not yet.
@param handle_size : see QETUtils::graphicsHandlerSize()
*/
TextResizeHandles::TextResizeHandles(QGraphicsTextItem *text, qreal handle_size) :
m_text(text)
{
setFlag(QGraphicsItem::ItemHasNoContents);
setParentItem(text);
for (int i = TextResize::TopLeft ; i <= TextResize::BottomLeft ; ++i)
{
auto *handle = new QetGraphicsHandlerItem(handle_size);
handle->setParentItem(this);
handle->setColor(Qt::darkGreen);
handle->setToolTip(tr("Glisser pour changer la largeur du texte, "
"double-cliquer pour une largeur automatique"));
handle->setCursor(i == TextResize::TopLeft || i == TextResize::BottomRight
? Qt::SizeFDiagCursor : Qt::SizeBDiagCursor);
m_handles << handle;
}
installHandleFilters();
m_size_connection = connect(text->document()->documentLayout(),
&QAbstractTextDocumentLayout::documentSizeChanged,
this, &TextResizeHandles::updateHandlesPos);
updateHandlesPos();
}
TextResizeHandles::~TextResizeHandles()
{
disconnect(m_size_connection);
}
QRectF TextResizeHandles::boundingRect() const {
return QRectF();
}
/**
@return the handle at corner
*/
QetGraphicsHandlerItem *TextResizeHandles::handle(TextResize::Corner corner) const {
return m_handles.at(corner);
}
/**
@brief TextResizeHandles::updateHandlesPos
Put the handles at the corners of the box of the text. Called when the
size of the text changes; the owner calls it when the box changes in an
other way.
The box is boundingRect(), the box a user sees selected and resizes,
not the tighter frame of a framed text (qelectrotech#591).
*/
void TextResizeHandles::updateHandlesPos()
{
const QRectF rect = m_text->boundingRect();
for (int i = TextResize::TopLeft ; i <= TextResize::BottomLeft ; ++i)
m_handles.at(i)->setPos(TextResize::cornerOf(rect, TextResize::Corner(i)));
}
/**
@brief TextResizeHandles::itemChange
A scene drops the event filters of an item that leaves it, so the
handles stop reacting to the mouse when the text is removed from its
scene and added back (undoing a delete does that), or when they were
created before the text was in a scene. Install them again each time
these handles enter a scene.
*/
QVariant TextResizeHandles::itemChange(GraphicsItemChange change, const QVariant &value)
{
if (change == QGraphicsItem::ItemSceneHasChanged)
installHandleFilters();
return QGraphicsObject::itemChange(change, value);
}
/**
@brief TextResizeHandles::installHandleFilters
Filter the mouse events of the handles, when they are in a scene.
*/
void TextResizeHandles::installHandleFilters()
{
if (!scene())
return;
for (QetGraphicsHandlerItem *handle : std::as_const(m_handles))
if (handle->scene() == scene())
handle->installSceneEventFilter(this);
}
bool TextResizeHandles::sceneEventFilter(QGraphicsItem *watched, QEvent *event)
{
const int index = m_handles.indexOf(static_cast<QetGraphicsHandlerItem *>(watched));
if (index < 0)
return false;
switch (event->type())
{
case QEvent::GraphicsSceneMousePress:
{
auto *mouse_event = static_cast<QGraphicsSceneMouseEvent *>(event);
if (mouse_event->button() != Qt::LeftButton)
return false;
pressed(TextResize::Corner(index), mouse_event->scenePos());
return true;
}
case QEvent::GraphicsSceneMouseMove:
if (m_dragging)
moved(static_cast<QGraphicsSceneMouseEvent *>(event)->scenePos());
return true;
case QEvent::GraphicsSceneMouseRelease:
released();
return true;
case QEvent::GraphicsSceneMouseDoubleClick:
resetToAutomaticWidth();
return true;
default:
return false;
}
}
void TextResizeHandles::pressed(TextResize::Corner corner, const QPointF &scene_pos)
{
m_dragging = true;
m_press_scene = scene_pos;
m_dragged = corner;
m_old_width = currentWidth();
m_old_pos = m_text->pos();
m_press_inverse = m_text->sceneTransform().inverted();
m_fixed_local = TextResize::cornerOf(m_text->boundingRect(), TextResize::opposite(corner));
m_fixed_scene = m_text->mapToScene(m_fixed_local);
m_min_width = TextResize::minimumWidth(m_text->document());
}
void TextResizeHandles::moved(const QPointF &scene_pos)
{
//A click without a move keeps an automatic width automatic
if (scene_pos == m_press_scene)
return;
//Whole pixels, as in the width spin boxes
const qreal width = qMax(qreal(qRound(TextResize::widthForDrag(m_press_inverse, m_fixed_local,
scene_pos, m_dragged, 0))),
std::ceil(m_min_width));
m_text->setProperty("textWidth", width);
TextResize::pinCorner(m_text, TextResize::opposite(m_dragged), m_fixed_scene);
updateHandlesPos();
}
void TextResizeHandles::released()
{
if (!m_dragging)
return;
m_dragging = false;
const qreal new_width = currentWidth();
if (!qFuzzyCompare(m_old_width, new_width) || m_old_pos != m_text->pos())
emit resizeFinished(m_old_width, new_width, m_old_pos, m_text->pos());
}
/**
@brief TextResizeHandles::resetToAutomaticWidth
Back to the automatic width (-1): the text keeps the point chosen by its
alignment in place, as it does for any other change of its width.
*/
void TextResizeHandles::resetToAutomaticWidth()
{
m_dragging = false;
const qreal old_width = currentWidth();
if (old_width < 0)
return;
const QPointF old_pos = m_text->pos();
m_text->setProperty("textWidth", qreal(-1));
updateHandlesPos();
emit resizeFinished(old_width, currentWidth(), old_pos, m_text->pos());
}
qreal TextResizeHandles::currentWidth() const {
return m_text->property("textWidth").toReal();
}
/**
@brief TextResizeCommand::TextResizeCommand
@param text : a text with a "textWidth" property
@param old_width
@param new_width
@param old_pos
@param new_pos
@param parent
*/
TextResizeCommand::TextResizeCommand(QGraphicsObject *text,
qreal old_width, qreal new_width,
const QPointF &old_pos, const QPointF &new_pos,
QUndoCommand *parent) :
QUndoCommand(parent),
m_text(text),
m_old_width(old_width),
m_new_width(new_width),
m_old_pos(old_pos),
m_new_pos(new_pos)
{
setText(tr("Redimensionner un texte"));
}
void TextResizeCommand::undo() {
apply(m_old_width, m_old_pos);
}
void TextResizeCommand::redo() {
apply(m_new_width, m_new_pos);
}
void TextResizeCommand::apply(qreal width, const QPointF &pos)
{
if (!m_text)
return;
m_text->setProperty("textWidth", width);
m_text->setPos(pos);
}
@@ -0,0 +1,124 @@
/*
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 TEXTRESIZEHANDLES_H
#define TEXTRESIZEHANDLES_H
#include "../textresize.h"
#include <QCoreApplication>
#include <QGraphicsObject>
#include <QPointer>
#include <QTransform>
#include <QUndoCommand>
#include <QVector>
class QetGraphicsHandlerItem;
class QGraphicsTextItem;
/**
@brief The TextResizeHandles class
Four corner handles to change the width of a text box with the mouse.
The opposite corner of the dragged one stays in place, the text wraps
to the new width and the height of the box follows the text.
Double-clicking a handle goes back to the automatic width.
The text must have a qreal "textWidth" property (-1 = automatic width).
The handles are a child of the text, so they follow it when it, or its
symbol, is moved, rotated or zoomed (qelectrotech#1002).
The width is changed live while dragging; the owner of the text makes
the change undoable when resizeFinished() is emitted, by pushing a
TextResizeCommand on its own undo stack.
*/
class TextResizeHandles : public QGraphicsObject
{
Q_OBJECT
public:
TextResizeHandles(QGraphicsTextItem *text, qreal handle_size = 10);
~TextResizeHandles() override;
QRectF boundingRect() const override;
void paint(QPainter *, const QStyleOptionGraphicsItem *, QWidget *) override {}
QetGraphicsHandlerItem *handle(TextResize::Corner corner) const;
void updateHandlesPos();
signals:
void resizeFinished(qreal old_width, qreal new_width,
QPointF old_pos, QPointF new_pos);
protected:
bool sceneEventFilter(QGraphicsItem *watched, QEvent *event) override;
QVariant itemChange(GraphicsItemChange change, const QVariant &value) override;
private:
void installHandleFilters();
void pressed(TextResize::Corner corner, const QPointF &scene_pos);
void moved(const QPointF &scene_pos);
void released();
void resetToAutomaticWidth();
qreal currentWidth() const;
QGraphicsTextItem *m_text;
QVector<QetGraphicsHandlerItem *> m_handles;
QMetaObject::Connection m_size_connection;
//The drag in progress
bool m_dragging = false;
TextResize::Corner m_dragged = TextResize::BottomRight;
QTransform m_press_inverse;
QPointF m_press_scene,
m_fixed_local,
m_fixed_scene,
m_old_pos;
qreal m_old_width = -1,
m_min_width = 0;
};
/**
@brief The TextResizeCommand class
Undo a width change made with TextResizeHandles: the width, then the
position, which keeps the opposite corner of the dragged one in place.
Both are set in the same order on undo and redo, because a text may
move itself when its width changes (to keep its alignment point).
*/
class TextResizeCommand : public QUndoCommand
{
Q_DECLARE_TR_FUNCTIONS(TextResizeCommand)
public:
TextResizeCommand(QGraphicsObject *text,
qreal old_width, qreal new_width,
const QPointF &old_pos, const QPointF &new_pos,
QUndoCommand *parent = nullptr);
void undo() override;
void redo() override;
private:
void apply(qreal width, const QPointF &pos);
QPointer<QGraphicsObject> m_text;
qreal m_old_width,
m_new_width;
QPointF m_old_pos,
m_new_pos;
};
#endif // TEXTRESIZEHANDLES_H
@@ -205,6 +205,7 @@ void TerminalStripTreeDockWidget::on_m_tree_view_currentItemChanged(QTreeWidgetI
Q_UNUSED(previous)
if (!current) {
m_current_is_free_terminal = false;
setCurrentStrip(nullptr);
return;
}
@@ -225,11 +226,12 @@ void TerminalStripTreeDockWidget::on_m_tree_view_currentItemChanged(QTreeWidgetI
}
}
if (strip_ != m_current_strip) {
setCurrentStrip(strip_);
} else if (current_is_free != m_current_is_free_terminal) {
//The flag must follow every selection change, or a reload of the tree
//(e.g. after moving a free terminal) leaves it stale and the next
//click on a free terminal shows nothing (#1306)
if (strip_ != m_current_strip || current_is_free != m_current_is_free_terminal) {
m_current_is_free_terminal = current_is_free;
emit currentStripChanged(nullptr);
setCurrentStrip(strip_);
}
}
+8 -3
View File
@@ -1248,6 +1248,8 @@ void projectDataBase::addDrawingItem(QGraphicsItem *item)
}
else if (auto text = qobject_cast<IndependentTextItem *>(object)) {
connect(text, &IndependentTextItem::uuidChanged, this, &projectDataBase::drawingItemChanged, unique);
//Also changes the height of the text, kept in the row
connect(text, &IndependentTextItem::textWidthChanged, this, &projectDataBase::drawingItemChanged, unique);
//Sent by the document, not the item: drawingItemChanged() walks
//back up to the item. It is the one signal that catches every way
//the text changes -- typing, undo, a script's setTextContent().
@@ -1424,6 +1426,9 @@ bool projectDataBase::writeDrawingItem(QObject *object)
query = &m_insert_independent_text_query;
query->bindValue(QStringLiteral(":text"), text->toPlainText());
query->bindValue(QStringLiteral(":rotation"), text->rotation());
//NULL for the automatic width
query->bindValue(QStringLiteral(":text_width"), text->textWidth() > 0
? QVariant(text->textWidth()) : QVariant());
}
else if (auto image = qobject_cast<DiagramImageItem *>(object))
{
@@ -1667,7 +1672,7 @@ bool projectDataBase::createDataBase()
"type VARCHAR(20), color VARCHAR(20), fill VARCHAR(20), "
"FOREIGN KEY (diagram_uuid) REFERENCES diagram (uuid))",
QStringLiteral("CREATE TABLE independent_text (") + drawing_columns +
"text TEXT, rotation REAL, "
"text TEXT, rotation REAL, text_width REAL, "
"FOREIGN KEY (diagram_uuid) REFERENCES diagram (uuid))",
QStringLiteral("CREATE TABLE image (") + drawing_columns +
"pixel_width INTEGER, pixel_height INTEGER, "
@@ -2123,8 +2128,8 @@ void projectDataBase::prepareQuery()
", :type, :color, :fill)");
m_insert_independent_text_query = QSqlQuery(m_data_base);
m_insert_independent_text_query.prepare("INSERT OR REPLACE INTO independent_text (" + drawing_columns +
", text, rotation) VALUES (" + drawing_values +
", :text, :rotation)");
", text, rotation, text_width) VALUES (" + drawing_values +
", :text, :rotation, :text_width)");
m_insert_image_query = QSqlQuery(m_data_base);
m_insert_image_query.prepare("INSERT OR REPLACE INTO image (" + drawing_columns +
", pixel_width, pixel_height) VALUES (" + drawing_values +
+3 -1
View File
@@ -35,6 +35,7 @@
#include "qetgraphicsitem/independenttextitem.h"
#include "qetgraphicsitem/qetshapeitem.h"
#include "qetgraphicsitem/terminal.h"
#include "textlines.h"
#include <QGraphicsSimpleTextItem>
#include <QSet>
@@ -358,7 +359,8 @@ void DxfExport::write(Diagram *diagram, int width, int height,
qreal y = dti->scenePos().y()
- ydir * fontSize * 1.8
- xdir * fontSize * 0.9;
QStringList lines = dti -> toPlainText().split('\n');
//As drawn: a text with a width is wrapped
QStringList lines = TextLines::layoutLines(dti -> document());
qreal offset = fontSize * 1.6;
foreach (QString line, lines) {
if (line.size() > 0 && line != "_" )
@@ -235,7 +235,10 @@ void PartDynamicTextField::fromXml(const QDomElement &dom_elmt) {
m_uuid = QUuid(dom_elmt.attribute("uuid", QUuid::createUuid().toString()));
setFrame(dom_elmt.attribute("frame", "false") == "true"? true : false);
//The saved position is the one to keep
m_block_alignment = true;
setTextWidth(dom_elmt.attribute("text_width", QString::number(-1)).toDouble());
m_block_alignment = false;
QMetaEnum me = DynamicElementTextItem::textFromMetaEnum();
m_text_from = DynamicElementTextItem::TextFrom(
@@ -431,13 +434,23 @@ bool PartDynamicTextField::frame() const
return m_frame;
}
/**
@brief PartDynamicTextField::setTextWidth
Set the width of this text (-1 = automatic width). The point of the text
chosen by its alignment stays in place, as when the text changes: a
right-aligned or centred field no longer grows to the right only, and
undoing a width change puts it back where it was.
@param width
*/
void PartDynamicTextField::setTextWidth(qreal width) {
prepareAlignment();
this -> document() -> setTextWidth(width);
//Adjust the width, to ideal width if needed
if(width > 0 && document() -> size().width() > width)
document() -> setTextWidth(document() -> idealWidth());
finishAlignment();
m_text_width = document() -> textWidth();
emit textWidthChanged(m_text_width);
}
+6 -2
View File
@@ -157,7 +157,9 @@ void DynamicTextFieldEditor::updateForm()
m_color_kpb -> setColor(m_text_field.data() -> color());
}
#endif
ui -> m_width_sb -> setValue(m_text_field.data() -> textWidth());
//Rounded as on_m_width_sb_editingFinished() compares it;
//setValue(int) would truncate 61.7 to 61
ui -> m_width_sb -> setValue(qRound(m_text_field.data() -> textWidth()));
ui -> m_font_pb -> setText(m_text_field -> font().family());
switch (m_text_field.data() -> textFrom()) {
@@ -363,7 +365,9 @@ void DynamicTextFieldEditor::on_m_width_sb_editingFinished()
qreal width = (qreal)ui -> m_width_sb -> value();
for (int i = 0; i < m_parts.length(); i++) {
if(width != m_parts[i] -> textWidth()) {
//The box shows whole pixels: a width fitted to the text is not
//rounded just by leaving the box.
if(ui -> m_width_sb -> value() != qRound(m_parts[i] -> textWidth())) {
QPropertyUndoCommand *undo = new QPropertyUndoCommand(m_parts[i], "textWidth", m_parts[i] -> textWidth(), width);
undo -> setText(tr("Modifier la largeur d'un texte"));
undoStack().push(undo);
+7 -1
View File
@@ -101,11 +101,17 @@
</item>
<item row="4" column="1">
<widget class="QSpinBox" name="m_width_sb">
<property name="specialValueText">
<string>Auto</string>
</property>
<property name="suffix">
<string>px</string>
</property>
<property name="minimum">
<number>-1</number>
</property>
<property name="maximum">
<number>500</number>
<number>10000</number>
</property>
</widget>
</item>
+37
View File
@@ -366,6 +366,7 @@ void QETDiagramEditor::setUpActions()
//export to pdf
m_export_to_pdf = new QAction(QET::Icons::PDF, tr("Exporter en pdf"), this);
ShortcutManager::instance().registerAction(m_export_to_pdf, "diagrameditor.export_to_pdf", tr("Éditeur de schémas"), QKeySequence());
m_export_to_pdf->setStatusTip(tr("Exporte un ou plusieurs folios du projet courant", "status bar tip"));
connect(m_export_to_pdf, &QAction::triggered, [this] () {
auto project = currentProject();
@@ -482,6 +483,7 @@ void QETDiagramEditor::setUpActions()
//setting, so it does not need a diagram open or anything
//selected the way m_duplicate itself does.
m_configure_duplicate = new QAction(tr("Configurer la duplication..."), this);
ShortcutManager::instance().registerAction(m_configure_duplicate, "diagrameditor.configure_duplicate", tr("Éditeur de schémas"), QKeySequence());
m_configure_duplicate->setStatusTip(tr("Choisir l'espacement et la direction utilisés par Dupliquer", "status bar tip"));
connect(m_configure_duplicate, &QAction::triggered, [this]() {
DuplicateOffsetDialog dialog(this);
@@ -517,6 +519,7 @@ void QETDiagramEditor::setUpActions()
//AutoBreakConductor
m_auto_break_conductor = new QAction (QET::Icons::Conductor, tr("Coupure automatique de conducteur(s)","Tool tip of auto break conductor"), this);
ShortcutManager::instance().registerAction(m_auto_break_conductor, "diagrameditor.auto_break_conductor", tr("Éditeur de schémas"), QKeySequence());
m_auto_break_conductor->setStatusTip (tr("Couper automatiquement les conducteurs existants lors du placement d'un élément", "Status tip of auto break conductor"));
m_auto_break_conductor->setCheckable (true);
{
@@ -535,6 +538,7 @@ void QETDiagramEditor::setUpActions()
//Draw or not the background grid
m_draw_grid = new QAction ( QET::Icons::Grid, tr("Afficher la grille"), this);
ShortcutManager::instance().registerAction(m_draw_grid, "diagrameditor.draw_grid", tr("Éditeur de schémas"), QKeySequence());
m_draw_grid->setStatusTip(tr("Affiche ou masque la grille des folios"));
QSettings settings;
m_draw_grid->setCheckable(true);
@@ -576,6 +580,7 @@ void QETDiagramEditor::setUpActions()
// Draw or not the custom guides
m_draw_guides = new QAction ( QIcon::fromTheme("guides"), tr("Afficher les guides"), this);
ShortcutManager::instance().registerAction(m_draw_guides, "diagrameditor.draw_guides", tr("Éditeur de schémas"), QKeySequence());
m_draw_guides->setStatusTip(tr("Affiche ou masque les guides"));
m_draw_guides->setCheckable(true);
m_draw_guides->setChecked(settings.value("diagrameditor/guides_display_startup", false).toBool());
@@ -588,6 +593,7 @@ void QETDiagramEditor::setUpActions()
//Keep the column numbers and row letters of the folio in sight
m_cell_rulers = new QAction(tr("Garder les en-têtes visibles"), this);
ShortcutManager::instance().registerAction(m_cell_rulers, "diagrameditor.cell_rulers", tr("Éditeur de schémas"), QKeySequence());
m_cell_rulers->setStatusTip(tr("Garde les numéros de colonne et les lettres de ligne du folio visibles au bord de la vue"));
m_cell_rulers->setCheckable(true);
m_cell_rulers->setChecked(settings.value("diagrameditor/cell_rulers", false).toBool());
@@ -600,6 +606,7 @@ void QETDiagramEditor::setUpActions()
//Draw the limits of the folio columns and rows across the drawing
m_cell_lines = new QAction(tr("Afficher les limites des cases"), this);
ShortcutManager::instance().registerAction(m_cell_lines, "diagrameditor.cell_lines", tr("Éditeur de schémas"), QKeySequence());
m_cell_lines->setStatusTip(tr("Trace les limites des colonnes et des lignes du folio sur le schéma, à l'écran seulement"));
m_cell_lines->setCheckable(true);
m_cell_lines->setChecked(settings.value("diagrameditor/cell_lines", false).toBool());
@@ -654,6 +661,7 @@ void QETDiagramEditor::setUpActions()
//Edit current project properties
m_project_edit_properties = new QAction(QET::Icons::ProjectProperties, tr("Propriétés du projet"), this);
ShortcutManager::instance().registerAction(m_project_edit_properties, "diagrameditor.project_edit_properties", tr("Éditeur de schémas"), QKeySequence());
connect(m_project_edit_properties, &QAction::triggered, [this]() {
editProjectProperties(currentProjectView());
});
@@ -669,10 +677,12 @@ void QETDiagramEditor::setUpActions()
//Remove current folio from current project
m_remove_diagram_from_project = new QAction(QET::Icons::DiagramDelete, tr("Supprimer le folio"), this);
ShortcutManager::instance().registerAction(m_remove_diagram_from_project, "diagrameditor.remove_diagram_from_project", tr("Éditeur de schémas"), QKeySequence());
connect(m_remove_diagram_from_project, &QAction::triggered, this, &QETDiagramEditor::removeDiagramFromProject);
//Clean the current project
m_clean_project = new QAction(QET::Icons::EditClear, tr("Nettoyer le projet"), this);
ShortcutManager::instance().registerAction(m_clean_project, "diagrameditor.clean_project", tr("Éditeur de schémas"), QKeySequence());
connect(m_clean_project, &QAction::triggered, [this]() {
if (ProjectView *current_project = currentProjectView()) {
if (current_project->cleanProject()) {
@@ -683,6 +693,7 @@ void QETDiagramEditor::setUpActions()
//Export nomenclature to CSV
m_csv_export = new QAction(QET::Icons::DocumentSpreadsheet, tr("Exporter au format CSV"), this);
ShortcutManager::instance().registerAction(m_csv_export, "diagrameditor.csv_export", tr("Éditeur de schémas"), QKeySequence());
connect(m_csv_export, &QAction::triggered, [this]() {
BOMExportDialog bom(currentProjectView()->project(), this);
bom.exec();
@@ -690,6 +701,7 @@ void QETDiagramEditor::setUpActions()
//Add a nomenclature item
m_add_nomenclature = new QAction(QET::Icons::TableOfContent, tr("Ajouter une nomenclature"), this);
ShortcutManager::instance().registerAction(m_add_nomenclature, "diagrameditor.add_nomenclature", tr("Éditeur de schémas"), QKeySequence());
connect(m_add_nomenclature, &QAction::triggered, this, [this]() {
if(this->currentDiagramView()) {
QetGraphicsTableFactory::createAndAddNomenclature(this->currentDiagramView()->diagram());
@@ -698,6 +710,7 @@ void QETDiagramEditor::setUpActions()
//Add a summary item
m_add_summary = new QAction(QET::Icons::TableOfContent, tr("Ajouter un sommaire"), this);
ShortcutManager::instance().registerAction(m_add_summary, "diagrameditor.add_summary", tr("Éditeur de schémas"), QKeySequence());
connect(m_add_summary, &QAction::triggered, this, [this]() {
if(this->currentDiagramView()) {
QetGraphicsTableFactory::createAndAddSummary(this->currentDiagramView()->diagram());
@@ -705,6 +718,7 @@ void QETDiagramEditor::setUpActions()
});
m_terminal_strip_dialog = new QAction(QET::Icons::TerminalStrip, tr("Gestionnaire de borniers (DEV)"), this);
ShortcutManager::instance().registerAction(m_terminal_strip_dialog, "diagrameditor.terminal_strip_dialog", tr("Éditeur de schémas"), QKeySequence());
connect(m_terminal_strip_dialog, &QAction::triggered, this, [this]()
{
if (auto project = this->currentProject())
@@ -715,10 +729,12 @@ void QETDiagramEditor::setUpActions()
//Launch the plugin of terminal generator
m_project_terminalBloc = new QAction(QET::Icons::TerminalStrip, tr("Lancer le plugin de création de borniers"), this);
ShortcutManager::instance().registerAction(m_project_terminalBloc, "diagrameditor.terminal_strip_plugin", tr("Éditeur de schémas"), QKeySequence());
connect(m_project_terminalBloc, &QAction::triggered, this, &QETDiagramEditor::generateTerminalBlock);
//Export conductor num to csv
m_project_export_conductor_num = new QAction(QET::Icons::DocumentSpreadsheet, tr("Exporter la liste des noms de conducteurs"), this);
ShortcutManager::instance().registerAction(m_project_export_conductor_num, "diagrameditor.export_conductor_names", tr("Éditeur de schémas"), QKeySequence());
connect(m_project_export_conductor_num, &QAction::triggered, [this]() {
QETProject *project = this->currentProject();
if (project)
@@ -729,6 +745,7 @@ void QETDiagramEditor::setUpActions()
});
// Export wiring list to CSV
m_project_export_wiring_list = new QAction(QET::Icons::DocumentSpreadsheet, tr("Exporter le plan de câblage"), this);
ShortcutManager::instance().registerAction(m_project_export_wiring_list, "diagrameditor.export_wiring_list", tr("Éditeur de schémas"), QKeySequence());
connect(m_project_export_wiring_list, &QAction::triggered, [this]() {
QETProject *project = this->currentProject();
if (project)
@@ -740,6 +757,7 @@ void QETDiagramEditor::setUpActions()
// Show the wiring list read from the project database
m_project_wiring_list_view = new QAction(QET::Icons::DocumentSpreadsheet, tr("Liste de câblage (base de données)"), this);
ShortcutManager::instance().registerAction(m_project_wiring_list_view, "diagrameditor.wiring_list_view", tr("Éditeur de schémas"), QKeySequence());
connect(m_project_wiring_list_view, &QAction::triggered, [this]() {
QETProject *project = this->currentProject();
if (project)
@@ -751,10 +769,12 @@ void QETDiagramEditor::setUpActions()
// Terminal Numbering
m_terminal_numbering = new QAction(QET::Icons::TerminalStrip, tr("Numérotation automatique des bornes"), this);
ShortcutManager::instance().registerAction(m_terminal_numbering, "diagrameditor.terminal_numbering", tr("Éditeur de schémas"), QKeySequence());
connect(m_terminal_numbering, &QAction::triggered, this, &QETDiagramEditor::slot_terminalNumbering);
// Reload element drawings from their current definition (bugtracker #802)
m_reload_element_drawings = new QAction(QET::Icons::ViewRefresh, tr("Recharger les dessins des éléments"), this);
ShortcutManager::instance().registerAction(m_reload_element_drawings, "diagrameditor.reload_element_drawings", tr("Éditeur de schémas"), QKeySequence());
m_reload_element_drawings->setStatusTip(
tr("Redessine chaque élément placé d'après sa définition actuelle,"
" sans avoir à fermer et rouvrir le projet (action non annulable)"));
@@ -763,6 +783,7 @@ void QETDiagramEditor::setUpActions()
#ifdef QET_HAS_SCRIPTING
// Run a JavaScript macro against the current project (bugtracker #162).
m_run_script = new QAction(tr("Exécuter un script..."), this);
ShortcutManager::instance().registerAction(m_run_script, "diagrameditor.run_script", tr("Éditeur de schémas"), QKeySequence());
m_run_script->setStatusTip(
tr("Exécute un script JavaScript sur le projet courant (voir qet.*"
" dans le script pour l'API disponible)"));
@@ -813,6 +834,7 @@ void QETDiagramEditor::setUpActions()
//Stored scripts are files in a folder, written by hand, by the
//script manager or by an assistant: open it to add one.
m_open_scripts_folder = new QAction(QET::Icons::FolderOpen, tr("Ouvrir le dossier des scripts"), this);
ShortcutManager::instance().registerAction(m_open_scripts_folder, "diagrameditor.open_scripts_folder", tr("Éditeur de schémas"), QKeySequence());
m_open_scripts_folder->setStatusTip(
tr("Chaque fichier .js de ce dossier qui commence par un en-tête"
" // ==QETScript== devient un bouton"));
@@ -824,6 +846,7 @@ void QETDiagramEditor::setUpActions()
#ifdef QET_EXPORT_PROJECT_DB
m_export_project_db = new QAction(QET::Icons::DocumentSpreadsheet, tr("Exporter la base de donnée interne du projet"), this);
ShortcutManager::instance().registerAction(m_export_project_db, "diagrameditor.export_project_db", tr("Éditeur de schémas"), QKeySequence());
connect(m_export_project_db, &QAction::triggered, [this]() {
projectDataBase::exportDb(this->currentProject()->dataBase(), this);
});
@@ -831,11 +854,13 @@ void QETDiagramEditor::setUpActions()
//MDI view style
m_tabbed_view_mode = new QAction(tr("en utilisant des onglets"), this);
ShortcutManager::instance().registerAction(m_tabbed_view_mode, "diagrameditor.tabbed_view_mode", tr("Éditeur de schémas"), QKeySequence());
m_tabbed_view_mode->setStatusTip(tr("Présente les différents projets ouverts des onglets", "status bar tip"));
m_tabbed_view_mode->setCheckable(true);
connect(m_tabbed_view_mode, &QAction::triggered, this, &QETDiagramEditor::setTabbedMode);
m_windowed_view_mode = new QAction(tr("en utilisant des fenêtres"), this);
ShortcutManager::instance().registerAction(m_windowed_view_mode, "diagrameditor.windowed_view_mode", tr("Éditeur de schémas"), QKeySequence());
m_windowed_view_mode->setStatusTip(tr("Présente les différents projets ouverts dans des sous-fenêtres", "status bar tip"));
m_windowed_view_mode->setCheckable(true);
connect(m_windowed_view_mode, &QAction::triggered, this, &QETDiagramEditor::setWindowedMode);
@@ -846,15 +871,18 @@ void QETDiagramEditor::setUpActions()
m_group_view_mode -> setExclusive(true);
m_tile_window = new QAction(tr("&Mosaïque"), this);
ShortcutManager::instance().registerAction(m_tile_window, "diagrameditor.tile_windows", tr("Éditeur de schémas"), QKeySequence());
m_tile_window->setStatusTip(tr("Dispose les fenêtres en mosaïque", "status bar tip"));
connect(m_tile_window, &QAction::triggered, &m_workspace, &QMdiArea::tileSubWindows);
m_cascade_window = new QAction(tr("&Cascade"), this);
ShortcutManager::instance().registerAction(m_cascade_window, "diagrameditor.cascade_windows", tr("Éditeur de schémas"), QKeySequence());
m_cascade_window->setStatusTip(tr("Dispose les fenêtres en cascade", "status bar tip"));
connect(m_cascade_window, &QAction::triggered, &m_workspace, &QMdiArea::cascadeSubWindows);
//Switch selection/view mode
m_mode_selection = new QAction(QET::Icons::PartSelect, tr("Mode Selection"), this);
ShortcutManager::instance().registerAction(m_mode_selection, "diagrameditor.mode_selection", tr("Éditeur de schémas"), QKeySequence());
m_mode_selection->setStatusTip(tr("Permet de sélectionner les éléments", "status bar tip"));
m_mode_selection->setCheckable(true);
m_mode_selection->setChecked(true);
@@ -867,6 +895,7 @@ void QETDiagramEditor::setUpActions()
});
m_mode_visualise = new QAction(QET::Icons::ViewMove, tr("Mode Visualisation"), this);
ShortcutManager::instance().registerAction(m_mode_visualise, "diagrameditor.mode_visualise", tr("Éditeur de schémas"), QKeySequence());
m_mode_visualise->setStatusTip(tr("Permet de visualiser le folio sans pouvoir le modifier", "status bar tip"));
m_mode_visualise->setCheckable(true);
connect(m_mode_visualise, &QAction::triggered, [this]() {
@@ -938,6 +967,12 @@ void QETDiagramEditor::setUpActions()
add_row ->setData("add_row");
remove_row ->setData("remove_row");
for (QAction *action : m_row_column_actions_group.actions()) {
ShortcutManager::instance().registerAction(
action, "diagrameditor." + action->data().toString(),
tr("Éditeur de schémas"), QKeySequence());
}
connect(&m_row_column_actions_group, &QActionGroup::triggered, this, &QETDiagramEditor::rowColumnGroupTriggered);
//Selections Actions (related to a selected item)
@@ -955,6 +990,8 @@ void QETDiagramEditor::setUpActions()
ShortcutManager::instance().registerAction(m_rotate_selection, "diagrameditor.rotate_selection", tr("Éditeur de schémas"), Qt::Key_Space);
ShortcutManager::instance().registerAction(m_rotate_group_selection, "diagrameditor.rotate_group_selection", tr("Éditeur de schémas"), Qt::SHIFT | Qt::Key_Space);
ShortcutManager::instance().registerAction(m_rotate_texts, "diagrameditor.rotate_texts", tr("Éditeur de schémas"), Qt::CTRL | Qt::Key_Space);
ShortcutManager::instance().registerAction(m_find_element, "diagrameditor.find_element", tr("Éditeur de schémas"), QKeySequence());
ShortcutManager::instance().registerAction(m_group_selected_texts, "diagrameditor.group_selected_texts", tr("Éditeur de schémas"), QKeySequence());
ShortcutManager::instance().registerAction(m_edit_selection, "diagrameditor.edit_selection", tr("Éditeur de schémas"), Qt::CTRL | Qt::Key_E);
//Re-enter placement mode with the element placed last. Bare A rather
+81 -70
View File
@@ -194,6 +194,30 @@ void DiagramImageItem::setPixmap(const QPixmap &pixmap) {
emit pixmapChanged();
}
/**
@brief DiagramImageItem::setImageSource
Set the picture's source -- original, crop rectangle and transparent
colours. Only stores them: the displayed pixmap is a property of its
own, set by the same undo command.
*/
void DiagramImageItem::setImageSource(const ImageSource &source)
{
m_base_pixmap = source.base;
m_crop_rect = source.crop;
m_transparent_colors = source.colors;
}
QVariant DiagramImageItem::imageSourceVariant() const
{
return QVariant::fromValue(imageSource());
}
void DiagramImageItem::setImageSourceVariant(const QVariant &source)
{
if (source.canConvert<ImageSource>())
setImageSource(source.value<ImageSource>());
}
/**
@brief DiagramImageItem::setScaleFactorX / setScaleFactorY / setRotationAngle
Matching QetShapeItem's own setters for the identical fields --
@@ -1814,21 +1838,13 @@ void DiagramImageItem::replaceImage()
// or region was cropped for the old one, so this starts that
// memory fresh rather than carrying over choices that would no
// longer make sense.
m_base_pixmap = newPixmap;
m_crop_rect = newPixmap.rect();
m_transparent_colors.clear();
const ImageSource oldSource = imageSource();
const ImageSource newSource{newPixmap, newPixmap.rect(), {}};
auto *undo = new QPropertyUndoCommand(this, "pixmap", oldPixmap, newPixmap);
undo->setText(tr("Remplacer une image"));
// Every call here is a separate, deliberate menu action with no
// compound child of its own (unlike crop(), which always chains a
// pos/rawPivot change and is naturally immune) -- two of them in a
// row would carry the exact same object, property, and text, which
// is indistinguishable from a legitimate merge to
// QPropertyUndoCommand::mergeWith(). A dummy child (already treated
// as "never merge" by that check) keeps each one its own, separate
// undo step regardless.
new QUndoCommand(undo);
new QPropertyUndoCommand(this, "imageSource", QVariant::fromValue(oldSource),
QVariant::fromValue(newSource), undo);
diagram()->undoStack().push(undo);
}
@@ -1854,7 +1870,9 @@ void DiagramImageItem::mirror(bool horizontal)
// colour values don't change when the image is mirrored, only their
// positions, so whatever was already keyed transparent should stay
// remembered and still apply correctly to the flipped version.
m_base_pixmap = m_base_pixmap.transformed(flip);
const ImageSource oldSource = imageSource();
ImageSource newSource = oldSource;
newSource.base = m_base_pixmap.transformed(flip);
// m_crop_rect, unlike the colour list, DOES need to change: it's
// defined in terms of positions within the base, and those
@@ -1864,19 +1882,16 @@ void DiagramImageItem::mirror(bool horizontal)
// whether this reads m_base_pixmap's size from before or after the
// assignment above).
if (horizontal)
m_crop_rect = QRect(m_base_pixmap.width() - m_crop_rect.left() - m_crop_rect.width(),
newSource.crop = QRect(m_base_pixmap.width() - m_crop_rect.left() - m_crop_rect.width(),
m_crop_rect.top(), m_crop_rect.width(), m_crop_rect.height());
else
m_crop_rect = QRect(m_crop_rect.left(), m_base_pixmap.height() - m_crop_rect.top() - m_crop_rect.height(),
newSource.crop = QRect(m_crop_rect.left(), m_base_pixmap.height() - m_crop_rect.top() - m_crop_rect.height(),
m_crop_rect.width(), m_crop_rect.height());
auto *undo = new QPropertyUndoCommand(this, "pixmap", oldPixmap, newPixmap);
undo->setText(horizontal ? tr("Miroir horizontal d'une image") : tr("Miroir vertical d'une image"));
// See replaceImage()'s identical comment: a separate, deliberate
// action with no compound child of its own, so a dummy one is
// needed to stop two consecutive same-direction mirrors (identical
// object, property, and text) from silently merging into one.
new QUndoCommand(undo);
new QPropertyUndoCommand(this, "imageSource", QVariant::fromValue(oldSource),
QVariant::fromValue(newSource), undo);
diagram()->undoStack().push(undo);
}
@@ -1919,41 +1934,20 @@ void DiagramImageItem::setTransparentColor()
if (dialog.exec() != QDialog::Accepted)
return;
m_transparent_colors = dialog.pickedColors();
const ImageSource oldSource = imageSource();
ImageSource newSource = oldSource;
newSource.colors = dialog.pickedColors();
const QPixmap oldPixmap = pixmap_;
const QPixmap newPixmap = dialog.resultPixmap();
auto *undo = new QPropertyUndoCommand(this, "pixmap", oldPixmap, newPixmap);
undo->setText(tr("Définir une couleur transparente"));
// See replaceImage()'s identical comment: a separate, deliberate
// action with no compound child of its own, so a dummy one is
// needed to stop two consecutive transparency edits (identical
// object, property, and text) from silently merging into one.
new QUndoCommand(undo);
new QPropertyUndoCommand(this, "imageSource", QVariant::fromValue(oldSource),
QVariant::fromValue(newSource), undo);
diagram()->undoStack().push(undo);
}
/**
@brief DiagramImageItem::crop
Context-menu action: opens ImageCropDialog against pixmap_ (the
current, already colour-keyed display, so cropping is WYSIWYG
against whatever is actually visible), then applies the chosen
rectangle to both pixmap_ and m_base_pixmap together -- kept in
sync the same way mirror() keeps them in sync, since cropping is a
permanent, geometric change to the image's own content, unlike
setTransparentColor()'s non-destructive colour keying.
pos() also needs adjusting, not just pixmap_: setPixmap() (called
via the "pixmap" undo command below) recomputes
transformOriginPoint() from the new, smaller boundingRect(), but
pos() itself is untouched by that -- without fixing it up here too,
the surviving content would visually jump to wherever local (0,0)
happens to land after shrinking, rather than staying exactly where
it already was. Chained into one undo step together with the pixmap
change, since undoing a crop has to restore both, or the restored
(larger) image ends up in the wrong place.
*/
/**
@brief DiagramImageItem::crop
Context-menu action: opens ImageCropDialog against m_base_pixmap
@@ -1962,27 +1956,7 @@ void DiagramImageItem::setTransparentColor()
destructive: nothing about the original content is ever discarded,
only which region of it is currently being shown, exactly the same
principle setTransparentColor() already follows for its own choices.
Recomputes pixmap_ via computeDisplayPixmap() so any already-picked
transparent colours are correctly re-applied to the newly-cropped
region, rather than lost (the crop dialog itself knows nothing
about them).
pos() also needs adjusting, not just pixmap_: setPixmap() (called
via the "pixmap" undo command below) recomputes
transformOriginPoint() from the new boundingRect(), but pos() itself
is untouched by that -- without fixing it up here too, the
surviving content would visually jump to wherever local (0,0) ends
up after the crop rect changes, rather than staying exactly where
it already was. This has to work whether this is the first crop
ever applied or an adjustment of an existing one, so the position
math is always done relative to the CURRENT crop rect (m_crop_rect,
before it's updated below) -- when there's no previous crop, that's
simply the whole base, which is what the very first version of this
method assumed unconditionally.
Chained into one undo step together with the pixmap change, since
undoing a crop has to restore both, or the restored (larger) image
ends up in the wrong place.
The crop itself is done by applyCrop().
*/
void DiagramImageItem::crop()
{
@@ -1994,9 +1968,41 @@ void DiagramImageItem::crop()
if (dialog.exec() != QDialog::Accepted)
return;
const QRect newCropRect = dialog.cropRect();
applyCrop(dialog.cropRect());
}
/**
@brief DiagramImageItem::applyCrop
Show @a cropRect of the original (in the original's own pixels),
keeping the centre of the kept region where it is on the folio.
Recomputes pixmap_ via computeDisplayPixmap() so any already-picked
transparent colours are correctly re-applied to the newly-cropped
region, rather than lost.
pos() also needs adjusting, not just pixmap_: setPixmap() (called
via the "pixmap" undo command below) recomputes
transformOriginPoint() from the new boundingRect(), but pos() itself
is untouched by that -- without fixing it up here too, the
surviving content would visually jump to wherever local (0,0) ends
up after the crop rect changes, rather than staying exactly where
it already was. This has to work whether this is the first crop
ever applied or an adjustment of an existing one, so the position
math is always done relative to the CURRENT crop rect (m_crop_rect,
before it's updated below) -- when there's no previous crop, that's
simply the whole base.
One undo step, which restores the pixmap, the position, the pivot
and the crop rectangle together.
@return false if nothing was cropped: read-only folio, or a
rectangle that is empty, outside the original, or the current one.
*/
bool DiagramImageItem::applyCrop(const QRect &cropRect)
{
if (!diagram() || diagram()->isReadOnly())
return false;
const QRect newCropRect = cropRect.intersected(m_base_pixmap.rect());
if (newCropRect.isEmpty() || newCropRect == m_crop_rect)
return; // nothing actually changed
return false; // nothing actually changed
// newCropRect is in m_base_pixmap's own coordinates; converting its
// center into the CURRENT local space (pixmap_'s own coordinates,
@@ -2009,7 +2015,9 @@ void DiagramImageItem::crop()
const QPixmap oldPixmap = pixmap_;
const QPixmap newPixmap = computeDisplayPixmap(m_base_pixmap, newCropRect, m_transparent_colors);
m_crop_rect = newCropRect;
const ImageSource oldSource = imageSource();
ImageSource newSource = oldSource;
newSource.crop = newCropRect;
// boundingRect() is exactly QRectF(pixmap_.rect()) (confirmed by
// reading the actual implementation, not assumed) -- so the new
@@ -2027,6 +2035,9 @@ void DiagramImageItem::crop()
undo->setText(tr("Rogner une image"));
new QPropertyUndoCommand(this, "pos", oldPos, newPos, undo);
new QPropertyUndoCommand(this, "rawPivot", oldPivot, newOriginPoint, undo);
new QPropertyUndoCommand(this, "imageSource", QVariant::fromValue(oldSource),
QVariant::fromValue(newSource), undo);
m_pivotIsCustom = false;
diagram()->undoStack().push(undo);
return true;
}
@@ -49,6 +49,10 @@ class DiagramImageItem : public QetGraphicsItem {
Q_PROPERTY(qreal skewY READ skewY WRITE setSkewY NOTIFY transformChanged)
Q_PROPERTY(QPointF pivot READ pivot WRITE setPivot NOTIFY transformChanged)
Q_PROPERTY(QString label READ label WRITE setLabel NOTIFY labelChanged)
// The picture's source -- original, crop rectangle, transparent
// colours -- as one value, so that every edit of it (crop, colour
// key, mirror, replace) is undone together with the displayed pixmap.
Q_PROPERTY(QVariant imageSource READ imageSourceVariant WRITE setImageSourceVariant)
// A second, deliberately non-compensating property on the SAME
// underlying value -- setPivot() (above) intentionally adjusts
// pos() to keep the image visually in place, which is exactly
@@ -65,6 +69,19 @@ class DiagramImageItem : public QetGraphicsItem {
DiagramImageItem(QetGraphicsItem * = nullptr);
DiagramImageItem(const QPixmap &pixmap, QetGraphicsItem * = nullptr);
~DiagramImageItem() override;
struct ImageSource
{
QPixmap base;
QRect crop;
QList<ImageTransparentColorDialog::PickedColor> colors;
};
ImageSource imageSource() const { return {m_base_pixmap, m_crop_rect, m_transparent_colors}; }
void setImageSource(const ImageSource &source);
QVariant imageSourceVariant() const;
void setImageSourceVariant(const QVariant &source);
QRect cropRect() const { return m_crop_rect; }
bool applyCrop(const QRect &cropRect);
// attributes
public:
@@ -246,4 +263,6 @@ class DiagramImageItem : public QetGraphicsItem {
QPointF m_label_scale{1.0, 1.0}; // scale the label rect was last computed for -- see updateLabelScale()
bool m_resizeCenterAnchored = false; // decided once, at press time -- see handlerMousePressEvent()'s comment for why, mirroring the identical fix already made for shape creation
};
Q_DECLARE_METATYPE(DiagramImageItem::ImageSource)
#endif
+3 -1
View File
@@ -269,7 +269,9 @@ void DiagramTextItem::setHtml(const QString &text)
block = block.next();
}
if (m_non_left_alignment) {
//Room for centred or right-aligned lines, unless the user has
//given this text a width of its own
if (m_non_left_alignment && !hasUserTextWidth()) {
document()->setTextWidth(document()->idealWidth() + 40.0);
}
}
@@ -110,6 +110,8 @@ class DiagramTextItem : public QGraphicsTextItem
void prepareAlignment();
void finishAlignment();
virtual bool hasUserTextWidth() const {return false;}
protected:
bool
@@ -26,7 +26,9 @@
#include "../utils/qetutils.h"
#include <QDomElement>
#include <QScopedPointer>
#include <QSettings>
#include <QtCore/qnumeric.h>
/**
Constructeur
@@ -36,6 +38,7 @@ IndependentTextItem::IndependentTextItem() :
DiagramTextItem(nullptr)
{
ShownKinds::tag(this, ShownKinds::FreeTexts);
wrapAtWords();
//Start from the font last applied to a text item this session,
//falling back to the app-wide Preferences default otherwise.
setFont(LastUsedStyle::hasTextFont() ? LastUsedStyle::textFont()
@@ -53,6 +56,19 @@ IndependentTextItem::IndependentTextItem(const QString &text) :
DiagramTextItem(text, nullptr)
{
ShownKinds::tag(this, ShownKinds::FreeTexts);
wrapAtWords();
}
/**
@brief IndependentTextItem::wrapAtWords
A text with a width wraps between words only: a word longer than the
width goes past it rather than being cut, as for the texts of symbols.
*/
void IndependentTextItem::wrapAtWords()
{
QTextOption option = document()->defaultTextOption();
option.setWrapMode(QTextOption::WordWrap);
document()->setDefaultTextOption(option);
}
/// Destructeur
@@ -78,6 +94,10 @@ void IndependentTextItem::fromXml(const QDomElement &e) {
QETUtils::fontFromString(font, e.attribute("font"));
setFont(font);
}
//Optional: absent for a text with the automatic width, the only
//kind older versions know (they show such a text unwrapped).
//Read after the text, setHtml() sets a width of its own.
setTextWidth(e.attribute(QStringLiteral("text_width"), QStringLiteral("-1")).toDouble());
}
/**
@@ -93,10 +113,41 @@ QDomElement IndependentTextItem::toXml(QDomDocument &document) const
result.setAttribute("text", toHtml());
result.setAttribute("rotation", QString::number(QET::correctAngle(rotation())));
result.setAttribute("font", QETUtils::fontToString(font()));
//Only when set, so a text with the automatic width is saved as before
if (m_text_width > 0)
result.setAttribute("text_width", QString::number(m_text_width));
return(result);
}
/**
@brief IndependentTextItem::setTextWidth
Set the width of this text (-1 = automatic width): the text wraps to
it, its top-left corner stays in place.
@param width
*/
void IndependentTextItem::setTextWidth(qreal width)
{
if (!qIsFinite(width) || width <= 0)
width = -1;
if (qFuzzyCompare(width, m_text_width))
return;
qreal document_width = width;
//The automatic width of a text with centred or right-aligned
//lines, as setHtml() gives it
if (width < 0 && m_non_left_alignment)
{
QScopedPointer<QTextDocument> natural(document()->clone());
natural->setTextWidth(-1);
document_width = natural->idealWidth() + 40.0;
}
document()->setTextWidth(document_width);
m_text_width = width;
emit textWidthChanged(width);
}
void IndependentTextItem::focusOutEvent(QFocusEvent *event)
{
DiagramTextItem::focusOutEvent(event);
@@ -29,10 +29,13 @@
class IndependentTextItem : public DiagramTextItem
{
Q_OBJECT
Q_PROPERTY(qreal textWidth READ textWidth WRITE setTextWidth NOTIFY textWidthChanged)
// constructors, destructor
signals:
void uuidChanged();
void textWidthChanged(qreal width);
public:
IndependentTextItem();
@@ -49,11 +52,23 @@ class IndependentTextItem : public DiagramTextItem
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
//Hide QGraphicsTextItem::textWidth()/setTextWidth(), which are not
//virtual: called through a QGraphicsTextItem or DiagramTextItem
//pointer they would change the document only, and the width would
//be neither saved nor shown in the properties. Use these, or the
//"textWidth" property.
qreal textWidth() const {return m_text_width;}
void setTextWidth(qreal width);
protected:
void focusOutEvent(QFocusEvent *event) override;
bool hasUserTextWidth() const override {return m_text_width > 0;}
private:
void wrapAtWords();
QUuid m_uuid = QUuid::createUuid();
qreal m_text_width = -1;
};
#endif
+10
View File
@@ -69,6 +69,7 @@ void QETMainWindow::initCommonActions()
QETApp *qet_app = QETApp::instance();
configure_action_ = new QAction(QET::Icons::Configure, tr("&Configurer QElectroTech"), this);
ShortcutManager::instance().registerAction(configure_action_, "mainwindow.configure", tr("Général"), QKeySequence());
configure_action_ -> setStatusTip(tr("Permet de régler différents paramètres de QElectroTech", "status bar tip"));
connect(configure_action_, &QAction::triggered, [qet_app]()
{
@@ -107,8 +108,10 @@ void QETMainWindow::initCommonActions()
connect(fullscreen_action_, &QAction::triggered, this, &QETMainWindow::toggleFullScreen);
whatsthis_action_ = QWhatsThis::createAction(this);
ShortcutManager::instance().registerAction(whatsthis_action_, "mainwindow.whats_this", tr("Général"), Qt::SHIFT | Qt::Key_F1);
about_qet_ = new QAction(QET::Icons::QETLogo, tr("À &propos de QElectroTech"), this);
ShortcutManager::instance().registerAction(about_qet_, "mainwindow.about_qet", tr("Général"), QKeySequence());
about_qet_ -> setStatusTip(tr("Affiche des informations sur QElectroTech", "status bar tip"));
connect(about_qet_, &QAction::triggered, qet_app, &QETApp::aboutQET);
@@ -123,6 +126,7 @@ void QETMainWindow::initCommonActions()
ShortcutManager::instance().registerAction(manual_online_, "mainwindow.manual_online", tr("Général"), Qt::Key_F1);
connect_ai_ = new QAction(tr("Connecter un assistant IA..."), this);
ShortcutManager::instance().registerAction(connect_ai_, "mainwindow.connect_ai", tr("Général"), QKeySequence());
connect_ai_ -> setStatusTip(tr("Affiche la configuration qui permet à un assistant IA d'utiliser QElectroTech", "status bar tip"));
connect(connect_ai_, &QAction::triggered, this, [this]() {
AiAssistantDialog dialog(this);
@@ -130,6 +134,7 @@ void QETMainWindow::initCommonActions()
});
youtube_ = new QAction(QET::Icons::QETVideo, tr("Chaine Youtube"), this);
ShortcutManager::instance().registerAction(youtube_, "mainwindow.youtube", tr("Général"), QKeySequence());
youtube_ -> setStatusTip(tr("Lance le navigateur par défaut vers la chaine Youtube de QElectroTech", "status bar tip"));
connect(youtube_, &QAction::triggered, [](bool) {
@@ -138,9 +143,11 @@ void QETMainWindow::initCommonActions()
});
upgrade_ = new QAction(QET::Icons::QETDownload, tr("Télécharger une nouvelle version (dev)"), this);
ShortcutManager::instance().registerAction(upgrade_, "mainwindow.download_windows", tr("Général"), QKeySequence());
upgrade_ -> setStatusTip(tr("Lance le navigateur par défaut vers le dépot Nightly en ligne de QElectroTech", "status bar tip"));
upgrade_M = new QAction(QET::Icons::QETDownload, tr("Télécharger une nouvelle version (dev)"), this);
ShortcutManager::instance().registerAction(upgrade_M, "mainwindow.download_mac", tr("Général"), QKeySequence());
upgrade_M -> setStatusTip(tr("Lance le navigateur par défaut vers le dépot Nightly en ligne de QElectroTech", "status bar tip"));
connect(upgrade_, &QAction::triggered, [](bool) {
@@ -154,6 +161,7 @@ void QETMainWindow::initCommonActions()
});
donate_ = new QAction(QET::Icons::QETDonate, tr("Soutenir le projet par un don"), this);
ShortcutManager::instance().registerAction(donate_, "mainwindow.donate", tr("Général"), QKeySequence());
donate_ -> setStatusTip(tr("Soutenir le projet QElectroTech par un don", "status bar tip"));
connect(donate_, &QAction::triggered, [](bool) {
@@ -162,10 +170,12 @@ void QETMainWindow::initCommonActions()
});
about_qt_ = new QAction(QET::Icons::QtLogo, tr("À propos de &Qt"), this);
ShortcutManager::instance().registerAction(about_qt_, "mainwindow.about_qt", tr("Général"), QKeySequence());
about_qt_ -> setStatusTip(tr("Affiche des informations sur la bibliothèque Qt", "status bar tip"));
connect(about_qt_, &QAction::triggered, qApp, &QApplication::aboutQt);
diagnostics_action_ = new QAction(QET::Icons::DialogInformation, tr("Enregistrer un rapport de diagnostic..."), this);
ShortcutManager::instance().registerAction(diagnostics_action_, "mainwindow.diagnostics_report", tr("Général"), QKeySequence());
diagnostics_action_ -> setStatusTip(tr("Génère un rapport avec les derniers messages de journalisation, pour l'inclure dans un rapport de bug", "status bar tip"));
connect(diagnostics_action_, &QAction::triggered, this, []() {
QETApp::instance()->showDiagnosticsReport();
+3 -1
View File
@@ -858,7 +858,9 @@ QJsonObject LiveServer::print(const QJsonObject &request)
if (fi.exists()) return failure(QStringLiteral("%1 already exists; printing never replaces a file").arg(file));
}
QPrinter printer = file.isEmpty() ? QPrinter(info) : QPrinter();
//info is still null when printing to a file, which is what QPrinter()
//uses. No ?: here: MSVC copies its result, and QPrinter cannot be copied
QPrinter printer(info);
//96 dpi, as the PDF export draws: symbols are replayed at the
//device's resolution, so at a printer's 600-1200 dpi they came out
//many times too big next to the wires (seen on a test print)
+38
View File
@@ -3475,6 +3475,44 @@ bool QetScriptApi::setImageRotation(int folioIndex, int imageIndex, double angle
return true;
}
/**
@brief QetScriptApi::cropImage
Show only the rectangle (x, y, width, height) of the image's original,
in the original's own pixels, as the crop tool does: one undo step,
the kept region staying where it is on the folio.
@return false if nothing was cropped: no such image, read-only
project, or a rectangle that is empty, outside the original, or the
current crop.
*/
bool QetScriptApi::cropImage(int folioIndex, int imageIndex, int x, int y, int width, int height)
{
if (!m_project) return false;
if (m_project->isReadOnly()) {
log(QStringLiteral("qet.cropImage: project is read-only"));
return false;
}
const QList<DiagramImageItem *> list = sortedImages(folioIndex);
if (imageIndex < 0 || imageIndex >= list.count()) {
log(QStringLiteral("qet.cropImage: folio %1 has %2 image(s), no index %3")
.arg(folioIndex).arg(list.count()).arg(imageIndex));
return false;
}
return list.at(imageIndex)->applyCrop(QRect(x, y, width, height));
}
/**
@brief QetScriptApi::imageCrop
@return the image's crop rectangle in its original's pixels, as
"x,y,width,height", or an empty string for no such image.
*/
QString QetScriptApi::imageCrop(int folioIndex, int imageIndex) const
{
const QList<DiagramImageItem *> list = sortedImages(folioIndex);
if (imageIndex < 0 || imageIndex >= list.count()) return QString();
const QRect r = list.at(imageIndex)->cropRect();
return QStringLiteral("%1,%2,%3,%4").arg(r.x()).arg(r.y()).arg(r.width()).arg(r.height());
}
bool QetScriptApi::deleteImage(int folioIndex, int imageIndex)
{
if (!m_project) return false;
+2
View File
@@ -540,6 +540,8 @@ class QetScriptApi : public QObject
Q_INVOKABLE int addImage(int folioIndex, const QString &filePath, double x, double y);
Q_INVOKABLE bool setImageScale(int folioIndex, int imageIndex, double factor);
Q_INVOKABLE bool setImageRotation(int folioIndex, int imageIndex, double angle);
Q_INVOKABLE bool cropImage(int folioIndex, int imageIndex, int x, int y, int width, int height);
Q_INVOKABLE QString imageCrop(int folioIndex, int imageIndex) const;
Q_INVOKABLE bool deleteImage(int folioIndex, int imageIndex);
Q_INVOKABLE int addPdfPage(int folioIndex, const QString &pdfPath, int pageNumber,
int dpi, double x, double y);
+7
View File
@@ -137,6 +137,13 @@ QList<ShortcutManager::ShortcutInfo> ShortcutManager::allShortcuts() const
info.description = entry.description;
info.default_sequence = entry.default_sequence;
info.current_sequence = savedSequence(id, entry.default_sequence);
for (const QPointer<QObject> &target : entry.targets) {
if (target) {
info.icon = target->property("icon").value<QIcon>();
info.action = qobject_cast<QAction *>(target.data());
break;
}
}
list << info;
}
return list;
+5
View File
@@ -19,6 +19,7 @@
#define SHORTCUTMANAGER_H
#include <QHash>
#include <QIcon>
#include <QKeySequence>
#include <QList>
#include <QPointer>
@@ -60,6 +61,10 @@ class ShortcutManager
QString description;
QKeySequence default_sequence;
QKeySequence current_sequence;
/// The command's icon, from its first live target; may be null
QIcon icon;
/// The first live target if it is a QAction, else nullptr
QAction *action = nullptr;
};
void registerAction(QObject *target, const QString &id,
+79
View File
@@ -0,0 +1,79 @@
/*
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 TEXTLINES_H
#define TEXTLINES_H
#include <QStringList>
#include <QTextBlock>
#include <QTextDocument>
#include <QTextLayout>
namespace TextLines
{
/**
@return the lines of document as they are laid out: a paragraph
wrapped to the width of the text gives several lines. For the
exports that write a text line by line (DXF).
A paragraph that is not wrapped gives exactly its line of
toPlainText().split('\n'), as these exports wrote before: trailing
spaces are kept, and non-breaking spaces become plain spaces. In a
wrapped paragraph, the lines lose their trailing spaces (the one a
line was broken at, and a line made only of spaces becomes empty).
*/
inline QStringList layoutLines(const QTextDocument *document)
{
document->size(); //Lay the document out
QStringList lines;
for (QTextBlock block = document->begin() ; block.isValid() ; block = block.next())
{
const QString text = block.text();
const QTextLayout *layout = block.layout();
const int count = layout ? layout->lineCount() : 0;
if (count == 0) {
lines << QString(text).replace(QChar::Nbsp, QLatin1Char(' '));
continue;
}
//Wrapped: a line ends without a line break typed by the user
bool wrapped = false;
for (int i = 0 ; i < count - 1 ; ++i) {
const QTextLine line = layout->lineAt(i);
const int end = line.textStart() + line.textLength();
if (end == 0 || text.at(end - 1) != QChar::LineSeparator)
wrapped = true;
}
for (int i = 0 ; i < count ; ++i)
{
const QTextLine line = layout->lineAt(i);
QString part = text.mid(line.textStart(), line.textLength());
if (part.endsWith(QChar::LineSeparator))
part.chop(1); //A line break typed by the user (Shift+Enter)
if (wrapped) {
while (!part.isEmpty() && part.back().isSpace())
part.chop(1);
}
lines << part.replace(QChar::Nbsp, QLatin1Char(' '));
}
}
return lines;
}
}
#endif // TEXTLINES_H
+120
View File
@@ -0,0 +1,120 @@
/*
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 TEXTRESIZE_H
#define TEXTRESIZE_H
#include <QGraphicsItem>
#include <QScopedPointer>
#include <QTextDocument>
#include <QTextOption>
#include <QTransform>
/**
The geometry of changing the width of a text box by dragging one of
its corners: the text wraps to the new width, the opposite corner
stays where it is drawn.
*/
namespace TextResize
{
/**
A corner of the box of a text, in the order of the resize handles.
*/
enum Corner {
TopLeft,
TopRight,
BottomRight,
BottomLeft
};
inline Corner opposite(Corner corner) {
return Corner((int(corner) + 2) % 4);
}
inline bool isLeft(Corner corner) {
return corner == TopLeft || corner == BottomLeft;
}
/**
@return the corner of rect, in the coordinates of rect.
*/
inline QPointF cornerOf(const QRectF &rect, Corner corner)
{
switch (corner) {
case TopLeft: return rect.topLeft();
case TopRight: return rect.topRight();
case BottomRight: return rect.bottomRight();
case BottomLeft: return rect.bottomLeft();
}
return rect.topLeft();
}
/**
@return the smallest width that document can be given: the width
of its longest word, which is never broken, plus the margins.
*/
inline qreal minimumWidth(const QTextDocument *document)
{
QScopedPointer<QTextDocument> narrow(document->clone());
QTextOption option = narrow->defaultTextOption();
option.setWrapMode(QTextOption::WordWrap);
narrow->setDefaultTextOption(option);
narrow->setTextWidth(0);
return narrow->size().width();
}
/**
@return the width given by dragging the corner dragged of a text
box to mouse_scene, while the opposite corner stays at fixed_local.
@param press_inverse : the inverse of the scene transform of the
text when the drag started. Moving the text to keep the opposite
corner in place changes its transform, using the transform of the
press keeps that move from feeding back into the width.
@param fixed_local : the opposite corner, in the coordinates of the
text when the drag started.
@param min_width : see minimumWidth().
Vertical movements of the mouse are ignored: the height of the box
follows the wrapped text.
*/
inline qreal widthForDrag(const QTransform &press_inverse,
const QPointF &fixed_local,
const QPointF &mouse_scene,
Corner dragged,
qreal min_width)
{
const QPointF mouse = press_inverse.map(mouse_scene);
const qreal width = isLeft(dragged) ? fixed_local.x() - mouse.x()
: mouse.x() - fixed_local.x();
return qMax(width, min_width);
}
/**
Move item so that its corner corner is at scene_point, in scene
coordinates. Right for any rotation, rotation point and alignment.
*/
inline void pinCorner(QGraphicsItem *item, Corner corner, const QPointF &scene_point)
{
const QPointF now = item->mapToScene(cornerOf(item->boundingRect(), corner));
QPointF delta = scene_point - now;
if (const QGraphicsItem *parent = item->parentItem())
delta = parent->mapFromScene(scene_point) - parent->mapFromScene(now);
item->setPos(item->pos() + delta);
}
}
#endif // TEXTRESIZE_H
+149 -14
View File
@@ -20,6 +20,9 @@
#include "../../qeticons.h"
#include "../../shortcutmanager.h"
#include <QAction>
#include <QApplication>
#include <QClipboard>
#include <QComboBox>
#include <QFrame>
#include <QHBoxLayout>
@@ -27,6 +30,8 @@
#include <QKeySequenceEdit>
#include <QLabel>
#include <QLineEdit>
#include <QMenu>
#include <QMenuBar>
#include <QPushButton>
#include <QRegularExpression>
#include <QToolButton>
@@ -43,6 +48,10 @@
this, so "general" matches "Général" and "Ctrl+S" matches "ctrl+s"
regardless of the keyboard layout the query was typed on.
*/
namespace {
enum Column { ActionColumn, MenuColumn, SequenceColumn, ResetColumn };
}
static QString normalizedForSearch(const QString &text)
{
const QString decomposed = text.normalized(QString::NormalizationForm_D);
@@ -85,20 +94,51 @@ ShortcutsConfigPage::ShortcutsConfigPage(QWidget *parent) :
connect(m_quick_filter, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &ShortcutsConfigPage::quickFilterChanged);
m_category_filter = new QComboBox(this);
m_category_filter->setObjectName(QStringLiteral("categoryFilterCombo"));
m_category_filter->addItem(tr("Toutes les catégories"));
connect(m_category_filter, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &ShortcutsConfigPage::quickFilterChanged);
m_count_label = new QLabel(this);
m_count_label->setObjectName(QStringLiteral("shortcutCountLabel"));
auto *filter_layout = new QHBoxLayout();
filter_layout->addWidget(m_filter_edit, 1);
filter_layout->addWidget(m_category_filter);
filter_layout->addWidget(m_quick_filter);
filter_layout->addWidget(m_count_label);
vlayout->addLayout(filter_layout);
//Press a key combination to list what uses it, without having to
//know how QElectroTech spells it ("Ctrl+Maj+S", "Ctrl+Shift+S"…)
m_key_search = new QKeySequenceEdit(this);
m_key_search->setObjectName(QStringLiteral("keySearchEdit"));
m_key_search->setToolTip(tr("Appuyez sur une combinaison de touches pour voir quelle commande l'utilise"));
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
m_key_search->setMaximumSequenceLength(1);
#endif
connect(m_key_search, &QKeySequenceEdit::keySequenceChanged, this, [this]() { applyFilter(); });
auto *clear_key_button = new QToolButton(this);
clear_key_button->setIcon(QET::Icons::EditClear);
clear_key_button->setToolTip(tr("Effacer la touche recherchée"));
clear_key_button->setAutoRaise(true);
connect(clear_key_button, &QToolButton::clicked, m_key_search, &QKeySequenceEdit::clear);
auto *key_layout = new QHBoxLayout();
key_layout->addWidget(new QLabel(tr("Rechercher par touche :"), this));
key_layout->addWidget(m_key_search);
key_layout->addWidget(clear_key_button);
key_layout->addStretch();
vlayout->addLayout(key_layout);
m_tree = new QTreeWidget(this);
m_tree->setHeaderLabels({tr("Action"), tr("Raccourci"), QString()});
m_tree->header()->setSectionResizeMode(0, QHeaderView::Stretch);
m_tree->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
m_tree->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
m_tree->setHeaderLabels({tr("Action"), tr("Menu"), tr("Raccourci"), QString()});
m_tree->header()->setSectionResizeMode(ActionColumn, QHeaderView::Stretch);
m_tree->header()->setSectionResizeMode(MenuColumn, QHeaderView::ResizeToContents);
m_tree->header()->setSectionResizeMode(SequenceColumn, QHeaderView::ResizeToContents);
m_tree->header()->setSectionResizeMode(ResetColumn, QHeaderView::ResizeToContents);
m_tree->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_tree->setSelectionMode(QAbstractItemView::NoSelection);
vlayout->addWidget(m_tree);
@@ -106,7 +146,12 @@ ShortcutsConfigPage::ShortcutsConfigPage(QWidget *parent) :
auto *reset_all_button = new QPushButton(tr("Tout réinitialiser"), this);
connect(reset_all_button, &QPushButton::clicked, this, &ShortcutsConfigPage::resetAllRows);
auto *copy_button = new QPushButton(QET::Icons::EditCopy, tr("Copier la liste"), this);
copy_button->setToolTip(tr("Copie les raccourcis affichés, à coller dans un tableur ou un document"));
connect(copy_button, &QPushButton::clicked, this, &ShortcutsConfigPage::copyList);
auto *bottom_layout = new QHBoxLayout();
bottom_layout->addWidget(copy_button);
bottom_layout->addStretch();
bottom_layout->addWidget(reset_all_button);
vlayout->addLayout(bottom_layout);
@@ -139,6 +184,9 @@ void ShortcutsConfigPage::populateTable()
m_tree->clear();
m_rows.clear();
while (m_category_filter->count() > 1) {
m_category_filter->removeItem(1);
}
m_rows.reserve(shortcuts.size());
QHash<QString, QTreeWidgetItem *> category_nodes;
@@ -147,18 +195,22 @@ void ShortcutsConfigPage::populateTable()
QTreeWidgetItem *category_item = category_nodes.value(info.category, nullptr);
if (!category_item) {
category_item = new QTreeWidgetItem(m_tree);
category_item->setText(0, info.category);
category_item->setText(ActionColumn, info.category);
category_item->setFlags(category_item->flags() & ~Qt::ItemIsEditable);
category_nodes.insert(info.category, category_item);
m_category_filter->addItem(info.category);
}
auto *child = new QTreeWidgetItem(category_item);
child->setText(0, info.description);
const QString menu_path = menuPath(info.action);
child->setText(ActionColumn, info.description);
child->setIcon(ActionColumn, info.icon);
child->setText(MenuColumn, menu_path);
child->setFlags(child->flags() & ~Qt::ItemIsEditable);
auto *edit = new QKeySequenceEdit(info.current_sequence, m_tree);
connect(edit, &QKeySequenceEdit::editingFinished, this, &ShortcutsConfigPage::checkConflicts);
m_tree->setItemWidget(child, 1, edit);
m_tree->setItemWidget(child, SequenceColumn, edit);
auto *reset_button = new QToolButton(m_tree);
reset_button->setIcon(QET::Icons::EditUndo);
@@ -166,9 +218,10 @@ void ShortcutsConfigPage::populateTable()
reset_button->setAutoRaise(true);
const int row_index = m_rows.size();
connect(reset_button, &QToolButton::clicked, this, [this, row_index]() { resetRow(row_index); });
m_tree->setItemWidget(child, 2, reset_button);
m_tree->setItemWidget(child, ResetColumn, reset_button);
m_rows << Row{info.id, info.category, info.description, info.default_sequence, edit, child, false};
m_rows << Row{info.id, info.category, info.description, menu_path,
info.default_sequence, edit, child, false};
}
checkConflicts();
@@ -210,6 +263,9 @@ void ShortcutsConfigPage::applyFilter()
: needle.split(QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts);
const int quick_filter = m_quick_filter->currentIndex();
const QString category = m_category_filter->currentIndex() > 0
? m_category_filter->currentText() : QString();
const QKeySequence key = m_key_search->keySequence();
int visible_actions = 0;
for (const Row &row : std::as_const(m_rows)) {
@@ -218,7 +274,8 @@ void ShortcutsConfigPage::applyFilter()
// of "Shift"), which is precisely the kind of false positive that hides
// the one binding the user is looking for.
const QString text_haystack = normalizedForSearch(
row.category + QLatin1Char(' ') + row.description);
row.category + QLatin1Char(' ') + row.description
+ QLatin1Char(' ') + row.menu_path);
const QString sequence_text = normalizedForSearch(row.edit->keySequence().toString());
bool matches = true;
@@ -230,6 +287,16 @@ void ShortcutsConfigPage::applyFilter()
}
}
if (matches && !category.isEmpty()) {
matches = row.category == category;
}
if (matches && !key.isEmpty()) {
//A pressed key also finds the sequences it starts, such
//as a two-key shortcut whose first key it is
const QKeySequence sequence = row.edit->keySequence();
matches = !sequence.isEmpty() && key.matches(sequence) != QKeySequence::NoMatch;
}
if (matches) {
switch (quick_filter) {
case BoundOnly:
@@ -252,7 +319,8 @@ void ShortcutsConfigPage::applyFilter()
}
}
const bool filtering = !needle.isEmpty() || quick_filter != ShowAll;
const bool filtering = !needle.isEmpty() || quick_filter != ShowAll
|| !category.isEmpty() || !key.isEmpty();
for (int i = 0; i < m_tree->topLevelItemCount(); ++i) {
QTreeWidgetItem *top = m_tree->topLevelItem(i);
bool any_visible = false;
@@ -322,11 +390,11 @@ void ShortcutsConfigPage::checkConflicts()
other_descriptions << m_rows.at(other_row).description;
}
}
current_row.item->setBackground(0, QColor(255, 205, 205));
current_row.item->setBackground(ActionColumn, QColor(255, 205, 205));
current_row.edit->setToolTip(
tr("Ce raccourci est aussi utilisé par : %1").arg(other_descriptions.join(QStringLiteral(", "))));
} else {
current_row.item->setBackground(0, QBrush());
current_row.item->setBackground(ActionColumn, QBrush());
current_row.edit->setToolTip(QString());
}
}
@@ -335,7 +403,9 @@ void ShortcutsConfigPage::checkConflicts()
// conflicts-only quick filter); refresh the visible set so the list doesn't
// show stale results.
const bool filtering = !m_filter_edit->text().trimmed().isEmpty()
|| m_quick_filter->currentIndex() != ShowAll;
|| m_quick_filter->currentIndex() != ShowAll
|| m_category_filter->currentIndex() > 0
|| !m_key_search->keySequence().isEmpty();
if (filtering) {
applyFilter();
}
@@ -374,6 +444,71 @@ void ShortcutsConfigPage::applyConf()
}
}
/**
@brief ShortcutsConfigPage::menuPath
@param action
@return where \a action is in the menu bar, such as "Projet › Scripts",
or an empty string for a command that is in no menu of the menu bar
(a toolbar or shortcut bar only command, or one with no live action)
*/
QString ShortcutsConfigPage::menuPath(const QAction *action)
{
if (!action) {
return QString();
}
for (QObject *object : action->associatedObjects()) {
QStringList titles;
for (auto *menu = qobject_cast<QMenu *>(object); menu; ) {
titles.prepend(menu->title().remove(QLatin1Char('&')));
QMenu *parent_menu = nullptr;
bool in_menu_bar = false;
for (QObject *owner : menu->menuAction()->associatedObjects()) {
if (qobject_cast<QMenuBar *>(owner)) {
in_menu_bar = true;
} else if (!parent_menu) {
parent_menu = qobject_cast<QMenu *>(owner);
}
}
if (in_menu_bar) {
return titles.join(QStringLiteral(" › "));
}
menu = parent_menu;
}
}
return QString();
}
/**
@brief ShortcutsConfigPage::listAsText
@return the shortcuts shown, as the edits currently hold them, one per
line with tab-separated columns and a header line: pasted into a
spreadsheet it fills one cell per column.
*/
QString ShortcutsConfigPage::listAsText() const
{
QStringList lines;
lines << QStringList{tr("Catégorie"), tr("Menu"), tr("Action"), tr("Raccourci")}
.join(QLatin1Char('\t'));
for (const Row &row : std::as_const(m_rows)) {
if (row.item->isHidden()) {
continue;
}
lines << QStringList{row.category, row.menu_path, row.description,
row.edit->keySequence().toString(QKeySequence::NativeText)}
.join(QLatin1Char('\t'));
}
return lines.join(QLatin1Char('\n')) + QLatin1Char('\n');
}
/**
@brief ShortcutsConfigPage::copyList
Put listAsText() on the clipboard.
*/
void ShortcutsConfigPage::copyList()
{
QApplication::clipboard()->setText(listAsText());
}
QString ShortcutsConfigPage::title() const
{
return tr("Raccourcis", "configuration page title");
@@ -22,6 +22,7 @@
#include <QKeySequence>
class QAction;
class QComboBox;
class QKeySequenceEdit;
class QLabel;
@@ -48,11 +49,15 @@ class ShortcutsConfigPage : public ConfigPage
QString title() const override;
QIcon icon() const override;
static QString menuPath(const QAction *action);
QString listAsText() const;
private slots:
void filterRows(const QString &filter_text);
void quickFilterChanged(int index);
void checkConflicts();
void resetAllRows();
void copyList();
private:
enum QuickFilter {
@@ -66,6 +71,7 @@ class ShortcutsConfigPage : public ConfigPage
QString id;
QString category;
QString description;
QString menu_path;
QKeySequence default_sequence;
QKeySequenceEdit *edit;
QTreeWidgetItem *item;
@@ -78,6 +84,8 @@ class ShortcutsConfigPage : public ConfigPage
QLineEdit *m_filter_edit;
QComboBox *m_quick_filter;
QComboBox *m_category_filter;
QKeySequenceEdit *m_key_search;
QLabel *m_count_label;
QTreeWidget *m_tree;
QList<Row> m_rows;
+83 -1
View File
@@ -104,6 +104,14 @@ 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)
# textlines.h is header-only: the lines of a text as they are laid out,
# for the exports that write a text line by line.
add_executable(tst_textlines tst_textlines.cpp)
add_test(NAME tst_textlines COMMAND tst_textlines)
set_tests_properties(tst_textlines PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
target_include_directories(tst_textlines PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_textlines PRIVATE Qt::Test Qt::Gui)
# elementviewgrid.h is header-only: the symbol editor's snap step at each
# zoom (bugtracker #112), tested without an editor window.
add_executable(tst_elementviewgrid tst_elementviewgrid.cpp)
@@ -155,6 +163,18 @@ set_tests_properties(tst_textanchor PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offs
target_include_directories(tst_textanchor PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_textanchor PRIVATE Qt::Test Qt::Widgets)
# TextResizeHandles: the corner handles that change the width of a text box,
# on a plain QGraphicsTextItem with the "textWidth" property QET texts have.
add_executable(
tst_textresizehandles
tst_textresizehandles.cpp
${QET_DIR}/sources/QetGraphicsItemModeler/qetgraphicshandleritem.cpp
${QET_DIR}/sources/QetGraphicsItemModeler/textresizehandles.cpp)
add_test(NAME tst_textresizehandles COMMAND tst_textresizehandles)
set_tests_properties(tst_textresizehandles PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
target_include_directories(tst_textresizehandles PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_textresizehandles PRIVATE Qt::Test Qt::Widgets)
# legacyelementtypes.h is header-only: the rewrite of element type names in
# nomenclature queries saved before June 2022.
add_executable(tst_legacyelementtypes tst_legacyelementtypes.cpp)
@@ -227,8 +247,25 @@ set_tests_properties(tst_commandsearch PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=o
target_include_directories(tst_commandsearch PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_commandsearch PRIVATE Qt::Test Qt::Widgets)
# ShortcutsConfigPage: icon and menu columns, category filter, finding a
# command by pressing its key, and the copied list.
add_executable(
tst_shortcutspage
tst_shortcutspage.cpp
${QET_DIR}/sources/ui/configpage/shortcutsconfigpage.cpp
${QET_DIR}/sources/ui/configpage/shortcutsconfigpage.h
${QET_DIR}/sources/ui/configpage/configpage.h
${QET_DIR}/sources/shortcutmanager.cpp
${QET_DIR}/sources/qeticons.cpp
${QET_DIR}/sources/qetpalette.cpp
${QET_DIR}/sources/qetstyle.cpp)
add_test(NAME tst_shortcutspage COMMAND tst_shortcutspage)
set_tests_properties(tst_shortcutspage PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
target_include_directories(tst_shortcutspage PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_shortcutspage PRIVATE Qt::Test Qt::Widgets Qt::Svg)
# ToolbarSettings and its configuration page: defaults change nothing,
# saved values reach every open window's toolbars, panels are left alone.
# saved values reach every open window that took them, panels are left alone.
add_executable(
tst_toolbarsettings
tst_toolbarsettings.cpp
@@ -407,6 +444,18 @@ target_link_libraries(tst_conductorselfretrace PRIVATE Qt::Test)
target_compile_definitions(tst_conductorselfretrace PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
# A free text with a width keeps it when the project is saved again (the
# attribute is written only when set) and is drawn wrapped to it.
add_executable(
tst_freetextwidth
tst_freetextwidth.cpp)
add_test(NAME tst_freetextwidth COMMAND tst_freetextwidth)
set_tests_properties(tst_freetextwidth PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
add_dependencies(tst_freetextwidth qelectrotech)
target_link_libraries(tst_freetextwidth PRIVATE Qt::Test Qt::Gui Qt::Xml)
target_compile_definitions(tst_freetextwidth PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
# The wiring list export (--export-cables) keeps every wire drawn to a folio
# report when a report has several, joins only one-to-one report pairs, and
# prints the folio number rather than its template. fixtures/wiring_list_arrows.qet
@@ -421,6 +470,16 @@ target_compile_definitions(tst_wiringlistexport PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
# --export-dxf writes a wrapped symbol text line by line, as it is drawn.
add_executable(
tst_dxfwrappedtext
tst_dxfwrappedtext.cpp)
add_test(NAME tst_dxfwrappedtext COMMAND tst_dxfwrappedtext)
add_dependencies(tst_dxfwrappedtext qelectrotech)
target_link_libraries(tst_dxfwrappedtext PRIVATE Qt::Test)
target_compile_definitions(tst_dxfwrappedtext PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
# --export-pdf puts an A3 folio on an A3 page whichever way it is turned:
# QPageSize matches standard sheets upright only, so a wide folio is matched
# upright and turned. fixtures/pdf_page_a3.qet has one folio each way.
@@ -549,6 +608,17 @@ if(QET_HAS_SCRIPTING)
target_compile_definitions(tst_scriptlayoutreads PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
# The project database keeps the width of a free text (independent_text),
# read with qet.query() on fixtures/free_text_width.qet.
add_executable(
tst_scriptfreetextwidth
tst_scriptfreetextwidth.cpp)
add_test(NAME tst_scriptfreetextwidth COMMAND tst_scriptfreetextwidth)
add_dependencies(tst_scriptfreetextwidth qelectrotech)
target_link_libraries(tst_scriptfreetextwidth PRIVATE Qt::Test)
target_compile_definitions(tst_scriptfreetextwidth PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
# QET_SETTINGS_DIR moves the settings into an INI file there (#1178): a
# script places a symbol only the folder's settings file can resolve.
add_executable(
@@ -611,6 +681,18 @@ if(QET_HAS_SCRIPTING)
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
# Undoing a crop of a picture restores its crop rectangle too, so a
# project saved after the undo does not record the undone crop.
add_executable(
tst_imagecropundo
tst_imagecropundo.cpp)
add_test(NAME tst_imagecropundo COMMAND tst_imagecropundo)
add_dependencies(tst_imagecropundo qelectrotech)
target_link_libraries(tst_imagecropundo PRIVATE Qt::Test Qt::Gui)
target_compile_definitions(tst_imagecropundo PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
# The same symbol placed twice in a project whose collection does not
# start with its "import" category (examples/lmdg.qet).
add_executable(
@@ -0,0 +1,60 @@
<project title="DXF wrapped texts" uuid="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7b10}" version="0.200.1">
<properties>
<property name="saveddate" show="1">10/2/26</property>
<property name="saveddate-eu" show="1">02-10-2026</property>
<property name="saveddate-us" show="1">2026-10-02</property>
<property name="savedfilename" show="1">dxf_wrapped_text</property>
<property name="savedtime" show="1">18:39</property>
</properties>
<usage enabled="true" time_spent="0"/>
<newdiagrams>
<border cols="17" colsize="60" displaycols="true" displayrows="true" rows="8" rowsize="80"/>
<inset author="" auto_page_num="" date="null" displayAt="bottom" filename="" folio="" indexrev="" locmach="" plant="" title="" version=""/>
<conductors bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
<report label="%f-%l%c"/>
<xrefs>
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="coil" xrefpos="AlignBottom"/>
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="commutator" xrefpos="AlignBottom"/>
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="plc" xrefpos="AlignBottom"/>
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="protection" xrefpos="AlignBottom"/>
</xrefs>
<conductors_autonums auto_break_conductors="false" current_autonum="" freeze_new_conductors="false"/>
<folio_autonums/>
<element_autonums current_autonum="" freeze_new_elements="false"/>
<guides/>
</newdiagrams>
<diagram author="" auto_page_num="" cols="17" colsize="60" date="null" displayAt="bottom" displaycols="true" displayrows="true" filename="" folio="" freezeNewConductor="false" freezeNewElement="false" height="660" indexrev="" locmach="" order="1" plant="" rows="8" rowsize="80" title="" uuid="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7a01}" version="0.200.1-dev">
<defaultconductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
<elements>
<element freezeLabel="false" is_movable="1" orientation="0" prefix="" type="embed://import/wrap.elmt" uuid="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7a02}" x="300" y="300" z="10">
<terminals/>
<inputs/>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="true" rotation="0" rotation_point_center="false" text_from="UserText" text_width="70" uuid="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7b01}" x="0" y="10"><text>WrapAlpha WrapBeta WrapGamma WrapDelta WrapEpsilon</text></dynamic_elmt_text>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="true" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7b02}" x="0" y="60"><text>LineAlpha LineBeta LineGamma LineDelta LineEpsilon</text></dynamic_elmt_text>
</dynamic_texts>
<texts_groups/>
</element>
</elements>
</diagram>
<collection>
<category name="import">
<names>
<name lang="en">Imported elements</name>
</names>
<element name="wrap.elmt">
<definition height="80" hotspot_x="10" hotspot_y="10" link_type="simple" type="element" version="0.100.0" width="120">
<uuid uuid="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7a05}"/>
<names>
<name lang="en">Wrapped symbol text</name>
</names>
<description>
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="0" x2="10" y1="0" y2="0"/>
<dynamic_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="true" rotation="0" z="2" text_from="UserText" text_width="70" uuid="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7b01}" x="0" y="10"><text>WrapAlpha WrapBeta WrapGamma WrapDelta WrapEpsilon</text></dynamic_text>
<dynamic_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="true" rotation="0" z="3" text_from="UserText" text_width="-1" uuid="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7b02}" x="0" y="60"><text>LineAlpha LineBeta LineGamma LineDelta LineEpsilon</text></dynamic_text>
</description>
</definition>
</element>
</category>
</collection>
</project>
+41
View File
@@ -0,0 +1,41 @@
<project title="Free text widths" uuid="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7a00}" version="0.200.1">
<properties>
<property name="saveddate" show="1">10/2/26</property>
<property name="saveddate-eu" show="1">02-10-2026</property>
<property name="saveddate-us" show="1">2026-10-02</property>
<property name="savedfilename" show="1">free_text_width</property>
<property name="savedtime" show="1">18:39</property>
</properties>
<usage enabled="true" time_spent="0"/>
<newdiagrams>
<border cols="17" colsize="60" displaycols="true" displayrows="true" rows="8" rowsize="80"/>
<inset author="" auto_page_num="" date="null" displayAt="bottom" filename="" folio="" indexrev="" locmach="" plant="" title="" version=""/>
<conductors bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
<report label="%f-%l%c"/>
<xrefs>
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="coil" xrefpos="AlignBottom"/>
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="commutator" xrefpos="AlignBottom"/>
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="plc" xrefpos="AlignBottom"/>
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="protection" xrefpos="AlignBottom"/>
</xrefs>
<conductors_autonums auto_break_conductors="false" current_autonum="" freeze_new_conductors="false"/>
<folio_autonums/>
<element_autonums current_autonum="" freeze_new_elements="false"/>
<guides/>
</newdiagrams>
<diagram author="" auto_page_num="" cols="17" colsize="60" date="null" displayAt="bottom" displaycols="true" displayrows="true" filename="" folio="" freezeNewConductor="false" freezeNewElement="false" height="660" indexrev="" locmach="" order="1" plant="" rows="8" rowsize="80" title="" uuid="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7a01}" version="0.200.1-dev">
<defaultconductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
<inputs>
<input font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" rotation="0" text="FreeAlpha FreeBeta FreeGamma FreeDelta FreeEpsilon" text_width="70" uuid="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7a03}" x="100" y="100"/>
<input font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" rotation="0" text="OpenAlpha OpenBeta OpenGamma OpenDelta OpenEpsilon" uuid="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7a04}" x="100" y="400"/>
<input font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" rotation="0" text="&lt;p align=&quot;center&quot;&gt;CentAlpha CentBeta CentGamma&lt;/p&gt;" text_width="120" uuid="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7a08}" x="400" y="100"/>
</inputs>
</diagram>
<collection>
<category name="import">
<names>
<name lang="en">Imported elements</name>
</names>
</category>
</collection>
</project>
+60
View File
@@ -0,0 +1,60 @@
/*
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 <QtTest>
#include <QProcess>
#include <QTemporaryDir>
/**
--export-dxf writes a symbol text wrapped to its width line by line,
as it is drawn, rather than as one long line. A text without a width
is still written on one line.
*/
class tst_dxfwrappedtext : public QObject
{
Q_OBJECT
private slots:
void wrappedLines()
{
const QString project = QFINDTESTDATA("fixtures/dxf_wrapped_text.qet");
QVERIFY2(!project.isEmpty(), "fixture project not found");
QTemporaryDir dir;
QVERIFY(dir.isValid());
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
QProcess proc;
proc.setProcessEnvironment(env);
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), {QStringLiteral("--export-dxf"), project, dir.path()});
QVERIFY2(proc.waitForFinished(60000), "--export-dxf timed out");
QCOMPARE(proc.exitCode(), 0);
QFile file(dir.filePath(QStringLiteral("01_diagram.dxf")));
QVERIFY(file.open(QIODevice::ReadOnly | QIODevice::Text));
const QStringList values = QString::fromUtf8(file.readAll()).split(QLatin1Char('\n'));
//One text entity per word of the text 70 px wide...
for (const QString &word : {QStringLiteral("WrapAlpha"), QStringLiteral("WrapGamma"), QStringLiteral("WrapEpsilon")})
QVERIFY2(values.contains(word), qPrintable(word));
//...one for the whole text without a width
QVERIFY(values.contains(QStringLiteral("LineAlpha LineBeta LineGamma LineDelta LineEpsilon")));
}
};
QTEST_APPLESS_MAIN(tst_dxfwrappedtext)
#include "tst_dxfwrappedtext.moc"
+6 -2
View File
@@ -47,14 +47,18 @@ class tst_elementautonumids : public QObject
{
const QString home = m_dir.filePath(QStringLiteral("home%1").arg(m_run++));
QDir().mkpath(home);
// QET_SETTINGS_DIR, not a file under XDG_CONFIG_HOME: macOS and
// Windows keep the settings elsewhere and would never read it.
const QString settings_dir = home + QStringLiteral("/settings");
if (!m_preferences.isEmpty()) {
QDir().mkpath(home + QStringLiteral("/config/QElectroTech"));
QFile settings(home + QStringLiteral("/config/QElectroTech/QElectroTech.conf"));
QDir().mkpath(settings_dir + QStringLiteral("/QElectroTech"));
QFile settings(settings_dir + QStringLiteral("/QElectroTech/QElectroTech.ini"));
if (settings.open(QIODevice::WriteOnly)) {
settings.write(m_preferences.toUtf8());
}
}
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("QET_SETTINGS_DIR"), settings_dir);
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
env.insert(QStringLiteral("HOME"), home);
+113
View File
@@ -0,0 +1,113 @@
/*
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 <QtTest>
#include <QDomDocument>
#include <QProcess>
#include <QRegularExpression>
#include <QTemporaryDir>
#include <QTextDocument>
/// Run the qelectrotech binary with arguments, without a display.
static bool runQet(const QStringList &arguments)
{
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
QProcess proc;
proc.setProcessEnvironment(env);
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), arguments);
return proc.waitForFinished(60000)
&& proc.exitStatus() == QProcess::NormalExit
&& proc.exitCode() == 0;
}
/// The number of lines the words starting with prefix are drawn on.
static int drawnLines(const QString &svg, const QString &prefix)
{
const QRegularExpression text_re(
QStringLiteral("<text[^>]*\\by=\"([^\"]+)\"[^>]*>\\s*%1").arg(prefix));
QSet<QString> lines;
auto it = text_re.globalMatch(svg);
while (it.hasNext())
lines.insert(it.next().captured(1));
return lines.size();
}
/**
A free text with a width (text_width on its <input>) keeps it when the
project is saved again, and wraps to it. A text without a width is saved
without the attribute, as before, so older versions read it unchanged.
*/
class tst_freetextwidth : public QObject
{
Q_OBJECT
private slots:
void initTestCase()
{
QVERIFY2(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)), "qelectrotech binary not found");
m_fixture = QFINDTESTDATA("fixtures/free_text_width.qet");
QVERIFY2(!m_fixture.isEmpty(), "fixture project not found");
QVERIFY(m_dir.isValid());
}
void widthIsSavedOnlyWhenSet()
{
const QString out = m_dir.filePath(QStringLiteral("resaved.qet"));
QVERIFY2(runQet({QStringLiteral("--resave"), m_fixture, out}), "--resave failed");
QFile file(out);
QVERIFY(file.open(QIODevice::ReadOnly));
QDomDocument document;
QVERIFY(document.setContent(&file));
QMap<QString, QString> widths; // first word -> text_width
const QDomNodeList inputs = document.elementsByTagName(QStringLiteral("input"));
for (int i = 0 ; i < inputs.size() ; ++i) {
const QDomElement input = inputs.at(i).toElement();
QTextDocument text;
text.setHtml(input.attribute(QStringLiteral("text")));
const QString first_word = text.toPlainText().section(QLatin1Char(' '), 0, 0);
widths.insert(first_word, input.hasAttribute(QStringLiteral("text_width"))
? input.attribute(QStringLiteral("text_width"))
: QStringLiteral("none"));
}
QCOMPARE(widths.value(QStringLiteral("FreeAlpha")), QStringLiteral("70"));
QCOMPARE(widths.value(QStringLiteral("OpenAlpha")), QStringLiteral("none"));
//Centred lines: the width of the user, not the one setHtml() gives
QCOMPARE(widths.value(QStringLiteral("CentAlpha")), QStringLiteral("120"));
}
void textWrapsToItsWidth()
{
QVERIFY2(runQet({QStringLiteral("--export-svg"), m_fixture, m_dir.path()}), "--export-svg failed");
QFile file(m_dir.filePath(QStringLiteral("01_diagram.svg")));
QVERIFY(file.open(QIODevice::ReadOnly | QIODevice::Text));
const QString svg = QString::fromUtf8(file.readAll());
QVERIFY(drawnLines(svg, QStringLiteral("Free")) > 1);
QCOMPARE(drawnLines(svg, QStringLiteral("Open")), 1);
}
private:
QString m_fixture;
QTemporaryDir m_dir;
};
QTEST_MAIN(tst_freetextwidth)
#include "tst_freetextwidth.moc"
+137
View File
@@ -0,0 +1,137 @@
/*
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 <QtTest>
#include <QDir>
#include <QFile>
#include <QImage>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcess>
#include <QProcessEnvironment>
#include <QRegularExpression>
#include <QTemporaryDir>
/**
Undoing a crop restores the crop rectangle too, not only the pixels
shown: before, a project saved after Ctrl+Z still recorded the undone
crop, and the picture came back cropped once reopened. Runs the real
binary on a script: add a picture, crop it, undo, save; redo, save.
*/
class tst_imagecropundo : public QObject
{
Q_OBJECT
QTemporaryDir m_dir;
QJsonObject run(const QString &script, const QString &project)
{
const QString path = m_dir.filePath(QStringLiteral("probe.js"));
const QString home = m_dir.filePath(QStringLiteral("home"));
QDir().mkpath(home);
QFile f(path);
if (!f.open(QIODevice::WriteOnly)) return {};
f.write(script.toUtf8());
f.close();
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
env.insert(QStringLiteral("HOME"), home);
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config"));
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data"));
env.insert(QStringLiteral("TMPDIR"), m_dir.path());
QProcess proc;
proc.setProcessEnvironment(env);
proc.start(QStringLiteral(QET_TEST_BINARY_PATH),
{QStringLiteral("--run"), path, project});
if (!proc.waitForFinished(120000)) return {};
const QString out = QString::fromUtf8(proc.readAllStandardOutput()
+ proc.readAllStandardError());
const QString mark = QStringLiteral("PROBE ");
for (const QString &line : out.split(QLatin1Char('\n'))) {
const int i = line.indexOf(mark);
if (i >= 0)
return QJsonDocument::fromJson(line.mid(i + mark.size()).toUtf8()).object();
}
return {};
}
static QByteArray read(const QString &path)
{
QFile f(path);
return f.open(QIODevice::ReadOnly) ? f.readAll() : QByteArray();
}
private slots:
void initTestCase()
{
QVERIFY(m_dir.isValid());
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
QImage img(40, 30, QImage::Format_RGB32);
img.fill(Qt::darkGreen);
QVERIFY(img.save(m_dir.filePath(QStringLiteral("pic.png"))));
}
void undoneCropIsNotSaved()
{
const QString undone = m_dir.filePath(QStringLiteral("undone.qet"));
const QString redone = m_dir.filePath(QStringLiteral("redone.qet"));
const QString script = QStringLiteral(R"JS(
var i = qet.addImage(0, '%1', 100, 100);
var r = {full: qet.imageCrop(0, i)};
r.cropped_ok = qet.cropImage(0, i, 10, 5, 20, 10);
r.cropped = qet.imageCrop(0, i);
r.same_ok = qet.cropImage(0, i, 10, 5, 20, 10);
r.empty_ok = qet.cropImage(0, i, 10, 5, 0, 10);
r.outside_ok = qet.cropImage(0, i, 100, 100, 20, 10);
qet.undo();
r.undone = qet.imageCrop(0, i);
qet.save('%2');
qet.redo();
r.redone = qet.imageCrop(0, i);
qet.save('%3');
qet.log('PROBE ' + JSON.stringify(r));
)JS").arg(m_dir.filePath(QStringLiteral("pic.png")), undone, redone);
const QJsonObject r = run(script, QStringLiteral(QET_EXAMPLES_DIR "/741.qet"));
QVERIFY2(!r.isEmpty(), "the script logged nothing");
QCOMPARE(r.value("full").toString(), QStringLiteral("0,0,40,30"));
QVERIFY(r.value("cropped_ok").toBool());
QCOMPARE(r.value("cropped").toString(), QStringLiteral("10,5,20,10"));
// Crops that change nothing report it, and push no undo step:
// the undo below still undoes the real crop.
QCOMPARE(r.value("same_ok").toBool(true), false);
QCOMPARE(r.value("empty_ok").toBool(true), false);
QCOMPARE(r.value("outside_ok").toBool(true), false);
QCOMPARE(r.value("undone").toString(), QStringLiteral("0,0,40,30"));
QCOMPARE(r.value("redone").toString(), QStringLiteral("10,5,20,10"));
const QByteArray undone_xml = read(undone);
QVERIFY(undone_xml.contains("<image "));
QVERIFY2(!undone_xml.contains("<crop "), "an undone crop was saved");
const QRegularExpressionMatch crop =
QRegularExpression(QStringLiteral("<crop ([^>]*)/>")).match(QString::fromUtf8(read(redone)));
QVERIFY2(crop.hasMatch(), "the redone crop was not saved");
for (const char *attribute : {R"(x="10")", R"(y="5")", R"(w="20")", R"(h="10")"})
QVERIFY2(crop.captured(1).contains(QLatin1String(attribute)), attribute);
}
};
QTEST_GUILESS_MAIN(tst_imagecropundo)
#include "tst_imagecropundo.moc"
+100
View File
@@ -0,0 +1,100 @@
/*
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 <QtTest>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcess>
#include <QTemporaryDir>
/**
The project database keeps the width of a free text (text_width in the
independent_text table, NULL for the automatic width), read here with
qet.query() from a script run on fixtures/free_text_width.qet.
*/
class tst_scriptfreetextwidth : public QObject
{
Q_OBJECT
QTemporaryDir m_dir;
/// The rows of the independent_text table, by the first word of the text.
QHash<QString, QJsonObject> rows()
{
const QString path = m_dir.filePath(QStringLiteral("probe.js"));
const QString home = m_dir.filePath(QStringLiteral("home"));
QDir().mkpath(home);
QFile f(path);
if (!f.open(QIODevice::WriteOnly)) return {};
f.write("qet.log('PROBE ' + JSON.stringify(qet.query("
"'SELECT text, text_width, width, height FROM independent_text')));\n");
f.close();
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
env.insert(QStringLiteral("HOME"), home);
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config"));
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data"));
QProcess proc;
proc.setProcessEnvironment(env);
proc.start(QStringLiteral(QET_TEST_BINARY_PATH),
{QStringLiteral("--run"), path, QFINDTESTDATA("fixtures/free_text_width.qet")});
if (!proc.waitForFinished(60000)) return {};
const QString out = QString::fromUtf8(proc.readAllStandardOutput() + proc.readAllStandardError());
const QString mark = QStringLiteral("PROBE ");
QHash<QString, QJsonObject> result;
for (const QString &line : out.split(QLatin1Char('\n'))) {
const int i = line.indexOf(mark);
if (i < 0) continue;
const QJsonArray array = QJsonDocument::fromJson(line.mid(i + mark.size()).toUtf8()).array();
for (const QJsonValue &row : array)
result.insert(row.toObject().value(QStringLiteral("text")).toString().section(QLatin1Char(' '), 0, 0),
row.toObject());
}
return result;
}
private slots:
void initTestCase()
{
QVERIFY(m_dir.isValid());
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
QVERIFY(!QFINDTESTDATA("fixtures/free_text_width.qet").isEmpty());
}
void widthIsInTheDatabase()
{
const QHash<QString, QJsonObject> r = rows();
QCOMPARE(r.size(), 3);
QCOMPARE(r.value(QStringLiteral("FreeAlpha")).value(QStringLiteral("text_width")).toDouble(), 70.0);
QCOMPARE(r.value(QStringLiteral("CentAlpha")).value(QStringLiteral("text_width")).toDouble(), 120.0);
//The automatic width is NULL
const QJsonValue open = r.value(QStringLiteral("OpenAlpha")).value(QStringLiteral("text_width"));
QVERIFY2(open.isNull() || open.toString().isEmpty(), qPrintable(open.toVariant().toString()));
//The box of the row is the wrapped one: taller than one line
QVERIFY(r.value(QStringLiteral("FreeAlpha")).value(QStringLiteral("height")).toDouble()
> 2 * r.value(QStringLiteral("OpenAlpha")).value(QStringLiteral("height")).toDouble());
}
};
QTEST_APPLESS_MAIN(tst_scriptfreetextwidth)
#include "tst_scriptfreetextwidth.moc"
+145
View File
@@ -0,0 +1,145 @@
/*
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 "shortcutmanager.h"
#include "ui/configpage/shortcutsconfigpage.h"
#include <QAction>
#include <QComboBox>
#include <QKeySequenceEdit>
#include <QMainWindow>
#include <QMenuBar>
#include <QPixmap>
#include <QSettings>
#include <QStandardPaths>
#include <QTest>
#include <QTreeWidget>
/**
The Shortcuts configuration page: the icon and menu columns, the
category filter, finding a command by pressing its key, and the
copied list.
*/
class tst_shortcutspage : public QObject
{
Q_OBJECT
QMainWindow m_window;
QAction *m_save = nullptr;
QAction *m_run = nullptr;
QAction *m_tool = nullptr;
QTreeWidgetItem *row(QTreeWidget *tree, const QString &text)
{
const auto found = tree->findItems(text, Qt::MatchExactly | Qt::MatchRecursive, 0);
return found.isEmpty() ? nullptr : found.first();
}
QStringList visible(QTreeWidget *tree)
{
QStringList texts;
for (const QString &text : {QStringLiteral("Enregistrer"), QStringLiteral("Exécuter"), QStringLiteral("Outil")}) {
if (QTreeWidgetItem *item = row(tree, text); item && !item->isHidden()) {
texts << text;
}
}
return texts;
}
private slots:
void initTestCase()
{
QStandardPaths::setTestModeEnabled(true);
QSettings().remove(QStringLiteral("shortcuts"));
QPixmap pixmap(16, 16);
pixmap.fill(Qt::red);
QMenu *file = m_window.menuBar()->addMenu(QStringLiteral("&Fichier"));
QMenu *scripts = file->addMenu(QStringLiteral("&Scripts"));
m_save = file->addAction(QIcon(pixmap), QStringLiteral("&Enregistrer"));
m_run = scripts->addAction(QStringLiteral("Exécuter"));
//On a toolbar only, in no menu
m_tool = new QAction(QStringLiteral("Outil"), &m_window);
ShortcutManager &manager = ShortcutManager::instance();
manager.registerAction(m_save, QStringLiteral("t.save"), QStringLiteral("Catégorie A"), QKeySequence(Qt::CTRL | Qt::Key_S));
manager.registerAction(m_run, QStringLiteral("t.run"), QStringLiteral("Catégorie A"), QKeySequence(Qt::CTRL | Qt::Key_R));
manager.registerAction(m_tool, QStringLiteral("t.tool"), QStringLiteral("Catégorie B"), QKeySequence());
}
void menuPath()
{
QCOMPARE(ShortcutsConfigPage::menuPath(m_save), QStringLiteral("Fichier"));
QCOMPARE(ShortcutsConfigPage::menuPath(m_run), QStringLiteral("Fichier › Scripts"));
QCOMPARE(ShortcutsConfigPage::menuPath(m_tool), QString());
QCOMPARE(ShortcutsConfigPage::menuPath(nullptr), QString());
}
void iconAndMenuColumns()
{
ShortcutsConfigPage page(nullptr);
auto *tree = page.findChild<QTreeWidget *>();
QVERIFY(row(tree, QStringLiteral("Enregistrer")));
QVERIFY(!row(tree, QStringLiteral("Enregistrer"))->icon(0).isNull());
QVERIFY(row(tree, QStringLiteral("Outil"))->icon(0).isNull());
QCOMPARE(row(tree, QStringLiteral("Exécuter"))->text(1), QStringLiteral("Fichier › Scripts"));
QCOMPARE(row(tree, QStringLiteral("Outil"))->text(1), QString());
}
void categoryFilter()
{
ShortcutsConfigPage page(nullptr);
auto *tree = page.findChild<QTreeWidget *>();
auto *combo = page.findChild<QComboBox *>(QStringLiteral("categoryFilterCombo"));
QCOMPARE(visible(tree).size(), 3);
combo->setCurrentIndex(combo->findText(QStringLiteral("Catégorie B")));
QCOMPARE(visible(tree), QStringList{QStringLiteral("Outil")});
combo->setCurrentIndex(0);
QCOMPARE(visible(tree).size(), 3);
}
void findByKey()
{
ShortcutsConfigPage page(nullptr);
auto *tree = page.findChild<QTreeWidget *>();
auto *key = page.findChild<QKeySequenceEdit *>(QStringLiteral("keySearchEdit"));
key->setKeySequence(QKeySequence(Qt::CTRL | Qt::Key_S));
QCOMPARE(visible(tree), QStringList{QStringLiteral("Enregistrer")});
key->setKeySequence(QKeySequence(Qt::CTRL | Qt::Key_J));
QVERIFY(visible(tree).isEmpty());
key->clear();
QCOMPARE(visible(tree).size(), 3);
}
void copiedList()
{
ShortcutsConfigPage page(nullptr);
auto *combo = page.findChild<QComboBox *>(QStringLiteral("categoryFilterCombo"));
combo->setCurrentIndex(combo->findText(QStringLiteral("Catégorie A")));
const QStringList lines = page.listAsText().split(QLatin1Char('\n'), Qt::SkipEmptyParts);
QCOMPARE(lines.size(), 3);
QCOMPARE(lines.at(0).count(QLatin1Char('\t')), 3);
QVERIFY(lines.contains(QStringLiteral("Catégorie A\tFichier\tEnregistrer\t")
+ QKeySequence(Qt::CTRL | Qt::Key_S).toString(QKeySequence::NativeText)));
QVERIFY(lines.contains(QStringLiteral("Catégorie A\tFichier › Scripts\tExécuter\t")
+ QKeySequence(Qt::CTRL | Qt::Key_R).toString(QKeySequence::NativeText)));
}
};
QTEST_MAIN(tst_shortcutspage)
#include "tst_shortcutspage.moc"
+115
View File
@@ -0,0 +1,115 @@
/*
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 <QtTest>
#include <QTextDocument>
#include <QTextOption>
#include "textlines.h"
class tst_textlines : public QObject
{
Q_OBJECT
private slots:
// A text without a width gives its own lines.
void unwrappedTextGivesItsLines()
{
QTextDocument document(QStringLiteral("Motor protection\nswitch Q12"));
QCOMPARE(TextLines::layoutLines(&document),
QStringList({QStringLiteral("Motor protection"), QStringLiteral("switch Q12")}));
}
// A text without a width gives exactly the lines the DXF export wrote
// before, toPlainText().split('\n'): trailing spaces and tabs kept,
// non-breaking spaces as plain spaces, <br> as a line break.
void unwrappedTextIsUnchanged_data()
{
QTest::addColumn<QString>("html");
QTest::newRow("trailing spaces") << QStringLiteral("K1 \nswitch\t");
QTest::newRow("non-breaking spaces") << QStringLiteral("<p>K1&nbsp;&nbsp;24V&nbsp;DC</p>");
QTest::newRow("line break") << QStringLiteral("<p>Wiper to 6<br>contacts</p>");
QTest::newRow("paragraphs") << QStringLiteral("<p>relais voyant </p><p> preventa</p>");
QTest::newRow("empty") << QString();
}
void unwrappedTextIsUnchanged()
{
QFETCH(QString, html);
QTextDocument document;
if (Qt::mightBeRichText(html))
document.setHtml(html);
else
document.setPlainText(html);
QCOMPARE(TextLines::layoutLines(&document),
document.toPlainText().split(QLatin1Char('\n')));
}
// A text wrapped to its width gives one line per drawn line, without
// the space the line was broken at.
void wrappedTextGivesTheDrawnLines()
{
QTextDocument document(QStringLiteral("Motor protection switch Q12"));
QTextOption option = document.defaultTextOption();
option.setWrapMode(QTextOption::WordWrap);
document.setDefaultTextOption(option);
document.setTextWidth(1);
QCOMPARE(TextLines::layoutLines(&document),
QStringList({QStringLiteral("Motor"), QStringLiteral("protection"),
QStringLiteral("switch"), QStringLiteral("Q12")}));
}
// A wrapped paragraph has no trailing spaces left, even on its last
// line: tabs and spaces that wrap to a line of their own (as in the
// affuteuse_250h example) give an empty line, which is not written but
// still keeps the line spacing.
void wrappedTextHasNoTrailingSpaces()
{
QTextDocument document(QStringLiteral("\tMotor\t\t "));
QTextOption option = document.defaultTextOption();
option.setWrapMode(QTextOption::WordWrap);
document.setDefaultTextOption(option);
document.setTextWidth(80);
const QStringList lines = TextLines::layoutLines(&document);
QVERIFY(lines.size() > 1);
QVERIFY(lines.contains(QStringLiteral("Motor")));
for (const QString &line : lines)
QVERIFY2(line.isEmpty() || !line.back().isSpace(), qPrintable(line));
}
// Non-breaking spaces also become plain spaces in a wrapped text.
void wrappedTextHasPlainSpaces()
{
QTextDocument document;
document.setHtml(QStringLiteral("<p>K1&nbsp;24V and more words</p>"));
QTextOption option = document.defaultTextOption();
option.setWrapMode(QTextOption::WordWrap);
document.setDefaultTextOption(option);
document.setTextWidth(1);
const QStringList lines = TextLines::layoutLines(&document);
QCOMPARE(lines.first(), QStringLiteral("K1 24V"));
for (const QString &line : lines)
QVERIFY(!line.contains(QChar::Nbsp));
}
};
QTEST_MAIN(tst_textlines)
#include "tst_textlines.moc"
+329
View File
@@ -0,0 +1,329 @@
/*
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 <QtTest>
#include <QGraphicsScene>
#include <QGraphicsSceneMouseEvent>
#include <QGraphicsTextItem>
#include <QSignalSpy>
#include <QTextBlock>
#include <QTextLayout>
#include <QTextOption>
#include <QUndoStack>
#include <cmath>
#include "QetGraphicsItemModeler/qetgraphicshandleritem.h"
#include "QetGraphicsItemModeler/textresizehandles.h"
using TextResize::Corner;
/**
A text with the "textWidth" property the texts of QElectroTech have.
*/
class WidthText : public QGraphicsTextItem
{
Q_OBJECT
Q_PROPERTY(qreal textWidth READ textWidth WRITE setWidth)
public:
WidthText(const QString &text) : QGraphicsTextItem(text)
{
QTextOption option = document()->defaultTextOption();
option.setWrapMode(QTextOption::WordWrap);
document()->setDefaultTextOption(option);
}
void setWidth(qreal width) {setTextWidth(width);}
};
/// The number of lines the text is laid out on.
static int lineCount(const QGraphicsTextItem &text)
{
text.document()->size();
int lines = 0;
for (QTextBlock block = text.document()->begin() ; block.isValid() ; block = block.next())
lines += block.layout()->lineCount();
return lines;
}
static bool samePoint(const QPointF &a, const QPointF &b)
{
return qAbs(a.x() - b.x()) < 1e-6 && qAbs(a.y() - b.y()) < 1e-6;
}
static QPointF sceneCorner(const QGraphicsTextItem &text, Corner corner)
{
return text.mapToScene(TextResize::cornerOf(text.boundingRect(), corner));
}
/// Send a mouse event of type to the handle at corner, as the scene would.
static void sendMouse(QGraphicsScene &scene, TextResizeHandles &handles,
Corner corner, QEvent::Type type, const QPointF &scene_pos)
{
QGraphicsSceneMouseEvent event(type);
event.setButton(Qt::LeftButton);
event.setButtons(type == QEvent::GraphicsSceneMouseRelease ? Qt::NoButton : Qt::LeftButton);
event.setScenePos(scene_pos);
scene.sendEvent(handles.handle(corner), &event);
}
/// Drag the handle at corner from where it is by offset, in scene coordinates.
static void drag(QGraphicsScene &scene, TextResizeHandles &handles,
WidthText &text, Corner corner, const QPointF &offset)
{
const QPointF start = sceneCorner(text, corner);
sendMouse(scene, handles, corner, QEvent::GraphicsSceneMousePress, start);
sendMouse(scene, handles, corner, QEvent::GraphicsSceneMouseMove, start + offset / 2);
sendMouse(scene, handles, corner, QEvent::GraphicsSceneMouseMove, start + offset);
sendMouse(scene, handles, corner, QEvent::GraphicsSceneMouseRelease, start + offset);
}
class tst_textresizehandles : public QObject
{
Q_OBJECT
private slots:
// Pure geometry: the width given by a drag does not change while the
// text moves to keep its opposite corner in place.
void widthForDragUsesThePressTransform()
{
WidthText text(QStringLiteral("Motor protection switch"));
text.setRotation(37);
text.setPos(10, 10);
const QTransform press_inverse = text.sceneTransform().inverted();
const QPointF fixed = TextResize::cornerOf(text.boundingRect(), TextResize::TopLeft);
const QPointF mouse = text.mapToScene(QPointF(80, 3));
const qreal width = TextResize::widthForDrag(press_inverse, fixed, mouse, TextResize::BottomRight, 0);
QCOMPARE(qRound(width * 1000), 80000);
text.setPos(500, -300);
QCOMPARE(TextResize::widthForDrag(press_inverse, fixed, mouse, TextResize::BottomRight, 0), width);
}
// The minimum width is the longest word: a text that narrow does not
// break any word, one narrower would.
void minimumWidthIsTheLongestWord()
{
WidthText text(QStringLiteral("a Motorschutzschalter b"));
const qreal minimum = TextResize::minimumWidth(text.document());
WidthText word(QStringLiteral("Motorschutzschalter"));
QVERIFY(qAbs(minimum - word.boundingRect().width()) < 0.5);
text.setWidth(minimum);
QVERIFY(qAbs(text.boundingRect().width() - minimum) < 0.5);
QCOMPARE(lineCount(text), 3);
}
// The minimum width never cuts a word, even for a document that would
// otherwise break a word anywhere.
void minimumWidthKeepsWords()
{
QGraphicsTextItem text(QStringLiteral("a Motorschutzschalter"));
QTextOption option = text.document()->defaultTextOption();
option.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
text.document()->setDefaultTextOption(option);
QGraphicsTextItem word(QStringLiteral("Motorschutzschalter"));
QVERIFY(qAbs(TextResize::minimumWidth(text.document()) - word.boundingRect().width()) < 0.5);
}
// pinCorner() puts a corner where it was, for each corner, rotation,
// rotation point and inside a rotated parent.
void pinCornerKeepsTheCorner()
{
QGraphicsRectItem parent(0, 0, 10, 10);
parent.setRotation(30);
for (bool with_parent : {false, true})
for (int c = TextResize::TopLeft ; c <= TextResize::BottomLeft ; ++c)
for (qreal angle : {0.0, 90.0, 37.0})
for (bool centre : {false, true}) {
WidthText text(QStringLiteral("Motor protection switch"));
if (with_parent)
text.setParentItem(&parent);
text.setRotation(angle);
text.setTransformOriginPoint(centre ? text.boundingRect().center() : QPointF());
const Corner corner = Corner(c);
const QPointF before = sceneCorner(text, corner);
text.setWidth(70);
if (centre)
text.setTransformOriginPoint(text.boundingRect().center());
TextResize::pinCorner(&text, corner, before);
QVERIFY2(samePoint(sceneCorner(text, corner), before),
qPrintable(QStringLiteral("corner %1 angle %2 centre %3 parent %4")
.arg(c).arg(angle).arg(centre).arg(with_parent)));
text.setParentItem(nullptr);
}
}
// Dragging a handle keeps the opposite corner in place, wraps the text
// and reports the change once, on release.
void dragKeepsTheOppositeCorner()
{
for (int c = TextResize::TopLeft ; c <= TextResize::BottomLeft ; ++c)
for (qreal angle : {0.0, 90.0, 37.0}) {
QGraphicsScene scene;
auto *text = new WidthText(QStringLiteral("Motor protection switch Q12"));
scene.addItem(text);
text->setPos(100, 100);
text->setRotation(angle);
auto *handles = new TextResizeHandles(text);
QSignalSpy finished(handles, &TextResizeHandles::resizeFinished);
const Corner corner = Corner(c);
const QPointF fixed = sceneCorner(*text, TextResize::opposite(corner));
const qreal natural = text->boundingRect().width();
const QPointF start_pos = text->pos();
//Narrower by 40 along the text, whatever its rotation;
//a vertical move of the mouse is ignored.
QTransform turn;
turn.rotate(angle);
const qreal sign = TextResize::isLeft(corner) ? 1 : -1;
drag(scene, *handles, *text, corner, turn.map(QPointF(sign * 40, 25)));
const QString what = QStringLiteral("corner %1 angle %2").arg(c).arg(angle);
QCOMPARE(text->textWidth(), qreal(qRound(natural - 40)));
QVERIFY2(lineCount(*text) > 1, qPrintable(what));
QVERIFY2(samePoint(sceneCorner(*text, TextResize::opposite(corner)), fixed), qPrintable(what));
QCOMPARE(finished.count(), 1);
const QList<QVariant> args = finished.first();
QCOMPARE(args.at(0).toReal(), qreal(-1));
QCOMPARE(args.at(1).toReal(), text->textWidth());
QCOMPARE(args.at(2).toPointF(), start_pos);
QCOMPARE(args.at(3).toPointF(), text->pos());
//The handles follow the new box
QVERIFY(samePoint(handles->handle(corner)->scenePos(), sceneCorner(*text, corner)));
}
}
// The handles still work after the text left its scene and came back,
// as undoing a delete does: Qt drops scene event filters on removal.
void dragAfterLeavingTheScene()
{
QGraphicsScene scene;
auto *text = new WidthText(QStringLiteral("Motor protection switch Q12"));
scene.addItem(text);
auto *handles = new TextResizeHandles(text);
scene.removeItem(text);
scene.addItem(text);
const QPointF start_pos = text->pos();
drag(scene, *handles, *text, TextResize::BottomRight, QPointF(-40, 0));
QVERIFY(text->textWidth() > 0);
QCOMPARE(text->pos(), start_pos);
delete text;
}
// The handles also work when they are created before the text is in a
// scene.
void dragWhenCreatedOutsideAScene()
{
QGraphicsScene scene;
auto *text = new WidthText(QStringLiteral("Motor protection switch Q12"));
auto *handles = new TextResizeHandles(text);
scene.addItem(text);
drag(scene, *handles, *text, TextResize::BottomRight, QPointF(-40, 0));
QVERIFY(text->textWidth() > 0);
}
// A text is never made narrower than its longest word.
void dragStopsAtTheLongestWord()
{
QGraphicsScene scene;
auto *text = new WidthText(QStringLiteral("a Motorschutzschalter b"));
scene.addItem(text);
auto *handles = new TextResizeHandles(text);
drag(scene, *handles, *text, TextResize::BottomRight, QPointF(-1000, 0));
QCOMPARE(text->textWidth(), std::ceil(TextResize::minimumWidth(text->document())));
}
// A press and release without moving changes nothing.
void clickChangesNothing()
{
QGraphicsScene scene;
auto *text = new WidthText(QStringLiteral("Motor protection switch"));
scene.addItem(text);
auto *handles = new TextResizeHandles(text);
QSignalSpy finished(handles, &TextResizeHandles::resizeFinished);
drag(scene, *handles, *text, TextResize::TopRight, QPointF());
QCOMPARE(finished.count(), 0);
QCOMPARE(text->textWidth(), qreal(-1));
}
// Double-clicking a handle goes back to the automatic width.
void doubleClickResetsToAutomaticWidth()
{
QGraphicsScene scene;
auto *text = new WidthText(QStringLiteral("Motor protection switch"));
scene.addItem(text);
text->setWidth(60);
auto *handles = new TextResizeHandles(text);
QSignalSpy finished(handles, &TextResizeHandles::resizeFinished);
sendMouse(scene, *handles, TextResize::BottomLeft, QEvent::GraphicsSceneMouseDoubleClick,
sceneCorner(*text, TextResize::BottomLeft));
QCOMPARE(text->textWidth(), qreal(-1));
QCOMPARE(finished.count(), 1);
QCOMPARE(finished.first().at(0).toReal(), qreal(60));
QCOMPARE(finished.first().at(1).toReal(), qreal(-1));
//Already automatic: nothing to undo
sendMouse(scene, *handles, TextResize::BottomLeft, QEvent::GraphicsSceneMouseDoubleClick,
sceneCorner(*text, TextResize::BottomLeft));
QCOMPARE(finished.count(), 1);
}
// One undo step gives back the width and the position, one redo
// applies them again.
void undoRestoresWidthAndPosition()
{
QGraphicsScene scene;
auto *text = new WidthText(QStringLiteral("Motor protection switch Q12"));
scene.addItem(text);
text->setPos(100, 100);
text->setRotation(90);
auto *handles = new TextResizeHandles(text);
QUndoStack stack;
connect(handles, &TextResizeHandles::resizeFinished, &stack,
[&stack, text](qreal ow, qreal nw, QPointF op, QPointF np) {
stack.push(new TextResizeCommand(text, ow, nw, op, np));
});
drag(scene, *handles, *text, TextResize::TopLeft, QPointF(0, 40));
QCOMPARE(stack.count(), 1);
const qreal new_width = text->textWidth();
const QPointF new_pos = text->pos();
QVERIFY(new_pos != QPointF(100, 100));
stack.undo();
QCOMPARE(text->textWidth(), qreal(-1));
QCOMPARE(text->pos(), QPointF(100, 100));
stack.redo();
QCOMPARE(text->textWidth(), new_width);
QCOMPARE(text->pos(), new_pos);
}
};
QTEST_MAIN(tst_textresizehandles)
#include "tst_textresizehandles.moc"