View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0017985 | Scribus | Styles | public | 2026-09-24 05:01 | 2026-09-25 12:56 |
| Reporter | buttar | Assigned To | |||
| Priority | normal | Severity | feature | Reproducibility | N/A |
| Status | new | Resolution | open | ||
| Platform | Linux | OS | Fedora Workstation | OS Version | 44 |
| Product Version | 1.7.4.svn | ||||
| Summary | 0017985: [patch] Create char and para styles from text selection | ||||
| Description | Scribus lacks a direct way to turn manually formatted text into reusable Character or Paragraph Styles. This patch series introduces Create Style from Selection for both style types. (through right-click menu entry) - Users can format text first, then create a style from that formatting with optional `Based on` inheritance. - The new style is applied back to the text, preserves local exceptions where appropriate, and supports Undo/Redo. - This makes the work-flow faster and more natural by reducing the need to recreate formatting manually in Style Manager. Here is a 3-patch series to achieve the same. Please test/review. (sfs = style from selection) ## sfs1-selection-style-extractor-v1.0.patch - Adds reusable selection-to-style extraction for character formatting. - Detects uniform/common formatting across a text range. - Tracks common parent styles and builds deltas relative to a chosen base style. ## sfs2-create-char-style-from-selection-v1.05.patch - Adds Create Style from Selection → Character Style…. - Creates a Character Style from selected text with optional Based on inheritance. - Rejects mixed formatting and invalid/duplicate names. - Applies the style to the selection with complete Undo/Redo. - Keeps Style Manager synchronised while preserving unapplied edits. ## sfs3-create-para-from-selection-v1.03.patch - Adds Create Style from Selection → Paragraph Style…. - Analyses the whole containing paragraph and restricts creation to one paragraph. - Promotes only formatting common to the whole paragraph while preserving local exceptions. - Preserves parent-style inheritance and removes only redundant direct formatting. - Supports complete Undo/Redo and Style Manager synchronisation. | ||||
| Tags | #please_test | ||||
| Attached Files | sfs1-selection-style-extractor-v1.0.patch (9,221 bytes)
Index: scribus/text/CMakeLists.txt
===================================================================
--- scribus/text/CMakeLists.txt (revision 27861)
+++ scribus/text/CMakeLists.txt (working copy)
@@ -21,6 +21,7 @@
text/textlayout.cpp
text/textlayoutpainter.cpp
text/textshaper.cpp
+ text/textstyleextractor.cpp
text/textcontext.cpp
)
Index: scribus/text/textstyleextractor.h
===================================================================
--- scribus/text/textstyleextractor.h (nonexistent)
+++ scribus/text/textstyleextractor.h (working copy)
@@ -0,0 +1,57 @@
+/*
+ For general Scribus (>=1.3.2) copyright and licensing information please refer
+ to the COPYING file provided with the program. Following this notice may exist
+ a copyright and/or license notice that predates the release of Scribus 1.3.2
+ for which a new license (GPL+exception) is in place.
+ */
+/***************************************************************************
+* *
+* This program 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. *
+* *
+***************************************************************************/
+
+#ifndef TEXTSTYLEEXTRACTOR_H
+#define TEXTSTYLEEXTRACTOR_H
+
+#include "scribusapi.h"
+#include "styles/charstyle.h"
+
+class StoryText;
+
+namespace TextStyleExtractor
+{
+
+struct SCRIBUS_API CharStyleSelection
+{
+ bool valid { false };
+ bool uniform { false };
+ bool hasCommonParent { false };
+ int characterCount { 0 };
+ QString commonParent;
+ CharStyle effectiveStyle;
+ CharStyle commonStyle;
+};
+
+/**
+ * Analyse effective character formatting in a text range.
+ *
+ * Paragraph separators are ignored. effectiveStyle is the fully materialised
+ * style of the first analysed character. commonStyle contains only properties
+ * whose effective values are common to every analysed character. uniform is
+ * true when every effective character-style property is common to the range.
+ */
+SCRIBUS_API CharStyleSelection extractCharStyle(const StoryText& story, int pos, int len);
+
+/**
+ * Return the explicit properties required to reproduce effectiveStyle against
+ * baseStyle. If baseStyle is null, all effective properties are materialised.
+ * Name, parent and shortcut are intentionally left unset for the caller.
+ */
+SCRIBUS_API CharStyle charStyleDelta(const CharStyle& effectiveStyle, const CharStyle* baseStyle = nullptr);
+
+} // namespace TextStyleExtractor
+
+#endif
Index: scribus/text/textstyleextractor.cpp
===================================================================
--- scribus/text/textstyleextractor.cpp (nonexistent)
+++ scribus/text/textstyleextractor.cpp (working copy)
@@ -0,0 +1,113 @@
+/*
+ For general Scribus (>=1.3.2) copyright and licensing information please refer
+ to the COPYING file provided with the program. Following this notice may exist
+ a copyright and/or license notice that predates the release of Scribus 1.3.2
+ for which a new license (GPL+exception) is in place.
+ */
+/***************************************************************************
+* *
+* This program 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. *
+* *
+***************************************************************************/
+
+#include "textstyleextractor.h"
+
+#include "specialchars.h"
+#include "storytext.h"
+#include "util_math.h"
+
+namespace
+{
+
+CharStyle materializedCharStyle(const CharStyle& source)
+{
+ CharStyle result;
+#define ATTRDEF(attr_TYPE, attr_GETTER, attr_NAME, attr_DEFAULT, attr_BREAKSHAPING) \
+ result.set##attr_NAME(source.attr_GETTER());
+#include "styles/charstyle.attrdefs.cxx"
+#undef ATTRDEF
+ result.updateFeatures();
+ return result;
+}
+
+} // namespace
+
+namespace TextStyleExtractor
+{
+
+CharStyleSelection extractCharStyle(const StoryText& story, int pos, int len)
+{
+ CharStyleSelection result;
+ if (pos < 0 || len <= 0 || pos > story.length() || len > story.length() - pos)
+ return result;
+
+ bool first = true;
+ bool commonParent = true;
+ QString parentName;
+
+#define ATTRDEF(attr_TYPE, attr_GETTER, attr_NAME, attr_DEFAULT, attr_BREAKSHAPING) \
+ bool common##attr_NAME = true;
+#include "styles/charstyle.attrdefs.cxx"
+#undef ATTRDEF
+
+ for (int i = pos; i < pos + len; ++i)
+ {
+ if (story.text(i) == SpecialChars::PARSEP)
+ continue;
+
+ const CharStyle& current = story.charStyle(i);
+ if (first)
+ {
+ result.effectiveStyle = materializedCharStyle(current);
+ parentName = current.parent();
+ first = false;
+ }
+ else
+ {
+#define ATTRDEF(attr_TYPE, attr_GETTER, attr_NAME, attr_DEFAULT, attr_BREAKSHAPING) \
+ if (common##attr_NAME && !isequiv(result.effectiveStyle.attr_GETTER(), current.attr_GETTER())) \
+ common##attr_NAME = false;
+#include "styles/charstyle.attrdefs.cxx"
+#undef ATTRDEF
+ if (commonParent && current.parent() != parentName)
+ commonParent = false;
+ }
+ ++result.characterCount;
+ }
+
+ if (first)
+ return result;
+
+ result.valid = true;
+ result.uniform = true;
+#define ATTRDEF(attr_TYPE, attr_GETTER, attr_NAME, attr_DEFAULT, attr_BREAKSHAPING) \
+ if (common##attr_NAME) \
+ result.commonStyle.set##attr_NAME(result.effectiveStyle.attr_GETTER()); \
+ else \
+ result.uniform = false;
+#include "styles/charstyle.attrdefs.cxx"
+#undef ATTRDEF
+ result.commonStyle.updateFeatures();
+
+ result.hasCommonParent = commonParent;
+ if (commonParent)
+ result.commonParent = parentName;
+ return result;
+}
+
+CharStyle charStyleDelta(const CharStyle& effectiveStyle, const CharStyle* baseStyle)
+{
+ CharStyle result;
+#define ATTRDEF(attr_TYPE, attr_GETTER, attr_NAME, attr_DEFAULT, attr_BREAKSHAPING) \
+ if (!baseStyle || !isequiv(effectiveStyle.attr_GETTER(), baseStyle->attr_GETTER())) \
+ result.set##attr_NAME(effectiveStyle.attr_GETTER());
+#include "styles/charstyle.attrdefs.cxx"
+#undef ATTRDEF
+ result.updateFeatures();
+ return result;
+}
+
+} // namespace TextStyleExtractor
Index: scribus/tests/testStoryText.h
===================================================================
--- scribus/tests/testStoryText.h (revision 27861)
+++ scribus/tests/testStoryText.h (working copy)
@@ -27,4 +27,7 @@
void removePars();
void applyCharStyle();
void removeCharStyle();
+ void extractUniformCharStyle();
+ void extractMixedCharStyle();
+ void extractCharStyleDelta();
};
Index: scribus/tests/testStoryText.cpp
===================================================================
--- scribus/tests/testStoryText.cpp (revision 27861)
+++ scribus/tests/testStoryText.cpp (working copy)
@@ -9,6 +9,7 @@
#include <QDebug>
#include "testStoryText.h"
+#include "text/textstyleextractor.h"
void TestStoryText::initST()
{
@@ -127,3 +128,62 @@
QCOMPARE(story.startOfRun(2), 5 + 26 + 1);
QCOMPARE(story.endOfRun(2), 11 + 26);
}
+
+void TestStoryText::extractUniformCharStyle()
+{
+ StoryText story;
+ story.insertChars(0, QString("Style me"));
+
+ CharStyle direct;
+ direct.setFontSize(135.0);
+ direct.setFillColor("Red");
+ story.applyCharStyle(0, story.length(), direct);
+
+ auto extracted = TextStyleExtractor::extractCharStyle(story, 0, story.length());
+ QVERIFY(extracted.valid);
+ QVERIFY(extracted.uniform);
+ QCOMPARE(extracted.characterCount, story.length());
+ QCOMPARE(extracted.effectiveStyle.fontSize(), 135.0);
+ QCOMPARE(extracted.commonStyle.fontSize(), 135.0);
+ QCOMPARE(extracted.commonStyle.fillColor(), QString("Red"));
+}
+
+void TestStoryText::extractMixedCharStyle()
+{
+ StoryText story;
+ story.insertChars(0, QString("mixed"));
+
+ CharStyle common;
+ common.setFontSize(120.0);
+ story.applyCharStyle(0, story.length(), common);
+
+ CharStyle local;
+ local.setFillColor("Blue");
+ story.applyCharStyle(2, 1, local);
+
+ auto extracted = TextStyleExtractor::extractCharStyle(story, 0, story.length());
+ QVERIFY(extracted.valid);
+ QVERIFY(!extracted.uniform);
+ QVERIFY(!extracted.commonStyle.isInhFontSize());
+ QCOMPARE(extracted.commonStyle.fontSize(), 120.0);
+ QVERIFY(extracted.commonStyle.isInhFillColor());
+}
+
+void TestStoryText::extractCharStyleDelta()
+{
+ CharStyle base;
+ base.setFontSize(100.0);
+ base.setFillColor("Black");
+ base.setTracking(0.0);
+
+ CharStyle effective;
+ effective.setFontSize(110.0);
+ effective.setFillColor("Black");
+ effective.setTracking(0.0);
+
+ CharStyle delta = TextStyleExtractor::charStyleDelta(effective, &base);
+ QVERIFY(!delta.isInhFontSize());
+ QCOMPARE(delta.fontSize(), 110.0);
+ QVERIFY(delta.isInhFillColor());
+ QVERIFY(delta.isInhTracking());
+}
sfs2-create-char-style-from-selection-v1.05.patch (28,009 bytes)
Index: scribus/CMakeLists_Sources.txt
===================================================================
--- scribus/CMakeLists_Sources.txt (revision 27861)
+++ scribus/CMakeLists_Sources.txt (working copy)
@@ -335,6 +335,7 @@
ui/contentpalette_default.cpp
ui/contentpalette_page.cpp
ui/contextmenu.cpp
+ ui/createstylefromselectiondialog.cpp
ui/cupsoptions.cpp
ui/curvewidget.cpp
ui/customfdialog.cpp
Index: scribus/ui/createstylefromselectiondialog.h
===================================================================
--- scribus/ui/createstylefromselectiondialog.h (nonexistent)
+++ scribus/ui/createstylefromselectiondialog.h (working copy)
@@ -0,0 +1,53 @@
+/*
+ For general Scribus (>=1.3.2) copyright and licensing information please refer
+ to the COPYING file provided with the program. Following this notice may exist
+ a copyright and/or license notice that predates the release of Scribus 1.3.2
+ for which a new license (GPL+exception) is in place.
+ */
+/***************************************************************************
+* *
+* This program 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. *
+* *
+***************************************************************************/
+
+#ifndef CREATESTYLEFROMSELECTIONDIALOG_H
+#define CREATESTYLEFROMSELECTIONDIALOG_H
+
+#include <QDialog>
+#include <QList>
+#include <QPair>
+#include <QString>
+#include <QStringList>
+
+class QComboBox;
+class QDialogButtonBox;
+class QLabel;
+class QLineEdit;
+
+class CreateStyleFromSelectionDialog : public QDialog
+{
+public:
+ CreateStyleFromSelectionDialog(const QString& windowTitle,
+ const QString& suggestedName,
+ const QStringList& existingNames,
+ const QList<QPair<QString, QString>>& parentStyles,
+ const QString& suggestedParent,
+ QWidget* parent = nullptr);
+
+ QString styleName() const;
+ QString parentStyle() const;
+
+private:
+ void updateCreateButton();
+
+ QStringList m_existingNames;
+ QLineEdit* m_nameEdit { nullptr };
+ QComboBox* m_parentCombo { nullptr };
+ QLabel* m_errorLabel { nullptr };
+ QDialogButtonBox* m_buttonBox { nullptr };
+};
+
+#endif
Index: scribus/ui/createstylefromselectiondialog.cpp
===================================================================
--- scribus/ui/createstylefromselectiondialog.cpp (nonexistent)
+++ scribus/ui/createstylefromselectiondialog.cpp (working copy)
@@ -0,0 +1,95 @@
+/*
+ For general Scribus (>=1.3.2) copyright and licensing information please refer
+ to the COPYING file provided with the program. Following this notice may exist
+ a copyright and/or license notice that predates the release of Scribus 1.3.2
+ for which a new license (GPL+exception) is in place.
+ */
+/***************************************************************************
+* *
+* This program 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. *
+* *
+***************************************************************************/
+
+#include "createstylefromselectiondialog.h"
+
+#include <QComboBox>
+#include <QDialogButtonBox>
+#include <QFormLayout>
+#include <QLabel>
+#include <QLineEdit>
+#include <QPushButton>
+#include <QVBoxLayout>
+
+CreateStyleFromSelectionDialog::CreateStyleFromSelectionDialog(const QString& windowTitle,
+ const QString& suggestedName,
+ const QStringList& existingNames,
+ const QList<QPair<QString, QString>>& parentStyles,
+ const QString& suggestedParent,
+ QWidget* parent)
+ : QDialog(parent),
+ m_existingNames(existingNames)
+{
+ setWindowTitle(windowTitle);
+ setModal(true);
+ setMinimumWidth(420);
+
+ auto* layout = new QVBoxLayout(this);
+ auto* formLayout = new QFormLayout();
+ formLayout->setLabelAlignment(Qt::AlignRight | Qt::AlignVCenter);
+
+ m_nameEdit = new QLineEdit(this);
+ m_nameEdit->setText(suggestedName);
+ m_nameEdit->selectAll();
+ formLayout->addRow(tr("Name:"), m_nameEdit);
+
+ m_parentCombo = new QComboBox(this);
+ m_parentCombo->addItem(tr("(No Parent)"), QString());
+ for (const auto& parentStyle : parentStyles)
+ m_parentCombo->addItem(parentStyle.first, parentStyle.second);
+ const int parentIndex = m_parentCombo->findData(suggestedParent);
+ m_parentCombo->setCurrentIndex(parentIndex >= 0 ? parentIndex : 0);
+ formLayout->addRow(tr("Based on:"), m_parentCombo);
+
+ layout->addLayout(formLayout);
+
+ m_errorLabel = new QLabel(this);
+ m_errorLabel->setWordWrap(true);
+ layout->addWidget(m_errorLabel);
+
+ m_buttonBox = new QDialogButtonBox(QDialogButtonBox::Cancel | QDialogButtonBox::Ok, this);
+ m_buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Create"));
+ layout->addWidget(m_buttonBox);
+
+ connect(m_buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
+ connect(m_buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
+ connect(m_nameEdit, &QLineEdit::textChanged, this, [this]() { updateCreateButton(); });
+
+ updateCreateButton();
+ m_nameEdit->setFocus();
+}
+
+QString CreateStyleFromSelectionDialog::styleName() const
+{
+ return m_nameEdit->text().trimmed();
+}
+
+QString CreateStyleFromSelectionDialog::parentStyle() const
+{
+ return m_parentCombo->currentData().toString();
+}
+
+void CreateStyleFromSelectionDialog::updateCreateButton()
+{
+ const QString name = styleName();
+ QString error;
+ if (name.isEmpty())
+ error = tr("Enter a style name.");
+ else if (m_existingNames.contains(name))
+ error = tr("A style named \"%1\" already exists.").arg(name);
+
+ m_errorLabel->setText(error);
+ m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(error.isEmpty());
+}
Index: scribus/ui/contextmenu.cpp
===================================================================
--- scribus/ui/contextmenu.cpp (revision 27861)
+++ scribus/ui/contextmenu.cpp (working copy)
@@ -26,6 +26,8 @@
#include "appmodes.h"
#include "canvas.h"
#include "canvasmode.h"
+#include "iconmanager.h"
+#include "pageitem_table.h"
#include "pageitem_textframe.h"
#include "scmimedata.h"
#include "scraction.h"
@@ -341,6 +343,23 @@
if (m_actionList.contains("itemAdjustFrameHeightToText"))
addAction(m_ScMW->scrActions["itemAdjustFrameHeightToText"]);
+ if ((m_doc->appMode == modeEdit || m_doc->appMode == modeEditTable) && selectedItemCount == 1)
+ {
+ PageItem* textItem = currItem;
+ if (currItem->isTable() && m_doc->appMode == modeEditTable)
+ textItem = currItem->asTable()->activeCell().textFrame();
+ if (textItem && textItem->itemText.hasSelection())
+ {
+ IconManager& iconManager = IconManager::instance();
+ QMenu* styleFromSelectionMenu = new QMenu(this);
+ QAction* styleFromSelectionAction = addMenu(styleFromSelectionMenu);
+ styleFromSelectionAction->setText(tr("Create Style from Selection"));
+ QAction* charStyleAction = styleFromSelectionMenu->addAction(
+ iconManager.loadIcon("character-style"), tr("Character Style..."));
+ connect(charStyleAction, SIGNAL(triggered()), m_ScMW, SLOT(createCharStyleFromSelection()));
+ }
+ }
+
if (!menuEditContent->actions().isEmpty() && currItem->isTable())
{
addSeparator();
Index: scribus/scribus.h
===================================================================
--- scribus/scribus.h (revision 27861)
+++ scribus/scribus.h (working copy)
@@ -521,6 +521,7 @@
void setNewDirection(int i);
void setNewParStyle(const QString& name);
void setNewCharStyle(const QString& name);
+ void createCharStyleFromSelection();
void setAlignmentValue(int i);
void setDirectionValue(int i);
void editItemsFromOutlines(PageItem *ite);
Index: scribus/scribus.cpp
===================================================================
--- scribus/scribus.cpp (revision 27861)
+++ scribus/scribus.cpp (working copy)
@@ -150,6 +150,7 @@
#include "serializer.h"
#include "storyloader.h"
#include "stylesearch.h"
+#include "text/textstyleextractor.h"
#include "textframespellchecker.h"
#include "textnote.h"
#include "tocgenerator.h"
@@ -168,6 +169,7 @@
#include "ui/colorpicker/colorpicker.h"
#include "ui/contentpalette.h"
#include "ui/contextmenu.h"
+#include "ui/createstylefromselectiondialog.h"
#include "ui/copypagetomasterpagedialog.h"
#include "ui/customfdialog.h"
#include "ui/delpages.h"
@@ -6348,6 +6350,93 @@
setTBvals(currItem);
}
+void ScribusMainWindow::createCharStyleFromSelection()
+{
+ if (!HaveDoc || !doc || doc->m_Selection->count() != 1)
+ return;
+
+ PageItem* currItem = doc->m_Selection->itemAt(0);
+ if (currItem->isTable() && doc->appMode == modeEditTable)
+ currItem = currItem->asTable()->activeCell().textFrame();
+ if (!currItem || !currItem->itemText.hasSelection())
+ return;
+
+ const int start = currItem->itemText.startOfSelection();
+ const int length = currItem->itemText.endOfSelection() - start;
+ const auto extracted = TextStyleExtractor::extractCharStyle(currItem->itemText, start, length);
+ if (!extracted.valid)
+ return;
+ if (!extracted.uniform)
+ {
+ ScMessageBox::information(this, tr("Create Character Style"),
+ tr("The selection contains mixed character formatting.\n"
+ "Select text with consistent formatting and try again."));
+ return;
+ }
+
+ QStringList existingNames;
+ QList<QPair<QString, QString>> parentStyles;
+ const QList<int> sortedStyles = doc->getSortedCharStyleList();
+ for (int index : sortedStyles)
+ {
+ const CharStyle& charStyle = doc->charStyles()[index];
+ if (!charStyle.hasName())
+ continue;
+ existingNames.append(charStyle.name());
+ parentStyles.append(qMakePair(charStyle.displayName(), charStyle.name()));
+ }
+
+ if (m_styleManager)
+ {
+ const QStringList cachedNames = m_styleManager->cachedCharacterStyleNames();
+ for (const QString& name : cachedNames)
+ {
+ if (!existingNames.contains(name))
+ existingNames.append(name);
+ }
+ }
+
+ QString suggestedParent;
+ if (extracted.hasCommonParent && doc->charStyles().contains(extracted.commonParent))
+ suggestedParent = extracted.commonParent;
+ const QString suggestedName = m_styleManager
+ ? m_styleManager->uniqueCharacterStyleName(tr("New Character Style"))
+ : doc->charStyles().getUniqueCopyName(tr("New Character Style"));
+
+ CreateStyleFromSelectionDialog dialog(tr("Create Character Style"),
+ suggestedName, existingNames, parentStyles,
+ suggestedParent, this);
+ if (dialog.exec() != QDialog::Accepted)
+ return;
+
+ const QString parentName = dialog.parentStyle();
+ const CharStyle* baseStyle = nullptr;
+ if (!parentName.isEmpty())
+ {
+ const int parentIndex = doc->charStyles().find(parentName);
+ if (parentIndex >= 0)
+ baseStyle = &doc->charStyles()[parentIndex];
+ }
+
+ CharStyle newStyle = TextStyleExtractor::charStyleDelta(extracted.effectiveStyle, baseStyle);
+ newStyle.setName(dialog.styleName());
+ newStyle.setDefaultStyle(false);
+ newStyle.setParent(parentName);
+
+ if (!doc->itemSelection_CreateNamedCharStyle(newStyle))
+ {
+ ScMessageBox::warning(this, CommonStrings::trWarning,
+ tr("The character style could not be created."));
+ return;
+ }
+
+ if (m_styleManager)
+ m_styleManager->syncCharacterStyle(newStyle.name(), true);
+ requestUpdate(reqCharStylesUpdate | reqStyleComboDocUpdate);
+ setTBvals(currItem);
+ view->DrawNew();
+}
+
void ScribusMainWindow::setAlignmentValue(int i)
{
contentPalette->updateTextAlignment(i);
Index: scribus/scribusdoc.h
===================================================================
--- scribus/scribusdoc.h (revision 27861)
+++ scribus/scribusdoc.h (working copy)
@@ -714,6 +714,7 @@
const CharStyle& charStyle(const QString& name) const { return m_docCharStyles.get(name); }
const StyleSet<CharStyle>& charStyles() const { return m_docCharStyles; }
+ bool addCharStyle(const CharStyle& style);
void redefineCharStyles(const StyleSet<CharStyle>& newStyles, bool removeUnused = false);
/**
* @brief Remove any reference to old styles and replace with new name. This needs to be
@@ -860,6 +861,7 @@
*/
void restoreMarks(UndoState* ss, bool isUndo);
void restoreNoteStyle(SimpleState* ss, bool isUndo);
+ void restoreCharStyleCreation(SimpleState* ss, bool isUndo);
void restoreDeleteNote(UndoState* ss, bool isUndo);
/**
@@ -1180,6 +1182,7 @@
void itemSelection_EraseCharStyle(Selection* customSelection = nullptr);
void itemSelection_SetNamedParagraphStyle(const QString & name, Selection* customSelection = nullptr);
void itemSelection_SetNamedCharStyle(const QString & name, Selection* customSelection = nullptr);
+ bool itemSelection_CreateNamedCharStyle(const CharStyle& style, Selection* customSelection = nullptr);
void itemSelection_SetNamedLineStyle(const QString & name, Selection* customSelection = nullptr);
void itemSelection_SetNamedCellStyle(const QString & name, Selection* customSelection = nullptr);
void itemSelection_SetNamedTableStyle(const QString & name, Selection* customSelection = nullptr);
Index: scribus/scribusdoc.cpp
===================================================================
--- scribus/scribusdoc.cpp (revision 27861)
+++ scribus/scribusdoc.cpp (working copy)
@@ -104,6 +104,7 @@
#include "ui/outlinepalette.h"
#include "ui/pagepalette.h"
#include "ui/storyeditor.h"
+#include "ui/stylemanager.h"
#include "ui/tablecolumnwidthsdialog.h"
#include "ui/tablerowheightsdialog.h"
#include "undomanager.h"
@@ -1576,6 +1577,28 @@
m_docCharStyles.invalidate();
}
+bool ScribusDoc::addCharStyle(const CharStyle& style)
+{
+ if (!style.hasName() || m_docCharStyles.contains(style.name()))
+ return false;
+
+ CharStyle newStyle(style);
+ newStyle.setDefaultStyle(false);
+ m_docCharStyles.create(newStyle);
+ m_docCharStyles.invalidate();
+
+ if (UndoManager::undoEnabled())
+ {
+ auto* state = new ScOldNewState<CharStyle>(tr("Create Character Style"), style.name());
+ state->set("CREATE_CHARSTYLE");
+ state->setStates(CharStyle(), newStyle);
+ m_undoManager->action(this, state);
+ }
+
+ changed();
+ return true;
+}
+
void ScribusDoc::redefineTableStyles(const StyleSet<TableStyle>& newStyles, bool removeUnused)
{
m_docTableStyles.redefine(newStyles, false);
@@ -1983,6 +2006,8 @@
}
else if (ss->contains("NSTYLE"))
restoreNoteStyle(ss, isUndo);
+ else if (ss->contains("CREATE_CHARSTYLE"))
+ restoreCharStyleCreation(ss, isUndo);
else if (ss->contains("DELETE_NOTE"))
restoreDeleteNote(state, isUndo);
else if (ss->contains("MARK"))
@@ -2513,6 +2538,36 @@
scMW()->nsEditor->updateNSList();
}
+void ScribusDoc::restoreCharStyleCreation(SimpleState* ss, bool isUndo)
+{
+ const auto* state = dynamic_cast<ScOldNewState<CharStyle>*>(ss);
+ if (!state)
+ return;
+
+ const CharStyle& style = state->getNewState();
+ if (isUndo)
+ {
+ const int index = m_docCharStyles.find(style.name());
+ if (index >= 0)
+ {
+ CharStyle* oldStyle = &m_docCharStyles[index];
+ m_docCharStyles.remove(index);
+ delete oldStyle;
+ }
+ }
+ else if (!m_docCharStyles.contains(style.name()))
+ m_docCharStyles.create(style);
+
+ m_docCharStyles.invalidate();
+ changed();
+ if (ScCore->usingGUI())
+ {
+ if (scMW()->styleMgr())
+ scMW()->styleMgr()->syncCharacterStyle(style.name(), !isUndo);
+ scMW()->requestUpdate(reqCharStylesUpdate | reqStyleComboDocUpdate);
+ }
+}
+
void ScribusDoc::restoreDeleteNote(UndoState* state, bool isUndo)
{
const auto *is = dynamic_cast<ScItemsState*>(state);
@@ -8196,6 +8251,32 @@
itemSelection_ApplyCharStyle(newStyle, customSelection, "NAMED_STYLE");
}
+bool ScribusDoc::itemSelection_CreateNamedCharStyle(const CharStyle& style, Selection* customSelection)
+{
+ Selection* itemSelection = (customSelection != nullptr) ? customSelection : m_Selection;
+ if (!itemSelection || itemSelection->count() == 0 || !style.hasName())
+ return false;
+
+ UndoTransaction transaction;
+ if (UndoManager::undoEnabled())
+ {
+ const PageItem* currItem = itemSelection->itemAt(0);
+ const QString targetName = (itemSelection->count() > 1) ? Um::SelectionGroup : currItem->getUName();
+ QPixmap* targetPixmap = (itemSelection->count() > 1) ? Um::IGroup : currItem->getUPixmap();
+ transaction = m_undoManager->beginTransaction(targetName, targetPixmap,
+ tr("Create Character Style"), style.name(), Um::IFont);
+ }
+
+ if (!addCharStyle(style))
+ return false;
+
+ itemSelection_SetNamedCharStyle(style.name(), itemSelection);
+
+ if (transaction)
+ transaction.commit();
+ return true;
+}
+
void ScribusDoc::itemSelection_SetNamedParagraphStyle(const QString& name, Selection* customSelection)
{
ParagraphStyle newStyle;
Index: scribus/ui/smtextstyles.h
===================================================================
--- scribus/ui/smtextstyles.h (revision 27861)
+++ scribus/ui/smtextstyles.h (working copy)
@@ -48,6 +48,8 @@
void languageChange() override;
void unitChange() override;
StyleSet<ParagraphStyle>& tmpStyles(); // butt ugly
+ bool mergeExternalStyle(const ParagraphStyle& style);
+ bool removeExternalStyle(const QString& documentName);
signals:
void selectionDirty();
@@ -162,6 +164,8 @@
void languageChange() override;
void unitChange() override;
StyleSet<CharStyle>& tmpStyles();
+ bool mergeExternalStyle(const CharStyle& style);
+ bool removeExternalStyle(const QString& documentName);
signals:
void selectionDirty();
Index: scribus/ui/smtextstyles.cpp
===================================================================
--- scribus/ui/smtextstyles.cpp (revision 27861)
+++ scribus/ui/smtextstyles.cpp (working copy)
@@ -90,6 +90,60 @@
return m_tmpStyles;
}
+bool SMParagraphStyle::mergeExternalStyle(const ParagraphStyle& style)
+{
+ if (!m_doc || !style.hasName() || m_tmpStyles.contains(style.name()))
+ return false;
+ for (const RemoveItem& item : m_deleted)
+ {
+ if (item.first == style.name())
+ return false;
+ }
+
+ ParagraphStyle cachedStyle(style);
+ QString parentName = cachedStyle.parent();
+ for (const RemoveItem& item : m_deleted)
+ {
+ if (item.first == parentName)
+ {
+ parentName = item.second;
+ break;
+ }
+ }
+ if (parentName == CommonStrings::trDefaultParagraphStyle)
+ parentName = CommonStrings::DefaultParagraphStyle;
+ if (!parentName.isEmpty() && !m_tmpStyles.contains(parentName))
+ parentName.clear();
+ cachedStyle.setParent(parentName);
+ m_tmpStyles.create(cachedStyle);
+ m_tmpStyles.invalidate();
+ return true;
+}
+
+bool SMParagraphStyle::removeExternalStyle(const QString& documentName)
+{
+ const int index = m_tmpStyles.find(documentName);
+ if (index < 0 || m_tmpStyles.isDefault(m_tmpStyles[index]))
+ return false;
+
+ for (int i = m_selection.count() - 1; i >= 0; --i)
+ {
+ if (m_selection[i]->name() == documentName)
+ m_selection.removeAt(i);
+ }
+ ParagraphStyle* oldStyle = &m_tmpStyles[index];
+ m_tmpStyles.remove(index);
+ delete oldStyle;
+
+ for (int i = m_tmpStyles.count() - 1; i >= 0; --i)
+ {
+ if (m_tmpStyles[i].parent() == documentName)
+ m_tmpStyles[i].setParent(QString());
+ }
+ m_tmpStyles.invalidate();
+ return true;
+}
+
QList<StyleName> SMParagraphStyle::styles(bool reloadFromDoc)
{
QList<StyleName> tmpList;
@@ -1900,6 +1954,64 @@
return m_tmpStyles;
}
+bool SMCharacterStyle::mergeExternalStyle(const CharStyle& style)
+{
+ if (!m_doc || !style.hasName() || m_tmpStyles.contains(style.name()))
+ return false;
+ for (const RemoveItem& item : m_deleted)
+ {
+ if (item.first == style.name())
+ return false;
+ }
+
+ CharStyle cachedStyle(style);
+ QString parentName = cachedStyle.parent();
+ for (const RemoveItem& item : m_deleted)
+ {
+ if (item.first == parentName)
+ {
+ parentName = item.second;
+ break;
+ }
+ }
+ if (parentName == CommonStrings::trDefaultCharacterStyle)
+ parentName = CommonStrings::DefaultCharacterStyle;
+ if (!parentName.isEmpty() && !m_tmpStyles.contains(parentName))
+ parentName.clear();
+ cachedStyle.setParent(parentName);
+ m_tmpStyles.create(cachedStyle);
+ m_tmpStyles.invalidate();
+ return true;
+}
+
+bool SMCharacterStyle::removeExternalStyle(const QString& documentName)
+{
+ const int index = m_tmpStyles.find(documentName);
+ if (index < 0 || m_tmpStyles.isDefault(m_tmpStyles[index]))
+ return false;
+
+ for (int i = m_selection.count() - 1; i >= 0; --i)
+ {
+ if (m_selection[i]->name() == documentName)
+ m_selection.removeAt(i);
+ }
+ CharStyle* oldStyle = &m_tmpStyles[index];
+ m_tmpStyles.remove(index);
+ delete oldStyle;
+
+ for (int i = m_tmpStyles.count() - 1; i >= 0; --i)
+ {
+ if (m_tmpStyles[i].parent() == documentName)
+ m_tmpStyles[i].setParent(QString());
+ }
+ m_tmpStyles.invalidate();
+
+ QList<RemoveItem> removedStyles;
+ removedStyles.append(RemoveItem(documentName, QString()));
+ emit charStylesDeleted(removedStyles);
+ return true;
+}
+
QList<StyleName> SMCharacterStyle::styles(bool reloadFromDoc)
{
QList<StyleName> tmpList;
Index: scribus/ui/stylemanager.h
===================================================================
--- scribus/ui/stylemanager.h (revision 27861)
+++ scribus/ui/stylemanager.h (working copy)
@@ -33,6 +33,12 @@
void addStyle(StyleItem *item);
void refreshLists();
+ QStringList cachedCharacterStyleNames();
+ QStringList cachedParagraphStyleNames();
+ QString uniqueCharacterStyleName(const QString& baseName);
+ QString uniqueParagraphStyleName(const QString& baseName);
+ void syncCharacterStyle(const QString& name, bool present);
+ void syncParagraphStyle(const QString& name, bool present);
void showAsNewParagraphStyle();
void showAsNewCharacterStyle();
@@ -164,6 +170,7 @@
void addNewType(StyleItem *item, bool loadFromDoc = true);
void createNewStyle(const QString &typeName, const QString &fromParent = QString());
void reloadStyleView(bool loadFromDoc = true); // are the styles loaded from doc or from tmp cache
+ void reloadStyleViewPreservingState();
bool shortcutExists(const QString &keys);
void updateActionName(const QString &oldName, const QString &newName);
/* QPair.first = type name and QPair.second = style name */
Index: scribus/ui/stylemanager.cpp
===================================================================
--- scribus/ui/stylemanager.cpp (revision 27861)
+++ scribus/ui/stylemanager.cpp (working copy)
@@ -295,6 +295,154 @@
}
}
+QStringList StyleManager::cachedCharacterStyleNames()
+{
+ QStringList names;
+ SMCharacterStyle* styleItem = item<SMCharacterStyle>();
+ if (!styleItem)
+ return names;
+
+ StyleSet<CharStyle>& styles = styleItem->tmpStyles();
+ for (int i = 0; i < styles.count(); ++i)
+ {
+ if (styles[i].hasName())
+ names.append(styles[i].name());
+ }
+ return names;
+}
+
+QStringList StyleManager::cachedParagraphStyleNames()
+{
+ QStringList names;
+ SMParagraphStyle* styleItem = item<SMParagraphStyle>();
+ if (!styleItem)
+ return names;
+
+ StyleSet<ParagraphStyle>& styles = styleItem->tmpStyles();
+ for (int i = 0; i < styles.count(); ++i)
+ {
+ if (styles[i].hasName())
+ names.append(styles[i].name());
+ }
+ return names;
+}
+
+QString StyleManager::uniqueCharacterStyleName(const QString& baseName)
+{
+ QStringList names = cachedCharacterStyleNames();
+ if (m_doc)
+ {
+ for (int i = 0; i < m_doc->charStyles().count(); ++i)
+ {
+ const QString name = m_doc->charStyles()[i].name();
+ if (!names.contains(name))
+ names.append(name);
+ }
+ }
+ if (!names.contains(baseName))
+ return baseName;
+ for (int suffix = 2; ; ++suffix)
+ {
+ const QString candidate = QStringLiteral("%1 (%2)").arg(baseName).arg(suffix);
+ if (!names.contains(candidate))
+ return candidate;
+ }
+}
+
+QString StyleManager::uniqueParagraphStyleName(const QString& baseName)
+{
+ QStringList names = cachedParagraphStyleNames();
+ if (m_doc)
+ {
+ for (int i = 0; i < m_doc->paragraphStyles().count(); ++i)
+ {
+ const QString name = m_doc->paragraphStyles()[i].name();
+ if (!names.contains(name))
+ names.append(name);
+ }
+ }
+ if (!names.contains(baseName))
+ return baseName;
+ for (int suffix = 2; ; ++suffix)
+ {
+ const QString candidate = QStringLiteral("%1 (%2)").arg(baseName).arg(suffix);
+ if (!names.contains(candidate))
+ return candidate;
+ }
+}
+
+void StyleManager::syncCharacterStyle(const QString& name, bool present)
+{
+ if (!m_doc)
+ return;
+
+ SMCharacterStyle* styleItem = item<SMCharacterStyle>();
+ if (!styleItem)
+ return;
+
+ bool changed = false;
+ if (present)
+ {
+ const int index = m_doc->charStyles().find(name);
+ if (index >= 0)
+ changed = styleItem->mergeExternalStyle(m_doc->charStyles()[index]);
+ }
+ else
+ {
+ changed = styleItem->removeExternalStyle(name);
+ if (changed)
+ {
+ const QString key = styleItem->typeName() + SEPARATOR + name;
+ ScrAction* action = m_styleActions.take(key);
+ if (action)
+ {
+ if (m_selectedStyleAction == action)
+ m_selectedStyleAction = nullptr;
+ delete action;
+ }
+ }
+ }
+
+ if (changed)
+ reloadStyleViewPreservingState();
+}
+
+void StyleManager::syncParagraphStyle(const QString& name, bool present)
+{
+ if (!m_doc)
+ return;
+
+ SMParagraphStyle* styleItem = item<SMParagraphStyle>();
+ if (!styleItem)
+ return;
+
+ bool changed = false;
+ if (present)
+ {
+ const int index = m_doc->paragraphStyles().find(name);
+ if (index >= 0)
+ changed = styleItem->mergeExternalStyle(m_doc->paragraphStyles()[index]);
+ }
+ else
+ {
+ changed = styleItem->removeExternalStyle(name);
+ if (changed)
+ {
+ const QString key = styleItem->typeName() + SEPARATOR + name;
+ ScrAction* action = m_styleActions.take(key);
+ if (action)
+ {
+ if (m_selectedStyleAction == action)
+ m_selectedStyleAction = nullptr;
+ delete action;
+ }
+ }
+ }
+
+ if (changed)
+ reloadStyleViewPreservingState();
+}
+
void StyleManager::showAsNewParagraphStyle()
{
setPaletteShown(true);
@@ -1466,6 +1614,39 @@
uniqueLabel->hide();
}
+void StyleManager::reloadStyleViewPreservingState()
+{
+ StyleItem* currentItem = m_item;
+ QSet<QString> dirty;
+ QTreeWidgetItemIterator it(styleView);
+
+ while (*it)
+ {
+ StyleViewItem* item = dynamic_cast<StyleViewItem*>(*it);
+ if (item && !item->isRoot() && item->isDirty())
+ {
+ const QString key = item->rootName() + SEPARATOR + item->text(NAME_COL);
+ dirty.insert(key);
+ }
+ ++it;
+ }
+
+ reloadStyleView(false);
+ m_item = currentItem;
+
+ QTreeWidgetItemIterator restoredIt(styleView, QTreeWidgetItemIterator::Selectable);
+ while (*restoredIt)
+ {
+ StyleViewItem* item = dynamic_cast<StyleViewItem*>(*restoredIt);
+ if (item)
+ {
+ const QString key = item->rootName() + SEPARATOR + item->text(NAME_COL);
+ item->setDirty(dirty.contains(key));
+ }
+ ++restoredIt;
+ }
+}
+
void StyleManager::reloadStyleView(bool loadFromDoc)
{
QTreeWidgetItemIterator it(styleView, QTreeWidgetItemIterator::Selected);
sfs3-create-para-from-selection-v1.03.patch (24,692 bytes)
Index: scribus/scribus.cpp
===================================================================
--- scribus/scribus.cpp (after sfs2)
+++ scribus/scribus.cpp (working copy)
@@ -6437,6 +6437,92 @@
view->DrawNew();
}
+void ScribusMainWindow::createParagraphStyleFromSelection()
+{
+ if (!HaveDoc || !doc || doc->m_Selection->count() != 1)
+ return;
+
+ PageItem* currItem = doc->m_Selection->itemAt(0);
+ if (currItem->isTable() && doc->appMode == modeEditTable)
+ currItem = currItem->asTable()->activeCell().textFrame();
+ if (!currItem || !currItem->itemText.hasSelection())
+ return;
+
+ const int start = currItem->itemText.startOfSelection();
+ const int length = currItem->itemText.endOfSelection() - start;
+ const auto extracted = TextStyleExtractor::extractParagraphStyle(currItem->itemText, start, length);
+ if (!extracted.valid)
+ {
+ ScMessageBox::information(this, tr("Create Paragraph Style"),
+ tr("Select text from a single paragraph and try again."));
+ return;
+ }
+
+ QStringList existingNames;
+ QList<QPair<QString, QString>> parentStyles;
+ const QList<int> sortedStyles = doc->getSortedStyleList();
+ for (int index : sortedStyles)
+ {
+ const ParagraphStyle& paragraphStyle = doc->paragraphStyles()[index];
+ if (!paragraphStyle.hasName())
+ continue;
+ existingNames.append(paragraphStyle.name());
+ parentStyles.append(qMakePair(paragraphStyle.displayName(), paragraphStyle.name()));
+ }
+
+ if (m_styleManager)
+ {
+ const QStringList cachedNames = m_styleManager->cachedParagraphStyleNames();
+ for (const QString& name : cachedNames)
+ {
+ if (!existingNames.contains(name))
+ existingNames.append(name);
+ }
+ }
+
+ QString suggestedParent;
+ if (!extracted.parent.isEmpty() && doc->paragraphStyles().contains(extracted.parent))
+ suggestedParent = extracted.parent;
+ const QString suggestedName = m_styleManager
+ ? m_styleManager->uniqueParagraphStyleName(tr("New Paragraph Style"))
+ : doc->paragraphStyles().getUniqueCopyName(tr("New Paragraph Style"));
+
+ CreateStyleFromSelectionDialog dialog(tr("Create Paragraph Style"),
+ suggestedName, existingNames, parentStyles,
+ suggestedParent, this);
+ if (dialog.exec() != QDialog::Accepted)
+ return;
+
+ const QString parentName = dialog.parentStyle();
+ const ParagraphStyle* baseStyle = nullptr;
+ if (!parentName.isEmpty())
+ {
+ const int parentIndex = doc->paragraphStyles().find(parentName);
+ if (parentIndex >= 0)
+ baseStyle = &doc->paragraphStyles()[parentIndex];
+ }
+
+ ParagraphStyle newStyle = TextStyleExtractor::paragraphStyleDelta(extracted.effectiveStyle, baseStyle);
+ newStyle.setName(dialog.styleName());
+ newStyle.setDefaultStyle(false);
+ newStyle.setParent(parentName);
+
+ if (!doc->itemSelection_CreateNamedParagraphStyle(newStyle, extracted.commonCharStyle,
+ extracted.paragraphStart,
+ extracted.paragraphEnd - extracted.paragraphStart))
+ {
+ ScMessageBox::warning(this, CommonStrings::trWarning,
+ tr("The paragraph style could not be created."));
+ return;
+ }
+
+ if (m_styleManager)
+ m_styleManager->syncParagraphStyle(newStyle.name(), true);
+ requestUpdate(reqParaStylesUpdate | reqStyleComboDocUpdate);
+ setTBvals(currItem);
+ view->DrawNew();
+}
+
void ScribusMainWindow::setAlignmentValue(int i)
{
contentPalette->updateTextAlignment(i);
Index: scribus/scribus.h
===================================================================
--- scribus/scribus.h (after sfs2)
+++ scribus/scribus.h (working copy)
@@ -522,6 +522,7 @@
void setNewParStyle(const QString& name);
void setNewCharStyle(const QString& name);
void createCharStyleFromSelection();
+ void createParagraphStyleFromSelection();
void setAlignmentValue(int i);
void setDirectionValue(int i);
void editItemsFromOutlines(PageItem *ite);
Index: scribus/scribusdoc.cpp
===================================================================
--- scribus/scribusdoc.cpp (after sfs2)
+++ scribus/scribusdoc.cpp (working copy)
@@ -1519,6 +1519,30 @@
replaceNamedResources(newNames);
}
+bool ScribusDoc::addParagraphStyle(const ParagraphStyle& style)
+{
+ if (!style.hasName() || m_docParagraphStyles.contains(style.name()))
+ return false;
+
+ ParagraphStyle newStyle(style);
+ newStyle.setDefaultStyle(false);
+ m_docParagraphStyles.create(newStyle);
+ m_docParagraphStyles.invalidate();
+ flag_Renumber = true;
+ flag_NumUpdateRequest = true;
+
+ if (UndoManager::undoEnabled())
+ {
+ auto* state = new ScOldNewState<ParagraphStyle>(tr("Create Paragraph Style"), style.name());
+ state->set("CREATE_PARASTYLE");
+ state->setStates(ParagraphStyle(), newStyle);
+ m_undoManager->action(this, state);
+ }
+
+ changed();
+ return true;
+}
+
void ScribusDoc::redefineStyles(const StyleSet<ParagraphStyle>& newStyles, bool removeUnused)
{
m_docParagraphStyles.redefine(newStyles, false);
@@ -2006,6 +2030,8 @@
}
else if (ss->contains("NSTYLE"))
restoreNoteStyle(ss, isUndo);
+ else if (ss->contains("CREATE_PARASTYLE"))
+ restoreParagraphStyleCreation(ss, isUndo);
else if (ss->contains("CREATE_CHARSTYLE"))
restoreCharStyleCreation(ss, isUndo);
else if (ss->contains("DELETE_NOTE"))
@@ -2538,6 +2564,38 @@
scMW()->nsEditor->updateNSList();
}
+void ScribusDoc::restoreParagraphStyleCreation(SimpleState* ss, bool isUndo)
+{
+ const auto* state = dynamic_cast<ScOldNewState<ParagraphStyle>*>(ss);
+ if (!state)
+ return;
+
+ const ParagraphStyle& style = state->getNewState();
+ if (isUndo)
+ {
+ const int index = m_docParagraphStyles.find(style.name());
+ if (index >= 0 && !m_docParagraphStyles.isDefault(m_docParagraphStyles[index]))
+ {
+ ParagraphStyle* oldStyle = &m_docParagraphStyles[index];
+ m_docParagraphStyles.remove(index);
+ delete oldStyle;
+ }
+ }
+ else if (!m_docParagraphStyles.contains(style.name()))
+ m_docParagraphStyles.create(style);
+
+ m_docParagraphStyles.invalidate();
+ flag_Renumber = true;
+ flag_NumUpdateRequest = true;
+ changed();
+ if (ScCore->usingGUI())
+ {
+ if (scMW()->styleMgr())
+ scMW()->styleMgr()->syncParagraphStyle(style.name(), !isUndo);
+ scMW()->requestUpdate(reqParaStylesUpdate | reqStyleComboDocUpdate);
+ }
+}
+
void ScribusDoc::restoreCharStyleCreation(SimpleState* ss, bool isUndo)
{
const auto* state = dynamic_cast<ScOldNewState<CharStyle>*>(ss);
@@ -8251,6 +8309,122 @@
itemSelection_ApplyCharStyle(newStyle, customSelection, "NAMED_STYLE");
}
+bool ScribusDoc::itemSelection_CreateNamedParagraphStyle(const ParagraphStyle& style, const CharStyle& cleanupStyle,
+ int paragraphStart, int paragraphLength,
+ Selection* customSelection)
+{
+ Selection* itemSelection = (customSelection != nullptr) ? customSelection : m_Selection;
+ if (!itemSelection || itemSelection->count() != 1 || !style.hasName())
+ return false;
+
+ PageItem* currItem = itemSelection->itemAt(0);
+ if (currItem->isTable() && appMode == modeEditTable)
+ currItem = currItem->asTable()->activeCell().textFrame();
+ if (!currItem || paragraphStart < 0 || paragraphLength < 0
+ || paragraphStart > currItem->itemText.length()
+ || paragraphLength > currItem->itemText.length() - paragraphStart)
+ return false;
+
+ UndoTransaction transaction;
+ if (UndoManager::undoEnabled())
+ {
+ transaction = m_undoManager->beginTransaction(currItem->getUName(), currItem->getUPixmap(),
+ tr("Create Paragraph Style"), style.name(), Um::IFont);
+ }
+
+ if (!addParagraphStyle(style))
+ return false;
+
+ ParagraphStyle namedStyle;
+ namedStyle.setParent(style.name());
+ if (UndoManager::undoEnabled())
+ {
+ auto* state = new ScOldNewState<ParagraphStyle>(Um::SetStyle);
+ state->set("APPLY_PARASTYLE");
+ state->set("POS", paragraphStart);
+ state->setStates(currItem->itemText.paragraphStyle(paragraphStart), namedStyle);
+ m_undoManager->action(currItem, state);
+ }
+ currItem->itemText.applyStyle(paragraphStart, namedStyle, false);
+
+ const int paragraphEnd = paragraphStart + paragraphLength;
+ int runStart = -1;
+ CharStyle runBefore;
+ CharStyle runAfter;
+ int runEnd = -1;
+ auto commitRun = [&]()
+ {
+ if (runStart < 0 || runEnd <= runStart)
+ return;
+ if (UndoManager::undoEnabled())
+ {
+ auto* state = new ScOldNewState<CharStyle>(Um::ApplyTextStyle);
+ state->set("SET_CHARSTYLE");
+ state->set("START", runStart);
+ state->set("LENGTH", runEnd - runStart);
+ state->setStates(runBefore, runAfter);
+ m_undoManager->action(currItem, state);
+ }
+ currItem->itemText.setCharStyle(runStart, runEnd - runStart, runAfter);
+ runStart = -1;
+ runEnd = -1;
+ };
+
+ for (int pos = paragraphStart; pos < paragraphEnd; ++pos)
+ {
+ CharStyle before = currItem->itemText.charStyle(pos);
+ CharStyle after(before);
+#define ATTRDEF(attr_TYPE, attr_GETTER, attr_NAME, attr_DEFAULT, attr_BREAKSHAPING) \
+ if (!cleanupStyle.isInh##attr_NAME() && !before.isInh##attr_NAME() \
+ && isequiv(before.attr_GETTER(), cleanupStyle.attr_GETTER())) \
+ { \
+ const attr_TYPE oldValue = before.attr_GETTER(); \
+ after.reset##attr_NAME(); \
+ if (!isequiv(after.attr_GETTER(), oldValue)) \
+ after.set##attr_NAME(oldValue); \
+ }
+#include "styles/charstyle.attrdefs.cxx"
+#undef ATTRDEF
+ after.updateFeatures();
+ if (before.equiv(after))
+ {
+ commitRun();
+ continue;
+ }
+
+ if (runStart >= 0 && before.equiv(runBefore) && after.equiv(runAfter))
+ {
+ runEnd = pos + 1;
+ continue;
+ }
+
+ commitRun();
+ runStart = pos;
+ runEnd = pos + 1;
+ runBefore = before;
+ runAfter = after;
+ }
+ commitRun();
+
+ currItem->invalid = true;
+ if (currItem->isPathText())
+ currItem->updatePolyClip();
+ if (currItem->isNoteFrame())
+ currItem->asNoteFrame()->updateNotesText();
+ else if (currItem->isTextFrame() && currItem->asTextFrame()->hasNoteFrame(nullptr, true))
+ setNotesChanged(true);
+ currItem->invalidateLayout();
+ flag_Renumber = true;
+ flag_NumUpdateRequest = true;
+
+ if (transaction)
+ transaction.commit();
+ changed();
+ regionsChanged()->update(QRectF());
+ changedPagePreview();
+ return true;
+}
+
bool ScribusDoc::itemSelection_CreateNamedCharStyle(const CharStyle& style, Selection* customSelection)
{
Selection* itemSelection = (customSelection != nullptr) ? customSelection : m_Selection;
Index: scribus/scribusdoc.h
===================================================================
--- scribus/scribusdoc.h (after sfs2)
+++ scribus/scribusdoc.h (working copy)
@@ -683,6 +683,7 @@
QList<int> getSortedTableStyleList() const;
QList<int> getSortedCellStyleList() const;
+ bool addParagraphStyle(const ParagraphStyle& style);
void redefineStyles(const StyleSet<ParagraphStyle>& newStyles, bool removeUnused = false);
/**
* @brief Remove any reference to old styles and replace with new name. This needs to be
@@ -861,6 +862,7 @@
*/
void restoreMarks(UndoState* ss, bool isUndo);
void restoreNoteStyle(SimpleState* ss, bool isUndo);
+ void restoreParagraphStyleCreation(SimpleState* ss, bool isUndo);
void restoreCharStyleCreation(SimpleState* ss, bool isUndo);
void restoreDeleteNote(UndoState* ss, bool isUndo);
@@ -1182,6 +1184,9 @@
void itemSelection_EraseCharStyle(Selection* customSelection = nullptr);
void itemSelection_SetNamedParagraphStyle(const QString & name, Selection* customSelection = nullptr);
void itemSelection_SetNamedCharStyle(const QString & name, Selection* customSelection = nullptr);
+ bool itemSelection_CreateNamedParagraphStyle(const ParagraphStyle& style, const CharStyle& cleanupStyle,
+ int paragraphStart, int paragraphLength,
+ Selection* customSelection = nullptr);
bool itemSelection_CreateNamedCharStyle(const CharStyle& style, Selection* customSelection = nullptr);
void itemSelection_SetNamedLineStyle(const QString & name, Selection* customSelection = nullptr);
void itemSelection_SetNamedCellStyle(const QString & name, Selection* customSelection = nullptr);
Index: scribus/tests/testStoryText.cpp
===================================================================
--- scribus/tests/testStoryText.cpp (after sfs2)
+++ scribus/tests/testStoryText.cpp (working copy)
@@ -167,6 +167,26 @@
QVERIFY(!extracted.commonStyle.isInhFontSize());
QCOMPARE(extracted.commonStyle.fontSize(), 120.0);
QVERIFY(extracted.commonStyle.isInhFillColor());
+ QVERIFY(!extracted.commonDirectStyle.isInhFontSize());
+ QCOMPARE(extracted.commonDirectStyle.fontSize(), 120.0);
+ QVERIFY(extracted.commonDirectStyle.isInhFillColor());
+}
+
+void TestStoryText::extractInheritedCharStyleNotDirect()
+{
+ StoryText story;
+ story.insertChars(0, QString("Inherited"));
+
+ ParagraphStyle paragraph;
+ paragraph.charStyle().setFontSize(105.0);
+ story.applyStyle(0, paragraph);
+
+ auto extracted = TextStyleExtractor::extractCharStyle(story, 0, story.length());
+ QVERIFY(extracted.valid);
+ QVERIFY(extracted.uniform);
+ QVERIFY(!extracted.commonStyle.isInhFontSize());
+ QCOMPARE(extracted.commonStyle.fontSize(), 105.0);
+ QVERIFY(extracted.commonDirectStyle.isInhFontSize());
}
void TestStoryText::extractCharStyleDelta()
@@ -187,3 +207,72 @@
QVERIFY(delta.isInhFillColor());
QVERIFY(delta.isInhTracking());
}
+
+void TestStoryText::extractParagraphStyle()
+{
+ StoryText story;
+ story.insertChars(0, QString("Paragraph"));
+
+ ParagraphStyle paragraph;
+ paragraph.setAlignment(ParagraphStyle::Centered);
+ paragraph.charStyle().setFontSize(100.0);
+ paragraph.charStyle().setFillColor("Black");
+ story.applyStyle(0, paragraph);
+
+ CharStyle wholeParagraph;
+ wholeParagraph.setFontSize(110.0);
+ wholeParagraph.setFillColor("Blue");
+ story.applyCharStyle(0, story.length(), wholeParagraph);
+
+ CharStyle localException;
+ localException.setTracking(50.0);
+ story.applyCharStyle(3, 1, localException);
+
+ auto extracted = TextStyleExtractor::extractParagraphStyle(story, 2, 3);
+ QVERIFY(extracted.valid);
+ QCOMPARE(extracted.paragraphStart, 0);
+ QCOMPARE(extracted.paragraphEnd, story.length());
+ QCOMPARE(extracted.effectiveStyle.alignment(), ParagraphStyle::Centered);
+ QCOMPARE(extracted.effectiveStyle.charStyle().fontSize(), 110.0);
+ QCOMPARE(extracted.effectiveStyle.charStyle().fillColor(), QString("Blue"));
+ QVERIFY(!extracted.commonCharStyle.isInhFontSize());
+ QVERIFY(!extracted.commonCharStyle.isInhFillColor());
+ QVERIFY(extracted.commonCharStyle.isInhTracking());
+}
+
+void TestStoryText::extractParagraphStyleDelta()
+{
+ ParagraphStyle base;
+ base.setAlignment(ParagraphStyle::LeftAligned);
+ base.setGapAfter(0.0);
+ base.charStyle().setFontSize(100.0);
+ base.charStyle().setFillColor("Black");
+
+ ParagraphStyle effective;
+ effective.setAlignment(ParagraphStyle::Centered);
+ effective.setGapAfter(0.0);
+ effective.charStyle().setFontSize(110.0);
+ effective.charStyle().setFillColor("Black");
+
+ ParagraphStyle delta = TextStyleExtractor::paragraphStyleDelta(effective, &base);
+ QVERIFY(!delta.isInhAlignment());
+ QCOMPARE(delta.alignment(), ParagraphStyle::Centered);
+ QVERIFY(delta.isInhGapAfter());
+ QVERIFY(!delta.charStyle().isInhFontSize());
+ QCOMPARE(delta.charStyle().fontSize(), 110.0);
+ QVERIFY(delta.charStyle().isInhFillColor());
+}
+
+void TestStoryText::rejectMultiParagraphStyleExtraction()
+{
+ StoryText story;
+ story.insertChars(0, QString("One") + SpecialChars::PARSEP + QString("Two"));
+
+ auto firstParagraph = TextStyleExtractor::extractParagraphStyle(story, 0, 4);
+ QVERIFY(firstParagraph.valid);
+ QCOMPARE(firstParagraph.paragraphStart, 0);
+ QCOMPARE(firstParagraph.paragraphEnd, 3);
+
+ auto extracted = TextStyleExtractor::extractParagraphStyle(story, 0, story.length());
+ QVERIFY(!extracted.valid);
+}
Index: scribus/tests/testStoryText.h
===================================================================
--- scribus/tests/testStoryText.h (after sfs2)
+++ scribus/tests/testStoryText.h (working copy)
@@ -29,5 +29,9 @@
void removeCharStyle();
void extractUniformCharStyle();
void extractMixedCharStyle();
+ void extractInheritedCharStyleNotDirect();
void extractCharStyleDelta();
+ void extractParagraphStyle();
+ void extractParagraphStyleDelta();
+ void rejectMultiParagraphStyleExtraction();
};
Index: scribus/text/textstyleextractor.cpp
===================================================================
--- scribus/text/textstyleextractor.cpp (after sfs2)
+++ scribus/text/textstyleextractor.cpp (working copy)
@@ -33,6 +33,27 @@
return result;
}
+ParagraphStyle materializedParagraphStyle(const ParagraphStyle& source)
+{
+ ParagraphStyle result;
+#define ATTRDEF(attr_TYPE, attr_GETTER, attr_NAME, attr_DEFAULT) \
+ result.set##attr_NAME(source.attr_GETTER());
+#include "styles/paragraphstyle.attrdefs.cxx"
+#undef ATTRDEF
+ result.charStyle() = materializedCharStyle(source.charStyle());
+ return result;
+}
+
+void applyCommonCharStyle(CharStyle& target, const CharStyle& commonStyle)
+{
+#define ATTRDEF(attr_TYPE, attr_GETTER, attr_NAME, attr_DEFAULT, attr_BREAKSHAPING) \
+ if (!commonStyle.isInh##attr_NAME()) \
+ target.set##attr_NAME(commonStyle.attr_GETTER());
+#include "styles/charstyle.attrdefs.cxx"
+#undef ATTRDEF
+ target.updateFeatures();
+}
+
} // namespace
namespace TextStyleExtractor
@@ -49,7 +70,8 @@
QString parentName;
#define ATTRDEF(attr_TYPE, attr_GETTER, attr_NAME, attr_DEFAULT, attr_BREAKSHAPING) \
- bool common##attr_NAME = true;
+ bool common##attr_NAME = true; \
+ bool commonDirect##attr_NAME = true;
#include "styles/charstyle.attrdefs.cxx"
#undef ATTRDEF
@@ -62,6 +84,12 @@
if (first)
{
result.effectiveStyle = materializedCharStyle(current);
+#define ATTRDEF(attr_TYPE, attr_GETTER, attr_NAME, attr_DEFAULT, attr_BREAKSHAPING) \
+ commonDirect##attr_NAME = !current.isInh##attr_NAME(); \
+ if (commonDirect##attr_NAME) \
+ result.commonDirectStyle.set##attr_NAME(current.attr_GETTER());
+#include "styles/charstyle.attrdefs.cxx"
+#undef ATTRDEF
parentName = current.parent();
first = false;
}
@@ -69,7 +97,10 @@
{
#define ATTRDEF(attr_TYPE, attr_GETTER, attr_NAME, attr_DEFAULT, attr_BREAKSHAPING) \
if (common##attr_NAME && !isequiv(result.effectiveStyle.attr_GETTER(), current.attr_GETTER())) \
- common##attr_NAME = false;
+ common##attr_NAME = false; \
+ if (commonDirect##attr_NAME \
+ && (current.isInh##attr_NAME() || !isequiv(result.commonDirectStyle.attr_GETTER(), current.attr_GETTER()))) \
+ commonDirect##attr_NAME = false;
#include "styles/charstyle.attrdefs.cxx"
#undef ATTRDEF
if (commonParent && current.parent() != parentName)
@@ -91,6 +122,12 @@
#include "styles/charstyle.attrdefs.cxx"
#undef ATTRDEF
result.commonStyle.updateFeatures();
+#define ATTRDEF(attr_TYPE, attr_GETTER, attr_NAME, attr_DEFAULT, attr_BREAKSHAPING) \
+ if (!commonDirect##attr_NAME) \
+ result.commonDirectStyle.reset##attr_NAME();
+#include "styles/charstyle.attrdefs.cxx"
+#undef ATTRDEF
+ result.commonDirectStyle.updateFeatures();
result.hasCommonParent = commonParent;
if (commonParent)
@@ -110,4 +147,53 @@
return result;
}
+ParagraphStyleSelection extractParagraphStyle(const StoryText& story, int pos, int len)
+{
+ ParagraphStyleSelection result;
+ if (pos < 0 || len <= 0 || pos > story.length() || len > story.length() - pos)
+ return result;
+
+ const int lastPos = pos + len - 1;
+ const uint paragraphIndex = story.nrOfParagraph(pos);
+ if (story.nrOfParagraph(lastPos) != paragraphIndex)
+ return result;
+
+ result.paragraphStart = story.startOfParagraph(paragraphIndex);
+ result.paragraphEnd = story.endOfParagraph(paragraphIndex);
+ const ParagraphStyle& paragraphStyle = story.paragraphStyle(result.paragraphStart);
+ result.parent = paragraphStyle.parent();
+ result.effectiveStyle = materializedParagraphStyle(paragraphStyle);
+
+ if (result.paragraphEnd > result.paragraphStart)
+ {
+ const auto chars = extractCharStyle(story, result.paragraphStart, result.paragraphEnd - result.paragraphStart);
+ if (!chars.valid)
+ return result;
+ result.characterCount = chars.characterCount;
+ result.commonCharStyle = chars.commonDirectStyle;
+ applyCommonCharStyle(result.effectiveStyle.charStyle(), result.commonCharStyle);
+ }
+ else
+ {
+ result.commonCharStyle = materializedCharStyle(paragraphStyle.charStyle());
+ }
+
+ result.valid = true;
+ return result;
+}
+
+ParagraphStyle paragraphStyleDelta(const ParagraphStyle& effectiveStyle, const ParagraphStyle* baseStyle)
+{
+ ParagraphStyle result;
+#define ATTRDEF(attr_TYPE, attr_GETTER, attr_NAME, attr_DEFAULT) \
+ if (!baseStyle || !isequiv(effectiveStyle.attr_GETTER(), baseStyle->attr_GETTER())) \
+ result.set##attr_NAME(effectiveStyle.attr_GETTER());
+#include "styles/paragraphstyle.attrdefs.cxx"
+#undef ATTRDEF
+
+ const CharStyle* baseCharStyle = baseStyle ? &baseStyle->charStyle() : nullptr;
+ result.charStyle() = charStyleDelta(effectiveStyle.charStyle(), baseCharStyle);
+ return result;
+}
+
} // namespace TextStyleExtractor
Index: scribus/text/textstyleextractor.h
===================================================================
--- scribus/text/textstyleextractor.h (after sfs2)
+++ scribus/text/textstyleextractor.h (working copy)
@@ -18,6 +18,7 @@
#include "scribusapi.h"
#include "styles/charstyle.h"
+#include "styles/paragraphstyle.h"
class StoryText;
@@ -33,6 +34,18 @@
QString commonParent;
CharStyle effectiveStyle;
CharStyle commonStyle;
+ CharStyle commonDirectStyle;
+};
+
+struct SCRIBUS_API ParagraphStyleSelection
+{
+ bool valid { false };
+ int paragraphStart { 0 };
+ int paragraphEnd { 0 };
+ int characterCount { 0 };
+ QString parent;
+ ParagraphStyle effectiveStyle;
+ CharStyle commonCharStyle;
};
/**
@@ -40,8 +53,10 @@
*
* Paragraph separators are ignored. effectiveStyle is the fully materialised
* style of the first analysed character. commonStyle contains only properties
- * whose effective values are common to every analysed character. uniform is
- * true when every effective character-style property is common to the range.
+ * whose effective values are common to every analysed character.
+ * commonDirectStyle contains only direct properties explicitly present with
+ * the same value on every analysed character. uniform is true when every
+ * effective character-style property is common to the range.
*/
SCRIBUS_API CharStyleSelection extractCharStyle(const StoryText& story, int pos, int len);
@@ -52,6 +67,25 @@
*/
SCRIBUS_API CharStyle charStyleDelta(const CharStyle& effectiveStyle, const CharStyle* baseStyle = nullptr);
+/**
+ * Analyse the single paragraph touched by a selection. The entire paragraph is
+ * inspected even when only part of it is selected. Selections spanning more
+ * than one paragraph are rejected. effectiveStyle combines the paragraph's
+ * effective paragraph/character defaults with direct character properties
+ * explicitly common to every character in the paragraph. commonCharStyle is
+ * the safe cleanup mask for those promoted direct properties.
+ */
+SCRIBUS_API ParagraphStyleSelection extractParagraphStyle(const StoryText& story, int pos, int len);
+
+/**
+ * Return the explicit paragraph and embedded character properties required to
+ * reproduce effectiveStyle against baseStyle. If baseStyle is null, all
+ * effective properties are materialised. Name, parent and shortcut are left
+ * unset for the caller.
+ */
+SCRIBUS_API ParagraphStyle paragraphStyleDelta(const ParagraphStyle& effectiveStyle,
+ const ParagraphStyle* baseStyle = nullptr);
+
} // namespace TextStyleExtractor
#endif
Index: scribus/ui/contextmenu.cpp
===================================================================
--- scribus/ui/contextmenu.cpp (after sfs2)
+++ scribus/ui/contextmenu.cpp (working copy)
@@ -357,6 +357,9 @@
QAction* charStyleAction = styleFromSelectionMenu->addAction(
iconManager.loadIcon("character-style"), tr("Character Style..."));
connect(charStyleAction, SIGNAL(triggered()), m_ScMW, SLOT(createCharStyleFromSelection()));
+ QAction* paragraphStyleAction = styleFromSelectionMenu->addAction(
+ iconManager.loadIcon("paragraph-style"), tr("Paragraph Style..."));
+ connect(paragraphStyleAction, SIGNAL(triggered()), m_ScMW, SLOT(createParagraphStyleFromSelection()));
}
}
| ||||
| Patch | Yes | ||||
| Date Modified | Username | Field | Change |
|---|---|---|---|
| 2026-09-24 05:01 | buttar | New Issue | |
| 2026-09-24 05:01 | buttar | Tag Attached: #please_test | |
| 2026-09-24 05:01 | buttar | File Added: sfs1-selection-style-extractor-v1.0.patch | |
| 2026-09-24 05:01 | buttar | File Added: sfs2-create-char-style-from-selection-v1.05.patch | |
| 2026-09-24 05:01 | buttar | File Added: sfs3-create-para-from-selection-v1.03.patch | |
| 2026-09-25 12:56 | buttar | Note Added: 0054496 |