diff --git a/ico/format-apply.svg b/ico/format-apply.svg
new file mode 100644
index 000000000..79320bed8
--- /dev/null
+++ b/ico/format-apply.svg
@@ -0,0 +1,13 @@
+
diff --git a/ico/format-copy.svg b/ico/format-copy.svg
new file mode 100644
index 000000000..033e2eeb3
--- /dev/null
+++ b/ico/format-copy.svg
@@ -0,0 +1,15 @@
+
diff --git a/lang/qet_de.ts b/lang/qet_de.ts
index a98faa095..00e7ead74 100644
--- a/lang/qet_de.ts
+++ b/lang/qet_de.ts
@@ -11805,6 +11805,41 @@ Verfügbare Optionen:
QETDiagramEditor
+
+
+ Copy text format
+ Beschriftungsart kopieren
+
+
+
+ Apply text format
+ Beschriftungsart zuweisen
+
+
+
+ Copy the text format of the selected element as a template
+ status bar tip
+ Kopiert das Beschriftungsformat des ausgewählten Elements als Vorlage
+
+
+
+ Apply the copied text format to the selected elements
+ status bar tip
+ Weist das kopierte Beschriftungsformat den markierten Elementen zu
+
+
+
+ Text format of "%1" copied
+ Beschriftungsart von „%1“ kopiert
+
+
+
+ Text format applied to %n element(s)
+
+ Beschriftungsart auf %n Element zugewiesen
+ Beschriftungsart auf %n Elemente zugewiesen
+
+
@@ -15138,6 +15173,11 @@ Was möchten Sie tun?
QObject
+
+
+ Apply text format
+ Beschriftungsart zuweisen
+
diff --git a/lang/qet_fr.ts b/lang/qet_fr.ts
index 4f4b125bf..c01a76d89 100644
--- a/lang/qet_fr.ts
+++ b/lang/qet_fr.ts
@@ -11817,6 +11817,41 @@ Options disponibles :
QETDiagramEditor
+
+
+ Copy text format
+ Copier le style de texte
+
+
+
+ Apply text format
+ Appliquer le style de texte
+
+
+
+ Copy the text format of the selected element as a template
+ status bar tip
+ Copie le format de texte de l'élément sélectionné comme modèle
+
+
+
+ Apply the copied text format to the selected elements
+ status bar tip
+ Applique le format de texte copié aux éléments sélectionnés
+
+
+
+ Text format of "%1" copied
+ Style de texte de « %1 » copié
+
+
+
+ Text format applied to %n element(s)
+
+ Style de texte appliqué à %n élément
+ Style de texte appliqué à %n éléments
+
+
@@ -15154,6 +15189,11 @@ Que désirez vous faire ?
QObject
+
+
+ Apply text format
+ Appliquer le style de texte
+
%n elements
diff --git a/qelectrotech.qrc b/qelectrotech.qrc
index cee4ba607..4b3ecff23 100644
--- a/qelectrotech.qrc
+++ b/qelectrotech.qrc
@@ -467,5 +467,7 @@
licenses/QElectroTech.LICENSE
licenses/QET-Elements.NOTICE
licenses/QET-Elements.LICENSE
+ ico/format-copy.svg
+ ico/format-apply.svg
diff --git a/sources/diagramview.cpp b/sources/diagramview.cpp
index 548606aa0..3cc9a7352 100644
--- a/sources/diagramview.cpp
+++ b/sources/diagramview.cpp
@@ -2119,6 +2119,8 @@ QList DiagramView::contextMenuActions() const
list << qde->m_cut;
list << qde->m_copy;
list << m_multi_paste;
+ list << qde->m_copy_text_format;
+ list << qde->m_apply_text_format;
list << qde->m_paste_element_info;
list << m_separators.at(0);
list << m_create_template; // Add the create template action
diff --git a/sources/elementtextpattern.cpp b/sources/elementtextpattern.cpp
index dfb594ba0..0e9be6bf5 100644
--- a/sources/elementtextpattern.cpp
+++ b/sources/elementtextpattern.cpp
@@ -26,6 +26,7 @@
#include "ui/importelementtextpatterndialog.h"
#include "undocommand/addelementtextcommand.h"
#include "undocommand/deleteqgraphicsitemcommand.h"
+#include "shownkinds.h"
#include
#include
@@ -272,3 +273,635 @@ void ImportElementTextPattern::apply(QString name, bool erase) const
undo_stack.endMacro();
}
+
+//=============================================================================
+// ElementTextFormatPattern & ApplyElementTextFormatCommand (Format Painter)
+//=============================================================================
+
+#include "terminal.h"
+
+ElementTextFormatPattern ElementTextFormatPattern::s_current_pattern;
+bool ElementTextFormatPattern::s_has_pattern = false;
+
+bool ElementTextFormatPattern::hasPattern()
+{
+ return s_has_pattern && s_current_pattern.isValid();
+}
+
+const ElementTextFormatPattern &ElementTextFormatPattern::currentPattern()
+{
+ return s_current_pattern;
+}
+
+void ElementTextFormatPattern::setCurrentPattern(const ElementTextFormatPattern &pattern)
+{
+ s_current_pattern = pattern;
+ s_has_pattern = pattern.isValid();
+}
+
+ElementTextFormatPattern ElementTextFormatPattern::fromElement(Element *element)
+{
+ ElementTextFormatPattern pattern;
+ if (!element) return pattern;
+
+ pattern.m_source_element_name = element->name();
+
+ // 1. Capture groups
+ for (ElementTextItemGroup *group : element->textGroups()) {
+ TextGroupFormatItem gItem;
+ gItem.name = group->name();
+ gItem.pos = group->pos();
+ gItem.rotation = group->rotation();
+ gItem.alignment = group->alignment();
+ gItem.verticalAdjustment = group->verticalAdjustment();
+ gItem.frame = group->frame();
+ gItem.holdToBottomPage = group->holdToBottomPage();
+ pattern.m_groups.append(gItem);
+ }
+
+ // 2. Capture all dynamic texts
+ int idx = 0;
+ // Ungrouped texts
+ for (DynamicElementTextItem *deti : element->dynamicTextItems()) {
+ DynamicTextFormatItem tItem;
+ tItem.textFrom = deti->textFrom();
+ tItem.infoName = deti->infoName();
+ tItem.compositeText = deti->toPlainText();
+ tItem.font = deti->font();
+ tItem.color = deti->color();
+ tItem.alignment = deti->alignment();
+ tItem.pos = deti->pos();
+ tItem.rotation = deti->rotation();
+ tItem.keepVisualRotation = deti->keepVisualRotation();
+ tItem.rotationPointCenter = deti->rotationPointCenter();
+ tItem.frame = deti->frame();
+ tItem.textWidth = deti->textWidth();
+ tItem.visible = ShownKinds::wantsVisible(deti);
+ tItem.groupName = QString();
+ tItem.index = idx++;
+ tItem.isTerminalText = isTerminalTextItem(deti, element);
+ pattern.m_text_items.append(tItem);
+ }
+
+ // Grouped texts
+ for (ElementTextItemGroup *group : element->textGroups()) {
+ for (DynamicElementTextItem *deti : group->texts()) {
+ DynamicTextFormatItem tItem;
+ tItem.textFrom = deti->textFrom();
+ tItem.infoName = deti->infoName();
+ tItem.compositeText = deti->toPlainText();
+ tItem.font = deti->font();
+ tItem.color = deti->color();
+ tItem.alignment = deti->alignment();
+ tItem.pos = deti->pos();
+ tItem.rotation = deti->rotation();
+ tItem.keepVisualRotation = deti->keepVisualRotation();
+ tItem.rotationPointCenter = deti->rotationPointCenter();
+ tItem.frame = deti->frame();
+ tItem.textWidth = deti->textWidth();
+ tItem.visible = ShownKinds::wantsVisible(deti);
+ tItem.groupName = group->name();
+ tItem.index = idx++;
+ tItem.isTerminalText = isTerminalTextItem(deti, element);
+ pattern.m_text_items.append(tItem);
+ }
+ }
+
+ return pattern;
+}
+
+bool ElementTextFormatPattern::hasTerminalTextFormat() const
+{
+ for (const DynamicTextFormatItem &item : m_text_items) {
+ if (item.isTerminalText) return true;
+ }
+ return false;
+}
+
+DynamicTextFormatItem ElementTextFormatPattern::terminalTextFormat() const
+{
+ for (const DynamicTextFormatItem &item : m_text_items) {
+ if (item.isTerminalText) return item;
+ }
+ return DynamicTextFormatItem();
+}
+
+bool ElementTextFormatPattern::isTerminalTextItem(const DynamicElementTextItem *deti, const Element *element)
+{
+ if (!deti || !element) return false;
+ if (deti->textFrom() != DynamicElementTextItem::UserText) return false;
+
+ const QString text = deti->toPlainText().trimmed();
+ if (text.isEmpty()) return false;
+
+ const QList terms = element->terminals();
+ for (const Terminal *t : terms) {
+ if (!t) continue;
+ if (t->name().trimmed() == text) {
+ return true;
+ }
+ }
+
+ // Secondary check: text is short (<= 6 chars, typical pin designation)
+ // and located close to a terminal (within 40 pixels)
+ if (text.length() <= 6) {
+ for (const Terminal *t : terms) {
+ if (!t) continue;
+ QPointF diff = deti->pos() - t->pos();
+ if ((diff.x() * diff.x() + diff.y() * diff.y()) <= (40.0 * 40.0)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+ApplyElementTextFormatCommand::ApplyElementTextFormatCommand(
+ Element *element,
+ const ElementTextFormatPattern &pattern,
+ QUndoCommand *parent)
+ : ApplyElementTextFormatCommand(QList{element}, pattern, parent)
+{
+}
+
+ApplyElementTextFormatCommand::ApplyElementTextFormatCommand(
+ const QList &elements,
+ const ElementTextFormatPattern &pattern,
+ QUndoCommand *parent)
+ : QUndoCommand(parent)
+{
+ setText(QObject::tr("Apply text format"));
+
+ for (Element *element : elements) {
+ if (!element) continue;
+
+ ElementFormatChange elChange;
+ elChange.element = element;
+
+ // Collect all existing texts on target element
+ QList targetTexts;
+ targetTexts << element->dynamicTextItems();
+ for (ElementTextItemGroup *g : element->textGroups()) {
+ targetTexts << g->texts();
+ }
+
+ // Setup group changes
+ for (const TextGroupFormatItem &gItem : pattern.groups()) {
+ ElementFormatChange::GroupChange gc;
+ gc.name = gItem.name;
+ ElementTextItemGroup *existingGroup = element->textGroup(gItem.name);
+ if (existingGroup) {
+ gc.group = existingGroup;
+ gc.wasCreated = false;
+ gc.oldPos = existingGroup->pos();
+ gc.oldRotation = existingGroup->rotation();
+ gc.oldAlignment = existingGroup->alignment();
+ gc.oldVAdjust = existingGroup->verticalAdjustment();
+ gc.oldFrame = existingGroup->frame();
+ gc.oldHoldBottom = existingGroup->holdToBottomPage();
+ } else {
+ gc.group = nullptr;
+ gc.wasCreated = true;
+ }
+ gc.newPos = gItem.pos;
+ gc.newRotation = gItem.rotation;
+ gc.newAlignment = gItem.alignment;
+ gc.newVAdjust = gItem.verticalAdjustment;
+ gc.newFrame = gItem.frame;
+ gc.newHoldBottom = gItem.holdToBottomPage;
+ elChange.groupChanges.append(gc);
+ }
+
+ // Text matching
+ QSet matchedTarget;
+ QSet matchedPatternIndices;
+
+ // Identify terminal / pole texts on the target element
+ QSet targetTerminalTexts;
+ for (DynamicElementTextItem *tDeti : targetTexts) {
+ if (ElementTextFormatPattern::isTerminalTextItem(tDeti, element)) {
+ targetTerminalTexts.insert(tDeti);
+ }
+ }
+
+ // Handle Terminal / Pole texts:
+ // They belong to physical terminals/poles of the element (e.g. 1..6, A1/A2, 13/14).
+ // Their positions, rotations, frames and visibilities are NEVER modified!
+ // If the pattern provides a terminal text style (font, color), apply it uniformly to all terminal texts.
+ const bool hasTermFmt = pattern.hasTerminalTextFormat();
+ const DynamicTextFormatItem termFmt = hasTermFmt ? pattern.terminalTextFormat() : DynamicTextFormatItem();
+
+ for (DynamicElementTextItem *tDeti : targetTerminalTexts) {
+ matchedTarget.insert(tDeti); // Exclude from general user text matching
+
+ // Only record a change if font or color actually changes
+ if (hasTermFmt && (tDeti->font() != termFmt.font || tDeti->color() != termFmt.color)) {
+ ElementFormatChange::TextChange tc;
+ tc.deti = tDeti;
+ tc.wasCreated = false;
+ tc.oldFont = tDeti->font();
+ tc.oldColor = tDeti->color();
+ tc.oldAlignment = tDeti->alignment();
+ tc.oldPos = tDeti->pos();
+ tc.oldRotation = tDeti->rotation();
+ tc.oldKeepVisualRotation = tDeti->keepVisualRotation();
+ tc.oldRotationPointCenter = tDeti->rotationPointCenter();
+ tc.oldFrame = tDeti->frame();
+ tc.oldTextWidth = tDeti->textWidth();
+ tc.oldVisible = ShownKinds::wantsVisible(tDeti);
+ tc.oldGroupName = tDeti->parentGroup() ? tDeti->parentGroup()->name() : QString();
+
+ tc.newFont = termFmt.font;
+ tc.newColor = termFmt.color;
+ // Positions, alignments, rotations, frames and visibilities are strictly preserved on terminals!
+ tc.newAlignment = tc.oldAlignment;
+ tc.newPos = tc.oldPos;
+ tc.newRotation = tc.oldRotation;
+ tc.newKeepVisualRotation = tc.oldKeepVisualRotation;
+ tc.newRotationPointCenter = tc.oldRotationPointCenter;
+ tc.newFrame = tc.oldFrame;
+ tc.newTextWidth = tc.oldTextWidth;
+ tc.newVisible = tc.oldVisible; // Always retain visibility!
+ tc.newGroupName = tc.oldGroupName;
+
+ elChange.textChanges.append(tc);
+ }
+ }
+
+ // Pass 1: ElementInfo with same infoName
+ for (int i = 0; i < pattern.textItems().size(); ++i) {
+ const DynamicTextFormatItem &pItem = pattern.textItems().at(i);
+ if (pItem.textFrom == DynamicElementTextItem::ElementInfo && !pItem.infoName.isEmpty()) {
+ for (DynamicElementTextItem *tDeti : targetTexts) {
+ if (!matchedTarget.contains(tDeti) &&
+ tDeti->textFrom() == DynamicElementTextItem::ElementInfo &&
+ tDeti->infoName() == pItem.infoName) {
+
+ matchedTarget.insert(tDeti);
+ matchedPatternIndices.insert(i);
+
+ ElementFormatChange::TextChange tc;
+ tc.deti = tDeti;
+ tc.wasCreated = false;
+ tc.oldFont = tDeti->font();
+ tc.oldColor = tDeti->color();
+ tc.oldAlignment = tDeti->alignment();
+ tc.oldPos = tDeti->pos();
+ tc.oldRotation = tDeti->rotation();
+ tc.oldKeepVisualRotation = tDeti->keepVisualRotation();
+ tc.oldRotationPointCenter = tDeti->rotationPointCenter();
+ tc.oldFrame = tDeti->frame();
+ tc.oldTextWidth = tDeti->textWidth();
+ tc.oldVisible = ShownKinds::wantsVisible(tDeti);
+ tc.oldGroupName = tDeti->parentGroup() ? tDeti->parentGroup()->name() : QString();
+
+ tc.newFont = pItem.font;
+ tc.newColor = pItem.color;
+ tc.newAlignment = pItem.alignment;
+ tc.newPos = pItem.pos;
+ tc.newRotation = pItem.rotation;
+ tc.newKeepVisualRotation = pItem.keepVisualRotation;
+ tc.newRotationPointCenter = pItem.rotationPointCenter;
+ tc.newFrame = pItem.frame;
+ tc.newTextWidth = pItem.textWidth;
+ tc.newVisible = pItem.visible;
+ tc.newGroupName = pItem.groupName;
+
+ elChange.textChanges.append(tc);
+ break;
+ }
+ }
+ }
+ }
+
+ // Pass 2: UserText (non-terminal texts only)
+ for (int i = 0; i < pattern.textItems().size(); ++i) {
+ if (matchedPatternIndices.contains(i)) continue;
+ const DynamicTextFormatItem &pItem = pattern.textItems().at(i);
+ if (pItem.isTerminalText) continue; // Skip terminal texts in generic user text matching
+ if (pItem.textFrom == DynamicElementTextItem::UserText) {
+ DynamicElementTextItem *foundDeti = nullptr;
+ for (DynamicElementTextItem *tDeti : targetTexts) {
+ if (!matchedTarget.contains(tDeti) &&
+ tDeti->textFrom() == DynamicElementTextItem::UserText &&
+ !pItem.compositeText.isEmpty() &&
+ tDeti->toPlainText() == pItem.compositeText) {
+ foundDeti = tDeti;
+ break;
+ }
+ }
+ if (!foundDeti) {
+ for (DynamicElementTextItem *tDeti : targetTexts) {
+ if (!matchedTarget.contains(tDeti) &&
+ tDeti->textFrom() == DynamicElementTextItem::UserText) {
+ foundDeti = tDeti;
+ break;
+ }
+ }
+ }
+ if (foundDeti) {
+ matchedTarget.insert(foundDeti);
+ matchedPatternIndices.insert(i);
+
+ ElementFormatChange::TextChange tc;
+ tc.deti = foundDeti;
+ tc.wasCreated = false;
+ tc.oldFont = foundDeti->font();
+ tc.oldColor = foundDeti->color();
+ tc.oldAlignment = foundDeti->alignment();
+ tc.oldPos = foundDeti->pos();
+ tc.oldRotation = foundDeti->rotation();
+ tc.oldKeepVisualRotation = foundDeti->keepVisualRotation();
+ tc.oldRotationPointCenter = foundDeti->rotationPointCenter();
+ tc.oldFrame = foundDeti->frame();
+ tc.oldTextWidth = foundDeti->textWidth();
+ tc.oldVisible = ShownKinds::wantsVisible(foundDeti);
+ tc.oldGroupName = foundDeti->parentGroup() ? foundDeti->parentGroup()->name() : QString();
+
+ tc.newFont = pItem.font;
+ tc.newColor = pItem.color;
+ tc.newAlignment = pItem.alignment;
+ tc.newPos = pItem.pos;
+ tc.newRotation = pItem.rotation;
+ tc.newKeepVisualRotation = pItem.keepVisualRotation;
+ tc.newRotationPointCenter = pItem.rotationPointCenter;
+ tc.newFrame = pItem.frame;
+ tc.newTextWidth = pItem.textWidth;
+ tc.newVisible = pItem.visible;
+ tc.newGroupName = pItem.groupName;
+
+ elChange.textChanges.append(tc);
+ }
+ }
+ }
+
+ // Pass 3: CompositeText or other unmatched (non-terminal)
+ for (int i = 0; i < pattern.textItems().size(); ++i) {
+ if (matchedPatternIndices.contains(i)) continue;
+ const DynamicTextFormatItem &pItem = pattern.textItems().at(i);
+ if (pItem.isTerminalText) continue;
+ DynamicElementTextItem *foundDeti = nullptr;
+ for (DynamicElementTextItem *tDeti : targetTexts) {
+ if (!matchedTarget.contains(tDeti) && tDeti->textFrom() == pItem.textFrom) {
+ foundDeti = tDeti;
+ break;
+ }
+ }
+ if (foundDeti) {
+ matchedTarget.insert(foundDeti);
+ matchedPatternIndices.insert(i);
+
+ ElementFormatChange::TextChange tc;
+ tc.deti = foundDeti;
+ tc.wasCreated = false;
+ tc.oldFont = foundDeti->font();
+ tc.oldColor = foundDeti->color();
+ tc.oldAlignment = foundDeti->alignment();
+ tc.oldPos = foundDeti->pos();
+ tc.oldRotation = foundDeti->rotation();
+ tc.oldKeepVisualRotation = foundDeti->keepVisualRotation();
+ tc.oldRotationPointCenter = foundDeti->rotationPointCenter();
+ tc.oldFrame = foundDeti->frame();
+ tc.oldTextWidth = foundDeti->textWidth();
+ tc.oldVisible = ShownKinds::wantsVisible(foundDeti);
+ tc.oldGroupName = foundDeti->parentGroup() ? foundDeti->parentGroup()->name() : QString();
+
+ tc.newFont = pItem.font;
+ tc.newColor = pItem.color;
+ tc.newAlignment = pItem.alignment;
+ tc.newPos = pItem.pos;
+ tc.newRotation = pItem.rotation;
+ tc.newKeepVisualRotation = pItem.keepVisualRotation;
+ tc.newRotationPointCenter = pItem.rotationPointCenter;
+ tc.newFrame = pItem.frame;
+ tc.newTextWidth = pItem.textWidth;
+ tc.newVisible = pItem.visible;
+ tc.newGroupName = pItem.groupName;
+
+ elChange.textChanges.append(tc);
+ }
+ }
+
+ // Pass 4: Pattern ElementInfo items that don't exist in target element
+ for (int i = 0; i < pattern.textItems().size(); ++i) {
+ if (matchedPatternIndices.contains(i)) continue;
+ const DynamicTextFormatItem &pItem = pattern.textItems().at(i);
+ if (pItem.textFrom == DynamicElementTextItem::ElementInfo && !pItem.infoName.isEmpty()) {
+ ElementFormatChange::TextChange tc;
+ tc.deti = nullptr;
+ tc.wasCreated = true;
+ tc.newFont = pItem.font;
+ tc.newColor = pItem.color;
+ tc.newAlignment = pItem.alignment;
+ tc.newPos = pItem.pos;
+ tc.newRotation = pItem.rotation;
+ tc.newKeepVisualRotation = pItem.keepVisualRotation;
+ tc.newRotationPointCenter = pItem.rotationPointCenter;
+ tc.newFrame = pItem.frame;
+ tc.newTextWidth = pItem.textWidth;
+ tc.newVisible = pItem.visible;
+ tc.newGroupName = pItem.groupName;
+ tc.oldGroupName = pItem.infoName; // infoName stored temporarily in oldGroupName
+ elChange.textChanges.append(tc);
+ }
+ }
+
+ // Pass 5 (Destructive hiding of unmatched texts) is completely removed:
+ // Non-matching target texts (such as additional poles on 3-pole/4-pole elements)
+ // are never hidden and retain their exact positions and visibilities!
+
+ m_changes.append(elChange);
+ }
+}
+
+ApplyElementTextFormatCommand::~ApplyElementTextFormatCommand()
+{
+ for (ElementFormatChange &elChange : m_changes) {
+ Element *element = elChange.element.data();
+ for (ElementFormatChange::TextChange &tc : elChange.textChanges) {
+ if (tc.wasCreated && tc.deti) {
+ DynamicElementTextItem *deti = tc.deti.data();
+ if (!deti) continue;
+ if (deti->parentGroup()) continue;
+ if (!element || !element->dynamicTextItems().contains(deti)) {
+ delete deti;
+ }
+ }
+ }
+ for (ElementFormatChange::GroupChange &gc : elChange.groupChanges) {
+ if (gc.wasCreated && gc.group) {
+ ElementTextItemGroup *group = gc.group.data();
+ if (!group) continue;
+ if (!element || !element->textGroups().contains(group)) {
+ delete group;
+ }
+ }
+ }
+ }
+}
+
+void ApplyElementTextFormatCommand::redo()
+{
+ for (ElementFormatChange &elChange : m_changes) {
+ Element *element = elChange.element.data();
+ if (!element) continue;
+
+ // 1. Create and update groups
+ for (ElementFormatChange::GroupChange &gc : elChange.groupChanges) {
+ ElementTextItemGroup *group = gc.group.data();
+ if (!group) {
+ group = element->textGroup(gc.name);
+ if (!group) {
+ group = element->addTextGroup(gc.name);
+ }
+ gc.group = group;
+ } else {
+ if (!element->textGroups().contains(group)) {
+ element->addTextGroup(group);
+ }
+ }
+ if (group) {
+ group->setPos(gc.newPos);
+ group->setRotation(gc.newRotation);
+ group->setAlignment(gc.newAlignment);
+ group->setVerticalAdjustment(gc.newVAdjust);
+ group->setFrame(gc.newFrame);
+ group->setHoldToBottomPage(gc.newHoldBottom);
+ }
+ }
+
+ // 2. Apply texts
+ for (ElementFormatChange::TextChange &tc : elChange.textChanges) {
+ DynamicElementTextItem *deti = tc.deti.data();
+ if (tc.wasCreated) {
+ if (!deti) {
+ deti = new DynamicElementTextItem(element);
+ deti->setTextFrom(DynamicElementTextItem::ElementInfo);
+ deti->setInfoName(tc.oldGroupName);
+ element->addDynamicTextItem(deti);
+ tc.deti = deti;
+ } else {
+ deti->setParentItem(element);
+ if (!element->dynamicTextItems().contains(deti)) {
+ element->addDynamicTextItem(deti);
+ }
+ }
+ }
+ if (!deti) continue;
+
+ // Group membership
+ if (!tc.newGroupName.isEmpty()) {
+ ElementTextItemGroup *targetGroup = element->textGroup(tc.newGroupName);
+ if (targetGroup) {
+ if (deti->parentGroup() != targetGroup) {
+ if (deti->parentGroup()) {
+ element->removeTextFromGroup(deti, deti->parentGroup());
+ }
+ element->addTextToGroup(deti, targetGroup);
+ }
+ }
+ } else {
+ if (deti->parentGroup()) {
+ element->removeTextFromGroup(deti, deti->parentGroup());
+ }
+ }
+
+ // Apply properties
+ deti->setFont(tc.newFont);
+ deti->setColor(tc.newColor);
+ deti->setAlignment(tc.newAlignment);
+ deti->setPos(tc.newPos);
+ deti->setRotation(tc.newRotation);
+ deti->setKeepVisualRotation(tc.newKeepVisualRotation);
+ deti->setRotationPointCenter(tc.newRotationPointCenter);
+ deti->setFrame(tc.newFrame);
+ deti->setTextWidth(tc.newTextWidth);
+ ShownKinds::setVisible(deti, tc.newVisible);
+ deti->update();
+ }
+
+ element->update();
+ if (element->diagram()) {
+ element->diagram()->update();
+ }
+ }
+}
+
+void ApplyElementTextFormatCommand::undo()
+{
+ for (ElementFormatChange &elChange : m_changes) {
+ Element *element = elChange.element.data();
+ if (!element) continue;
+
+ // 1. Restore texts
+ for (ElementFormatChange::TextChange &tc : elChange.textChanges) {
+ DynamicElementTextItem *deti = tc.deti.data();
+ if (!deti) continue;
+
+ if (tc.wasCreated) {
+ if (deti->parentGroup()) {
+ element->removeTextFromGroup(deti, deti->parentGroup());
+ }
+ element->removeDynamicTextItem(deti);
+ if (deti->scene()) {
+ deti->scene()->removeItem(deti);
+ }
+ continue;
+ }
+
+ // Restore group membership
+ if (!tc.oldGroupName.isEmpty()) {
+ ElementTextItemGroup *origGroup = element->textGroup(tc.oldGroupName);
+ if (origGroup) {
+ if (deti->parentGroup() != origGroup) {
+ if (deti->parentGroup()) {
+ element->removeTextFromGroup(deti, deti->parentGroup());
+ }
+ element->addTextToGroup(deti, origGroup);
+ }
+ }
+ } else {
+ if (deti->parentGroup()) {
+ element->removeTextFromGroup(deti, deti->parentGroup());
+ }
+ }
+
+ // Restore properties
+ deti->setFont(tc.oldFont);
+ deti->setColor(tc.oldColor);
+ deti->setAlignment(tc.oldAlignment);
+ deti->setPos(tc.oldPos);
+ deti->setRotation(tc.oldRotation);
+ deti->setKeepVisualRotation(tc.oldKeepVisualRotation);
+ deti->setRotationPointCenter(tc.oldRotationPointCenter);
+ deti->setFrame(tc.oldFrame);
+ deti->setTextWidth(tc.oldTextWidth);
+ ShownKinds::setVisible(deti, tc.oldVisible);
+ deti->update();
+ }
+
+ // 2. Restore groups
+ for (ElementFormatChange::GroupChange &gc : elChange.groupChanges) {
+ ElementTextItemGroup *group = gc.group.data();
+ if (!group) continue;
+
+ if (gc.wasCreated) {
+ element->removeTextGroup(group);
+ continue;
+ }
+
+ group->setPos(gc.oldPos);
+ group->setRotation(gc.oldRotation);
+ group->setAlignment(gc.oldAlignment);
+ group->setVerticalAdjustment(gc.oldVAdjust);
+ group->setFrame(gc.oldFrame);
+ group->setHoldToBottomPage(gc.oldHoldBottom);
+ }
+
+ element->update();
+ if (element->diagram()) {
+ element->diagram()->update();
+ }
+ }
+}
diff --git a/sources/elementtextpattern.h b/sources/elementtextpattern.h
index fb5eaf0e9..b1178b039 100644
--- a/sources/elementtextpattern.h
+++ b/sources/elementtextpattern.h
@@ -20,9 +20,17 @@
#include
#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "qetgraphicsitem/dynamicelementtextitem.h"
class Element;
class QWidget;
+class ElementTextItemGroup;
class ExportElementTextPattern
{
@@ -55,4 +63,153 @@ class ImportElementTextPattern
Element *m_element = nullptr;
};
+/**
+ * @brief Description of formatting and layout for an element text item.
+ */
+struct DynamicTextFormatItem {
+ DynamicElementTextItem::TextFrom textFrom = DynamicElementTextItem::UserText;
+ QString infoName;
+ QString compositeText;
+ QFont font;
+ QColor color = Qt::black;
+ Qt::Alignment alignment = (Qt::AlignLeft | Qt::AlignTop);
+ QPointF pos;
+ qreal rotation = 0;
+ bool keepVisualRotation = false;
+ bool rotationPointCenter = false;
+ bool frame = false;
+ qreal textWidth = -1;
+ bool visible = true;
+ QString groupName;
+ int index = 0;
+ bool isTerminalText = false;
+};
+
+/**
+ * @brief Description of formatting and layout for an element text group.
+ */
+struct TextGroupFormatItem {
+ QString name;
+ QPointF pos;
+ qreal rotation = 0;
+ Qt::Alignment alignment = Qt::AlignLeft;
+ int verticalAdjustment = 0;
+ bool frame = false;
+ bool holdToBottomPage = false;
+};
+
+/**
+ * @brief Stores the formatting pattern (font, size, style, colors, alignment, positions, offsets, frame)
+ * captured from a source Element. Acts as the Format Painter template clipboard.
+ */
+class ElementTextFormatPattern
+{
+ public:
+ ElementTextFormatPattern() = default;
+
+ static ElementTextFormatPattern fromElement(Element *element);
+ static bool hasPattern();
+ static const ElementTextFormatPattern ¤tPattern();
+ static void setCurrentPattern(const ElementTextFormatPattern &pattern);
+ static bool isTerminalTextItem(const DynamicElementTextItem *deti, const Element *element);
+
+ bool isValid() const { return !m_text_items.isEmpty(); }
+ QString sourceElementName() const { return m_source_element_name; }
+
+ const QList &textItems() const { return m_text_items; }
+ const QList &groups() const { return m_groups; }
+
+ bool hasTerminalTextFormat() const;
+ DynamicTextFormatItem terminalTextFormat() const;
+
+ private:
+ QString m_source_element_name;
+ QList m_text_items;
+ QList m_groups;
+
+ static ElementTextFormatPattern s_current_pattern;
+ static bool s_has_pattern;
+};
+
+/**
+ * @brief Undo command to apply a text formatting pattern to one or multiple elements.
+ * Field content (labels, designations, comments, article numbers) is preserved unchanged.
+ */
+class ApplyElementTextFormatCommand : public QUndoCommand
+{
+ public:
+ ApplyElementTextFormatCommand(const QList &elements,
+ const ElementTextFormatPattern &pattern,
+ QUndoCommand *parent = nullptr);
+ ApplyElementTextFormatCommand(Element *element,
+ const ElementTextFormatPattern &pattern,
+ QUndoCommand *parent = nullptr);
+ ~ApplyElementTextFormatCommand() override;
+
+ void undo() override;
+ void redo() override;
+
+ private:
+ struct ElementFormatChange {
+ QPointer element;
+
+ struct TextChange {
+ QPointer deti;
+ bool wasCreated = false;
+
+ // Old state
+ QFont oldFont;
+ QColor oldColor;
+ Qt::Alignment oldAlignment;
+ QPointF oldPos;
+ qreal oldRotation = 0;
+ bool oldKeepVisualRotation = false;
+ bool oldRotationPointCenter = false;
+ bool oldFrame = false;
+ qreal oldTextWidth = -1;
+ bool oldVisible = true;
+ QString oldGroupName;
+
+ // New state
+ QFont newFont;
+ QColor newColor;
+ Qt::Alignment newAlignment;
+ QPointF newPos;
+ qreal newRotation = 0;
+ bool newKeepVisualRotation = false;
+ bool newRotationPointCenter = false;
+ bool newFrame = false;
+ qreal newTextWidth = -1;
+ bool newVisible = true;
+ QString newGroupName;
+ };
+ QList textChanges;
+
+ struct GroupChange {
+ QString name;
+ QPointer group;
+ bool wasCreated = false;
+
+ // Old state
+ QPointF oldPos;
+ qreal oldRotation = 0;
+ Qt::Alignment oldAlignment = Qt::AlignLeft;
+ int oldVAdjust = 0;
+ bool oldFrame = false;
+ bool oldHoldBottom = false;
+
+ // New state
+ QPointF newPos;
+ qreal newRotation = 0;
+ Qt::Alignment newAlignment = Qt::AlignLeft;
+ int newVAdjust = 0;
+ bool newFrame = false;
+ bool newHoldBottom = false;
+ };
+ QList groupChanges;
+ };
+
+ QList m_changes;
+};
+
#endif // ELEMENTTEXTCONFIGURATION_H
diff --git a/sources/qetdiagrameditor.cpp b/sources/qetdiagrameditor.cpp
index a25842303..a55248f09 100644
--- a/sources/qetdiagrameditor.cpp
+++ b/sources/qetdiagrameditor.cpp
@@ -53,6 +53,7 @@
#include "cable/editcablecommand.h"
#include "diagramevent/diagrameventaddpaste.h"
#include "diagramview.h"
+#include "elementtextpattern.h"
#include "elementspanelwidget.h"
#include "factory/elementpicturefactory.h"
#include "factory/qetgraphicstablefactory.h"
@@ -469,6 +470,19 @@ void QETDiagramEditor::setUpActions()
connect(m_paste_element_info, &QAction::triggered,
this, &QETDiagramEditor::pasteElementInformations);
+ //Format painter actions
+ m_copy_text_format = new QAction(QET::Icons::CopyElementTextFormat, tr("Copy text format"), this);
+ m_apply_text_format = new QAction(QET::Icons::ApplyElementTextFormat, tr("Apply text format"), this);
+
+ ShortcutManager::instance().registerAction(m_copy_text_format, "diagrameditor.copy_text_format", tr("Diagram editors"), QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_C));
+ ShortcutManager::instance().registerAction(m_apply_text_format, "diagrameditor.apply_text_format", tr("Diagram editors"), QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_V));
+
+ m_copy_text_format -> setStatusTip(tr("Copy the text format of the selected element as a template", "status bar tip"));
+ m_apply_text_format -> setStatusTip(tr("Apply the copied text format to the selected elements", "status bar tip"));
+
+ connect(m_copy_text_format, &QAction::triggered, this, &QETDiagramEditor::copyTextFormat);
+ connect(m_apply_text_format, &QAction::triggered, this, &QETDiagramEditor::applyTextFormat);
+
//Duplicate: copy the selection and place it at a configured,
//grid-step offset immediately -- no interactive follow-the-
//cursor step, unlike Ctrl+V above. That is deliberate (#991):
@@ -1531,6 +1545,8 @@ void QETDiagramEditor::setUpMenu()
menu_edition -> addAction(m_cut);
menu_edition -> addAction(m_copy);
menu_edition -> addAction(m_paste);
+ menu_edition -> addAction(m_copy_text_format);
+ menu_edition -> addAction(m_apply_text_format);
menu_edition -> addAction(m_paste_origin);
menu_edition -> addAction(m_paste_element_info);
menu_edition -> addAction(m_duplicate);
@@ -2871,6 +2887,8 @@ void QETDiagramEditor::slot_updateComplexActions()
// number of selected elements
int selected_elements_count = dc.count(DiagramContent::Elements);
m_find_element->setEnabled(selected_elements_count == 1);
+ m_copy_text_format->setEnabled(selected_elements_count >= 1);
+ m_apply_text_format->setEnabled(!ro && selected_elements_count >= 1 && ElementTextFormatPattern::hasPattern());
m_mirror_horizontal->setEnabled(!ro && selected_elements_count);
m_mirror_vertical->setEnabled(!ro && selected_elements_count);
@@ -4690,3 +4708,56 @@ void QETDiagramEditor::updateTextGridButton()
//A divisor the menu does not offer, set by hand in the config file
m_text_grid_button->setText(tr("Texts %1").arg(TextGrid::ratioLabel(divisor)));
}
+/**
+ * @brief QETDiagramEditor::copyTextFormat
+ * Copies the text/label layout and formatting from the currently selected element as a template.
+ */
+void QETDiagramEditor::copyTextFormat()
+{
+ DiagramView *dv = currentDiagramView();
+ if (!dv || !dv->diagram()) return;
+
+ Element *sourceElmt = nullptr;
+ for (QGraphicsItem *item : dv->diagram()->selectedItems()) {
+ if (Element *elmt = qgraphicsitem_cast(item)) {
+ sourceElmt = elmt;
+ break;
+ }
+ }
+
+ if (!sourceElmt) return;
+
+ ElementTextFormatPattern pattern = ElementTextFormatPattern::fromElement(sourceElmt);
+ if (pattern.isValid()) {
+ ElementTextFormatPattern::setCurrentPattern(pattern);
+ slot_updateComplexActions();
+ statusBar()->showMessage(tr("Text format of \"%1\" copied").arg(sourceElmt->name()), 3000);
+ }
+}
+
+/**
+ * @brief QETDiagramEditor::applyTextFormat
+ * Applies the stored text formatting template to all selected elements in the active diagram.
+ */
+void QETDiagramEditor::applyTextFormat()
+{
+ if (!ElementTextFormatPattern::hasPattern()) return;
+
+ DiagramView *dv = currentDiagramView();
+ if (!dv || !dv->diagram()) return;
+ if (dv->diagram()->isReadOnly()) return;
+
+ QList targetElements;
+ for (QGraphicsItem *item : dv->diagram()->selectedItems()) {
+ if (Element *elmt = qgraphicsitem_cast(item)) {
+ targetElements.append(elmt);
+ }
+ }
+
+ if (targetElements.isEmpty()) return;
+
+ const ElementTextFormatPattern &pattern = ElementTextFormatPattern::currentPattern();
+ dv->diagram()->undoStack().push(new ApplyElementTextFormatCommand(targetElements, pattern));
+
+ statusBar()->showMessage(tr("Text format applied to %n element(s)", "", targetElements.size()), 3000);
+}
diff --git a/sources/qetdiagrameditor.h b/sources/qetdiagrameditor.h
index 2ff471b3f..268e47e4a 100644
--- a/sources/qetdiagrameditor.h
+++ b/sources/qetdiagrameditor.h
@@ -202,6 +202,8 @@ class QETDiagramEditor : public QETMainWindow
void showError(const QETResult &);
void showError(const QString &);
void subWindowActivated(QMdiSubWindow *subWindows);
+ void copyTextFormat();
+ void applyTextFormat();
private slots:
void updateTextGridButton();
@@ -215,7 +217,9 @@ class QETDiagramEditor : public QETMainWindow
*m_cut, ///< Cut selection to clipboard
*m_copy, ///< Copy selection to clipboard
*m_paste_element_info, ///< Copy the information of the copied element onto the selected elements (#1375)
- *m_insert_last_element = nullptr; ///< Place the last placed element again
+ *m_insert_last_element = nullptr, ///< Place the last placed element again
+ *m_copy_text_format = nullptr, ///< Copy label/text formatting template
+ *m_apply_text_format = nullptr; ///< Apply label/text formatting template
QActionGroup
m_row_column_actions_group, /// Action related to add/remove rows/column in diagram
diff --git a/sources/qeticons.cpp b/sources/qeticons.cpp
index e64fdcae6..783a8072c 100644
--- a/sources/qeticons.cpp
+++ b/sources/qeticons.cpp
@@ -75,6 +75,8 @@ namespace QET {
QIcon EditDelete;
QIcon EditOpacity;
QIcon EditPaste;
+ QIcon CopyElementTextFormat;
+ QIcon ApplyElementTextFormat;
QIcon EditRedo;
QIcon EditRename;
QIcon EditSelectAll;
@@ -464,6 +466,8 @@ void QET::Icons::initIcons()
EditDelete = QIcon::fromTheme("edit-delete");
EditOpacity = QIcon::fromTheme("edit-opacity");
EditPaste = QIcon::fromTheme("edit-paste");
+ CopyElementTextFormat = QIcon(":/ico/format-copy.svg");
+ ApplyElementTextFormat = QIcon(":/ico/format-apply.svg");
if (rtl) {
EditRedo.addPixmap(QIcon::fromTheme("edit-redo").pixmap(16).transformed(reverse));
EditRedo.addPixmap(QIcon::fromTheme("edit-redo").pixmap(22).transformed(reverse));
diff --git a/sources/qeticons.h b/sources/qeticons.h
index 35aedfcc5..2465922ac 100644
--- a/sources/qeticons.h
+++ b/sources/qeticons.h
@@ -82,6 +82,8 @@ namespace QET {
extern QIcon EditDelete;
extern QIcon EditOpacity;
extern QIcon EditPaste;
+ extern QIcon CopyElementTextFormat;
+ extern QIcon ApplyElementTextFormat;
extern QIcon EditRedo;
extern QIcon EditRename;
extern QIcon EditSelectAll;