View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0017858 | Scribus | Styles | public | 2026-06-21 14:23 | 2026-09-04 06:26 |
| Reporter | ale | Assigned To | ale | ||
| Priority | normal | Severity | feature | Reproducibility | N/A |
| Status | assigned | Resolution | open | ||
| Product Version | 1.7.4.svn | ||||
| Summary | 0017858: Create a styles browser | ||||
| Description | Currently, 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 | ||||
| Patch | No | ||||
|
|
#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;
|
| 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 |