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
@@ -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;