View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0010107 | Scribus | Text Frames / Story Editor | public | 2011-07-07 17:56 | 2026-09-03 05:18 |
| Reporter | ale | Assigned To | |||
| Priority | normal | Severity | feature | Reproducibility | N/A |
| Status | new | Resolution | open | ||
| Product Version | 1.5.0svn | ||||
| Summary | 0010107: Allow style matching when importing ODT files | ||||
| Description | we should be able to manually match the styles from an ODT file to the styles already in the .sla. | ||||
| Tags | #patch_to_be_reviewed, #please_test, mockup, ODF, styles | ||||
| Attached Files | |||||
| Patch | |||||
|
|
i've attached a mockup showing how it could be done with colors... |
|
|
This is very interesting and useful. Here is my attempt at the same. It is 3 patches in sequence (osm = odt style matching). ## `osm1-gettext2-cancel-safety-v1.0.patch` - Makes the generic GetText2 import path cancel-safe in replace mode. - Removes the premature story clear from `gtgettext.cpp`; importers now replace text only when the import actually commits. - Prevents importer-side dialogues/cancellation from leaving the target frame empty. - Minimal prerequisite for interactive ODT import workflows; no API/ABI changes. ## `osm2-style-mapping-engine-v1.1.patch` - Adds the ODT style-mapping backend to `odt2im`. - Splits import into style discovery → mapping → style materialisation → text import. - Supports paragraph and character style mappings, including mapped ancestors and inherited child-style deltas. - Makes mapped Scribus styles authoritative: mapped ODT style definitions are not recreated or reapplied as local overrides. - Preserves lower-level ODT formatting differences where appropriate. - Adds collision-safe import of unmapped ODT styles, including unique renaming and corrected parent references. - Supports many-to-one mappings, prefixing, and ODT/FODT parity without changing the generic GetText2 ABI. ## `osm3-style-matching-dialog-v1.25.patch` - Adds the Match ODT Styles UI before ODT style materialisation. - Provides per-style mapping for paragraph and character styles, with Import as New Style, Match by Name, optional case-insensitive matching, and bulk mapping of remaining styles. - Adds explicit mapping-state handling so manual, name-matched, and bulk assignments interact predictably. - Adds per-tab filtering, Mapped only / Unmapped only, and reset support for larger style sets. - Surfaces style-name collisions and ambiguous case-insensitive matches without silently guessing. - Adds keyboard/accessibility handling, controlled TAB order, mnemonics, and protection against accidental mouse-wheel changes in mapping combos. - Keeps the dialogue compact and tab-scoped while leaving the mapping engine itself in `osm2`. osm1-gettext2-cancel-safety-v1.0.patch (476 bytes)
Index: scribus/gtgettext.cpp
===================================================================
--- scribus/gtgettext.cpp (revision 27785)
+++ scribus/gtgettext.cpp (working copy)
@@ -255,8 +255,6 @@
fp_GetText2 = (gt2ptr) PluginManager::resolveSym(gtplugin,"GetText2");
if (fp_GetText2)
{
- if (!append)
- importItem->itemText.clear();
// Execute the importer's "GetText2" method.
(*fp_GetText2)(filePath, encoding, textOnly, prefix, append, importItem);
}
osm2-style-mapping-engine-v1.1.patch (23,594 bytes)
Index: scribus/plugins/gettext/odt2im/importodt.h
===================================================================
--- scribus/plugins/gettext/odt2im/importodt.h (revision 27810)
+++ scribus/plugins/gettext/odt2im/importodt.h (working copy)
@@ -22,8 +22,10 @@
#include <QDomDocument>
#include <QDomElement>
#include <QHash>
+#include <QList>
#include <QStack>
#include <QString>
+#include <QStringList>
class ScZipHandler;
@@ -69,12 +71,22 @@
class ODTIm
{
public:
+ struct StyleInfo
+ {
+ QString sourceName;
+ QString displayName;
+ QString family;
+ };
+
ODTIm(PageItem *textItem, bool prefix, bool append);
~ODTIm() = default;
ODTIm(ODTIm&) = delete;
ODTIm& operator=(const ODTIm&) = delete;
+ bool collectStyles(const QString& fileName);
+ QList<StyleInfo> namedStyles() const;
+ void setStyleMappings(const QHash<QString, QString>& paragraphMappings, const QHash<QString, QString>& characterMappings);
bool importFile(const QString& fileName, bool textOnly);
private:
@@ -127,6 +139,7 @@
bool parseStyleSheets(const QString& designMap);
bool parseStyleSheetsXML(const QDomDocument &designMapDom);
void parseStyles(const QDomElement &sp, const QString& type);
+ void materializeNamedStyles();
bool parseDocReference(const QString& designMap);
bool parseDocReferenceXML(const QDomDocument &designMapDom);
void parseTextSpan(const QDomElement &elem, PageItem* item, const ParagraphStyle &tmpStyle, const CharStyle &tmpCStyle, const ObjStyleODT& tmpOStyle, int &posC);
@@ -138,7 +151,11 @@
void applyCharacterStyle(CharStyle &tmpCStyle, const ObjStyleODT &oStyle);
void applyParagraphStyle(ParagraphStyle &tmpStyle, const ObjStyleODT &oStyle);
void resolveStyle(ObjStyleODT &tmpOStyle, const QString& pAttrs);
- bool findNamedParagraphStyle(const QString& styleName, QString& sourceStyleName, QString& importedStyleName) const;
+ bool findNamedParagraphStyle(const QString& styleName, QString& sourceStyleName, QString& importedStyleName);
+ bool findNamedCharacterStyle(const QString& styleName, QString& sourceStyleName, QString& importedStyleName);
+ QString importedStyleName(const QString& sourceStyleName, const QString& family);
+ bool hasMappedAncestor(const QString& styleName, const QHash<QString, QString>& mappings) const;
+ bool applyMappedCharacterStyle(const QString& styleName, CharStyle& style, const ObjStyleODT& resolvedStyle);
double parseUnit(const QString &unit) const;
QString parseColor( const QString &s );
QString constructFontName(const QString& fontBaseName, const QString& fontStyle);
@@ -152,6 +169,11 @@
QHash<QString, QString> map_ID_to_Name;
QHash<QString, QString> m_fontMap;
QHash<QString, DrawStyle> m_Styles;
+ QStringList m_namedStyleOrder;
+ QHash<QString, QString> m_parStyleMappings;
+ QHash<QString, QString> m_charStyleMappings;
+ QHash<QString, QString> m_parImportedStyleNames;
+ QHash<QString, QString> m_charImportedStyleNames;
QStack<QString> m_textStylesStack;
DrawStyle parDefaultStyle;
DrawStyle txtDefaultStyle;
Index: scribus/plugins/gettext/odt2im/importodt.cpp
===================================================================
--- scribus/plugins/gettext/odt2im/importodt.cpp (revision 27810)
+++ scribus/plugins/gettext/odt2im/importodt.cpp (working copy)
@@ -15,6 +15,7 @@
#include <QApplication>
#include <QByteArray>
#include <QDebug>
+#include <QRegularExpression>
#include <QScopedPointer>
#include "scribusdoc.h"
@@ -55,8 +56,86 @@
// Nothing else to do
}
+bool ODTIm::collectStyles(const QString& fileName)
+{
+ m_fontMap.clear();
+ m_Styles.clear();
+ m_namedStyleOrder.clear();
+ m_parImportedStyleNames.clear();
+ m_charImportedStyleNames.clear();
+ parDefaultStyle = DrawStyle();
+ txtDefaultStyle = DrawStyle();
+
+ QFileInfo fi(fileName);
+ if (fi.suffix().compare("fodt", Qt::CaseInsensitive) == 0)
+ {
+ QByteArray xmlData;
+ QDomDocument designMapDom;
+ if (!loadRawText(fileName, xmlData))
+ return false;
+#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
+ if (!designMapDom.setContent(xmlData, QDomDocument::ParseOption::PreserveSpacingOnlyNodes))
+ return false;
+#else
+ if (!designMapDom.setContent(xmlData))
+ return false;
+#endif
+ return parseStyleSheetsXML(designMapDom);
+ }
+
+ m_zip = std::make_unique<ScZipHandler>();
+ if (!m_zip || !m_zip->open(fileName))
+ return false;
+ bool result = true;
+ if (m_zip->contains("styles.xml"))
+ result = parseStyleSheets("styles.xml");
+ if (result && m_zip->contains("content.xml"))
+ result = parseStyleSheets("content.xml");
+ m_zip->close();
+ m_zip.reset();
+ return result;
+}
+
+QList<ODTIm::StyleInfo> ODTIm::namedStyles() const
+{
+ QList<StyleInfo> result;
+ for (const QString& sourceName : m_namedStyleOrder)
+ {
+ auto it = m_Styles.constFind(sourceName);
+ if (it == m_Styles.constEnd())
+ continue;
+ const DrawStyle& style = it.value();
+ if (!style.styleOrigin.valid || style.styleOrigin.value != "styles" || !style.styleType.valid)
+ continue;
+ if (style.styleType.value != "paragraph" && style.styleType.value != "text")
+ continue;
+ StyleInfo info;
+ info.sourceName = sourceName;
+ info.displayName = style.displayName.valid ? style.displayName.value : sourceName;
+ info.family = style.styleType.value;
+ result.append(info);
+ }
+ return result;
+}
+
+void ODTIm::setStyleMappings(const QHash<QString, QString>& paragraphMappings, const QHash<QString, QString>& characterMappings)
+{
+ m_parStyleMappings = paragraphMappings;
+ m_charStyleMappings = characterMappings;
+}
+
bool ODTIm::importFile(const QString& fileName, bool textOnly)
{
+ if (!textOnly)
+ {
+ m_fontMap.clear();
+ m_Styles.clear();
+ m_namedStyleOrder.clear();
+ m_parImportedStyleNames.clear();
+ m_charImportedStyleNames.clear();
+ parDefaultStyle = DrawStyle();
+ txtDefaultStyle = DrawStyle();
+ }
QFileInfo fi(fileName);
QString ext = fi.suffix().toLower();
if (ext == "fodt")
@@ -584,62 +663,163 @@
}
currStyle.displayName = AttributeValue(spd.attribute("style:display-name", ""));
m_Styles.insert(spd.attribute("style:name"), currStyle);
- if (type == "styles")
+ const QString sourceName = spd.attribute("style:name");
+ if (type == "styles" && !m_namedStyleOrder.contains(sourceName))
+ m_namedStyleOrder.append(sourceName);
+ }
+ }
+}
+
+
+QString ODTIm::importedStyleName(const QString& sourceStyleName, const QString& family)
+{
+ const bool paragraphFamily = (family == "paragraph");
+ QHash<QString, QString>& importedNames = paragraphFamily ? m_parImportedStyleNames : m_charImportedStyleNames;
+ auto nameIt = importedNames.constFind(sourceStyleName);
+ if (nameIt != importedNames.constEnd())
+ return nameIt.value();
+
+ auto styleIt = m_Styles.constFind(sourceStyleName);
+ QString styleName = sourceStyleName;
+ if (styleIt != m_Styles.constEnd() && styleIt->displayName.valid)
+ styleName = styleIt->displayName.value;
+ if (m_prefixName)
+ styleName.prepend(m_item->itemName() + "_");
+
+ auto nameExists = [this, paragraphFamily, &importedNames](const QString& name)
+ {
+ if (importedNames.values().contains(name))
+ return true;
+ return paragraphFamily ? m_Doc->paragraphStyles().contains(name) : m_Doc->charStyles().contains(name);
+ };
+
+ if (nameExists(styleName))
+ {
+ QString prefix = styleName;
+ int suffixNum = 1;
+ static const QRegularExpression suffixRx("^(.*)\\s+\\((\\d+)\\)$");
+ const QRegularExpressionMatch match = suffixRx.match(styleName);
+ if (match.hasMatch())
+ {
+ prefix = match.captured(1);
+ suffixNum = match.captured(2).toInt();
+ }
+ do
+ {
+ ++suffixNum;
+ styleName = prefix + " (" + QString::number(suffixNum) + ")";
+ }
+ while (nameExists(styleName));
+ }
+
+ importedNames.insert(sourceStyleName, styleName);
+ return styleName;
+}
+
+bool ODTIm::hasMappedAncestor(const QString& styleName, const QHash<QString, QString>& mappings) const
+{
+ QString currentName = styleName;
+ for (auto remaining = m_Styles.size(); remaining > 0 && !currentName.isEmpty(); --remaining)
+ {
+ if (mappings.contains(currentName))
+ return true;
+ auto it = m_Styles.constFind(currentName);
+ if (it == m_Styles.constEnd() || !it->parentStyle.valid)
+ break;
+ currentName = it->parentStyle.value;
+ }
+ return false;
+}
+
+void ODTIm::materializeNamedStyles()
+{
+ for (const QString& sourceName : m_namedStyleOrder)
+ {
+ auto it = m_Styles.constFind(sourceName);
+ if (it == m_Styles.constEnd())
+ continue;
+ const DrawStyle& currStyle = it.value();
+ if (!currStyle.styleOrigin.valid || currStyle.styleOrigin.value != "styles" || !currStyle.styleType.valid)
+ continue;
+
+ if (currStyle.styleType.value == "paragraph")
+ {
+ if (m_parStyleMappings.contains(sourceName))
+ continue;
+
+ ObjStyleODT tmpOStyle;
+ resolveStyle(tmpOStyle, sourceName);
+ ParagraphStyle newStyle;
+ newStyle.erase();
+ newStyle.setDefaultStyle(false);
+ newStyle.setLineSpacingMode(ParagraphStyle::AutomaticLineSpacing);
+ newStyle.setName(importedStyleName(sourceName, "paragraph"));
+
+ QString parentName = CommonStrings::DefaultParagraphStyle;
+ if (currStyle.parentStyle.valid && m_Styles.contains(currStyle.parentStyle.value))
{
- ObjStyleODT tmpOStyle;
- resolveStyle(tmpOStyle, spd.attribute("style:name"));
- if (spd.attribute("style:family") == "paragraph")
- {
- ParagraphStyle newStyle;
- newStyle.erase();
- newStyle.setDefaultStyle(false);
- newStyle.setLineSpacingMode(ParagraphStyle::AutomaticLineSpacing);
- QString styleName = spd.attribute("style:name");
- if (currStyle.displayName.valid)
- styleName = currStyle.displayName.value;
- if (m_prefixName)
- newStyle.setName(m_item->itemName() + "_" + styleName);
- else
- newStyle.setName(styleName);
- QString parentName = CommonStrings::DefaultParagraphStyle;
- if (currStyle.parentStyle.valid)
- {
- if (m_Styles.contains(currStyle.parentStyle.value))
- {
- DrawStyle pStyle = m_Styles[currStyle.parentStyle.value];
- parentName = currStyle.parentStyle.value;
- if (pStyle.displayName.valid)
- parentName = pStyle.displayName.value;
- }
- }
- if (m_prefixName && (parentName != CommonStrings::DefaultParagraphStyle))
- newStyle.setParent(m_item->itemName() + "_" + parentName);
- else
- newStyle.setParent(parentName);
- applyParagraphStyle(newStyle, tmpOStyle);
- applyCharacterStyle(newStyle.charStyle(), tmpOStyle);
- StyleSet<ParagraphStyle>tmp;
- tmp.create(newStyle);
- m_Doc->redefineStyles(tmp, false);
- }
- else if (spd.attribute("style:family") == "text")
- {
- CharStyle newStyle;
- newStyle.setDefaultStyle(false);
- QString styleName = spd.attribute("style:name");
- if (currStyle.displayName.valid)
- styleName = currStyle.displayName.value;
- if (m_prefixName)
- newStyle.setName(m_item->itemName() + "_" + styleName);
- else
- newStyle.setName(styleName);
- newStyle.setParent(CommonStrings::DefaultCharacterStyle);
- applyCharacterStyle(newStyle, tmpOStyle);
- StyleSet<CharStyle> temp;
- temp.create(newStyle);
- m_Doc->redefineCharStyles(temp, false);
- }
+ const QString parentSourceName = currStyle.parentStyle.value;
+ auto mappingIt = m_parStyleMappings.constFind(parentSourceName);
+ parentName = (mappingIt != m_parStyleMappings.constEnd()) ? mappingIt.value() : importedStyleName(parentSourceName, "paragraph");
+ }
+ newStyle.setParent(parentName);
+ applyParagraphStyle(newStyle, tmpOStyle);
+ applyCharacterStyle(newStyle.charStyle(), tmpOStyle);
+
+ // If the direct parent is mapped, keep only this style's delta above it.
+ if (currStyle.parentStyle.valid && hasMappedAncestor(currStyle.parentStyle.value, m_parStyleMappings))
+ {
+ ObjStyleODT parentOStyle;
+ resolveStyle(parentOStyle, currStyle.parentStyle.value);
+ ParagraphStyle parentStyle;
+ parentStyle.erase();
+ applyParagraphStyle(parentStyle, parentOStyle);
+ applyCharacterStyle(parentStyle.charStyle(), parentOStyle);
+ newStyle.eraseStyle(parentStyle);
+ newStyle.setParent(parentName);
+ }
+
+ StyleSet<ParagraphStyle> temp;
+ temp.create(newStyle);
+ m_Doc->redefineStyles(temp, false);
+ }
+ else if (currStyle.styleType.value == "text")
+ {
+ if (m_charStyleMappings.contains(sourceName))
+ continue;
+
+ ObjStyleODT tmpOStyle;
+ resolveStyle(tmpOStyle, sourceName);
+ CharStyle newStyle;
+ newStyle.erase();
+ newStyle.setDefaultStyle(false);
+ newStyle.setName(importedStyleName(sourceName, "text"));
+
+ QString parentName = CommonStrings::DefaultCharacterStyle;
+ if (currStyle.parentStyle.valid && m_Styles.contains(currStyle.parentStyle.value)
+ && hasMappedAncestor(currStyle.parentStyle.value, m_charStyleMappings))
+ {
+ const QString parentSourceName = currStyle.parentStyle.value;
+ auto mappingIt = m_charStyleMappings.constFind(parentSourceName);
+ parentName = (mappingIt != m_charStyleMappings.constEnd()) ? mappingIt.value() : importedStyleName(parentSourceName, "text");
+ }
+ newStyle.setParent(parentName);
+ applyCharacterStyle(newStyle, tmpOStyle);
+
+ if (currStyle.parentStyle.valid && hasMappedAncestor(currStyle.parentStyle.value, m_charStyleMappings))
+ {
+ ObjStyleODT parentOStyle;
+ resolveStyle(parentOStyle, currStyle.parentStyle.value);
+ CharStyle parentStyle;
+ parentStyle.erase();
+ applyCharacterStyle(parentStyle, parentOStyle);
+ newStyle.eraseCharStyle(parentStyle);
+ newStyle.setParent(parentName);
}
+
+ StyleSet<CharStyle> temp;
+ temp.create(newStyle);
+ m_Doc->redefineCharStyles(temp, false);
}
}
}
@@ -675,47 +855,52 @@
bool ODTIm::parseDocReferenceXML(const QDomDocument &designMapDom)
{
QDomElement docElem = designMapDom.documentElement();
+
+ // Collect every style definition before creating Scribus styles or importing
+ // body text. This allows style mappings to replace an ODT named style without
+ // first redefining a same-named style already present in the document.
for (QDomElement drawPag = docElem.firstChildElement(); !drawPag.isNull(); drawPag = drawPag.nextSiblingElement())
{
if (drawPag.tagName() == "office:font-face-decls")
{
- for (QDomElement spf = drawPag.firstChildElement(); !spf.isNull(); spf = spf.nextSiblingElement() )
+ for (QDomElement spf = drawPag.firstChildElement(); !spf.isNull(); spf = spf.nextSiblingElement())
{
- if (spf.tagName() == "style:font-face")
- {
- if (!spf.attribute("style:name").isEmpty())
- {
- QString fontFamily = spf.attribute("svg:font-family");
- if (fontFamily.startsWith(QChar('\'')))
- fontFamily = fontFamily.mid(1);
- if (fontFamily.endsWith(QChar('\'')))
- fontFamily.chop(1);
- m_fontMap.insert(spf.attribute("style:name"), fontFamily);
- }
- }
+ if (spf.tagName() != "style:font-face" || spf.attribute("style:name").isEmpty())
+ continue;
+ QString fontFamily = spf.attribute("svg:font-family");
+ if (fontFamily.startsWith(QChar('\'')))
+ fontFamily = fontFamily.mid(1);
+ if (fontFamily.endsWith(QChar('\'')))
+ fontFamily.chop(1);
+ m_fontMap.insert(spf.attribute("style:name"), fontFamily);
}
}
else if (drawPag.tagName() == "office:styles")
parseStyles(drawPag, "styles");
else if (drawPag.tagName() == "office:automatic-styles")
parseStyles(drawPag, "auto");
- else if (drawPag.tagName() == "office:body")
+ }
+
+ materializeNamedStyles();
+
+ for (QDomElement drawPag = docElem.firstChildElement(); !drawPag.isNull(); drawPag = drawPag.nextSiblingElement())
+ {
+ if (drawPag.tagName() != "office:body")
+ continue;
+ for (QDomElement sp = drawPag.firstChildElement(); !sp.isNull(); sp = sp.nextSiblingElement())
{
- for (QDomElement sp = drawPag.firstChildElement(); !sp.isNull(); sp = sp.nextSiblingElement() )
+ if (sp.tagName() == "office:text")
{
- if (sp.tagName() == "office:text")
- {
- ObjStyleODT tmpOStyle;
- resolveStyle(tmpOStyle, "standard");
- parseText(sp, m_item, tmpOStyle);
- }
+ ObjStyleODT tmpOStyle;
+ resolveStyle(tmpOStyle, "standard");
+ parseText(sp, m_item, tmpOStyle);
}
}
}
return true;
}
-bool ODTIm::findNamedParagraphStyle(const QString& styleName, QString& sourceStyleName, QString& importedStyleName) const
+bool ODTIm::findNamedParagraphStyle(const QString& styleName, QString& sourceStyleName, QString& importedStyleName)
{
sourceStyleName.clear();
importedStyleName.clear();
@@ -733,9 +918,8 @@
&& style.styleType.value == "paragraph")
{
sourceStyleName = currentName;
- importedStyleName = style.displayName.valid ? style.displayName.value : currentName;
- if (m_prefixName)
- importedStyleName.prepend(m_item->itemName() + "_");
+ auto mappingIt = m_parStyleMappings.constFind(currentName);
+ importedStyleName = (mappingIt != m_parStyleMappings.constEnd()) ? mappingIt.value() : this->importedStyleName(currentName, "paragraph");
return true;
}
@@ -747,6 +931,62 @@
return false;
}
+bool ODTIm::findNamedCharacterStyle(const QString& styleName, QString& sourceStyleName, QString& importedStyleName)
+{
+ sourceStyleName.clear();
+ importedStyleName.clear();
+
+ QString currentName = styleName;
+ for (auto remaining = m_Styles.size(); remaining > 0 && !currentName.isEmpty(); --remaining)
+ {
+ auto styleIt = m_Styles.constFind(currentName);
+ if (styleIt == m_Styles.constEnd())
+ break;
+
+ const DrawStyle& style = styleIt.value();
+ if (style.styleOrigin.valid && style.styleType.valid
+ && style.styleOrigin.value == "styles"
+ && style.styleType.value == "text")
+ {
+ sourceStyleName = currentName;
+ auto mappingIt = m_charStyleMappings.constFind(currentName);
+ importedStyleName = (mappingIt != m_charStyleMappings.constEnd()) ? mappingIt.value() : this->importedStyleName(currentName, "text");
+ return true;
+ }
+
+ if (!style.parentStyle.valid)
+ break;
+ currentName = style.parentStyle.value;
+ }
+ return false;
+}
+
+bool ODTIm::applyMappedCharacterStyle(const QString& styleName, CharStyle& style, const ObjStyleODT& resolvedStyle)
+{
+ QString sourceStyleName;
+ QString targetStyleName;
+ if (!findNamedCharacterStyle(styleName, sourceStyleName, targetStyleName)
+ || !hasMappedAncestor(sourceStyleName, m_charStyleMappings))
+ return false;
+
+ ObjStyleODT inheritedOdtStyle;
+ resolveStyle(inheritedOdtStyle, sourceStyleName);
+ CharStyle inheritedStyle;
+ inheritedStyle.erase();
+ applyCharacterStyle(inheritedStyle, inheritedOdtStyle);
+
+ CharStyle deltaStyle;
+ deltaStyle.erase();
+ applyCharacterStyle(deltaStyle, resolvedStyle);
+ deltaStyle.eraseCharStyle(inheritedStyle);
+
+ style.eraseDirectFormatting();
+ style.setParent(targetStyleName);
+ style.applyStyle(deltaStyle);
+ return true;
+}
+
+
void ODTIm::parseTextSpan(const QDomElement &elem, PageItem* item, const ParagraphStyle &tmpStyle, const CharStyle &tmpCStyle, const ObjStyleODT &tmpOStyle, int &posC)
{
if (!elem.hasChildNodes())
@@ -756,34 +996,22 @@
CharStyle cStyle = tmpCStyle;
QString textStyleName = elem.attribute("text:style-name");
+ bool mappedCharacterStyle = false;
if (!textStyleName.isEmpty())
{
resolveStyle(odtStyle, textStyleName);
- if (m_Styles.contains(textStyleName))
+ mappedCharacterStyle = applyMappedCharacterStyle(textStyleName, cStyle, odtStyle);
+ if (!mappedCharacterStyle && m_Styles.contains(textStyleName))
{
- DrawStyle currStyle = m_Styles[textStyleName];
- if (currStyle.styleOrigin.value == "styles")
- {
- QString charStyleName;
- if (m_prefixName)
- {
- charStyleName = m_item->itemName() + "_" + textStyleName;
- if (currStyle.displayName.valid)
- charStyleName = m_item->itemName() + "_" + currStyle.displayName.value;
- }
- else
- {
- charStyleName = textStyleName;
- if (currStyle.displayName.valid)
- charStyleName = currStyle.displayName.value;
- }
- cStyle.setParent(charStyleName);
- }
+ const DrawStyle& currStyle = m_Styles[textStyleName];
+ if (currStyle.styleOrigin.valid && currStyle.styleOrigin.value == "styles")
+ cStyle.setParent(importedStyleName(textStyleName, "text"));
}
m_textStylesStack.push(textStyleName);
}
-
- applyCharacterStyle(cStyle, odtStyle);
+
+ if (!mappedCharacterStyle)
+ applyCharacterStyle(cStyle, odtStyle);
for (QDomNode spn = elem.firstChild(); !spn.isNull(); spn = spn.nextSibling())
{
@@ -832,13 +1060,16 @@
CharStyle cStyle = tmpCStyle;
QString textStyleName = elem.attribute("text:style-name");
+ bool mappedCharacterStyle = false;
if (!textStyleName.isEmpty())
{
resolveStyle(odtStyle, textStyleName);
+ mappedCharacterStyle = applyMappedCharacterStyle(textStyleName, cStyle, odtStyle);
m_textStylesStack.push(textStyleName);
}
-
- applyCharacterStyle(cStyle, odtStyle);
+
+ if (!mappedCharacterStyle)
+ applyCharacterStyle(cStyle, odtStyle);
for (QDomNode spn = elem.firstChild(); !spn.isNull(); spn = spn.nextSibling())
{
@@ -926,32 +1157,56 @@
insertChars(item, txt, tmpStyle, tmpCStyle, posC);
}
- if (hasNamedParagraphStyle)
+ const bool mappedParagraphStyle = hasNamedParagraphStyle && hasMappedAncestor(namedOdtStyleName, m_parStyleMappings);
+ if (mappedParagraphStyle)
{
- // A paragraph assigned to a named style should not retain the importer's
- // default paragraph values as local overrides.
+ // The mapped Scribus style is authoritative. Apply the resolved ODT style
+ // only long enough to calculate formatting introduced below the mapped
+ // named style, then erase that named ODT baseline.
tmpStyle.erase();
tmpStyle.setParent(parStyleName);
- }
- applyParagraphStyle(tmpStyle, pStyle);
+ applyParagraphStyle(tmpStyle, pStyle);
+ applyCharacterStyle(tmpStyle.charStyle(), pStyle);
- if (hasNamedParagraphStyle)
- {
ObjStyleODT inheritedStyle = tmpOStyle;
resolveStyle(inheritedStyle, namedOdtStyleName);
-
ParagraphStyle inheritedParagraphStyle;
inheritedParagraphStyle.erase();
applyParagraphStyle(inheritedParagraphStyle, inheritedStyle);
-
- // Keep the named Scribus style as the paragraph base and retain only
- // paragraph properties introduced by an ODT automatic style.
+ applyCharacterStyle(inheritedParagraphStyle.charStyle(), inheritedStyle);
tmpStyle.eraseStyle(inheritedParagraphStyle);
tmpStyle.setParent(parStyleName);
}
+ else
+ {
+ if (hasNamedParagraphStyle)
+ {
+ // A paragraph assigned to a named style should not retain the importer's
+ // default paragraph values as local overrides.
+ tmpStyle.erase();
+ tmpStyle.setParent(parStyleName);
+ }
+ applyParagraphStyle(tmpStyle, pStyle);
+
+ if (hasNamedParagraphStyle)
+ {
+ ObjStyleODT inheritedStyle = tmpOStyle;
+ resolveStyle(inheritedStyle, namedOdtStyleName);
+
+ ParagraphStyle inheritedParagraphStyle;
+ inheritedParagraphStyle.erase();
+ applyParagraphStyle(inheritedParagraphStyle, inheritedStyle);
+
+ // Keep the named Scribus style as the paragraph base and retain only
+ // paragraph properties introduced by an ODT automatic style.
+ tmpStyle.eraseStyle(inheritedParagraphStyle);
+ tmpStyle.setParent(parStyleName);
+ }
+ }
tmpCStyle = tmpStyle.charStyle();
- applyCharacterStyle(tmpCStyle, pStyle);
+ if (!mappedParagraphStyle)
+ applyCharacterStyle(tmpCStyle, pStyle);
for (QDomNode spn = elem.firstChild(); !spn.isNull(); spn = spn.nextSibling())
{
osm3-style-matching-dialog-v1.25.patch (24,521 bytes)
Index: scribus/plugins/gettext/odt2im/CMakeLists.txt
===================================================================
--- scribus/plugins/gettext/odt2im/CMakeLists.txt (revision 27810)
+++ scribus/plugins/gettext/odt2im/CMakeLists.txt (working copy)
@@ -5,6 +5,7 @@
set(ODT_IM_PLUGIN_SOURCES
importodt.cpp
+ odtstylemappingdialog.cpp
)
set(SCRIBUS_ODT_IM_PLUGIN "odtimplugin")
Index: scribus/plugins/gettext/odt2im/importodt.cpp
===================================================================
--- scribus/plugins/gettext/odt2im/importodt.cpp (revision 27810)
+++ scribus/plugins/gettext/odt2im/importodt.cpp (working copy)
@@ -11,6 +11,7 @@
email : Franz.Schmid@altmuehlnet.de
***************************************************************************/
#include "importodt.h"
+#include "odtstylemappingdialog.h"
#include <QApplication>
#include <QByteArray>
@@ -43,8 +44,20 @@
void GetText2(const QString& filename, const QString& /*encoding*/, bool textOnly, bool prefix, bool append, PageItem *textItem)
{
- auto docxim = std::make_unique<ODTIm>(textItem, prefix, append);
- docxim->importFile(filename, textOnly);
+ auto odtim = std::make_unique<ODTIm>(textItem, prefix, append);
+ if (!textOnly && odtim->collectStyles(filename))
+ {
+ const QList<ODTIm::StyleInfo> styles = odtim->namedStyles();
+ if (!styles.isEmpty())
+ {
+ const QString importPrefix = prefix ? textItem->itemName() + "_" : QString();
+ OdtStyleMappingDialog dialog(textItem->doc(), styles, importPrefix, QApplication::activeWindow());
+ if (dialog.exec() != QDialog::Accepted)
+ return;
+ odtim->setStyleMappings(dialog.paragraphMappings(), dialog.characterMappings());
+ }
+ }
+ odtim->importFile(filename, textOnly);
}
ODTIm::ODTIm(PageItem *textItem, bool prefix, bool append)
Index: scribus/plugins/gettext/odt2im/odtstylemappingdialog.h
===================================================================
--- scribus/plugins/gettext/odt2im/odtstylemappingdialog.h (nonexistent)
+++ scribus/plugins/gettext/odt2im/odtstylemappingdialog.h (working copy)
@@ -0,0 +1,79 @@
+/*
+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.
+*/
+#ifndef ODTSTYLEMAPPINGDIALOG_H
+#define ODTSTYLEMAPPINGDIALOG_H
+
+#include <QDialog>
+#include <QHash>
+#include <QList>
+#include <QString>
+#include <QStringList>
+
+#include "importodt.h"
+
+class QComboBox;
+class QLabel;
+class QTableWidget;
+class QTableWidgetItem;
+class ScribusDoc;
+
+class OdtStyleMappingDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ OdtStyleMappingDialog(ScribusDoc* doc, const QList<ODTIm::StyleInfo>& styles, const QString& importPrefix, QWidget* parent = nullptr);
+
+ QHash<QString, QString> paragraphMappings() const;
+ QHash<QString, QString> characterMappings() const;
+
+private:
+ enum class MappingOrigin
+ {
+ Untouched,
+ Bulk,
+ NameMatch,
+ Manual
+ };
+
+ struct MappingRow
+ {
+ QString sourceName;
+ QString displayName;
+ QString idToolTip;
+ QString importName;
+ QComboBox* targetCombo { nullptr };
+ QTableWidgetItem* sourceItem { nullptr };
+ MappingOrigin origin { MappingOrigin::Untouched };
+ bool hasNameCollision { false };
+ bool ambiguousName { false };
+ bool wasMatchedByName { false };
+ };
+
+ QWidget* createTabPage(const QList<ODTIm::StyleInfo>& styles, const QString& family,
+ const QStringList& targetStyles, QList<MappingRow>& rows);
+ QTableWidget* createTable(const QList<ODTIm::StyleInfo>& styles, const QString& family,
+ const QStringList& targetStyles, QList<MappingRow>& rows);
+ void matchIdenticalNames(bool ignoreCase);
+ void mapRemaining(QList<MappingRow>& rows, const QString& targetStyle);
+ void resetMappings(QList<MappingRow>& rows);
+ void clearAmbiguities();
+ void updateRowWarning(MappingRow& row);
+ void updateAmbiguityWarning();
+ QHash<QString, QString> mappings(const QList<MappingRow>& rows) const;
+ QStringList paragraphStyleNames() const;
+ QStringList characterStyleNames() const;
+ void addTargetStyles(QComboBox* combo, const QStringList& targetStyles, const QString& family, bool includeImport = true) const;
+
+ ScribusDoc* m_doc { nullptr };
+ QString m_importPrefix;
+ QLabel* m_ambiguityWarningLabel { nullptr };
+ QList<MappingRow> m_paragraphRows;
+ QList<MappingRow> m_characterRows;
+};
+
+#endif
Index: scribus/plugins/gettext/odt2im/odtstylemappingdialog.cpp
===================================================================
--- scribus/plugins/gettext/odt2im/odtstylemappingdialog.cpp (nonexistent)
+++ scribus/plugins/gettext/odt2im/odtstylemappingdialog.cpp (working copy)
@@ -0,0 +1,569 @@
+/*
+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.
+*/
+#include "odtstylemappingdialog.h"
+
+#include <QAbstractItemView>
+#include <QApplication>
+#include <QCheckBox>
+#include <QComboBox>
+#include <QDialogButtonBox>
+#include <QHeaderView>
+#include <QHBoxLayout>
+#include <QIcon>
+#include <QKeyEvent>
+#include <QLabel>
+#include <QLineEdit>
+#include <QPushButton>
+#include <QStyle>
+#include <QTabWidget>
+#include <QTableWidget>
+#include <QTableWidgetItem>
+#include <QVBoxLayout>
+#include <QWheelEvent>
+
+#include "commonstrings.h"
+#include "iconmanager.h"
+#include "scribusdoc.h"
+#include "styles/charstyle.h"
+#include "styles/paragraphstyle.h"
+
+namespace
+{
+
+class OdtMappingComboBox : public QComboBox
+{
+public:
+ using QComboBox::QComboBox;
+
+protected:
+ void wheelEvent(QWheelEvent* event) override
+ {
+ event->ignore();
+ }
+};
+
+class OdtFilterLineEdit : public QLineEdit
+{
+public:
+ using QLineEdit::QLineEdit;
+
+protected:
+ void keyPressEvent(QKeyEvent* event) override
+ {
+ if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter)
+ {
+ event->accept();
+ return;
+ }
+ QLineEdit::keyPressEvent(event);
+ }
+};
+
+QIcon filterIcon()
+{
+ return IconManager::instance().loadIcon("action-filter");
+}
+
+int compactFieldWidth(const QWidget* widget, const QString& text)
+{
+ return widget->fontMetrics().horizontalAdvance(text) + 48;
+}
+
+}
+
+OdtStyleMappingDialog::OdtStyleMappingDialog(ScribusDoc* doc, const QList<ODTIm::StyleInfo>& styles, const QString& importPrefix, QWidget* parent)
+ : QDialog(parent),
+ m_doc(doc),
+ m_importPrefix(importPrefix)
+{
+ setModal(true);
+ setWindowTitle(tr("Match ODT Styles"));
+ setWindowIcon(QIcon(IconManager::instance().loadIcon("app-icon")));
+ resize(640, 420);
+
+ auto* mainLayout = new QVBoxLayout(this);
+ mainLayout->setSpacing(10);
+
+ auto* description = new QLabel(
+ tr("Map ODT styles to existing Scribus styles, or import them as new styles."), this);
+ description->setWordWrap(true);
+ mainLayout->addWidget(description);
+
+ auto* tabs = new QTabWidget(this);
+ const QStringList paragraphTargets = paragraphStyleNames();
+ const QStringList characterTargets = characterStyleNames();
+
+ QWidget* paragraphPage = createTabPage(styles, "paragraph", paragraphTargets, m_paragraphRows);
+ if (paragraphPage)
+ tabs->addTab(paragraphPage, tr("&Paragraph styles (%1)").arg(m_paragraphRows.size()));
+
+ QWidget* characterPage = createTabPage(styles, "text", characterTargets, m_characterRows);
+ if (characterPage)
+ tabs->addTab(characterPage, tr("Character styles (%1)").arg(m_characterRows.size()));
+
+ mainLayout->addWidget(tabs, 1);
+
+ auto* footerLayout = new QHBoxLayout;
+ footerLayout->setSpacing(8);
+ auto* matchButton = new QPushButton(tr("Match by &Name"), this);
+ matchButton->setToolTip(tr("Match ODT styles to Scribus styles with the same name."));
+ footerLayout->addWidget(matchButton);
+ auto* ignoreCaseCheck = new QCheckBox(tr("&Ignore case"), this);
+ ignoreCaseCheck->setToolTip(tr("Ignore letter case when matching style names."));
+ footerLayout->addWidget(ignoreCaseCheck);
+ m_ambiguityWarningLabel = new QLabel(this);
+ m_ambiguityWarningLabel->setPixmap(QApplication::style()->standardIcon(QStyle::SP_MessageBoxWarning).pixmap(16, 16));
+ m_ambiguityWarningLabel->setVisible(false);
+ footerLayout->addWidget(m_ambiguityWarningLabel);
+ footerLayout->addStretch();
+ auto* buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
+ footerLayout->addWidget(buttonBox);
+ mainLayout->addLayout(footerLayout);
+
+ auto* cancelButton = buttonBox->button(QDialogButtonBox::Cancel);
+ auto* okButton = buttonBox->button(QDialogButtonBox::Ok);
+ QWidget::setTabOrder(matchButton, ignoreCaseCheck);
+ if (cancelButton && okButton)
+ {
+ QWidget::setTabOrder(ignoreCaseCheck, cancelButton);
+ QWidget::setTabOrder(cancelButton, okButton);
+ }
+
+ connect(matchButton, &QPushButton::clicked, this, [this, ignoreCaseCheck]()
+ {
+ matchIdenticalNames(ignoreCaseCheck->isChecked());
+ });
+ connect(ignoreCaseCheck, &QCheckBox::toggled, this, [this](bool checked)
+ {
+ if (!checked)
+ clearAmbiguities();
+ });
+ connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
+ connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
+}
+
+QHash<QString, QString> OdtStyleMappingDialog::paragraphMappings() const
+{
+ return mappings(m_paragraphRows);
+}
+
+QHash<QString, QString> OdtStyleMappingDialog::characterMappings() const
+{
+ return mappings(m_characterRows);
+}
+
+QWidget* OdtStyleMappingDialog::createTabPage(const QList<ODTIm::StyleInfo>& styles, const QString& family,
+ const QStringList& targetStyles, QList<MappingRow>& rows)
+{
+ auto* page = new QWidget(this);
+ auto* pageLayout = new QVBoxLayout(page);
+ pageLayout->setContentsMargins(8, 8, 8, 8);
+ pageLayout->setSpacing(8);
+
+ QTableWidget* table = createTable(styles, family, targetStyles, rows);
+ if (table->rowCount() == 0)
+ {
+ delete table;
+ delete page;
+ return nullptr;
+ }
+
+ auto* filterLayout = new QHBoxLayout;
+ filterLayout->setSpacing(8);
+ auto* filterEdit = new OdtFilterLineEdit(page);
+ filterEdit->setPlaceholderText(tr("Filter styles…"));
+ filterEdit->addAction(filterIcon(), QLineEdit::LeadingPosition);
+ filterEdit->setClearButtonEnabled(true);
+ filterEdit->setFixedWidth(compactFieldWidth(filterEdit, tr("Import as New Style")));
+ filterLayout->addWidget(filterEdit);
+ auto* mappedOnlyCheck = new QCheckBox(tr("&Mapped only"), page);
+ mappedOnlyCheck->setToolTip(tr("Show only styles mapped to an existing Scribus style."));
+ filterLayout->addWidget(mappedOnlyCheck);
+ auto* unmappedOnlyCheck = new QCheckBox(tr("&Unmapped only"), page);
+ unmappedOnlyCheck->setToolTip(tr("Show only styles not mapped to an existing Scribus style."));
+ filterLayout->addWidget(unmappedOnlyCheck);
+ filterLayout->addStretch();
+ pageLayout->addLayout(filterLayout);
+ pageLayout->addWidget(table, 1);
+
+ auto* bulkLayout = new QHBoxLayout;
+ bulkLayout->setSpacing(0);
+ bulkLayout->setContentsMargins(0, 8, 0, 0);
+ auto* bulkLabel = new QLabel(tr("Map &remaining styles to:"), page);
+ bulkLayout->addWidget(bulkLabel);
+ bulkLayout->addSpacing(8);
+ auto* bulkCombo = new OdtMappingComboBox(page);
+ addTargetStyles(bulkCombo, targetStyles, family);
+ bulkCombo->setToolTip(tr("Choose a target for unmapped styles."));
+ bulkCombo->setFixedWidth(compactFieldWidth(bulkCombo, tr("Import as New Style")) + 5);
+ bulkLabel->setBuddy(bulkCombo);
+ bulkLayout->addWidget(bulkCombo);
+ bulkLayout->addSpacing(8);
+ auto* applyButton = new QPushButton(tr("&Apply"), page);
+ applyButton->setToolTip(tr("Apply this target to all remaining unmapped styles."));
+ applyButton->setFixedWidth(applyButton->sizeHint().width() - 10);
+ bulkLayout->addWidget(applyButton);
+ bulkLayout->addStretch();
+ auto* resetButton = new QPushButton(IconManager::instance().loadIcon("reset"), tr("Reset"), page);
+ resetButton->setToolTip(tr("Reset all mappings in this tab."));
+ bulkLayout->addWidget(resetButton);
+ pageLayout->addLayout(bulkLayout);
+
+ QWidget::setTabOrder(filterEdit, mappedOnlyCheck);
+ QWidget::setTabOrder(mappedOnlyCheck, unmappedOnlyCheck);
+ QWidget* previousWidget = unmappedOnlyCheck;
+ for (const MappingRow& row : rows)
+ {
+ QWidget::setTabOrder(previousWidget, row.targetCombo);
+ previousWidget = row.targetCombo;
+ }
+ QWidget::setTabOrder(previousWidget, bulkCombo);
+ QWidget::setTabOrder(bulkCombo, applyButton);
+ QWidget::setTabOrder(applyButton, resetButton);
+
+ QList<MappingRow>* rowsPtr = &rows;
+ auto updateFilter = [table, filterEdit, unmappedOnlyCheck, mappedOnlyCheck, rowsPtr]()
+ {
+ const QString filter = filterEdit->text();
+ for (int row = 0; row < table->rowCount(); ++row)
+ {
+ const QTableWidgetItem* item = table->item(row, 0);
+ if (!item)
+ continue;
+
+ const bool textMatches = filter.isEmpty()
+ || item->text().contains(filter, Qt::CaseInsensitive)
+ || item->data(Qt::UserRole).toString().contains(filter, Qt::CaseInsensitive);
+ const bool isUnmapped = row < rowsPtr->size()
+ && rowsPtr->at(row).targetCombo->currentData().toString().isEmpty();
+ const bool mappingMatches = (!unmappedOnlyCheck->isChecked() && !mappedOnlyCheck->isChecked())
+ || (unmappedOnlyCheck->isChecked() && isUnmapped)
+ || (mappedOnlyCheck->isChecked() && !isUnmapped);
+ table->setRowHidden(row, !(textMatches && mappingMatches));
+ }
+ };
+
+ connect(filterEdit, &QLineEdit::textChanged, this, [updateFilter](const QString&)
+ {
+ updateFilter();
+ });
+ connect(unmappedOnlyCheck, &QCheckBox::toggled, this, [mappedOnlyCheck, updateFilter](bool checked)
+ {
+ if (checked)
+ mappedOnlyCheck->setChecked(false);
+ updateFilter();
+ });
+ connect(mappedOnlyCheck, &QCheckBox::toggled, this, [unmappedOnlyCheck, updateFilter](bool checked)
+ {
+ if (checked)
+ unmappedOnlyCheck->setChecked(false);
+ updateFilter();
+ });
+ for (const MappingRow& row : rows)
+ {
+ connect(row.targetCombo, &QComboBox::currentIndexChanged, this, [updateFilter](int)
+ {
+ updateFilter();
+ });
+ }
+
+ connect(applyButton, &QPushButton::clicked, this, [this, rowsPtr, bulkCombo]()
+ {
+ mapRemaining(*rowsPtr, bulkCombo->currentData().toString());
+ });
+ connect(resetButton, &QPushButton::clicked, this, [this, rowsPtr]()
+ {
+ resetMappings(*rowsPtr);
+ });
+
+ return page;
+}
+
+QTableWidget* OdtStyleMappingDialog::createTable(const QList<ODTIm::StyleInfo>& styles, const QString& family,
+ const QStringList& targetStyles, QList<MappingRow>& rows)
+{
+ auto* table = new QTableWidget(this);
+ table->setColumnCount(2);
+ table->setHorizontalHeaderLabels(QStringList() << tr("ODT Style") << tr("Scribus Style"));
+ table->verticalHeader()->setVisible(false);
+ auto* header = table->horizontalHeader();
+ header->setMinimumHeight(header->sizeHint().height() + 5);
+ header->setSectionResizeMode(0, QHeaderView::Stretch);
+ header->setSectionResizeMode(1, QHeaderView::Stretch);
+ table->setSelectionMode(QAbstractItemView::NoSelection);
+ table->setEditTriggers(QAbstractItemView::NoEditTriggers);
+ table->setAlternatingRowColors(true);
+ table->setFocusPolicy(Qt::NoFocus);
+ table->setTabKeyNavigation(false);
+
+ for (const ODTIm::StyleInfo& style : styles)
+ {
+ if (style.family != family)
+ continue;
+
+ const int row = table->rowCount();
+ table->insertRow(row);
+
+ auto* sourceItem = new QTableWidgetItem(style.displayName);
+ sourceItem->setData(Qt::UserRole, style.sourceName);
+ table->setItem(row, 0, sourceItem);
+
+ auto* combo = new OdtMappingComboBox(table);
+ addTargetStyles(combo, targetStyles, family);
+ table->setCellWidget(row, 1, combo);
+
+ MappingRow mappingRow;
+ mappingRow.sourceName = style.sourceName;
+ mappingRow.displayName = style.displayName;
+ mappingRow.idToolTip = (style.sourceName != style.displayName)
+ ? tr("ODT style ID: %1").arg(style.sourceName) : QString();
+ mappingRow.importName = m_importPrefix + style.displayName;
+ mappingRow.targetCombo = combo;
+ mappingRow.sourceItem = sourceItem;
+ mappingRow.hasNameCollision = targetStyles.contains(mappingRow.importName);
+ rows.append(mappingRow);
+
+ QList<MappingRow>* rowsPtr = &rows;
+ connect(combo, &QComboBox::currentIndexChanged, this, [this, rowsPtr, combo](int)
+ {
+ for (MappingRow& mappedRow : *rowsPtr)
+ {
+ if (mappedRow.targetCombo != combo)
+ continue;
+ updateRowWarning(mappedRow);
+ break;
+ }
+ });
+ connect(combo, &QComboBox::activated, this, [this, rowsPtr, combo](int)
+ {
+ for (MappingRow& mappedRow : *rowsPtr)
+ {
+ if (mappedRow.targetCombo != combo)
+ continue;
+ mappedRow.origin = MappingOrigin::Manual;
+ mappedRow.ambiguousName = false;
+ updateRowWarning(mappedRow);
+ updateAmbiguityWarning();
+ break;
+ }
+ });
+ updateRowWarning(rows.last());
+ }
+
+ return table;
+}
+
+void OdtStyleMappingDialog::matchIdenticalNames(bool ignoreCase)
+{
+ clearAmbiguities();
+
+ auto matchRows = [this, ignoreCase](QList<MappingRow>& rows)
+ {
+ for (MappingRow& row : rows)
+ {
+ if (row.origin == MappingOrigin::Manual && !row.wasMatchedByName)
+ continue;
+
+ auto findMatchingTarget = [ignoreCase, &row](const QString& sourceName)
+ {
+ if (!ignoreCase)
+ return row.targetCombo->findData(sourceName);
+
+ int matchIndex = -1;
+ for (int i = 0; i < row.targetCombo->count(); ++i)
+ {
+ const QString targetName = row.targetCombo->itemData(i).toString();
+ if (targetName.isEmpty() || QString::compare(targetName, sourceName, Qt::CaseInsensitive) != 0)
+ continue;
+ if (matchIndex >= 0)
+ return -2;
+ matchIndex = i;
+ }
+ return matchIndex;
+ };
+
+ int index = findMatchingTarget(row.displayName);
+ if (index == -1 && row.sourceName != row.displayName)
+ index = findMatchingTarget(row.sourceName);
+
+ if (index == -2)
+ {
+ row.ambiguousName = true;
+ row.origin = MappingOrigin::Untouched;
+ const int importIndex = row.targetCombo->findData(QString());
+ if (importIndex >= 0)
+ row.targetCombo->setCurrentIndex(importIndex);
+ updateRowWarning(row);
+ continue;
+ }
+
+ if (index >= 0)
+ {
+ row.targetCombo->setCurrentIndex(index);
+ row.origin = MappingOrigin::NameMatch;
+ row.wasMatchedByName = true;
+ }
+ updateRowWarning(row);
+ }
+ };
+
+ matchRows(m_paragraphRows);
+ matchRows(m_characterRows);
+ updateAmbiguityWarning();
+}
+
+void OdtStyleMappingDialog::mapRemaining(QList<MappingRow>& rows, const QString& targetStyle)
+{
+ for (MappingRow& row : rows)
+ {
+ if (row.origin == MappingOrigin::Manual || row.origin == MappingOrigin::NameMatch)
+ continue;
+
+ const int index = row.targetCombo->findData(targetStyle);
+ if (index >= 0)
+ {
+ row.targetCombo->setCurrentIndex(index);
+ row.origin = MappingOrigin::Bulk;
+ row.ambiguousName = false;
+ updateRowWarning(row);
+ }
+ }
+ updateAmbiguityWarning();
+}
+
+void OdtStyleMappingDialog::resetMappings(QList<MappingRow>& rows)
+{
+ for (MappingRow& row : rows)
+ {
+ const int importIndex = row.targetCombo->findData(QString());
+ if (importIndex >= 0)
+ row.targetCombo->setCurrentIndex(importIndex);
+ row.origin = MappingOrigin::Untouched;
+ row.ambiguousName = false;
+ row.wasMatchedByName = false;
+ updateRowWarning(row);
+ }
+ updateAmbiguityWarning();
+}
+
+void OdtStyleMappingDialog::clearAmbiguities()
+{
+ for (MappingRow& row : m_paragraphRows)
+ {
+ row.ambiguousName = false;
+ updateRowWarning(row);
+ }
+ for (MappingRow& row : m_characterRows)
+ {
+ row.ambiguousName = false;
+ updateRowWarning(row);
+ }
+ updateAmbiguityWarning();
+}
+
+void OdtStyleMappingDialog::updateRowWarning(MappingRow& row)
+{
+ if (!row.sourceItem || !row.targetCombo)
+ return;
+
+ QStringList toolTips;
+ if (!row.idToolTip.isEmpty())
+ toolTips.append(row.idToolTip);
+
+ const bool collisionWarning = row.hasNameCollision && row.targetCombo->currentData().toString().isEmpty();
+ if (collisionWarning)
+ toolTips.append(tr("A Scribus style named \"%1\" already exists.\nThe imported ODT style will be renamed.").arg(row.importName));
+ if (row.ambiguousName)
+ toolTips.append(tr("Multiple Scribus styles match this name when case is ignored."));
+
+ if (collisionWarning || row.ambiguousName)
+ row.sourceItem->setIcon(QApplication::style()->standardIcon(QStyle::SP_MessageBoxWarning));
+ else
+ row.sourceItem->setIcon(QIcon());
+ row.sourceItem->setToolTip(toolTips.join('\n'));
+}
+
+void OdtStyleMappingDialog::updateAmbiguityWarning()
+{
+ if (!m_ambiguityWarningLabel)
+ return;
+
+ int ambiguousCount = 0;
+ for (const MappingRow& row : m_paragraphRows)
+ ambiguousCount += row.ambiguousName ? 1 : 0;
+ for (const MappingRow& row : m_characterRows)
+ ambiguousCount += row.ambiguousName ? 1 : 0;
+
+ m_ambiguityWarningLabel->setVisible(ambiguousCount > 0);
+ if (ambiguousCount == 1)
+ m_ambiguityWarningLabel->setToolTip(tr("1 style name has multiple matches when case is ignored and was left unmapped."));
+ else if (ambiguousCount > 1)
+ m_ambiguityWarningLabel->setToolTip(tr("%1 style names have multiple matches when case is ignored and were left unmapped.").arg(ambiguousCount));
+ else
+ m_ambiguityWarningLabel->setToolTip(QString());
+}
+
+QHash<QString, QString> OdtStyleMappingDialog::mappings(const QList<MappingRow>& rows) const
+{
+ QHash<QString, QString> result;
+ for (const MappingRow& row : rows)
+ {
+ const QString targetStyle = row.targetCombo->currentData().toString();
+ if (!targetStyle.isEmpty())
+ result.insert(row.sourceName, targetStyle);
+ }
+ return result;
+}
+
+QStringList OdtStyleMappingDialog::paragraphStyleNames() const
+{
+ QStringList result;
+ if (!m_doc)
+ return result;
+
+ for (int i = 0; i < m_doc->paragraphStyles().count(); ++i)
+ {
+ const ParagraphStyle& style = m_doc->paragraphStyles()[i];
+ if (!style.name().isEmpty() && !style.isDefaultStyle())
+ result.append(style.name());
+ }
+ result.sort(Qt::CaseInsensitive);
+ result.prepend(CommonStrings::DefaultParagraphStyle);
+ return result;
+}
+
+QStringList OdtStyleMappingDialog::characterStyleNames() const
+{
+ QStringList result;
+ if (!m_doc)
+ return result;
+
+ for (int i = 0; i < m_doc->charStyles().count(); ++i)
+ {
+ const CharStyle& style = m_doc->charStyles()[i];
+ if (!style.name().isEmpty() && !style.isDefaultStyle())
+ result.append(style.name());
+ }
+ result.sort(Qt::CaseInsensitive);
+ result.prepend(CommonStrings::DefaultCharacterStyle);
+ return result;
+}
+
+void OdtStyleMappingDialog::addTargetStyles(QComboBox* combo, const QStringList& targetStyles, const QString& family, bool includeImport) const
+{
+ if (includeImport)
+ combo->addItem(tr("Import as New Style"), QString());
+ for (const QString& targetStyle : targetStyles)
+ {
+ QString displayName = targetStyle;
+ if (family == "paragraph" && targetStyle == CommonStrings::DefaultParagraphStyle)
+ displayName = CommonStrings::trDefaultParagraphStyle;
+ else if (family == "text" && targetStyle == CommonStrings::DefaultCharacterStyle)
+ displayName = CommonStrings::trDefaultCharacterStyle;
+ combo->addItem(displayName, targetStyle);
+ }
+}
Index: resources/iconsets/1_7_0/1_7_0.xml
===================================================================
--- resources/iconsets/1_7_0/1_7_0.xml (revision 27810)
+++ resources/iconsets/1_7_0/1_7_0.xml (working copy)
@@ -41,6 +41,7 @@
<!-- Action -->
<icon id="add" file="16/action-add.svg" />
+ <icon id="action-filter" file="16/action-filter.svg" />
<icon id="chain-closed" file="16/action-link.svg" />
<icon id="chain-open" file="16/action-unlink.svg" />
<icon id="clear-right" file="16/action-backspace-reverse.svg" />
Index: resources/iconsets/1_7_0/16/action-filter.svg
===================================================================
--- resources/iconsets/1_7_0/16/action-filter.svg (nonexistent)
+++ resources/iconsets/1_7_0/16/action-filter.svg (working copy)
@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><style>@import '../colors.css';</style>
+<path d="M1 2H15L9.5 8V13.5L6.5 15V8L1 2Z"/>
+</svg>
|
|
|
#Files ## Changed existing files - `scribus/gtgettext.cpp` - `scribus/plugins/gettext/odt2im/importodt.h` - `scribus/plugins/gettext/odt2im/importodt.cpp` - `scribus/plugins/gettext/odt2im/CMakeLists.txt` - `resources/iconsets/1_7_0/1_7_0.xml` ## New files added - `scribus/plugins/gettext/odt2im/odtstylemappingdialog.h` - `scribus/plugins/gettext/odt2im/odtstylemappingdialog.cpp` - `resources/iconsets/1_7_0/16/action-filter.svg` ## Existing Scribus resources reused - Existing `reset` icon — referenced through `IconManager`; its resource file is not modified or duplicated. |
|
|
Character Style tab added. A small change to only osm3. osm3-style-matching-dialog-v1.26.patch (24,784 bytes)
Index: scribus/plugins/gettext/odt2im/CMakeLists.txt
===================================================================
--- scribus/plugins/gettext/odt2im/CMakeLists.txt (revision 27810)
+++ scribus/plugins/gettext/odt2im/CMakeLists.txt (working copy)
@@ -5,6 +5,7 @@
set(ODT_IM_PLUGIN_SOURCES
importodt.cpp
+ odtstylemappingdialog.cpp
)
set(SCRIBUS_ODT_IM_PLUGIN "odtimplugin")
Index: scribus/plugins/gettext/odt2im/importodt.cpp
===================================================================
--- scribus/plugins/gettext/odt2im/importodt.cpp (revision 27810)
+++ scribus/plugins/gettext/odt2im/importodt.cpp (working copy)
@@ -11,6 +11,7 @@
email : Franz.Schmid@altmuehlnet.de
***************************************************************************/
#include "importodt.h"
+#include "odtstylemappingdialog.h"
#include <QApplication>
#include <QByteArray>
@@ -43,8 +44,20 @@
void GetText2(const QString& filename, const QString& /*encoding*/, bool textOnly, bool prefix, bool append, PageItem *textItem)
{
- auto docxim = std::make_unique<ODTIm>(textItem, prefix, append);
- docxim->importFile(filename, textOnly);
+ auto odtim = std::make_unique<ODTIm>(textItem, prefix, append);
+ if (!textOnly && odtim->collectStyles(filename))
+ {
+ const QList<ODTIm::StyleInfo> styles = odtim->namedStyles();
+ if (!styles.isEmpty())
+ {
+ const QString importPrefix = prefix ? textItem->itemName() + "_" : QString();
+ OdtStyleMappingDialog dialog(textItem->doc(), styles, importPrefix, QApplication::activeWindow());
+ if (dialog.exec() != QDialog::Accepted)
+ return;
+ odtim->setStyleMappings(dialog.paragraphMappings(), dialog.characterMappings());
+ }
+ }
+ odtim->importFile(filename, textOnly);
}
ODTIm::ODTIm(PageItem *textItem, bool prefix, bool append)
Index: scribus/plugins/gettext/odt2im/odtstylemappingdialog.h
===================================================================
--- scribus/plugins/gettext/odt2im/odtstylemappingdialog.h (nonexistent)
+++ scribus/plugins/gettext/odt2im/odtstylemappingdialog.h (working copy)
@@ -0,0 +1,79 @@
+/*
+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.
+*/
+#ifndef ODTSTYLEMAPPINGDIALOG_H
+#define ODTSTYLEMAPPINGDIALOG_H
+
+#include <QDialog>
+#include <QHash>
+#include <QList>
+#include <QString>
+#include <QStringList>
+
+#include "importodt.h"
+
+class QComboBox;
+class QLabel;
+class QTableWidget;
+class QTableWidgetItem;
+class ScribusDoc;
+
+class OdtStyleMappingDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ OdtStyleMappingDialog(ScribusDoc* doc, const QList<ODTIm::StyleInfo>& styles, const QString& importPrefix, QWidget* parent = nullptr);
+
+ QHash<QString, QString> paragraphMappings() const;
+ QHash<QString, QString> characterMappings() const;
+
+private:
+ enum class MappingOrigin
+ {
+ Untouched,
+ Bulk,
+ NameMatch,
+ Manual
+ };
+
+ struct MappingRow
+ {
+ QString sourceName;
+ QString displayName;
+ QString idToolTip;
+ QString importName;
+ QComboBox* targetCombo { nullptr };
+ QTableWidgetItem* sourceItem { nullptr };
+ MappingOrigin origin { MappingOrigin::Untouched };
+ bool hasNameCollision { false };
+ bool ambiguousName { false };
+ bool wasMatchedByName { false };
+ };
+
+ QWidget* createTabPage(const QList<ODTIm::StyleInfo>& styles, const QString& family,
+ const QStringList& targetStyles, QList<MappingRow>& rows);
+ QTableWidget* createTable(const QList<ODTIm::StyleInfo>& styles, const QString& family,
+ const QStringList& targetStyles, QList<MappingRow>& rows);
+ void matchIdenticalNames(bool ignoreCase);
+ void mapRemaining(QList<MappingRow>& rows, const QString& targetStyle);
+ void resetMappings(QList<MappingRow>& rows);
+ void clearAmbiguities();
+ void updateRowWarning(MappingRow& row);
+ void updateAmbiguityWarning();
+ QHash<QString, QString> mappings(const QList<MappingRow>& rows) const;
+ QStringList paragraphStyleNames() const;
+ QStringList characterStyleNames() const;
+ void addTargetStyles(QComboBox* combo, const QStringList& targetStyles, const QString& family, bool includeImport = true) const;
+
+ ScribusDoc* m_doc { nullptr };
+ QString m_importPrefix;
+ QLabel* m_ambiguityWarningLabel { nullptr };
+ QList<MappingRow> m_paragraphRows;
+ QList<MappingRow> m_characterRows;
+};
+
+#endif
Index: scribus/plugins/gettext/odt2im/odtstylemappingdialog.cpp
===================================================================
--- scribus/plugins/gettext/odt2im/odtstylemappingdialog.cpp (nonexistent)
+++ scribus/plugins/gettext/odt2im/odtstylemappingdialog.cpp (working copy)
@@ -0,0 +1,572 @@
+/*
+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.
+*/
+#include "odtstylemappingdialog.h"
+
+#include <QAbstractItemView>
+#include <QApplication>
+#include <QCheckBox>
+#include <QComboBox>
+#include <QDialogButtonBox>
+#include <QHeaderView>
+#include <QHBoxLayout>
+#include <QIcon>
+#include <QKeyEvent>
+#include <QLabel>
+#include <QLineEdit>
+#include <QPushButton>
+#include <QStyle>
+#include <QTabWidget>
+#include <QTableWidget>
+#include <QTableWidgetItem>
+#include <QVBoxLayout>
+#include <QWheelEvent>
+
+#include "commonstrings.h"
+#include "iconmanager.h"
+#include "scribusdoc.h"
+#include "styles/charstyle.h"
+#include "styles/paragraphstyle.h"
+
+namespace
+{
+
+class OdtMappingComboBox : public QComboBox
+{
+public:
+ using QComboBox::QComboBox;
+
+protected:
+ void wheelEvent(QWheelEvent* event) override
+ {
+ event->ignore();
+ }
+};
+
+class OdtFilterLineEdit : public QLineEdit
+{
+public:
+ using QLineEdit::QLineEdit;
+
+protected:
+ void keyPressEvent(QKeyEvent* event) override
+ {
+ if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter)
+ {
+ event->accept();
+ return;
+ }
+ QLineEdit::keyPressEvent(event);
+ }
+};
+
+QIcon filterIcon()
+{
+ return IconManager::instance().loadIcon("action-filter");
+}
+
+int compactFieldWidth(const QWidget* widget, const QString& text)
+{
+ return widget->fontMetrics().horizontalAdvance(text) + 48;
+}
+
+}
+
+OdtStyleMappingDialog::OdtStyleMappingDialog(ScribusDoc* doc, const QList<ODTIm::StyleInfo>& styles, const QString& importPrefix, QWidget* parent)
+ : QDialog(parent),
+ m_doc(doc),
+ m_importPrefix(importPrefix)
+{
+ setModal(true);
+ setWindowTitle(tr("Match ODT Styles"));
+ setWindowIcon(QIcon(IconManager::instance().loadIcon("app-icon")));
+ resize(640, 420);
+
+ auto* mainLayout = new QVBoxLayout(this);
+ mainLayout->setSpacing(10);
+
+ auto* description = new QLabel(
+ tr("Map ODT styles to existing Scribus styles, or import them as new styles."), this);
+ description->setWordWrap(true);
+ mainLayout->addWidget(description);
+
+ auto* tabs = new QTabWidget(this);
+ const QStringList paragraphTargets = paragraphStyleNames();
+ const QStringList characterTargets = characterStyleNames();
+
+ QWidget* paragraphPage = createTabPage(styles, "paragraph", paragraphTargets, m_paragraphRows);
+ tabs->addTab(paragraphPage, tr("&Paragraph styles (%1)").arg(m_paragraphRows.size()));
+
+ QWidget* characterPage = createTabPage(styles, "text", characterTargets, m_characterRows);
+ tabs->addTab(characterPage, tr("&Character styles (%1)").arg(m_characterRows.size()));
+ if (m_paragraphRows.isEmpty() && !m_characterRows.isEmpty())
+ tabs->setCurrentWidget(characterPage);
+
+ mainLayout->addWidget(tabs, 1);
+
+ auto* footerLayout = new QHBoxLayout;
+ footerLayout->setSpacing(8);
+ auto* matchButton = new QPushButton(tr("Match by &Name"), this);
+ matchButton->setToolTip(tr("Match ODT styles to Scribus styles with the same name."));
+ footerLayout->addWidget(matchButton);
+ auto* ignoreCaseCheck = new QCheckBox(tr("&Ignore case"), this);
+ ignoreCaseCheck->setToolTip(tr("Ignore letter case when matching style names."));
+ footerLayout->addWidget(ignoreCaseCheck);
+ m_ambiguityWarningLabel = new QLabel(this);
+ m_ambiguityWarningLabel->setPixmap(QApplication::style()->standardIcon(QStyle::SP_MessageBoxWarning).pixmap(16, 16));
+ m_ambiguityWarningLabel->setVisible(false);
+ footerLayout->addWidget(m_ambiguityWarningLabel);
+ footerLayout->addStretch();
+ auto* buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
+ footerLayout->addWidget(buttonBox);
+ mainLayout->addLayout(footerLayout);
+
+ auto* cancelButton = buttonBox->button(QDialogButtonBox::Cancel);
+ auto* okButton = buttonBox->button(QDialogButtonBox::Ok);
+ QWidget::setTabOrder(matchButton, ignoreCaseCheck);
+ if (cancelButton && okButton)
+ {
+ QWidget::setTabOrder(ignoreCaseCheck, cancelButton);
+ QWidget::setTabOrder(cancelButton, okButton);
+ }
+
+ connect(matchButton, &QPushButton::clicked, this, [this, ignoreCaseCheck]()
+ {
+ matchIdenticalNames(ignoreCaseCheck->isChecked());
+ });
+ connect(ignoreCaseCheck, &QCheckBox::toggled, this, [this](bool checked)
+ {
+ if (!checked)
+ clearAmbiguities();
+ });
+ connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
+ connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
+}
+
+QHash<QString, QString> OdtStyleMappingDialog::paragraphMappings() const
+{
+ return mappings(m_paragraphRows);
+}
+
+QHash<QString, QString> OdtStyleMappingDialog::characterMappings() const
+{
+ return mappings(m_characterRows);
+}
+
+QWidget* OdtStyleMappingDialog::createTabPage(const QList<ODTIm::StyleInfo>& styles, const QString& family,
+ const QStringList& targetStyles, QList<MappingRow>& rows)
+{
+ auto* page = new QWidget(this);
+ auto* pageLayout = new QVBoxLayout(page);
+ pageLayout->setContentsMargins(8, 8, 8, 8);
+ pageLayout->setSpacing(8);
+
+ QTableWidget* table = createTable(styles, family, targetStyles, rows);
+ if (table->rowCount() == 0)
+ {
+ delete table;
+ auto* emptyLabel = new QLabel(
+ family == "text" ? tr("No character styles found.") : tr("No paragraph styles found."), page);
+ emptyLabel->setAlignment(Qt::AlignCenter);
+ pageLayout->addWidget(emptyLabel, 1);
+ return page;
+ }
+
+ auto* filterLayout = new QHBoxLayout;
+ filterLayout->setSpacing(8);
+ auto* filterEdit = new OdtFilterLineEdit(page);
+ filterEdit->setPlaceholderText(tr("Filter styles…"));
+ filterEdit->addAction(filterIcon(), QLineEdit::LeadingPosition);
+ filterEdit->setClearButtonEnabled(true);
+ filterEdit->setFixedWidth(compactFieldWidth(filterEdit, tr("Import as New Style")));
+ filterLayout->addWidget(filterEdit);
+ auto* mappedOnlyCheck = new QCheckBox(tr("&Mapped only"), page);
+ mappedOnlyCheck->setToolTip(tr("Show only styles mapped to an existing Scribus style."));
+ filterLayout->addWidget(mappedOnlyCheck);
+ auto* unmappedOnlyCheck = new QCheckBox(tr("&Unmapped only"), page);
+ unmappedOnlyCheck->setToolTip(tr("Show only styles not mapped to an existing Scribus style."));
+ filterLayout->addWidget(unmappedOnlyCheck);
+ filterLayout->addStretch();
+ pageLayout->addLayout(filterLayout);
+ pageLayout->addWidget(table, 1);
+
+ auto* bulkLayout = new QHBoxLayout;
+ bulkLayout->setSpacing(0);
+ bulkLayout->setContentsMargins(0, 8, 0, 0);
+ auto* bulkLabel = new QLabel(tr("Map &remaining styles to:"), page);
+ bulkLayout->addWidget(bulkLabel);
+ bulkLayout->addSpacing(8);
+ auto* bulkCombo = new OdtMappingComboBox(page);
+ addTargetStyles(bulkCombo, targetStyles, family);
+ bulkCombo->setToolTip(tr("Choose a target for unmapped styles."));
+ bulkCombo->setFixedWidth(compactFieldWidth(bulkCombo, tr("Import as New Style")) + 5);
+ bulkLabel->setBuddy(bulkCombo);
+ bulkLayout->addWidget(bulkCombo);
+ bulkLayout->addSpacing(8);
+ auto* applyButton = new QPushButton(tr("&Apply"), page);
+ applyButton->setToolTip(tr("Apply this target to all remaining unmapped styles."));
+ applyButton->setFixedWidth(applyButton->sizeHint().width() - 10);
+ bulkLayout->addWidget(applyButton);
+ bulkLayout->addStretch();
+ auto* resetButton = new QPushButton(IconManager::instance().loadIcon("reset"), tr("Reset"), page);
+ resetButton->setToolTip(tr("Reset all mappings in this tab."));
+ bulkLayout->addWidget(resetButton);
+ pageLayout->addLayout(bulkLayout);
+
+ QWidget::setTabOrder(filterEdit, mappedOnlyCheck);
+ QWidget::setTabOrder(mappedOnlyCheck, unmappedOnlyCheck);
+ QWidget* previousWidget = unmappedOnlyCheck;
+ for (const MappingRow& row : rows)
+ {
+ QWidget::setTabOrder(previousWidget, row.targetCombo);
+ previousWidget = row.targetCombo;
+ }
+ QWidget::setTabOrder(previousWidget, bulkCombo);
+ QWidget::setTabOrder(bulkCombo, applyButton);
+ QWidget::setTabOrder(applyButton, resetButton);
+
+ QList<MappingRow>* rowsPtr = &rows;
+ auto updateFilter = [table, filterEdit, unmappedOnlyCheck, mappedOnlyCheck, rowsPtr]()
+ {
+ const QString filter = filterEdit->text();
+ for (int row = 0; row < table->rowCount(); ++row)
+ {
+ const QTableWidgetItem* item = table->item(row, 0);
+ if (!item)
+ continue;
+
+ const bool textMatches = filter.isEmpty()
+ || item->text().contains(filter, Qt::CaseInsensitive)
+ || item->data(Qt::UserRole).toString().contains(filter, Qt::CaseInsensitive);
+ const bool isUnmapped = row < rowsPtr->size()
+ && rowsPtr->at(row).targetCombo->currentData().toString().isEmpty();
+ const bool mappingMatches = (!unmappedOnlyCheck->isChecked() && !mappedOnlyCheck->isChecked())
+ || (unmappedOnlyCheck->isChecked() && isUnmapped)
+ || (mappedOnlyCheck->isChecked() && !isUnmapped);
+ table->setRowHidden(row, !(textMatches && mappingMatches));
+ }
+ };
+
+ connect(filterEdit, &QLineEdit::textChanged, this, [updateFilter](const QString&)
+ {
+ updateFilter();
+ });
+ connect(unmappedOnlyCheck, &QCheckBox::toggled, this, [mappedOnlyCheck, updateFilter](bool checked)
+ {
+ if (checked)
+ mappedOnlyCheck->setChecked(false);
+ updateFilter();
+ });
+ connect(mappedOnlyCheck, &QCheckBox::toggled, this, [unmappedOnlyCheck, updateFilter](bool checked)
+ {
+ if (checked)
+ unmappedOnlyCheck->setChecked(false);
+ updateFilter();
+ });
+ for (const MappingRow& row : rows)
+ {
+ connect(row.targetCombo, &QComboBox::currentIndexChanged, this, [updateFilter](int)
+ {
+ updateFilter();
+ });
+ }
+
+ connect(applyButton, &QPushButton::clicked, this, [this, rowsPtr, bulkCombo]()
+ {
+ mapRemaining(*rowsPtr, bulkCombo->currentData().toString());
+ });
+ connect(resetButton, &QPushButton::clicked, this, [this, rowsPtr]()
+ {
+ resetMappings(*rowsPtr);
+ });
+
+ return page;
+}
+
+QTableWidget* OdtStyleMappingDialog::createTable(const QList<ODTIm::StyleInfo>& styles, const QString& family,
+ const QStringList& targetStyles, QList<MappingRow>& rows)
+{
+ auto* table = new QTableWidget(this);
+ table->setColumnCount(2);
+ table->setHorizontalHeaderLabels(QStringList() << tr("ODT Style") << tr("Scribus Style"));
+ table->verticalHeader()->setVisible(false);
+ auto* header = table->horizontalHeader();
+ header->setMinimumHeight(header->sizeHint().height() + 5);
+ header->setSectionResizeMode(0, QHeaderView::Stretch);
+ header->setSectionResizeMode(1, QHeaderView::Stretch);
+ table->setSelectionMode(QAbstractItemView::NoSelection);
+ table->setEditTriggers(QAbstractItemView::NoEditTriggers);
+ table->setAlternatingRowColors(true);
+ table->setFocusPolicy(Qt::NoFocus);
+ table->setTabKeyNavigation(false);
+
+ for (const ODTIm::StyleInfo& style : styles)
+ {
+ if (style.family != family)
+ continue;
+
+ const int row = table->rowCount();
+ table->insertRow(row);
+
+ auto* sourceItem = new QTableWidgetItem(style.displayName);
+ sourceItem->setData(Qt::UserRole, style.sourceName);
+ table->setItem(row, 0, sourceItem);
+
+ auto* combo = new OdtMappingComboBox(table);
+ addTargetStyles(combo, targetStyles, family);
+ table->setCellWidget(row, 1, combo);
+
+ MappingRow mappingRow;
+ mappingRow.sourceName = style.sourceName;
+ mappingRow.displayName = style.displayName;
+ mappingRow.idToolTip = (style.sourceName != style.displayName)
+ ? tr("ODT style ID: %1").arg(style.sourceName) : QString();
+ mappingRow.importName = m_importPrefix + style.displayName;
+ mappingRow.targetCombo = combo;
+ mappingRow.sourceItem = sourceItem;
+ mappingRow.hasNameCollision = targetStyles.contains(mappingRow.importName);
+ rows.append(mappingRow);
+
+ QList<MappingRow>* rowsPtr = &rows;
+ connect(combo, &QComboBox::currentIndexChanged, this, [this, rowsPtr, combo](int)
+ {
+ for (MappingRow& mappedRow : *rowsPtr)
+ {
+ if (mappedRow.targetCombo != combo)
+ continue;
+ updateRowWarning(mappedRow);
+ break;
+ }
+ });
+ connect(combo, &QComboBox::activated, this, [this, rowsPtr, combo](int)
+ {
+ for (MappingRow& mappedRow : *rowsPtr)
+ {
+ if (mappedRow.targetCombo != combo)
+ continue;
+ mappedRow.origin = MappingOrigin::Manual;
+ mappedRow.ambiguousName = false;
+ updateRowWarning(mappedRow);
+ updateAmbiguityWarning();
+ break;
+ }
+ });
+ updateRowWarning(rows.last());
+ }
+
+ return table;
+}
+
+void OdtStyleMappingDialog::matchIdenticalNames(bool ignoreCase)
+{
+ clearAmbiguities();
+
+ auto matchRows = [this, ignoreCase](QList<MappingRow>& rows)
+ {
+ for (MappingRow& row : rows)
+ {
+ if (row.origin == MappingOrigin::Manual && !row.wasMatchedByName)
+ continue;
+
+ auto findMatchingTarget = [ignoreCase, &row](const QString& sourceName)
+ {
+ if (!ignoreCase)
+ return row.targetCombo->findData(sourceName);
+
+ int matchIndex = -1;
+ for (int i = 0; i < row.targetCombo->count(); ++i)
+ {
+ const QString targetName = row.targetCombo->itemData(i).toString();
+ if (targetName.isEmpty() || QString::compare(targetName, sourceName, Qt::CaseInsensitive) != 0)
+ continue;
+ if (matchIndex >= 0)
+ return -2;
+ matchIndex = i;
+ }
+ return matchIndex;
+ };
+
+ int index = findMatchingTarget(row.displayName);
+ if (index == -1 && row.sourceName != row.displayName)
+ index = findMatchingTarget(row.sourceName);
+
+ if (index == -2)
+ {
+ row.ambiguousName = true;
+ row.origin = MappingOrigin::Untouched;
+ const int importIndex = row.targetCombo->findData(QString());
+ if (importIndex >= 0)
+ row.targetCombo->setCurrentIndex(importIndex);
+ updateRowWarning(row);
+ continue;
+ }
+
+ if (index >= 0)
+ {
+ row.targetCombo->setCurrentIndex(index);
+ row.origin = MappingOrigin::NameMatch;
+ row.wasMatchedByName = true;
+ }
+ updateRowWarning(row);
+ }
+ };
+
+ matchRows(m_paragraphRows);
+ matchRows(m_characterRows);
+ updateAmbiguityWarning();
+}
+
+void OdtStyleMappingDialog::mapRemaining(QList<MappingRow>& rows, const QString& targetStyle)
+{
+ for (MappingRow& row : rows)
+ {
+ if (row.origin == MappingOrigin::Manual || row.origin == MappingOrigin::NameMatch)
+ continue;
+
+ const int index = row.targetCombo->findData(targetStyle);
+ if (index >= 0)
+ {
+ row.targetCombo->setCurrentIndex(index);
+ row.origin = MappingOrigin::Bulk;
+ row.ambiguousName = false;
+ updateRowWarning(row);
+ }
+ }
+ updateAmbiguityWarning();
+}
+
+void OdtStyleMappingDialog::resetMappings(QList<MappingRow>& rows)
+{
+ for (MappingRow& row : rows)
+ {
+ const int importIndex = row.targetCombo->findData(QString());
+ if (importIndex >= 0)
+ row.targetCombo->setCurrentIndex(importIndex);
+ row.origin = MappingOrigin::Untouched;
+ row.ambiguousName = false;
+ row.wasMatchedByName = false;
+ updateRowWarning(row);
+ }
+ updateAmbiguityWarning();
+}
+
+void OdtStyleMappingDialog::clearAmbiguities()
+{
+ for (MappingRow& row : m_paragraphRows)
+ {
+ row.ambiguousName = false;
+ updateRowWarning(row);
+ }
+ for (MappingRow& row : m_characterRows)
+ {
+ row.ambiguousName = false;
+ updateRowWarning(row);
+ }
+ updateAmbiguityWarning();
+}
+
+void OdtStyleMappingDialog::updateRowWarning(MappingRow& row)
+{
+ if (!row.sourceItem || !row.targetCombo)
+ return;
+
+ QStringList toolTips;
+ if (!row.idToolTip.isEmpty())
+ toolTips.append(row.idToolTip);
+
+ const bool collisionWarning = row.hasNameCollision && row.targetCombo->currentData().toString().isEmpty();
+ if (collisionWarning)
+ toolTips.append(tr("A Scribus style named \"%1\" already exists.\nThe imported ODT style will be renamed.").arg(row.importName));
+ if (row.ambiguousName)
+ toolTips.append(tr("Multiple Scribus styles match this name when case is ignored."));
+
+ if (collisionWarning || row.ambiguousName)
+ row.sourceItem->setIcon(QApplication::style()->standardIcon(QStyle::SP_MessageBoxWarning));
+ else
+ row.sourceItem->setIcon(QIcon());
+ row.sourceItem->setToolTip(toolTips.join('\n'));
+}
+
+void OdtStyleMappingDialog::updateAmbiguityWarning()
+{
+ if (!m_ambiguityWarningLabel)
+ return;
+
+ int ambiguousCount = 0;
+ for (const MappingRow& row : m_paragraphRows)
+ ambiguousCount += row.ambiguousName ? 1 : 0;
+ for (const MappingRow& row : m_characterRows)
+ ambiguousCount += row.ambiguousName ? 1 : 0;
+
+ m_ambiguityWarningLabel->setVisible(ambiguousCount > 0);
+ if (ambiguousCount == 1)
+ m_ambiguityWarningLabel->setToolTip(tr("1 style name has multiple matches when case is ignored and was left unmapped."));
+ else if (ambiguousCount > 1)
+ m_ambiguityWarningLabel->setToolTip(tr("%1 style names have multiple matches when case is ignored and were left unmapped.").arg(ambiguousCount));
+ else
+ m_ambiguityWarningLabel->setToolTip(QString());
+}
+
+QHash<QString, QString> OdtStyleMappingDialog::mappings(const QList<MappingRow>& rows) const
+{
+ QHash<QString, QString> result;
+ for (const MappingRow& row : rows)
+ {
+ const QString targetStyle = row.targetCombo->currentData().toString();
+ if (!targetStyle.isEmpty())
+ result.insert(row.sourceName, targetStyle);
+ }
+ return result;
+}
+
+QStringList OdtStyleMappingDialog::paragraphStyleNames() const
+{
+ QStringList result;
+ if (!m_doc)
+ return result;
+
+ for (int i = 0; i < m_doc->paragraphStyles().count(); ++i)
+ {
+ const ParagraphStyle& style = m_doc->paragraphStyles()[i];
+ if (!style.name().isEmpty() && !style.isDefaultStyle())
+ result.append(style.name());
+ }
+ result.sort(Qt::CaseInsensitive);
+ result.prepend(CommonStrings::DefaultParagraphStyle);
+ return result;
+}
+
+QStringList OdtStyleMappingDialog::characterStyleNames() const
+{
+ QStringList result;
+ if (!m_doc)
+ return result;
+
+ for (int i = 0; i < m_doc->charStyles().count(); ++i)
+ {
+ const CharStyle& style = m_doc->charStyles()[i];
+ if (!style.name().isEmpty() && !style.isDefaultStyle())
+ result.append(style.name());
+ }
+ result.sort(Qt::CaseInsensitive);
+ result.prepend(CommonStrings::DefaultCharacterStyle);
+ return result;
+}
+
+void OdtStyleMappingDialog::addTargetStyles(QComboBox* combo, const QStringList& targetStyles, const QString& family, bool includeImport) const
+{
+ if (includeImport)
+ combo->addItem(tr("Import as New Style"), QString());
+ for (const QString& targetStyle : targetStyles)
+ {
+ QString displayName = targetStyle;
+ if (family == "paragraph" && targetStyle == CommonStrings::DefaultParagraphStyle)
+ displayName = CommonStrings::trDefaultParagraphStyle;
+ else if (family == "text" && targetStyle == CommonStrings::DefaultCharacterStyle)
+ displayName = CommonStrings::trDefaultCharacterStyle;
+ combo->addItem(displayName, targetStyle);
+ }
+}
Index: resources/iconsets/1_7_0/1_7_0.xml
===================================================================
--- resources/iconsets/1_7_0/1_7_0.xml (revision 27810)
+++ resources/iconsets/1_7_0/1_7_0.xml (working copy)
@@ -41,6 +41,7 @@
<!-- Action -->
<icon id="add" file="16/action-add.svg" />
+ <icon id="action-filter" file="16/action-filter.svg" />
<icon id="chain-closed" file="16/action-link.svg" />
<icon id="chain-open" file="16/action-unlink.svg" />
<icon id="clear-right" file="16/action-backspace-reverse.svg" />
Index: resources/iconsets/1_7_0/16/action-filter.svg
===================================================================
--- resources/iconsets/1_7_0/16/action-filter.svg (nonexistent)
+++ resources/iconsets/1_7_0/16/action-filter.svg (working copy)
@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><style>@import '../colors.css';</style>
+<path d="M1 2H15L9.5 8V13.5L6.5 15V8L1 2Z"/>
+</svg>
|
| Date Modified | Username | Field | Change |
|---|---|---|---|
| 2011-07-07 17:56 | ale | New Issue | |
| 2011-07-07 17:57 | ale | File Added: scribus_import_odt_styles.png | |
| 2011-07-07 17:57 | ale | Note Added: 0026551 | |
| 2015-09-17 20:08 | Kunda | Category | Story Editor / Text Frames => Story Ed/Txt Frames |
| 2015-09-17 20:12 | Kunda | Category | Story Ed/Txt Frames => Story Editor / Text Frames |
| 2015-11-25 05:08 | Kunda | Relationship added | related to 0011157 |
| 2016-05-09 15:17 | Kunda | Relationship added | related to 0009464 |
| 2016-05-09 15:17 | Kunda | Tag Attached: styles | |
| 2016-05-09 15:18 | Kunda | Tag Attached: mockup | |
| 2016-05-09 15:33 | Kunda | Tag Attached: ODF | |
| 2025-04-27 19:16 | cbradney | Category | Story Editor / Text Frames => Text Frames / Story Editor |
| 2026-09-03 03:12 | qirat | Note Added: 0054402 | |
| 2026-09-03 03:12 | qirat | File Added: osm1-gettext2-cancel-safety-v1.0.patch | |
| 2026-09-03 03:12 | qirat | File Added: osm2-style-mapping-engine-v1.1.patch | |
| 2026-09-03 03:12 | qirat | File Added: osm3-style-matching-dialog-v1.25.patch | |
| 2026-09-03 03:12 | qirat | File Added: osm-odt-style-matching-PROPOSED.png | |
| 2026-09-03 03:13 | qirat | Tag Attached: #please_test | |
| 2026-09-03 03:19 | qirat | Note Added: 0054403 | |
| 2026-09-03 05:18 | qirat | Note Added: 0054404 | |
| 2026-09-03 05:18 | qirat | File Added: osm3-style-matching-dialog-v1.26.patch | |
| 2026-09-03 05:18 | qirat | File Added: osm-odt-style-matching-PROPOSED-v1.26.png | |
| 2026-09-03 05:18 | qirat | Tag Attached: #patch_to_be_reviewed |