View Issue Details

IDProjectCategoryView StatusLast Update
0017922ScribusUser Interfacepublic2026-08-16 07:46
Reporterqirat Assigned To 
PrioritynormalSeverityfeatureReproducibilityN/A
Status newResolutionopen 
PlatformLinuxOSFedora WorkstationOS Version44
Product Version1.7.4.svn 
Summary0017922: [PATCH] Preview paragraph/character styles with their own font and colour in PP combos
DescriptionParaStyleComboBox/CharStyleComboBox go from plain name lists to per-row previews.

* styleComboPreviewFont() — builds each row's QFont from the style's own family/bold/italic/underline/strikethrough/caps, via Qt::FontRole. Italic checks both ScFace::isItalic() and the face's style-string (catches Scribus's faked-slant fonts too).
* styleComboResolveColor() — resolves font/background color+shade via ScColorEngine::getDisplayColor(), Qt::ForegroundRole/BackgroundRole. Paragraph rows prefer the paragraph's own background over the character style's.
* styleComboPreviewGlyph() — Character Style only. 14×14 glyph via QFontDatabase::writingSystems(); non-Latin scripts get their own sample char, Latin (and Greek/Cyrillic/Vietnamese/Symbol, excluded as unreliable secondary-script noise) get a fixed "A".

All wired through one shared styleComboSetPreviewData(), called per-item instead of the old bulk addItems().

Read-only display logic — no document model or file-format changes.
Additional InformationWritten against r27771.

It is a small patch to my surprise.
(AI helped a lot.)
TagsNo tags attached.
Attached Files
r27771-p1-stylecombo-font-preview-v1.06.patch (9,748 bytes)   
Index: scribus/ui/stylecombos.cpp
===================================================================
--- scribus/ui/stylecombos.cpp	(revision 27771)
+++ scribus/ui/stylecombos.cpp	(working copy)
@@ -22,14 +22,161 @@
  ***************************************************************************/
 
 #include "commonstrings.h"
+#include "sccolorengine.h"
 #include "scribusdoc.h"
 #include "stylecombos.h"
 
+#include "fonts/scface.h"
+#include <QFont>
+#include <QFontDatabase>
 #include <QListView>
+#include <QPainter>
+#include <QPixmap>
 
 #include "scpage.h"
 #include "util.h"
 
+namespace {
+// Builds a QFont that reflects a style's own character formatting, so a
+// combo row previews roughly what the style looks like instead of just
+// naming it. Deliberately partial: point size is left at the combo's own
+// size (a heading-sized style shouldn't grow its own row, or the whole
+// list); stroke and shadow effects have no equivalent in QFont or Qt's
+// item-data roles and would need custom painting to show at all.
+QFont styleComboPreviewFont(const CharStyle& cstyle)
+{
+	const ScFace& face = cstyle.font();
+	QFont font;
+	font.setFamily(face.family());
+	font.setBold(face.isBold());
+	// A font with no real italic file can still be marked italic via its
+	// Style field (e.g. "EB Garamond" + "Italic"), which Scribus renders as
+	// a faked slant rather than the font's own design. isItalic() alone
+	// only catches the case where the chosen face itself is truly italic,
+	// so check the style name too - either source counts as "wants italic"
+	// for preview purposes, matching what actually renders on canvas.
+	bool italicByStyleName = face.style().contains(QLatin1String("Italic"), Qt::CaseInsensitive)
+		|| face.style().contains(QLatin1String("Oblique"), Qt::CaseInsensitive);
+	font.setItalic(face.isItalic() || italicByStyleName);
+	font.setUnderline(cstyle.features().contains(CharStyle::UNDERLINE));
+	font.setStrikeOut(cstyle.features().contains(CharStyle::STRIKETHROUGH));
+	if (cstyle.features().contains(CharStyle::ALLCAPS))
+		font.setCapitalization(QFont::AllUppercase);
+	else if (cstyle.features().contains(CharStyle::SMALLCAPS))
+		font.setCapitalization(QFont::SmallCaps);
+	return font;
+}
+
+// Resolves a named document color (as stored on a style) to a QColor ready
+// for display, at the given shade - "None" or a name no longer in the
+// document's color list both mean "nothing to show", not black.
+QColor styleComboResolveColor(const QString& colorName, double shade, const ScribusDoc* doc)
+{
+	if (colorName.isEmpty() || colorName == CommonStrings::None || !doc->PageColors.contains(colorName))
+		return QColor();
+	return ScColorEngine::getDisplayColor(doc->PageColors[colorName], doc, shade);
+}
+
+// A single glyph identifying the row's own font, sized to match the
+// paragraph-style badges used elsewhere in Style Scope. Checks the font's
+// full list of supported writing systems (not just its single primary
+// one, which can be Latin even for a font whose real purpose is another
+// script - several "Noto Sans <Script>" fonts are like this) and prefers
+// the first genuinely non-Latin one found there. A few systems are
+// excluded even when present: Vietnamese and Symbol commonly turn up as
+// incidental secondary coverage on an ordinary Latin font (extended accent
+// support, a handful of dingbats) rather than a real second script; Greek
+// and Cyrillic are excluded for a different reason - several well-known
+// Latin serif families (Garamond-derived ones among them) deliberately
+// ship a matching classical Greek design alongside their Latin one, which
+// is a real script but not the one worth showing here. None of these
+// exclusions are reversible from the data Qt exposes without misreading
+// genuinely Greek/Cyrillic/Vietnamese-purposed fonts as Latin instead, so
+// this is a practical trade-off, not a perfect classification method. When
+// a non-excluded system is found, the glyph is Qt's own representative
+// sample character for it - a font's family name is often Latin-
+// transliterated and gives no visual sense of the script itself, so this
+// fills that gap. Otherwise, the glyph is a fixed "A": the point of this
+// preview is comparing a font's shape against others at a glance, which
+// needs the same letter every time, not a different one per style name.
+QIcon styleComboPreviewGlyph(const QFont& font)
+{
+	QString glyph = QStringLiteral("A");
+	const QList<QFontDatabase::WritingSystem> systems = QFontDatabase::writingSystems(font.family());
+	for (QFontDatabase::WritingSystem system : systems)
+	{
+		if (system == QFontDatabase::Latin || system == QFontDatabase::Any
+			|| system == QFontDatabase::Vietnamese || system == QFontDatabase::Symbol
+			|| system == QFontDatabase::Greek || system == QFontDatabase::Cyrillic)
+			continue;
+		QString sample = QFontDatabase::writingSystemSample(system);
+		if (!sample.isEmpty())
+		{
+			glyph = sample.left(1);
+			break;
+		}
+	}
+
+	// Family only, at neutral weight/slant - the glyph identifies the font,
+	// it isn't meant to also carry the style's own bold/italic/etc (the row
+	// text itself already does that).
+	QFont glyphFont(font.family());
+
+	const int size = 14;
+	QPixmap pixmap(size, size);
+	pixmap.fill(Qt::transparent);
+	QPainter painter(&pixmap);
+	painter.setFont(glyphFont);
+	painter.drawText(QRect(0, 0, size, size), Qt::AlignCenter, glyph);
+	painter.end();
+
+	// Qt auto-generates a dimmed "Selected" variant of an icon unless both
+	// modes are given the same pixmap explicitly - without this, the glyph
+	// would wash out on a selected row.
+	QIcon icon;
+	icon.addPixmap(pixmap, QIcon::Normal);
+	icon.addPixmap(pixmap, QIcon::Selected);
+	return icon;
+}
+
+// Applies a style's preview (font, colors, and - for the character style
+// combo only - a per-row glyph) to one combo item. showPreviewGlyph is
+// false for the paragraph style combo: a plain list reads better there,
+// since paragraph styles are usually distinguished by more than font
+// alone, and every row already carries the font/color preview regardless.
+void styleComboSetPreviewData(QComboBox* combo, int itemIndex, const CharStyle& cstyle, const ScribusDoc* doc,
+                               const QString& paraBackColorName = QString(), double paraBackShade = 100.0,
+                               bool showPreviewGlyph = false)
+{
+	QFont font = styleComboPreviewFont(cstyle);
+	combo->setItemData(itemIndex, font, Qt::FontRole);
+
+	if (showPreviewGlyph)
+	{
+		QIcon glyph = styleComboPreviewGlyph(font);
+		if (!glyph.isNull())
+			combo->setItemData(itemIndex, glyph, Qt::DecorationRole);
+	}
+
+	QColor fg = styleComboResolveColor(cstyle.fillColor(), cstyle.fillShade(), doc);
+	if (fg.isValid())
+		combo->setItemData(itemIndex, fg, Qt::ForegroundRole);
+
+	// A paragraph's own background (set on the Properties tab, shading the
+	// whole paragraph) takes priority over its default character style's
+	// background here - the two are layered separately on canvas, but a
+	// combo row only has room for one swatch, and the paragraph-wide one is
+	// the more paragraph-style-appropriate signal. Character style rows
+	// never pass paraBackColorName, so they always fall through to their
+	// own backColor.
+	QColor bg = styleComboResolveColor(paraBackColorName, paraBackShade, doc);
+	if (!bg.isValid())
+		bg = styleComboResolveColor(cstyle.backColor(), cstyle.backShade(), doc);
+	if (bg.isValid())
+		combo->setItemData(itemIndex, bg, Qt::BackgroundRole);
+}
+}
+
 ParaStyleComboBox::ParaStyleComboBox(QWidget* parent) : QComboBox(parent)
 {
 	setEditable(false);
@@ -124,9 +271,15 @@
 	clear();
 	if (m_doc != nullptr)
 	{
-		QStringList st;
 		addItem( firstItemString()  );
+
+		int defaultIndex = m_doc->paragraphStyles().find(CommonStrings::DefaultParagraphStyle);
 		addItem( CommonStrings::trDefaultParagraphStyle );
+		if (defaultIndex >= 0)
+			styleComboSetPreviewData(this, count() - 1, m_doc->paragraphStyles()[defaultIndex].charStyle(), m_doc,
+				m_doc->paragraphStyles()[defaultIndex].backgroundColor(), m_doc->paragraphStyles()[defaultIndex].backgroundShade());
+
+		QStringList st;
 		for (int i = 0; i < m_doc->paragraphStyles().count(); ++i)
 		{
 			const ParagraphStyle& paraStyle = m_doc->paragraphStyles()[i];
@@ -134,7 +287,14 @@
 				st.append(paraStyle.name());
 		}
 		st.sort();
-		addItems(st);
+		for (const QString& styleName : st)
+		{
+			addItem(styleName);
+			int idx = m_doc->paragraphStyles().find(styleName);
+			if (idx >= 0)
+				styleComboSetPreviewData(this, count() - 1, m_doc->paragraphStyles()[idx].charStyle(), m_doc,
+					m_doc->paragraphStyles()[idx].backgroundColor(), m_doc->paragraphStyles()[idx].backgroundShade());
+		}
 
 		if (oldStyleName.length() > 0)
 		{
@@ -261,9 +421,14 @@
 	clear();
 	if (m_doc != nullptr)
 	{
-		QStringList st;
 		addItem( firstItemString()  );
+
+		int defaultIndex = m_doc->charStyles().find(CommonStrings::DefaultCharacterStyle);
 		addItem( CommonStrings::trDefaultCharacterStyle );
+		if (defaultIndex >= 0)
+			styleComboSetPreviewData(this, count() - 1, m_doc->charStyles()[defaultIndex], m_doc, QString(), 100.0, true);
+
+		QStringList st;
 		for (int i = 0; i < m_doc->charStyles().count(); ++i)
 		{
 			const CharStyle& charStyle = m_doc->charStyles()[i];
@@ -271,7 +436,13 @@
 				st.append(charStyle.name());
 		}
 		st.sort();
-		addItems(st);
+		for (const QString& styleName : st)
+		{
+			addItem(styleName);
+			int idx = m_doc->charStyles().find(styleName);
+			if (idx >= 0)
+				styleComboSetPreviewData(this, count() - 1, m_doc->charStyles()[idx], m_doc, QString(), 100.0, true);
+		}
 
 		if (oldStyleName.length() > 0)
 		{
PatchYes

Activities

qirat

2026-08-14 02:47

reporter   ~0054203

Preview of what it does.
pp-char-style-combo-preview.png (40,665 bytes)   
pp-char-style-combo-preview.png (40,665 bytes)   
pp-para-style-combo-preview.png (42,555 bytes)   
pp-para-style-combo-preview.png (42,555 bytes)   

qirat

2026-08-14 04:05

reporter   ~0054204

It can do more: the font/BG color in right shade, Small/All-caps etc. Para Style's BG color too.

qirat

2026-08-14 06:39

reporter   ~0054205

ale

2026-08-15 07:28

manager   ~0054206

Not my cup of tea, but: that won't probably work for dark themes...

At first, I thought about the need of a setting, if people want a more calm UI, but then I realized that people who want a calm UI, are also people who will refrain from going crazy with the colors and fonts of their styles : - )

Mixed feelings about this...
But I admit that I tend to apply the styles by name, not by look.

qirat

2026-08-15 07:47

reporter   ~0054207

Yes, I agree @ale. I am not big on colours either. I just exaggerated things to show the example, what it can do.

Also, I have no issue with a little preview of the style showing in the combo. I would like that. (the first two screenshots).

And, thanks a lot for you valuable comment. Look forward to hear more from you on other patches as well. When you find time, of course.

qirat

2026-08-16 07:46

reporter   ~0054211

Fixed, show as per theme.
stylecombo-font-preview-v1.15.patch (18,887 bytes)   
Index: scribus/ui/stylecombos.h
===================================================================
--- scribus/ui/stylecombos.h	(revision 27773)
+++ scribus/ui/stylecombos.h	(working copy)
@@ -29,6 +29,7 @@
 #include "scribusapi.h"
 
 class QEvent;
+class QPaintEvent;
 class ScribusDoc;
 
 /**
@@ -95,6 +96,7 @@
 
 	void changeEvent(QEvent *e) override;
 	void languageChange();
+	void paintEvent(QPaintEvent *e) override;
 
 protected slots:
 	void selectedStyle(int e);
Index: scribus/ui/stylecombos.cpp
===================================================================
--- scribus/ui/stylecombos.cpp	(revision 27773)
+++ scribus/ui/stylecombos.cpp	(working copy)
@@ -22,14 +22,292 @@
  ***************************************************************************/
 
 #include "commonstrings.h"
+#include "sccolorengine.h"
 #include "scribusdoc.h"
 #include "stylecombos.h"
 
+#include "fonts/scface.h"
+#include <QApplication>
+#include <QFont>
+#include <QFontDatabase>
+#include <QFontMetrics>
 #include <QListView>
+#include <QPainter>
+#include <QPixmap>
+#include <QStyleOptionComboBox>
+#include <QStyledItemDelegate>
 
 #include "scpage.h"
 #include "util.h"
 
+namespace {
+// Custom item-data roles used only by the character-style combo's glyph
+// preview - see StyleComboGlyphDelegate.
+constexpr int StyleComboGlyphCharRole = Qt::UserRole + 1;
+
+// Builds a QFont that reflects a style's own character formatting, so a
+// combo row previews roughly what the style looks like instead of just
+// naming it. Deliberately partial: point size is left at the combo's own
+// size (a heading-sized style shouldn't grow its own row, or the whole
+// list); stroke and shadow effects have no equivalent in QFont or Qt's
+// item-data roles and would need custom painting to show at all.
+QFont styleComboPreviewFont(const CharStyle& cstyle)
+{
+	const ScFace& face = cstyle.font();
+	QFont font;
+	font.setFamily(face.family());
+	font.setBold(face.isBold());
+	// A font with no real italic file can still be marked italic via its
+	// Style field (e.g. "EB Garamond" + "Italic"), which Scribus renders as
+	// a faked slant rather than the font's own design. isItalic() alone
+	// only catches the case where the chosen face itself is truly italic,
+	// so check the style name too - either source counts as "wants italic"
+	// for preview purposes, matching what actually renders on canvas.
+	bool italicByStyleName = face.style().contains(QLatin1String("Italic"), Qt::CaseInsensitive)
+		|| face.style().contains(QLatin1String("Oblique"), Qt::CaseInsensitive);
+	font.setItalic(face.isItalic() || italicByStyleName);
+	font.setUnderline(cstyle.features().contains(CharStyle::UNDERLINE));
+	font.setStrikeOut(cstyle.features().contains(CharStyle::STRIKETHROUGH));
+	if (cstyle.features().contains(CharStyle::ALLCAPS))
+		font.setCapitalization(QFont::AllUppercase);
+	else if (cstyle.features().contains(CharStyle::SMALLCAPS))
+		font.setCapitalization(QFont::SmallCaps);
+	return font;
+}
+
+// Resolves a named document color (as stored on a style) to a QColor ready
+// for display, at the given shade - "None" or a name no longer in the
+// document's color list both mean "nothing to show", not black.
+QColor styleComboResolveColor(const QString& colorName, double shade, const ScribusDoc* doc)
+{
+	if (colorName.isEmpty() || colorName == CommonStrings::None || !doc->PageColors.contains(colorName))
+		return QColor();
+	return ScColorEngine::getDisplayColor(doc->PageColors[colorName], doc, shade);
+}
+
+// Quick perceived-brightness estimate (0..255), good enough for a contrast
+// check - not a color-managed luminance calculation.
+double styleComboPerceivedBrightness(const QColor& c)
+{
+	return 0.299 * c.red() + 0.587 * c.green() + 0.114 * c.blue();
+}
+
+// A style's own ink color is shown as-is whenever it reads fine - a black
+// heading style should still look black. It only becomes a problem where
+// the row it lands on doesn't share the document's white-paper assumption,
+// namely the app's own (possibly dark-themed) list background behind rows
+// with no explicit paragraph/character background of their own. There,
+// black-on-near-black (or the light-theme mirror, a pale style on white)
+// can go effectively invisible. Rather than hide the color or replace it
+// with a generic default, step its HSL lightness away from the background
+// in small increments, stopping as soon as contrast is enough - not
+// jumping straight to a fixed near-white/near-black lightness, which
+// would wash any saturated hue (magenta, blue) out to where it's barely
+// distinguishable from any other adjusted color. Never touches the
+// style's actual stored color - this is preview-only.
+QColor styleComboEnsureContrast(const QColor& fg, const QColor& bg)
+{
+	if (!fg.isValid() || !bg.isValid())
+		return fg;
+
+	const double minBrightnessDiff = 110.0;
+	const double bgBrightness = styleComboPerceivedBrightness(bg);
+	if (qAbs(styleComboPerceivedBrightness(fg) - bgBrightness) >= minBrightnessDiff)
+		return fg;
+
+	int h, s, l, a;
+	fg.getHsl(&h, &s, &l, &a);
+	const bool lighten = bgBrightness <= 128.0;
+
+	QColor adjusted = fg;
+	for (int step = l; lighten ? step <= 255 : step >= 0; step += lighten ? 5 : -5)
+	{
+		QColor candidate;
+		candidate.setHsl(h, s, step, a);
+		adjusted = candidate;
+		if (qAbs(styleComboPerceivedBrightness(candidate) - bgBrightness) >= minBrightnessDiff)
+			break;
+	}
+	return adjusted;
+}
+
+// A single glyph identifying the row's own font, sized to match the
+// paragraph-style badges used elsewhere in Style Scope. Checks the font's
+// full list of supported writing systems (not just its single primary
+// one, which can be Latin even for a font whose real purpose is another
+// script - several "Noto Sans <Script>" fonts are like this) and prefers
+// the first genuinely non-Latin one found there. A few systems are
+// excluded even when present: Vietnamese and Symbol commonly turn up as
+// incidental secondary coverage on an ordinary Latin font (extended accent
+// support, a handful of dingbats) rather than a real second script; Greek
+// and Cyrillic are excluded for a different reason - several well-known
+// Latin serif families (Garamond-derived ones among them) deliberately
+// ship a matching classical Greek design alongside their Latin one, which
+// is a real script but not the one worth showing here. None of these
+// exclusions are reversible from the data Qt exposes without misreading
+// genuinely Greek/Cyrillic/Vietnamese-purposed fonts as Latin instead, so
+// this is a practical trade-off, not a perfect classification method. When
+// a non-excluded system is found, the glyph is Qt's own representative
+// sample character for it - a font's family name is often Latin-
+// transliterated and gives no visual sense of the script itself, so this
+// fills that gap. Otherwise, the glyph is a fixed "A": the point of this
+// preview is comparing a font's shape against others at a glance, which
+// needs the same letter every time, not a different one per style name.
+// Returns just the character - StyleComboGlyphDelegate does the actual
+// painting, at paint time, so it can always use the palette that's
+// current right then rather than one baked in at populate time.
+QString styleComboPreviewGlyphChar(const QFont& font)
+{
+	QString glyph = QStringLiteral("A");
+	const QList<QFontDatabase::WritingSystem> systems = QFontDatabase::writingSystems(font.family());
+	for (QFontDatabase::WritingSystem system : systems)
+	{
+		if (system == QFontDatabase::Latin || system == QFontDatabase::Any
+			|| system == QFontDatabase::Vietnamese || system == QFontDatabase::Symbol
+			|| system == QFontDatabase::Greek || system == QFontDatabase::Cyrillic)
+			continue;
+		QString sample = QFontDatabase::writingSystemSample(system);
+		if (!sample.isEmpty())
+		{
+			glyph = sample.left(1);
+			break;
+		}
+	}
+	return glyph;
+}
+
+// Family only, at neutral weight/slant - the glyph identifies the font,
+// it isn't meant to also carry the style's own bold/italic/etc (the row
+// text itself already does that). Kept at its natural size, only shrunk
+// if that would overflow the space available to it.
+QFont styleComboFittedGlyphFont(const QString& family, int availableHeight)
+{
+	QFont font(family);
+	if (QFontMetrics(font).height() > availableHeight)
+		font.setPixelSize(qMax(8, availableHeight - 2));
+	return font;
+}
+
+// Paints the character-style combo's font-identifying glyph directly, on
+// every row paint, rather than baking a colored pixmap once at populate
+// time. The row's font/color/background preview data uses plain
+// Qt::FontRole/ForegroundRole/BackgroundRole values, which Qt itself
+// re-resolves fresh on every paint - this delegate gives the glyph that
+// same always-current guarantee. Wraps the combo's own popup delegate
+// (rather than replacing it) so the native row painting and sizing -
+// including spacing Qt's combo-specific delegate applies that a plain
+// QStyledItemDelegate does not - stays intact; the glyph is drawn as an
+// addition on top of that, not a substitute for it.
+class StyleComboGlyphDelegate : public QStyledItemDelegate
+{
+public:
+	StyleComboGlyphDelegate(QAbstractItemDelegate* baseDelegate, QObject* parent)
+		: QStyledItemDelegate(parent), m_baseDelegate(baseDelegate)
+	{
+	}
+
+	void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override
+	{
+		if (m_baseDelegate)
+			m_baseDelegate->paint(painter, option, index);
+		else
+			QStyledItemDelegate::paint(painter, option, index);
+
+		const QString glyph = index.data(StyleComboGlyphCharRole).toString();
+		if (glyph.isEmpty())
+			return;
+
+		QStyleOptionViewItem opt(option);
+		initStyleOption(&opt, index);
+
+		QStyle* style = opt.widget ? opt.widget->style() : QApplication::style();
+		QRect glyphRect = style->subElementRect(QStyle::SE_ItemViewItemDecoration, &opt, opt.widget);
+		if (glyphRect.isEmpty())
+			return;
+
+		QFont glyphFont = styleComboFittedGlyphFont(index.data(Qt::FontRole).value<QFont>().family(), glyphRect.height());
+
+		const bool selected = opt.state & QStyle::State_Selected;
+		const QPalette::ColorGroup group = (opt.state & QStyle::State_Enabled) ? QPalette::Normal : QPalette::Disabled;
+
+		painter->save();
+		painter->setFont(glyphFont);
+		painter->setPen(opt.palette.color(group, selected ? QPalette::HighlightedText : QPalette::Text));
+		painter->drawText(glyphRect, Qt::AlignCenter, glyph);
+		painter->restore();
+	}
+
+	QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override
+	{
+		QSize hint = m_baseDelegate ? m_baseDelegate->sizeHint(option, index) : QStyledItemDelegate::sizeHint(option, index);
+
+		// Every row renders in its own preview font, but that font varying
+		// by family (not just point size) also varies its natural line
+		// metrics, which otherwise carries straight into the row's height -
+		// a family swap reads as uneven list spacing rather than as
+		// anything meaningful about the styles. Re-based here to the
+		// view's own font, the single height every row in an ordinary
+		// (non-previewing) style list already gets.
+		const QFont rowFont = index.data(Qt::FontRole).value<QFont>();
+		const QFont referenceFont = option.widget ? option.widget->font() : rowFont;
+		const int delta = QFontMetrics(referenceFont).height() - QFontMetrics(rowFont).height();
+		hint.setHeight(hint.height() + delta);
+		return hint;
+	}
+
+private:
+	QAbstractItemDelegate* m_baseDelegate;
+};
+
+// Applies a style's preview (font, colors, and - for the character style
+// combo only - a per-row glyph) to one combo item. showPreviewGlyph is
+// false for the paragraph style combo: a plain list reads better there,
+// since paragraph styles are usually distinguished by more than font
+// alone, and every row already carries the font/color preview regardless.
+void styleComboSetPreviewData(QComboBox* combo, int itemIndex, const CharStyle& cstyle, const ScribusDoc* doc,
+                               const QString& paraBackColorName = QString(), double paraBackShade = 100.0,
+                               bool showPreviewGlyph = false)
+{
+	QFont font = styleComboPreviewFont(cstyle);
+	combo->setItemData(itemIndex, font, Qt::FontRole);
+
+	if (showPreviewGlyph)
+	{
+		combo->setItemData(itemIndex, styleComboPreviewGlyphChar(font), StyleComboGlyphCharRole);
+
+		// A transparent placeholder only reserves the decoration column's
+		// layout space - StyleComboGlyphDelegate paints the real,
+		// currently-colored glyph into that space itself.
+		QPixmap placeholder(14, 14);
+		placeholder.fill(Qt::transparent);
+		combo->setItemData(itemIndex, QIcon(placeholder), Qt::DecorationRole);
+	}
+
+	// A paragraph's own background (set on the Properties tab, shading the
+	// whole paragraph) takes priority over its default character style's
+	// background here - the two are layered separately on canvas, but a
+	// combo row only has room for one swatch, and the paragraph-wide one is
+	// the more paragraph-style-appropriate signal. Character style rows
+	// never pass paraBackColorName, so they always fall through to their
+	// own backColor.
+	QColor bg = styleComboResolveColor(paraBackColorName, paraBackShade, doc);
+	if (!bg.isValid())
+		bg = styleComboResolveColor(cstyle.backColor(), cstyle.backShade(), doc);
+	if (bg.isValid())
+		combo->setItemData(itemIndex, bg, Qt::BackgroundRole);
+
+	// Resolved first so the foreground contrast check below has a real
+	// background to check against: the row's own background when set,
+	// otherwise the app's current (possibly dark-themed) list background.
+	QColor fg = styleComboResolveColor(cstyle.fillColor(), cstyle.fillShade(), doc);
+	if (fg.isValid())
+	{
+		QColor effectiveBg = bg.isValid() ? bg : combo->palette().color(QPalette::Base);
+		combo->setItemData(itemIndex, styleComboEnsureContrast(fg, effectiveBg), Qt::ForegroundRole);
+	}
+}
+}
+
 ParaStyleComboBox::ParaStyleComboBox(QWidget* parent) : QComboBox(parent)
 {
 	setEditable(false);
@@ -74,6 +352,13 @@
 		languageChange();
 		return;
 	}
+	if (e->type() == QEvent::PaletteChange || e->type() == QEvent::ApplicationPaletteChange)
+	{
+		// Re-derives the font/color preview data (including the glyph
+		// icon) against the now-current palette, queued so it runs after
+		// the posted ApplicationPaletteChange event has actually landed.
+		QMetaObject::invokeMethod(this, [this]() { updateStyleList(); }, Qt::QueuedConnection);
+	}
 	QComboBox::changeEvent(e);
 }
 
@@ -124,9 +409,15 @@
 	clear();
 	if (m_doc != nullptr)
 	{
-		QStringList st;
 		addItem( firstItemString()  );
+
+		int defaultIndex = m_doc->paragraphStyles().find(CommonStrings::DefaultParagraphStyle);
 		addItem( CommonStrings::trDefaultParagraphStyle );
+		if (defaultIndex >= 0)
+			styleComboSetPreviewData(this, count() - 1, m_doc->paragraphStyles()[defaultIndex].charStyle(), m_doc,
+				m_doc->paragraphStyles()[defaultIndex].backgroundColor(), m_doc->paragraphStyles()[defaultIndex].backgroundShade());
+
+		QStringList st;
 		for (int i = 0; i < m_doc->paragraphStyles().count(); ++i)
 		{
 			const ParagraphStyle& paraStyle = m_doc->paragraphStyles()[i];
@@ -134,7 +425,14 @@
 				st.append(paraStyle.name());
 		}
 		st.sort();
-		addItems(st);
+		for (const QString& styleName : st)
+		{
+			addItem(styleName);
+			int idx = m_doc->paragraphStyles().find(styleName);
+			if (idx >= 0)
+				styleComboSetPreviewData(this, count() - 1, m_doc->paragraphStyles()[idx].charStyle(), m_doc,
+					m_doc->paragraphStyles()[idx].backgroundColor(), m_doc->paragraphStyles()[idx].backgroundShade());
+		}
 
 		if (oldStyleName.length() > 0)
 		{
@@ -172,6 +470,10 @@
 	setEditable(false);
 	m_defaultStyle = CommonStrings::DefaultCharacterStyle;
 
+	// Paints the font-identifying glyph fresh on every row paint - see
+	// StyleComboGlyphDelegate.
+	view()->setItemDelegate(new StyleComboGlyphDelegate(view()->itemDelegate(), this));
+
 	addItem( firstItemString()  );
 	addItem( CommonStrings::trDefaultCharacterStyle );
 	connect(this, SIGNAL(activated(int)), this, SLOT(selectedStyle(int)));
@@ -211,6 +513,11 @@
 		languageChange();
 		return;
 	}
+	if (e->type() == QEvent::PaletteChange || e->type() == QEvent::ApplicationPaletteChange)
+	{
+		// See ParaStyleComboBox::changeEvent() for why this is queued.
+		QMetaObject::invokeMethod(this, [this]() { updateStyleList(); }, Qt::QueuedConnection);
+	}
 	QComboBox::changeEvent(e);
 }
 
@@ -226,6 +533,33 @@
 	this->blockSignals(sigBlocked);
 }
 
+// The popup list's glyph is painted by StyleComboGlyphDelegate, which only
+// covers the popup - the closed box's own current-selection display is
+// drawn by QComboBox's own paintEvent() instead, reading Qt::DecorationRole
+// directly. That role is a transparent placeholder (see
+// styleComboSetPreviewData()), so without this override the closed box
+// would just show the empty reserved space and no glyph.
+void CharStyleComboBox::paintEvent(QPaintEvent *e)
+{
+	QComboBox::paintEvent(e);
+
+	const QString glyph = itemData(currentIndex(), StyleComboGlyphCharRole).toString();
+	if (glyph.isEmpty())
+		return;
+
+	QStyleOptionComboBox opt;
+	initStyleOption(&opt);
+	QRect editRect = style()->subControlRect(QStyle::CC_ComboBox, &opt, QStyle::SC_ComboBoxEditField, this);
+	QRect glyphRect(editRect.left(), editRect.top(), iconSize().width(), editRect.height());
+
+	QFont glyphFont = styleComboFittedGlyphFont(itemData(currentIndex(), Qt::FontRole).value<QFont>().family(), glyphRect.height());
+
+	QPainter painter(this);
+	painter.setFont(glyphFont);
+	painter.setPen(palette().color(isEnabled() ? QPalette::Active : QPalette::Disabled, QPalette::Text));
+	painter.drawText(glyphRect, Qt::AlignCenter, glyph);
+}
+
 void CharStyleComboBox::setDoc(ScribusDoc *newCurrentDoc)
 {
 	m_doc = newCurrentDoc;
@@ -261,9 +595,14 @@
 	clear();
 	if (m_doc != nullptr)
 	{
-		QStringList st;
 		addItem( firstItemString()  );
+
+		int defaultIndex = m_doc->charStyles().find(CommonStrings::DefaultCharacterStyle);
 		addItem( CommonStrings::trDefaultCharacterStyle );
+		if (defaultIndex >= 0)
+			styleComboSetPreviewData(this, count() - 1, m_doc->charStyles()[defaultIndex], m_doc, QString(), 100.0, true);
+
+		QStringList st;
 		for (int i = 0; i < m_doc->charStyles().count(); ++i)
 		{
 			const CharStyle& charStyle = m_doc->charStyles()[i];
@@ -271,7 +610,13 @@
 				st.append(charStyle.name());
 		}
 		st.sort();
-		addItems(st);
+		for (const QString& styleName : st)
+		{
+			addItem(styleName);
+			int idx = m_doc->charStyles().find(styleName);
+			if (idx >= 0)
+				styleComboSetPreviewData(this, count() - 1, m_doc->charStyles()[idx], m_doc, QString(), 100.0, true);
+		}
 
 		if (oldStyleName.length() > 0)
 		{

Issue History

Date Modified Username Field Change
2026-08-14 02:41 qirat New Issue
2026-08-14 02:41 qirat File Added: r27771-p1-stylecombo-font-preview-v1.06.patch
2026-08-14 02:47 qirat Note Added: 0054203
2026-08-14 02:47 qirat File Added: pp-char-style-combo-preview.png
2026-08-14 02:47 qirat File Added: pp-para-style-combo-preview.png
2026-08-14 04:05 qirat Note Added: 0054204
2026-08-14 04:05 qirat File Added: pp-char-style-combo-preview_2.png
2026-08-14 06:39 qirat Note Added: 0054205
2026-08-14 06:39 qirat File Added: pp-char-style-combo-preview_3.png
2026-08-15 07:28 ale Note Added: 0054206
2026-08-15 07:47 qirat Note Added: 0054207
2026-08-16 07:46 qirat Note Added: 0054211
2026-08-16 07:46 qirat File Added: stylecombo-font-preview-v1.15.patch