View Issue Details

IDProjectCategoryView StatusLast Update
0017858ScribusStylespublic2026-09-04 06:26
Reporterale Assigned Toale  
PrioritynormalSeverityfeatureReproducibilityN/A
Status assignedResolutionopen 
Product Version1.7.4.svn 
Summary0017858: Create a styles browser
DescriptionCurrently, the Styles Manager, in its collapsed form, is also a Styles browser.

- The code would be much cleaner if there were two different dialogs
- The Code browser (but not the style manager) should be dockable
- The two dialogs should probably use the same Tree view

Once we have a Style browser, we can make the Style manager always expanded.
Tags#please_test
PatchNo

Relationships

related to 0017859 assignedale Simplify the Style manager 

Activities

buttar

2026-09-04 06:25

reporter   ~0054409

#Style Browser: palette + interaction
- Adds a dockable Styles Browser as the document-facing style browsing/applying UI, separate from Style Manager’s editing/management role.

##SB1 adds the browser infrastructure:
  - Paragraph, Character, Table, Cell and Line style families
  - parent/child hierarchy construction
  - default-style-first ordering
  - search/filtering with ancestor retention
  - expansion-state preservation across refreshes
  - document/window/workspace integration
  - defensive filtering of unnamed/internal style entries
  
##SB2 adds interaction/state handling:
  - single-click style application through existing Scribus style APIs
  - double-click and context-menu handoff to the exact style in Style Manager
  - active-style indication independent of tree selection
  - mixed selections reported at the family root, e.g. `Paragraph Styles — Mixed`
  - synchronization with document selection, text cursor/selection, table/cell selection, style updates and Undo/Redo
- Style Manager handoff uses stable style-family/name routing instead of top-level tree index assumptions.
- No new style model, Undo layer or `.sla` format changes.
- Patch order: SB1 → SB2.
sb1-styles-browser-palette-v1.02.patch (24,661 bytes)   
Index: scribus/CMakeLists_Sources.txt
===================================================================
--- scribus/CMakeLists_Sources.txt	(revision 27810)
+++ scribus/CMakeLists_Sources.txt	(working copy)
@@ -546,6 +546,7 @@
 	ui/storyeditor.cpp
 	ui/stylecombos.cpp
 	ui/stylemanager.cpp
+	ui/stylesbrowser.cpp
 	ui/stylesearchdialog.cpp
 	ui/styleselect.cpp
 	ui/styleview.cpp
Index: scribus/actionmanager.cpp
===================================================================
--- scribus/actionmanager.cpp	(revision 27810)
+++ scribus/actionmanager.cpp	(working copy)
@@ -875,6 +875,8 @@
 	name = "toolsSymbols";
 	scrActions->insert(name, new ScrAction("", defaultKey(name), mainWindow));
 	name = "toolsInline";
+	scrActions->insert(name, new ScrAction("", defaultKey(name), mainWindow));
+	name = "toolsStylesBrowser";
 	scrActions->insert(name, new ScrAction("", defaultKey(name), mainWindow));
 	name = "toolsToolbarTools";
 	scrActions->insert(name, new ScrAction("", defaultKey(name), mainWindow));
@@ -968,6 +970,7 @@
 	(*scrActions)["toolsAlignDistribute"]->setShortcutContext(Qt::ApplicationShortcut);
 	(*scrActions)["toolsSymbols"]->setShortcutContext(Qt::ApplicationShortcut);
 	(*scrActions)["toolsInline"]->setShortcutContext(Qt::ApplicationShortcut);
+	(*scrActions)["toolsStylesBrowser"]->setShortcutContext(Qt::ApplicationShortcut);
 
 	(*scrActions)["toolsDownloads"]->setToggleAction(true);
 	(*scrActions)["toolsMeasurements"]->setToggleAction(true);
@@ -1796,6 +1799,7 @@
 	(*scrActions)["toolsAlignDistribute"]->setTexts( tr("&Align and Distribute"));
 	(*scrActions)["toolsSymbols"]->setTexts( tr("Symbols"));
 	(*scrActions)["toolsInline"]->setTexts( tr("Inline Items"));
+	(*scrActions)["toolsStylesBrowser"]->setTexts( tr("S&tyles Browser"));
 	(*scrActions)["toolsToolbarTools"]->setTexts( tr("&Tools"));
 	(*scrActions)["toolsToolbarPDF"]->setTexts( tr("P&DF Tools"));
 	(*scrActions)["toolsToolbarView"]->setTexts( tr("&View Tools"));
@@ -2502,6 +2506,7 @@
 		<< "toolsAlignDistribute"
 		<< "toolsSymbols"
 		<< "toolsInline"
+		<< "toolsStylesBrowser"
 		<< "toolsToolbarTools"
 		<< "toolsToolbarPDF"
 		<< "toolsToolbarView";
Index: scribus/manager/dock_manager.cpp
===================================================================
--- scribus/manager/dock_manager.cpp	(revision 27810)
+++ scribus/manager/dock_manager.cpp	(working copy)
@@ -41,6 +41,7 @@
 #include "ui/pagepalette.h"
 #include "ui/propertiespalette.h"
 #include "ui/scrapbookpalette.h"
+#include "ui/stylesbrowser.h"
 #include "ui/symbolpalette.h"
 #include "undogui.h"
 
@@ -74,6 +75,7 @@
 	pagePalette = new PagePalette((QWidget *) this->parent());
 	propertiesPalette = new PropertiesPalette(this);
 	scrapbookPalette = new Biblio(this);
+	stylesBrowser = new StylesBrowser(this);
 	symbolPalette = new SymbolPalette(this);
 	undoPalette = new UndoPalette(this);
 
@@ -87,6 +89,7 @@
 	configureDock(pagePalette);
 	configureDock(propertiesPalette);
 	configureDock(scrapbookPalette);
+	configureDock(stylesBrowser);
 	configureDock(symbolPalette);
 	configureDock(undoPalette);
 
@@ -262,6 +265,7 @@
 	// Left
 	auto *areaLeft = addDockWidget(LeftDockWidgetArea, pagePalette, areaCenter);
 	addDockWidgetTabToArea(outlinePalette, areaLeft);
+	addDockWidgetTabToArea(stylesBrowser, areaLeft);
 
 	// Left	Center
 	auto *areaCenterLeft = addDockWidget(LeftDockWidgetArea, inlinePalette, areaCenter);
@@ -301,6 +305,7 @@
 	layerPalette->closeDockWidget();
 	undoPalette->closeDockWidget();
 	outlinePalette->closeDockWidget();
+	stylesBrowser->closeDockWidget();
 
 	// active palettes
 	areaLeft->setCurrentDockWidget(pagePalette);
Index: scribus/manager/dock_manager.h
===================================================================
--- scribus/manager/dock_manager.h	(revision 27810)
+++ scribus/manager/dock_manager.h	(working copy)
@@ -44,6 +44,7 @@
 class BookPalette;
 class UndoPalette;
 class SymbolPalette;
+class StylesBrowser;
 class DockCentralWidget;
 
 class SCRIBUS_API DockManager : public CDockManager
@@ -95,6 +96,7 @@
 	BookPalette *bookPalette {nullptr};
 	UndoPalette *undoPalette {nullptr};
 	SymbolPalette *symbolPalette {nullptr};
+	StylesBrowser *stylesBrowser {nullptr};
 
 public slots:
 	/**
Index: scribus/scribus.cpp
===================================================================
--- scribus/scribus.cpp	(revision 27810)
+++ scribus/scribus.cpp	(working copy)
@@ -235,6 +235,7 @@
 #include "ui/smtextstyles.h"
 #include "ui/storyeditor.h"
 #include "ui/stylemanager.h"
+#include "ui/stylesbrowser.h"
 #include "ui/stylesearchdialog.h"
 #include "ui/symbolpalette.h"
 #include "ui/tabmanager.h"
@@ -802,6 +803,11 @@
 	connect(inlinePalette, SIGNAL(endEdit()), this, SLOT(editInlineEnd()));
 	connect(inlinePalette, SIGNAL(objectDropped(QString)), this, SLOT(PutToInline(QString)));
 	inlinePalette->installEventFilter(this);
+
+	// Styles Browser
+	stylesBrowser = dockManager->stylesBrowser;
+	stylesBrowser->setMainWindow(this);
+	stylesBrowser->setToggleViewAction(scrActions["toolsStylesBrowser"]);
 
 	// Undo
 	undoPalette = dockManager->undoPalette;
@@ -1410,6 +1416,7 @@
 	scrMenuMgr->addMenuItemString("toolsScrapbook", "Windows");
 	scrMenuMgr->addMenuItemString("toolsSymbols", "Windows");
 	scrMenuMgr->addMenuItemString("toolsInline", "Windows");
+	scrMenuMgr->addMenuItemString("toolsStylesBrowser", "Windows");
 	scrMenuMgr->addMenuItemString("SEPARATOR", "Windows");
 	scrMenuMgr->addMenuItemString("toolsMeasurements", "Windows");
 	scrMenuMgr->addMenuItemString("toolsPreflightVerifier", "Windows");
@@ -2035,6 +2042,7 @@
 		doc->hasName = true;
 		updateActiveWindowCaption(doc->documentFileName());
 		outlinePalette->setDoc(doc);
+		stylesBrowser->setDoc(doc);
 		if (outlinePalette->isVisible())
 			outlinePalette->BuildTree();
 	}
@@ -2548,6 +2556,7 @@
 	if (!doc->masterPageMode())
 		pagePalette->rebuild();
 	outlinePalette->setDoc(doc);
+	stylesBrowser->setDoc(doc);
 	if (outlinePalette->isVisible())
 	{
 		outlinePalette->BuildTree(false);
@@ -2608,6 +2617,7 @@
 	guidePalette->setDoc(doc);
 	charPalette->setDoc(doc);
 	outlinePalette->setDoc(doc);
+	stylesBrowser->setDoc(doc);
 	symbolPalette->setDoc(doc);
 	inlinePalette->setDoc(doc);
 	documentLogViewer->setDocument(doc->uuidString());
@@ -2660,6 +2670,7 @@
 	guidePalette->setDoc(doc);
 	charPalette->setDoc(doc);
 	outlinePalette->setDoc(doc);
+	stylesBrowser->setDoc(doc);
 	if (outlinePalette->isVisible())
 		outlinePalette->BuildTree();
 	rebuildLayersList();
@@ -3499,6 +3510,7 @@
 		}
 		symbolPalette->setDoc(doc);
 		outlinePalette->setDoc(doc);
+		stylesBrowser->setDoc(doc);
 		fileLoader->informReplacementFonts();
 		setCurrentComboItem(unitSwitcher, unitGetStrFromIndex(doc->unitIndex()));
 		const QSignalBlocker blocker(zoomSpinBox);
@@ -4228,6 +4240,7 @@
 	bookmarkPalette->BView->First = 1;
 	bookmarkPalette->BView->Last = 0;
 	outlinePalette->unsetDoc();
+	stylesBrowser->unsetDoc();
 	alignDistributePalette->setDoc(nullptr);
 	//>>
 
@@ -6787,6 +6800,7 @@
 	m_styleManager->startup();
 	marksManager->startup();
 	nsEditor->startup();
+	stylesBrowser->startup();
 	symbolPalette->startup();
 
 	// try to load custom layout from preferences
@@ -10087,6 +10101,7 @@
 	outlinePalette->setEnabled(!(doc->drawAsPreview && !doc->editOnPreview));
 	propertiesPalette->setEnabled(!(doc->drawAsPreview && !doc->editOnPreview));
 	contentPalette->setEnabled(!(doc->drawAsPreview && !doc->editOnPreview));
+	stylesBrowser->setEnabled(!(doc->drawAsPreview && !doc->editOnPreview));
 	scrMenuMgr->setMenuEnabled("Edit", !doc->drawAsPreview);
 	scrMenuMgr->setMenuEnabled("Item", !doc->drawAsPreview);
 	scrMenuMgr->setMenuEnabled("Insert", !doc->drawAsPreview);
Index: scribus/scribus.h
===================================================================
--- scribus/scribus.h	(revision 27810)
+++ scribus/scribus.h	(working copy)
@@ -111,6 +111,7 @@
 class SimpleState;
 class StoryEditor;
 class StyleManager;
+class StylesBrowser;
 class SymbolPalette;
 class TOCGenerator;
 class UndoManager;
@@ -269,6 +270,7 @@
 	DownloadsPalette *downloadsPalette {nullptr};
 	SymbolPalette *symbolPalette {nullptr};
 	InlinePalette *inlinePalette {nullptr};
+	StylesBrowser *stylesBrowser {nullptr};
 	Measurements* measurementPalette {nullptr};
 	CheckDocument * docCheckerPalette {nullptr};
 	UndoPalette* undoPalette {nullptr};
Index: scribus/ui/stylesbrowser.cpp
===================================================================
--- scribus/ui/stylesbrowser.cpp	(revision 0)
+++ scribus/ui/stylesbrowser.cpp	(working copy)
@@ -0,0 +1,410 @@
+/*
+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 "stylesbrowser.h"
+
+#include <algorithm>
+#include <utility>
+
+#include <QEvent>
+#include <QHash>
+#include <QSignalBlocker>
+#include <QTreeWidgetItem>
+#include <QTreeWidgetItemIterator>
+
+#include "iconmanager.h"
+#include "scribus.h"
+#include "scribusdoc.h"
+#include "scribusstructs.h"
+#include "styles/cellstyle.h"
+#include "styles/charstyle.h"
+#include "styles/paragraphstyle.h"
+#include "styles/tablestyle.h"
+
+StylesBrowser::StylesBrowser(QWidget* parent)
+	: DockPanelBase(QStringLiteral("StylesBrowser"), QStringLiteral("paragraph-style"), parent)
+{
+	setWindowTitle(tr("Styles Browser"));
+	setMinimumSize(QSize(220, 240));
+
+	m_container = new QWidget(this);
+	m_ui.setupUi(m_container);
+	setWidget(m_container);
+
+	m_ui.filterEdit->setClearButtonEnabled(true);
+
+	connect(m_ui.filterEdit, &QLineEdit::textChanged, this, &StylesBrowser::filterStyles);
+	connect(this, &CDockWidget::visibilityChanged, this, &StylesBrowser::handleVisibilityChanged);
+	connect(m_ui.stylesTree, &QTreeWidget::itemExpanded, this, [this](QTreeWidgetItem*)
+	{
+		if (m_ui.filterEdit->text().isEmpty())
+			captureExpansionState();
+	});
+	connect(m_ui.stylesTree, &QTreeWidget::itemCollapsed, this, [this](QTreeWidgetItem*)
+	{
+		if (m_ui.filterEdit->text().isEmpty())
+			captureExpansionState();
+	});
+
+	setEnabled(false);
+}
+
+void StylesBrowser::changeEvent(QEvent* event)
+{
+	if (event->type() == QEvent::LanguageChange)
+	{
+		setWindowTitle(tr("Styles Browser"));
+		m_ui.retranslateUi(m_container);
+		m_treeDirty = true;
+		if (isVisible())
+			updateStylesTree();
+	}
+	DockPanelBase::changeEvent(event);
+}
+
+void StylesBrowser::setMainWindow(ScribusMainWindow* mainWindow)
+{
+	if (m_mainWindow == mainWindow)
+		return;
+
+	if (m_mainWindow)
+		disconnect(m_mainWindow, &ScribusMainWindow::UpdateRequest, this, &StylesBrowser::handleUpdateRequest);
+
+	m_mainWindow = mainWindow;
+	if (m_mainWindow)
+		connect(m_mainWindow, &ScribusMainWindow::UpdateRequest, this, &StylesBrowser::handleUpdateRequest, Qt::UniqueConnection);
+	else
+		unsetDoc();
+}
+
+void StylesBrowser::setDoc(ScribusDoc* doc)
+{
+	if (!m_mainWindow)
+		doc = nullptr;
+
+	if (m_doc == doc)
+	{
+		if (m_doc)
+			setEnabled(!(m_doc->drawAsPreview && !m_doc->editOnPreview));
+		return;
+	}
+
+	m_doc = doc;
+	m_treeDirty = true;
+
+	if (!m_doc)
+	{
+		m_ui.stylesTree->clear();
+		setEnabled(false);
+		return;
+	}
+
+	setEnabled(!(m_doc->drawAsPreview && !m_doc->editOnPreview));
+	if (isVisible())
+		updateStylesTree();
+}
+
+void StylesBrowser::unsetDoc()
+{
+	m_doc = nullptr;
+	m_treeDirty = true;
+	m_ui.stylesTree->clear();
+	setEnabled(false);
+}
+
+void StylesBrowser::handleUpdateRequest(int updateFlags)
+{
+	const int styleUpdates = reqTextStylesUpdate | reqLineStylesUpdate | reqTableStylesUpdate | reqStyleComboDocUpdate;
+	if (!(updateFlags & styleUpdates))
+		return;
+
+	m_treeDirty = true;
+	if (isVisible())
+		updateStylesTree();
+}
+
+void StylesBrowser::updateStylesTree()
+{
+	if (!m_doc || !m_treeDirty)
+		return;
+
+	rebuildTree();
+	m_treeDirty = false;
+}
+
+void StylesBrowser::handleVisibilityChanged(bool visible)
+{
+	if (visible)
+		updateStylesTree();
+}
+
+void StylesBrowser::rebuildTree()
+{
+	QString selectedKey;
+	if (m_ui.stylesTree->currentItem())
+		selectedKey = itemKey(m_ui.stylesTree->currentItem());
+
+	if (m_haveExpansionState && m_ui.filterEdit->text().isEmpty())
+		captureExpansionState();
+
+	m_ui.stylesTree->clear();
+
+	auto collectStyles = [](const auto& styles)
+	{
+		QList<StyleEntry> entries;
+		entries.reserve(styles.count());
+		for (int i = 0; i < styles.count(); ++i)
+		{
+			const auto& style = styles[i];
+			if (!style.hasName())
+				continue;
+
+			StyleEntry entry;
+			entry.name = style.name();
+			entry.displayName = style.displayName();
+			entry.parent = style.parent();
+			entry.isDefault = style.isDefaultStyle();
+			entries.append(entry);
+		}
+		return entries;
+	};
+
+	addStyleCategory(StyleType::Paragraph, tr("Paragraph Styles"), QStringLiteral("paragraph-style"), collectStyles(m_doc->paragraphStyles()));
+	addStyleCategory(StyleType::Character, tr("Character Styles"), QStringLiteral("character-style"), collectStyles(m_doc->charStyles()));
+	addStyleCategory(StyleType::Table, tr("Table Styles"), QStringLiteral("table-style"), collectStyles(m_doc->tableStyles()));
+	addStyleCategory(StyleType::Cell, tr("Cell Styles"), QStringLiteral("table-cell-style"), collectStyles(m_doc->cellStyles()));
+
+	QList<StyleEntry> lineEntries;
+	QStringList lineNames = m_doc->lineStyles().keys();
+	lineNames.sort(Qt::CaseInsensitive);
+	for (const QString& name : std::as_const(lineNames))
+	{
+		if (name.isEmpty())
+			continue;
+
+		StyleEntry entry;
+		entry.name = name;
+		entry.displayName = name;
+		lineEntries.append(entry);
+	}
+	addStyleCategory(StyleType::Line, tr("Line Styles"), QStringLiteral("stroke-style"), lineEntries);
+
+	if (!m_haveExpansionState)
+	{
+		m_ui.stylesTree->expandAll();
+		captureExpansionState();
+		m_haveExpansionState = true;
+	}
+	else if (m_ui.filterEdit->text().isEmpty())
+		restoreExpansionState();
+
+	if (!selectedKey.isEmpty())
+	{
+		QTreeWidgetItemIterator it(m_ui.stylesTree);
+		while (*it)
+		{
+			if (itemKey(*it) == selectedKey)
+			{
+				m_ui.stylesTree->setCurrentItem(*it);
+				break;
+			}
+			++it;
+		}
+	}
+
+	filterStyles(m_ui.filterEdit->text());
+}
+
+void StylesBrowser::addStyleCategory(StyleType type, const QString& label, const QString& iconName, QList<StyleEntry> entries)
+{
+	std::sort(entries.begin(), entries.end(), [](const StyleEntry& left, const StyleEntry& right)
+	{
+		if (left.isDefault != right.isDefault)
+			return left.isDefault;
+		const int displayCompare = QString::localeAwareCompare(left.displayName, right.displayName);
+		if (displayCompare != 0)
+			return displayCompare < 0;
+		return left.name < right.name;
+	});
+
+	QHash<QString, StyleEntry> byName;
+	for (const StyleEntry& entry : std::as_const(entries))
+		byName.insert(entry.name, entry);
+
+	QHash<QString, int> depthByName;
+	int maxDepth = 0;
+	for (const StyleEntry& entry : std::as_const(entries))
+	{
+		int depth = 0;
+		QSet<QString> seen;
+		QString parent = entry.parent;
+		while (!parent.isEmpty() && byName.contains(parent) && !seen.contains(parent))
+		{
+			seen.insert(parent);
+			parent = byName.value(parent).parent;
+			++depth;
+		}
+		depthByName.insert(entry.name, depth);
+		maxDepth = qMax(maxDepth, depth);
+	}
+
+	QList<StyleEntry> ordered;
+	ordered.reserve(entries.count());
+	for (int level = 0; level <= maxDepth; ++level)
+	{
+		for (const StyleEntry& entry : std::as_const(entries))
+		{
+			if (depthByName.value(entry.name) == level)
+				ordered.append(entry);
+		}
+	}
+
+	auto* root = new QTreeWidgetItem(m_ui.stylesTree);
+	root->setText(0, label);
+	root->setIcon(0, IconManager::instance().loadIcon(iconName));
+	root->setData(0, ItemKindRole, CategoryItem);
+	root->setData(0, StyleTypeRole, static_cast<int>(type));
+	root->setFlags(root->flags() & ~Qt::ItemIsSelectable);
+
+	QHash<QString, QTreeWidgetItem*> itemsByName;
+	for (const StyleEntry& entry : std::as_const(ordered))
+	{
+		QTreeWidgetItem* parentItem = root;
+		if (!entry.parent.isEmpty() && itemsByName.contains(entry.parent))
+			parentItem = itemsByName.value(entry.parent);
+
+		auto* item = new QTreeWidgetItem(parentItem);
+		item->setText(0, entry.displayName);
+		item->setData(0, ItemKindRole, StyleItem);
+		item->setData(0, StyleTypeRole, static_cast<int>(type));
+		item->setData(0, StyleNameRole, entry.name);
+		itemsByName.insert(entry.name, item);
+	}
+}
+
+void StylesBrowser::filterStyles(const QString& text)
+{
+	const QString filter = text.trimmed();
+	for (int i = 0; i < m_ui.stylesTree->topLevelItemCount(); ++i)
+	{
+		QTreeWidgetItem* root = m_ui.stylesTree->topLevelItem(i);
+		bool hasVisibleStyle = false;
+		for (int child = 0; child < root->childCount(); ++child)
+			hasVisibleStyle = filterStyleItem(root->child(child), filter) || hasVisibleStyle;
+		root->setHidden(!filter.isEmpty() && !hasVisibleStyle);
+		if (!filter.isEmpty() && hasVisibleStyle)
+			root->setExpanded(true);
+	}
+
+	if (filter.isEmpty() && m_haveExpansionState)
+		restoreExpansionState();
+}
+
+bool StylesBrowser::filterStyleItem(QTreeWidgetItem* item, const QString& text)
+{
+	bool childMatches = false;
+	for (int i = 0; i < item->childCount(); ++i)
+		childMatches = filterStyleItem(item->child(i), text) || childMatches;
+
+	const bool ownMatch = text.isEmpty() || item->text(0).contains(text, Qt::CaseInsensitive);
+	const bool visible = ownMatch || childMatches;
+	item->setHidden(!visible);
+	if (!text.isEmpty() && childMatches)
+		item->setExpanded(true);
+	return visible;
+}
+
+QString StylesBrowser::itemKey(const QTreeWidgetItem* item) const
+{
+	if (!item)
+		return QString();
+
+	const int kind = item->data(0, ItemKindRole).toInt();
+	const int type = item->data(0, StyleTypeRole).toInt();
+	if (kind == CategoryItem)
+		return QStringLiteral("category:%1").arg(type);
+	if (kind == StyleItem)
+		return QStringLiteral("style:%1:%2").arg(type).arg(item->data(0, StyleNameRole).toString());
+	return QString();
+}
+
+void StylesBrowser::captureExpansionState()
+{
+	m_expandedItems.clear();
+	m_knownItems.clear();
+	m_expandedBranchDescendants.clear();
+
+	QTreeWidgetItemIterator it(m_ui.stylesTree);
+	while (*it)
+	{
+		QTreeWidgetItem* item = *it;
+		const QString key = itemKey(item);
+		if (!key.isEmpty())
+			m_knownItems.insert(key);
+
+		if (item->isExpanded())
+		{
+			if (!key.isEmpty())
+				m_expandedItems.insert(key);
+
+			QList<QTreeWidgetItem*> descendants;
+			for (int child = 0; child < item->childCount(); ++child)
+				descendants.append(item->child(child));
+			while (!descendants.isEmpty())
+			{
+				QTreeWidgetItem* descendant = descendants.takeFirst();
+				const QString descendantKey = itemKey(descendant);
+				if (!descendantKey.isEmpty())
+					m_expandedBranchDescendants.insert(descendantKey);
+				for (int child = 0; child < descendant->childCount(); ++child)
+					descendants.append(descendant->child(child));
+			}
+		}
+		++it;
+	}
+}
+
+void StylesBrowser::restoreExpansionState()
+{
+	// Restoring expansion emits itemExpanded/itemCollapsed. Block those signals
+	// so the partially restored tree cannot overwrite the saved state.
+	const QSignalBlocker blocker(m_ui.stylesTree);
+
+	QTreeWidgetItemIterator it(m_ui.stylesTree);
+	while (*it)
+	{
+		QTreeWidgetItem* item = *it;
+		const QString key = itemKey(item);
+		bool expanded = !key.isEmpty() && m_expandedItems.contains(key);
+
+		// A renamed parent has a new key. Preserve its open branch when one of
+		// its surviving descendants belonged to a previously expanded branch.
+		if (!expanded && !key.isEmpty() && !m_knownItems.contains(key) &&
+			item->data(0, ItemKindRole).toInt() == StyleItem)
+		{
+			QList<QTreeWidgetItem*> descendants;
+			for (int child = 0; child < item->childCount(); ++child)
+				descendants.append(item->child(child));
+			while (!descendants.isEmpty() && !expanded)
+			{
+				QTreeWidgetItem* descendant = descendants.takeFirst();
+				const QString descendantKey = itemKey(descendant);
+				if (m_expandedBranchDescendants.contains(descendantKey))
+					expanded = true;
+				for (int child = 0; child < descendant->childCount(); ++child)
+					descendants.append(descendant->child(child));
+			}
+		}
+
+		if (!key.isEmpty())
+			item->setExpanded(expanded);
+		++it;
+	}
+
+	// Adopt any renamed keys so subsequent rebuilds preserve the new state.
+	captureExpansionState();
+}
Index: scribus/ui/stylesbrowser.h
===================================================================
--- scribus/ui/stylesbrowser.h	(revision 0)
+++ scribus/ui/stylesbrowser.h	(working copy)
@@ -0,0 +1,94 @@
+/*
+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 STYLESBROWSER_H
+#define STYLESBROWSER_H
+
+#include <QList>
+#include <QSet>
+#include <QString>
+
+#include "scribusapi.h"
+#include "ui/docks/dock_panelbase.h"
+#include "ui_stylesbrowser.h"
+
+class QEvent;
+class QTreeWidgetItem;
+class ScribusDoc;
+class ScribusMainWindow;
+
+class SCRIBUS_API StylesBrowser : public DockPanelBase
+{
+	Q_OBJECT
+
+public:
+	explicit StylesBrowser(QWidget* parent = nullptr);
+
+	void setMainWindow(ScribusMainWindow* mainWindow);
+	void setDoc(ScribusDoc* doc);
+	void unsetDoc();
+
+	enum class StyleType
+	{
+		Paragraph,
+		Character,
+		Table,
+		Cell,
+		Line
+	};
+
+public slots:
+	void handleUpdateRequest(int updateFlags);
+	void updateStylesTree();
+
+protected:
+	void changeEvent(QEvent* event) override;
+
+private slots:
+	void filterStyles(const QString& text);
+	void handleVisibilityChanged(bool visible);
+
+private:
+	struct StyleEntry
+	{
+		QString name;
+		QString displayName;
+		QString parent;
+		bool isDefault { false };
+	};
+
+	enum ItemKind
+	{
+		CategoryItem = 0,
+		StyleItem = 1
+	};
+
+	enum ItemDataRole
+	{
+		ItemKindRole = Qt::UserRole,
+		StyleTypeRole,
+		StyleNameRole
+	};
+
+	void rebuildTree();
+	void addStyleCategory(StyleType type, const QString& label, const QString& iconName, QList<StyleEntry> entries);
+	bool filterStyleItem(QTreeWidgetItem* item, const QString& text);
+	QString itemKey(const QTreeWidgetItem* item) const;
+	void captureExpansionState();
+	void restoreExpansionState();
+
+	Ui::StylesBrowserWidget m_ui;
+	QWidget* m_container { nullptr };
+	ScribusDoc* m_doc { nullptr };
+	ScribusMainWindow* m_mainWindow { nullptr };
+	bool m_treeDirty { true };
+	bool m_haveExpansionState { false };
+	QSet<QString> m_expandedItems;
+	QSet<QString> m_knownItems;
+	QSet<QString> m_expandedBranchDescendants;
+};
+
+#endif
Index: scribus/ui/stylesbrowser.ui
===================================================================
--- scribus/ui/stylesbrowser.ui	(revision 0)
+++ scribus/ui/stylesbrowser.ui	(working copy)
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>StylesBrowserWidget</class>
+ <widget class="QWidget" name="StylesBrowserWidget">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>280</width>
+    <height>420</height>
+   </rect>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <property name="leftMargin">
+    <number>6</number>
+   </property>
+   <property name="topMargin">
+    <number>6</number>
+   </property>
+   <property name="rightMargin">
+    <number>6</number>
+   </property>
+   <property name="bottomMargin">
+    <number>6</number>
+   </property>
+   <property name="spacing">
+    <number>6</number>
+   </property>
+   <item>
+    <widget class="QLineEdit" name="filterEdit">
+     <property name="placeholderText">
+      <string>Filter styles…</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QTreeWidget" name="stylesTree">
+     <property name="alternatingRowColors">
+      <bool>true</bool>
+     </property>
+     <property name="uniformRowHeights">
+      <bool>true</bool>
+     </property>
+     <property name="selectionMode">
+      <enum>QAbstractItemView::SingleSelection</enum>
+     </property>
+     <property name="headerHidden">
+      <bool>true</bool>
+     </property>
+     <property name="sortingEnabled">
+      <bool>false</bool>
+     </property>
+     <column>
+      <property name="text">
+       <string>Style</string>
+      </property>
+     </column>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
sb2-styles-browser-interaction-v1.02.patch (21,805 bytes)   
Index: scribus/ui/stylemanager.cpp
===================================================================
--- scribus/ui/stylemanager.cpp	(revision 27810)
+++ scribus/ui/stylemanager.cpp	(working copy)
@@ -213,40 +213,23 @@
 		okButton->setText(CommonStrings::tr_OK);
 }
 
-void StyleManager::editStyleByName(int rcType, const QString &name)
+void StyleManager::editStyleByName(const QString& typeName, const QString& name)
 {
 	m_rcStyle = name;
-	int i = 0;
-
-	// nitramr:
-	// We need a better way to find rcType than by ID of top level items. If the order of the root style changes it will break.
-	// It is better to store an unchangeable identifier in StyleViewItem::data(int column, int role).
-	// https://doc.qt.io/qt-6/qtreewidgetitem.html#data
+	m_rcType = typeName;
 
 	QTreeWidgetItemIterator it(styleView);
 	while (*it)
 	{
-		StyleViewItem *item = dynamic_cast<StyleViewItem*>(*it);
-		if (item)
-		{
-			// find rcType by ID
-			if (item->isRoot())
-			{
-				if (i == rcType)
-					m_rcType = item->text(0);
-				++i;
-			}
-
-			// double click item with matching style name in right root style
-			if (item->rootName() == m_rcType && item->text(0) == m_rcStyle)
-			{
-				slotDoubleClick(item, 0);
-				break;
-			}
-
+		StyleViewItem* item = dynamic_cast<StyleViewItem*>(*it);
+		if (item && !item->isRoot() && item->rootName() == m_rcType && item->text(0) == m_rcStyle)
+		{
+			slotDoubleClick(item, 0);
+			break;
 		}
 		++it;
 	}
+
 	m_rcStyle.clear();
 	m_rcType.clear();
 }
@@ -318,10 +301,38 @@
 	if (name.isEmpty())
 		return;
 
+	SMCharacterStyle* styleItem = item<SMCharacterStyle>();
+	if (!styleItem)
+		return;
+
 	setPaletteShown(true);
-
-	// 1 = top level id of character style in viewStyles list
-	editStyleByName(1, name);
+	editStyleByName(styleItem->typeName(), name);
+}
+
+void StyleManager::showAsEditTableStyle(const QString &name)
+{
+	if (name.isEmpty())
+		return;
+
+	SMTableStyle* styleItem = item<SMTableStyle>();
+	if (!styleItem)
+		return;
+
+	setPaletteShown(true);
+	editStyleByName(styleItem->typeName(), name);
+}
+
+void StyleManager::showAsEditCellStyle(const QString &name)
+{
+	if (name.isEmpty())
+		return;
+
+	SMCellStyle* styleItem = item<SMCellStyle>();
+	if (!styleItem)
+		return;
+
+	setPaletteShown(true);
+	editStyleByName(styleItem->typeName(), name);
 }
 
 void StyleManager::showAsEditLineStyle(const QString &name)
@@ -329,10 +340,12 @@
 	if (name.isEmpty())
 		return;
 
+	SMLineStyle* styleItem = item<SMLineStyle>();
+	if (!styleItem)
+		return;
+
 	setPaletteShown(true);
-
-	// 4 = top level id of line style in viewStyles list
-	editStyleByName(4, name);
+	editStyleByName(styleItem->typeName(), name);
 }
 
 void StyleManager::reloadStyles()
@@ -361,11 +374,12 @@
 	if (name.isEmpty())
 		return;
 
+	SMParagraphStyle* styleItem = item<SMParagraphStyle>();
+	if (!styleItem)
+		return;
+
 	setPaletteShown(true);
-
-	// 0 = top level id of paragraph style in viewStyles list
-	editStyleByName(0, name);
-
+	editStyleByName(styleItem->typeName(), name);
 }
 
 void StyleManager::addStyle(StyleItem *item)
Index: scribus/ui/stylemanager.h
===================================================================
--- scribus/ui/stylemanager.h	(revision 27810)
+++ scribus/ui/stylemanager.h	(working copy)
@@ -39,6 +39,8 @@
 	void showAsNewLineStyle();
 	void showAsEditParagraphStyle(const QString& name);
 	void showAsEditCharacterStyle(const QString& name);
+	void showAsEditTableStyle(const QString& name);
+	void showAsEditCellStyle(const QString& name);
 	void showAsEditLineStyle(const QString& name);
 	/**
 	 * @brief Reload all style data from the document and rebuild the
@@ -170,7 +172,7 @@
 	void setSelection(const QList<QPair<QString, QString> > &selected);
 	void setOkButtonText();
 
-	void editStyleByName(int rcType, const QString &name);
+	void editStyleByName(const QString& typeName, const QString& name);
 
 	template<class ItemType> ItemType* item();
 
Index: scribus/ui/stylesbrowser.cpp
===================================================================
--- scribus/ui/stylesbrowser.cpp	(revision 0)
+++ scribus/ui/stylesbrowser.cpp	(working copy)
@@ -10,20 +10,26 @@
 #include <algorithm>
 #include <utility>
 
+#include <QApplication>
 #include <QEvent>
 #include <QHash>
+#include <QMenu>
 #include <QSignalBlocker>
 #include <QTreeWidgetItem>
 #include <QTreeWidgetItemIterator>
 
 #include "iconmanager.h"
+#include "pageitem.h"
+#include "pageitem_table.h"
 #include "scribus.h"
 #include "scribusdoc.h"
 #include "scribusstructs.h"
+#include "selection.h"
 #include "styles/cellstyle.h"
 #include "styles/charstyle.h"
 #include "styles/paragraphstyle.h"
 #include "styles/tablestyle.h"
+#include "ui/stylemanager.h"
 
 StylesBrowser::StylesBrowser(QWidget* parent)
 	: DockPanelBase(QStringLiteral("StylesBrowser"), QStringLiteral("paragraph-style"), parent)
@@ -36,9 +42,17 @@
 	setWidget(m_container);
 
 	m_ui.filterEdit->setClearButtonEnabled(true);
+	m_ui.stylesTree->setContextMenuPolicy(Qt::CustomContextMenu);
+
+	m_applyTimer.setSingleShot(true);
+	m_applyTimer.setInterval(QApplication::doubleClickInterval());
 
 	connect(m_ui.filterEdit, &QLineEdit::textChanged, this, &StylesBrowser::filterStyles);
 	connect(this, &CDockWidget::visibilityChanged, this, &StylesBrowser::handleVisibilityChanged);
+	connect(&m_applyTimer, &QTimer::timeout, this, &StylesBrowser::applyPendingStyle);
+	connect(m_ui.stylesTree, &QTreeWidget::itemClicked, this, &StylesBrowser::handleStyleItemClicked);
+	connect(m_ui.stylesTree, &QTreeWidget::itemDoubleClicked, this, &StylesBrowser::handleStyleItemDoubleClicked);
+	connect(m_ui.stylesTree, &QTreeWidget::customContextMenuRequested, this, &StylesBrowser::showStyleContextMenu);
 	connect(m_ui.stylesTree, &QTreeWidget::itemExpanded, this, [this](QTreeWidgetItem*)
 	{
 		if (m_ui.filterEdit->text().isEmpty())
@@ -72,15 +86,36 @@
 		return;
 
 	if (m_mainWindow)
-		disconnect(m_mainWindow, &ScribusMainWindow::UpdateRequest, this, &StylesBrowser::handleUpdateRequest);
+		disconnect(m_mainWindow, nullptr, this, nullptr);
 
 	m_mainWindow = mainWindow;
 	if (m_mainWindow)
+	{
 		connect(m_mainWindow, &ScribusMainWindow::UpdateRequest, this, &StylesBrowser::handleUpdateRequest, Qt::UniqueConnection);
+		connect(m_mainWindow, &ScribusMainWindow::TextStyle, this, [this](const ParagraphStyle&)
+		{
+			if (isVisible())
+				updateAppliedStyles();
+		});
+	}
 	else
 		unsetDoc();
 }
 
+void StylesBrowser::disconnectDocumentSignals()
+{
+	if (m_connectedTable)
+		disconnect(m_connectedTable.data(), nullptr, this, nullptr);
+	m_connectedTable = nullptr;
+
+	if (!m_doc)
+		return;
+
+	if (m_doc->m_Selection)
+		disconnect(m_doc->m_Selection, nullptr, this, nullptr);
+	disconnect(m_doc, nullptr, this, nullptr);
+}
+
 void StylesBrowser::setDoc(ScribusDoc* doc)
 {
 	if (!m_mainWindow)
@@ -89,12 +124,20 @@
 	if (m_doc == doc)
 	{
 		if (m_doc)
+		{
 			setEnabled(!(m_doc->drawAsPreview && !m_doc->editOnPreview));
-		return;
-	}
-
+			reconnectTableSignals();
+			if (isVisible())
+				updateAppliedStyles();
+		}
+		return;
+	}
+
+	disconnectDocumentSignals();
 	m_doc = doc;
 	m_treeDirty = true;
+	m_pendingStyleName.clear();
+	m_applyTimer.stop();
 
 	if (!m_doc)
 	{
@@ -102,6 +145,14 @@
 		setEnabled(false);
 		return;
 	}
+
+	connect(m_doc->m_Selection, &Selection::selectionChanged, this, &StylesBrowser::handleSelectionChanged);
+	connect(m_doc, &ScribusDoc::docChanged, this, [this]()
+	{
+		if (isVisible())
+			updateAppliedStyles();
+	});
+	reconnectTableSignals();
 
 	setEnabled(!(m_doc->drawAsPreview && !m_doc->editOnPreview));
 	if (isVisible())
@@ -110,8 +161,11 @@
 
 void StylesBrowser::unsetDoc()
 {
+	disconnectDocumentSignals();
 	m_doc = nullptr;
 	m_treeDirty = true;
+	m_pendingStyleName.clear();
+	m_applyTimer.stop();
 	m_ui.stylesTree->clear();
 	setEnabled(false);
 }
@@ -129,17 +183,65 @@
 
 void StylesBrowser::updateStylesTree()
 {
-	if (!m_doc || !m_treeDirty)
-		return;
-
-	rebuildTree();
-	m_treeDirty = false;
+	if (!m_doc)
+		return;
+
+	if (m_treeDirty)
+	{
+		rebuildTree();
+		m_treeDirty = false;
+	}
+	else
+		updateAppliedStyles();
 }
 
 void StylesBrowser::handleVisibilityChanged(bool visible)
 {
 	if (visible)
 		updateStylesTree();
+	else
+	{
+		m_applyTimer.stop();
+		m_pendingStyleName.clear();
+	}
+}
+
+void StylesBrowser::handleSelectionChanged()
+{
+	if (!m_doc)
+		return;
+
+	reconnectTableSignals();
+	if (isVisible())
+		updateAppliedStyles();
+}
+
+void StylesBrowser::reconnectTableSignals()
+{
+	PageItem_Table* selectedTable = nullptr;
+	if (m_doc && m_doc->m_Selection && m_doc->m_Selection->count() == 1)
+		selectedTable = m_doc->m_Selection->itemAt(0)->asTable();
+
+	if (m_connectedTable == selectedTable)
+		return;
+
+	if (m_connectedTable)
+		disconnect(m_connectedTable.data(), nullptr, this, nullptr);
+	m_connectedTable = selectedTable;
+
+	if (m_connectedTable)
+	{
+		connect(m_connectedTable.data(), &PageItem_Table::selectionChanged, this, [this]()
+		{
+			if (isVisible())
+				updateAppliedStyles();
+		});
+		connect(m_connectedTable.data(), &PageItem_Table::changed, this, [this]()
+		{
+			if (isVisible())
+				updateAppliedStyles();
+		});
+	}
 }
 
 void StylesBrowser::rebuildTree()
@@ -216,6 +318,7 @@
 		}
 	}
 
+	updateAppliedStyles();
 	filterStyles(m_ui.filterEdit->text());
 }
 
@@ -268,6 +371,7 @@
 	root->setIcon(0, IconManager::instance().loadIcon(iconName));
 	root->setData(0, ItemKindRole, CategoryItem);
 	root->setData(0, StyleTypeRole, static_cast<int>(type));
+	root->setData(0, CategoryLabelRole, label);
 	root->setFlags(root->flags() & ~Qt::ItemIsSelectable);
 
 	QHash<QString, QTreeWidgetItem*> itemsByName;
@@ -279,10 +383,347 @@
 
 		auto* item = new QTreeWidgetItem(parentItem);
 		item->setText(0, entry.displayName);
+		item->setToolTip(0, tr("Click to apply this style; double-click to edit it."));
 		item->setData(0, ItemKindRole, StyleItem);
 		item->setData(0, StyleTypeRole, static_cast<int>(type));
 		item->setData(0, StyleNameRole, entry.name);
 		itemsByName.insert(entry.name, item);
+	}
+}
+
+void StylesBrowser::handleStyleItemClicked(QTreeWidgetItem* item, int column)
+{
+	if (!item || column != 0 || item->data(0, ItemKindRole).toInt() != StyleItem)
+		return;
+
+	m_pendingStyleType = static_cast<StyleType>(item->data(0, StyleTypeRole).toInt());
+	m_pendingStyleName = item->data(0, StyleNameRole).toString();
+	m_applyTimer.start();
+}
+
+void StylesBrowser::handleStyleItemDoubleClicked(QTreeWidgetItem* item, int column)
+{
+	if (!item || column != 0 || item->data(0, ItemKindRole).toInt() != StyleItem)
+		return;
+
+	m_applyTimer.stop();
+	m_pendingStyleName.clear();
+	editStyle(static_cast<StyleType>(item->data(0, StyleTypeRole).toInt()), item->text(0));
+}
+
+void StylesBrowser::applyPendingStyle()
+{
+	if (m_pendingStyleName.isEmpty())
+		return;
+
+	const StyleType type = m_pendingStyleType;
+	const QString styleName = m_pendingStyleName;
+	m_pendingStyleName.clear();
+	applyStyle(type, styleName);
+}
+
+void StylesBrowser::showStyleContextMenu(const QPoint& point)
+{
+	QTreeWidgetItem* item = m_ui.stylesTree->itemAt(point);
+	if (!item || item->data(0, ItemKindRole).toInt() != StyleItem)
+		return;
+
+	m_applyTimer.stop();
+	m_pendingStyleName.clear();
+
+	const StyleType type = static_cast<StyleType>(item->data(0, StyleTypeRole).toInt());
+	const QString styleName = item->data(0, StyleNameRole).toString();
+	const QString displayName = item->text(0);
+
+	QMenu menu(this);
+	QAction* applyAction = menu.addAction(tr("Apply"));
+	const AppliedStyleState state = appliedStyleState(type);
+	applyAction->setEnabled(canApplyStyle(type) && (state.mixed || !state.hasTarget || state.name != styleName));
+	QAction* editAction = menu.addAction(tr("Edit in Style Manager"));
+
+	QAction* selectedAction = menu.exec(m_ui.stylesTree->viewport()->mapToGlobal(point));
+	if (selectedAction == applyAction)
+		applyStyle(type, styleName);
+	else if (selectedAction == editAction)
+		editStyle(type, displayName);
+}
+
+bool StylesBrowser::canApplyStyle(StyleType type) const
+{
+	if (!m_doc || !m_doc->m_Selection || m_doc->m_Selection->isEmpty())
+		return false;
+
+	for (int i = 0; i < m_doc->m_Selection->count(); ++i)
+	{
+		PageItem* item = m_doc->m_Selection->itemAt(i);
+		switch (type)
+		{
+			case StyleType::Paragraph:
+			case StyleType::Character:
+				if (textTarget(item))
+					return true;
+				break;
+			case StyleType::Table:
+			case StyleType::Cell:
+				if (item->isTable())
+					return true;
+				break;
+			case StyleType::Line:
+				return true;
+		}
+	}
+	return false;
+}
+
+void StylesBrowser::applyStyle(StyleType type, const QString& styleName)
+{
+	if (!m_doc || styleName.isEmpty() || !canApplyStyle(type))
+		return;
+
+	const AppliedStyleState state = appliedStyleState(type);
+	if (state.hasTarget && !state.mixed && state.name == styleName)
+		return;
+
+	switch (type)
+	{
+		case StyleType::Paragraph:
+			m_doc->itemSelection_SetNamedParagraphStyle(styleName);
+			break;
+		case StyleType::Character:
+			m_doc->itemSelection_SetNamedCharStyle(styleName);
+			break;
+		case StyleType::Table:
+			m_doc->itemSelection_SetNamedTableStyle(styleName);
+			break;
+		case StyleType::Cell:
+			m_doc->itemSelection_SetNamedCellStyle(styleName);
+			break;
+		case StyleType::Line:
+			m_doc->itemSelection_SetNamedLineStyle(styleName);
+			break;
+	}
+
+	updateAppliedStyles();
+}
+
+void StylesBrowser::editStyle(StyleType type, const QString& displayName)
+{
+	if (!m_mainWindow || !m_mainWindow->styleMgr() || displayName.isEmpty())
+		return;
+
+	StyleManager* styleManager = m_mainWindow->styleMgr();
+	switch (type)
+	{
+		case StyleType::Paragraph:
+			styleManager->showAsEditParagraphStyle(displayName);
+			break;
+		case StyleType::Character:
+			styleManager->showAsEditCharacterStyle(displayName);
+			break;
+		case StyleType::Table:
+			styleManager->showAsEditTableStyle(displayName);
+			break;
+		case StyleType::Cell:
+			styleManager->showAsEditCellStyle(displayName);
+			break;
+		case StyleType::Line:
+			styleManager->showAsEditLineStyle(displayName);
+			break;
+	}
+}
+
+PageItem* StylesBrowser::textTarget(PageItem* item) const
+{
+	if (!item || !m_doc)
+		return nullptr;
+
+	if (item->isTable())
+	{
+		if (m_doc->appMode != modeEditTable)
+			return nullptr;
+		TableCell cell = item->asTable()->activeCell();
+		return cell.isValid() ? cell.textFrame() : nullptr;
+	}
+
+	if (item->isTextFrame() || item->isPathText())
+		return item;
+	return nullptr;
+}
+
+void StylesBrowser::addAppliedStyle(AppliedStyleState& state, const QString& styleName) const
+{
+	if (!state.hasTarget)
+	{
+		state.name = styleName;
+		state.hasTarget = true;
+		return;
+	}
+
+	if (state.name != styleName)
+		state.mixed = true;
+}
+
+QString StylesBrowser::normalizedStyleName(StyleType type, const QString& styleName) const
+{
+	if (!m_doc || !styleName.isEmpty())
+		return styleName;
+
+	switch (type)
+	{
+		case StyleType::Paragraph:
+		{
+			const ParagraphStyle* style = m_doc->paragraphStyles().getDefault();
+			return style ? style->name() : QString();
+		}
+		case StyleType::Character:
+			return QString();
+		case StyleType::Table:
+		{
+			const TableStyle* style = m_doc->tableStyles().getDefault();
+			return style ? style->name() : QString();
+		}
+		case StyleType::Cell:
+		{
+			const CellStyle* style = m_doc->cellStyles().getDefault();
+			return style ? style->name() : QString();
+		}
+		case StyleType::Line:
+			return QString();
+	}
+	return QString();
+}
+
+StylesBrowser::AppliedStyleState StylesBrowser::appliedStyleState(StyleType type) const
+{
+	AppliedStyleState state;
+	if (!m_doc || !m_doc->m_Selection)
+		return state;
+
+	const bool textEditMode = (m_doc->appMode == modeEdit || m_doc->appMode == modeEditTable);
+
+	for (int i = 0; i < m_doc->m_Selection->count() && !state.mixed; ++i)
+	{
+		PageItem* item = m_doc->m_Selection->itemAt(i);
+
+		if (type == StyleType::Paragraph || type == StyleType::Character)
+		{
+			PageItem* textItem = textTarget(item);
+			if (!textItem)
+				continue;
+
+			if (!textEditMode)
+			{
+				const QString styleName = type == StyleType::Paragraph
+					? textItem->itemText.defaultStyle().parent()
+					: textItem->itemText.defaultStyle().charStyle().parent();
+				addAppliedStyle(state, normalizedStyleName(type, styleName));
+				continue;
+			}
+
+			if (!textItem->itemText.hasSelection())
+			{
+				const QString styleName = type == StyleType::Paragraph
+					? textItem->currentStyle().parent()
+					: textItem->currentCharStyle().parent();
+				addAppliedStyle(state, normalizedStyleName(type, styleName));
+				continue;
+			}
+
+			const int start = textItem->itemText.startOfSelection();
+			const int end = textItem->itemText.endOfSelection();
+			if (start >= end)
+				continue;
+
+			if (type == StyleType::Paragraph)
+			{
+				int pos = qBound(0, start, textItem->itemText.length());
+				while (pos <= textItem->itemText.length())
+				{
+					addAppliedStyle(state, normalizedStyleName(type, textItem->itemText.paragraphStyle(pos).parent()));
+					if (state.mixed)
+						break;
+					const int separator = textItem->itemText.nextParagraph(pos);
+					if (separator >= end || separator >= textItem->itemText.length())
+						break;
+					pos = separator + 1;
+				}
+			}
+			else
+			{
+				for (int pos = start; pos < end; ++pos)
+				{
+					addAppliedStyle(state, normalizedStyleName(type, textItem->itemText.charStyle(pos).parent()));
+					if (state.mixed)
+						break;
+				}
+			}
+			continue;
+		}
+
+		if (type == StyleType::Line)
+		{
+			addAppliedStyle(state, item->customLineStyle());
+			continue;
+		}
+
+		PageItem_Table* table = item->asTable();
+		if (!table)
+			continue;
+
+		if (type == StyleType::Table)
+		{
+			addAppliedStyle(state, normalizedStyleName(type, table->styleName()));
+			continue;
+		}
+
+		QSet<TableCell> cells;
+		if (m_doc->appMode == modeEditTable)
+		{
+			cells = table->selectedCells();
+			if (cells.isEmpty() && table->activeCell().isValid())
+				cells.insert(table->activeCell());
+		}
+		else
+			cells = table->cells();
+
+		for (const TableCell& cell : std::as_const(cells))
+		{
+			addAppliedStyle(state, normalizedStyleName(type, cell.styleName()));
+			if (state.mixed)
+				break;
+		}
+	}
+
+	return state;
+}
+
+void StylesBrowser::updateAppliedStyles()
+{
+	if (!m_doc || m_ui.stylesTree->topLevelItemCount() == 0)
+		return;
+
+	const QIcon activeIcon = IconManager::instance().loadIcon(QStringLiteral("ok"));
+	for (int i = 0; i < m_ui.stylesTree->topLevelItemCount(); ++i)
+	{
+		QTreeWidgetItem* root = m_ui.stylesTree->topLevelItem(i);
+		const StyleType type = static_cast<StyleType>(root->data(0, StyleTypeRole).toInt());
+		const QString categoryLabel = root->data(0, CategoryLabelRole).toString();
+		const AppliedStyleState state = appliedStyleState(type);
+
+		root->setText(0, state.mixed ? tr("%1 — Mixed").arg(categoryLabel) : categoryLabel);
+
+		QTreeWidgetItemIterator it(m_ui.stylesTree);
+		while (*it)
+		{
+			QTreeWidgetItem* item = *it;
+			if (item->data(0, ItemKindRole).toInt() == StyleItem &&
+				item->data(0, StyleTypeRole).toInt() == static_cast<int>(type))
+			{
+				const bool active = state.hasTarget && !state.mixed && item->data(0, StyleNameRole).toString() == state.name;
+				item->setIcon(0, active ? activeIcon : QIcon());
+			}
+			++it;
+		}
 	}
 }
 
Index: scribus/ui/stylesbrowser.h
===================================================================
--- scribus/ui/stylesbrowser.h	(revision 0)
+++ scribus/ui/stylesbrowser.h	(working copy)
@@ -8,15 +8,20 @@
 #define STYLESBROWSER_H
 
 #include <QList>
+#include <QPointer>
 #include <QSet>
 #include <QString>
+#include <QTimer>
 
 #include "scribusapi.h"
 #include "ui/docks/dock_panelbase.h"
 #include "ui_stylesbrowser.h"
 
 class QEvent;
+class QPoint;
 class QTreeWidgetItem;
+class PageItem;
+class PageItem_Table;
 class ScribusDoc;
 class ScribusMainWindow;
 
@@ -50,6 +55,11 @@
 private slots:
 	void filterStyles(const QString& text);
 	void handleVisibilityChanged(bool visible);
+	void applyPendingStyle();
+	void handleSelectionChanged();
+	void handleStyleItemClicked(QTreeWidgetItem* item, int column);
+	void handleStyleItemDoubleClicked(QTreeWidgetItem* item, int column);
+	void showStyleContextMenu(const QPoint& point);
 
 private:
 	struct StyleEntry
@@ -58,6 +68,13 @@
 		QString displayName;
 		QString parent;
 		bool isDefault { false };
+	};
+
+	struct AppliedStyleState
+	{
+		QString name;
+		bool hasTarget { false };
+		bool mixed { false };
 	};
 
 	enum ItemKind
@@ -70,7 +87,8 @@
 	{
 		ItemKindRole = Qt::UserRole,
 		StyleTypeRole,
-		StyleNameRole
+		StyleNameRole,
+		CategoryLabelRole
 	};
 
 	void rebuildTree();
@@ -80,10 +98,25 @@
 	void captureExpansionState();
 	void restoreExpansionState();
 
+	void applyStyle(StyleType type, const QString& styleName);
+	void editStyle(StyleType type, const QString& displayName);
+	bool canApplyStyle(StyleType type) const;
+	void updateAppliedStyles();
+	AppliedStyleState appliedStyleState(StyleType type) const;
+	void addAppliedStyle(AppliedStyleState& state, const QString& styleName) const;
+	QString normalizedStyleName(StyleType type, const QString& styleName) const;
+	PageItem* textTarget(PageItem* item) const;
+	void reconnectTableSignals();
+	void disconnectDocumentSignals();
+
 	Ui::StylesBrowserWidget m_ui;
 	QWidget* m_container { nullptr };
 	ScribusDoc* m_doc { nullptr };
 	ScribusMainWindow* m_mainWindow { nullptr };
+	QPointer<PageItem_Table> m_connectedTable;
+	QTimer m_applyTimer;
+	StyleType m_pendingStyleType { StyleType::Paragraph };
+	QString m_pendingStyleName;
 	bool m_treeDirty { true };
 	bool m_haveExpansionState { false };
 	QSet<QString> m_expandedItems;
style-browser.png (50,535 bytes)   
style-browser.png (50,535 bytes)   

Issue History

Date Modified Username Field Change
2026-06-21 14:23 ale New Issue
2026-06-21 14:25 ale Relationship added related to 0017859
2026-06-21 16:17 ale Assigned To => ale
2026-06-21 16:17 ale Status new => assigned
2026-09-04 06:25 buttar Note Added: 0054409
2026-09-04 06:25 buttar File Added: sb1-styles-browser-palette-v1.02.patch
2026-09-04 06:25 buttar File Added: sb2-styles-browser-interaction-v1.02.patch
2026-09-04 06:25 buttar File Added: style-browser.png
2026-09-04 06:26 buttar Tag Attached: #please_test