View Issue Details

IDProjectCategoryView StatusLast Update
0017980ScribusMaster Pagespublic2026-09-23 16:41
Reporterbuttar Assigned To 
PrioritynormalSeverityfeatureReproducibilityN/A
Status newResolutionopen 
PlatformLinuxOSFedora WorkstationOS Version44
Product Version1.7.4.svn 
Summary0017980: Allow Delete Unused for master pages as well
DescriptionIt will be a really nice quality of life improvement. With many master pages, it is confusing knowing which are actually used in the document and which are just sitting there causing clutter.
Tags#please_test
PatchNo

Activities

buttar

2026-09-23 12:50

reporter   ~0054490

This 2-patch series adds a safer, more complete master-page deletion work-flow, including reliable deletion/undo behaviour, protected default masters, Delete Unused, Select Unused, and explicit replacement choices for masters that are still in use.

The series depends on `mid-foundation-v1.0.patch` (from 0017984) for stable master-page identity, so that deletion, protection, replacement, and undo/redo operate on the actual master rather than its editable name. (mdu = master delete unused)

## `mdu1-deletion-hardening-v1.0.patch`

- Protects built-in/default masters by identity.
- Unifies deletion behaviour across Edit Master Pages, Arrange Pages, and scripting.
- Makes used-master replacement part of the same undoable deletion operation.
- Restores the deleted master’s essential state correctly through undo/redo.

## `mdu2-delete-unused-v1.33.patch`

- Adds Delete Unused... and Select Unused for custom master pages.
- Adds one confirmation dialog showing usage and replacement behaviour before deletion.
- Supports side-aware per-master replacement choices and optional bulk Left/Right replacement.
- Keeps protected defaults out of the deletion set and reports that they are retained.
- Performs confirmed multi-master deletion as one undoable operation.
mdu1-deletion-hardening-v1.0.patch (27,227 bytes)   
Index: scribus/fileloader.cpp
===================================================================
--- scribus/fileloader.cpp	(revision 27861)
+++ scribus/fileloader.cpp	(working copy)
@@ -261,6 +261,8 @@
 				break;
 		}
 	}
+	if (ret)
+		currDoc->ensureDefaultMasterPages();
 	return ret;
 }
 
Index: scribus/plugins/fileloader/scribus171format/scribus171format.cpp
===================================================================
--- scribus/plugins/fileloader/scribus171format/scribus171format.cpp	(revision 27861)
+++ scribus/plugins/fileloader/scribus171format/scribus171format.cpp	(working copy)
@@ -4816,6 +4816,8 @@
 	m_Doc->setMasterPageMode(!pageName.isEmpty());
 	const QUuid storedMasterId(attrs.valueAsString("MasterPageId"));
 	ScPage* newPage = pageName.isEmpty() ? doc->addPage(pageNum) : doc->addMasterPage(pageNum, pageName, storedMasterId);
+	if (!pageName.isEmpty() && attrs.valueAsBool("DefaultMaster", false))
+		doc->setDefaultMasterPage(newPage->masterPageId());
 
 	//Remove uppercase in 1.8
 	if (attrs.hasAttribute("LEFT"))
Index: scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp
===================================================================
--- scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp	(revision 27861)
+++ scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp	(working copy)
@@ -1798,6 +1798,8 @@
 		docu.writeAttribute("MasterPageName",page->masterPageName());
 		if (!page->masterPageId().isNull())
 			docu.writeAttribute("MasterPageId", page->masterPageId().toString(QUuid::WithoutBraces));
+		if (master && doc->isDefaultMasterPage(page->masterPageId()))
+			docu.writeAttribute("DefaultMaster", 1);
 		if (!page->appliedMasterPageId().isNull())
 			docu.writeAttribute("AppliedMasterPageId", page->appliedMasterPageId().toString(QUuid::WithoutBraces));
 		PageSize ps(page->width(), page->height());
Index: scribus/plugins/scriptplugin/cmddoc.cpp
===================================================================
--- scribus/plugins/scriptplugin/cmddoc.cpp	(revision 27861)
+++ scribus/plugins/scriptplugin/cmddoc.cpp	(working copy)
@@ -502,14 +502,15 @@
 		PyErr_SetString(PyExc_ValueError, "Master page does not exist");
 		return nullptr;
 	}
-	if (masterPageName == "Normal")
+	const QUuid masterId = currentDoc->masterPageId(masterPageName);
+	if (!currentDoc->canDeleteMasterPage(masterId))
 	{
-		PyErr_SetString(PyExc_ValueError, "Can not delete the Normal master page");
+		PyErr_SetString(PyExc_ValueError, "Can not delete a default master page");
 		return nullptr;
 	}
 	bool oldMode = currentDoc->masterPageMode();
 	currentDoc->setMasterPageMode(true);
-	ScCore->primaryMainWindow()->deletePage2(currentDoc->MasterNames[masterPageName]);
+	ScCore->primaryMainWindow()->deletePage2(currentDoc->masterPageIndex(masterId));
 	currentDoc->setMasterPageMode(oldMode);
 
 	Py_RETURN_NONE;
Index: scribus/scribus.cpp
===================================================================
--- scribus/scribus.cpp	(revision 27861)
+++ scribus/scribus.cpp	(working copy)
@@ -6099,6 +6099,13 @@
 	if (HaveDoc && doc->appMode == modeEditClip)
 		view->requestMode(submodeEndNodeEdit);
 	view->deselectItems(true);
+	if (doc->masterPageMode())
+	{
+		if (pg < 0 || pg >= doc->Pages->count())
+			return;
+		if (!doc->canDeleteMasterPage(doc->Pages->at(pg)->masterPageId()))
+			return;
+	}
 	if (doc->Pages->count() == 1)
 		return;
 	deletePage(pg + 1, pg + 1);
@@ -6127,6 +6134,15 @@
 		activeTransaction = m_undoManager->beginTransaction(doc->documentFileName(), Um::IDocument,
 														  (from - to == 0) ? Um::DeletePage : Um::DeletePages, QString(),
 														  Um::IDelete);
+	if (doc->masterPageMode())
+	{
+		for (int a = to - 1; a >= from - 1; --a)
+		{
+			const QUuid masterId = doc->Pages->at(a)->masterPageId();
+			if (doc->canDeleteMasterPage(masterId))
+				doc->replaceMasterPage(masterId);
+		}
+	}
 	PageItem* ite;
 	doc->m_Selection->clear();
 	Selection tmpSelection(this, false);
@@ -6162,14 +6178,34 @@
 	{
 		if (UndoManager::undoEnabled())
 		{
+			ScPage* page = doc->Pages->at(a);
 			auto *ss = new SimpleState(Um::DeletePage, QString(), Um::ICreate);
 			ss->set("DELETE_PAGE");
 			ss->set("PAGENR", a + 1);
-			ss->set("PAGENAME",   doc->Pages->at(a)->pageName());
-			ss->set("MASTERPAGE", doc->Pages->at(a)->masterPageName());
-			ss->set("MASTER_PAGE_ID", doc->Pages->at(a)->masterPageId().toString(QUuid::WithoutBraces));
-			ss->set("APPLIED_MASTER_PAGE_ID", doc->Pages->at(a)->appliedMasterPageId().toString(QUuid::WithoutBraces));
+			ss->set("PAGENAME",   page->pageName());
+			ss->set("MASTERPAGE", page->masterPageName());
+			ss->set("MASTER_PAGE_ID", page->masterPageId().toString(QUuid::WithoutBraces));
+			ss->set("APPLIED_MASTER_PAGE_ID", page->appliedMasterPageId().toString(QUuid::WithoutBraces));
 			ss->set("MASTER_PAGE_MODE",  doc->masterPageMode());
+			if (doc->masterPageMode())
+			{
+				ss->set("PAGE_WIDTH", page->width());
+				ss->set("PAGE_HEIGHT", page->height());
+				ss->set("PAGE_INITIAL_WIDTH", page->initialWidth());
+				ss->set("PAGE_INITIAL_HEIGHT", page->initialHeight());
+				ss->set("PAGE_ORIENTATION", page->orientation());
+				ss->set("PAGE_SIZE", page->size());
+				ss->set("PAGE_LEFT", page->LeftPg);
+				ss->set("PAGE_MARGIN_PRESET", page->marginPreset);
+				ss->set("PAGE_MARGIN_TOP", page->Margins.top());
+				ss->set("PAGE_MARGIN_BOTTOM", page->Margins.bottom());
+				ss->set("PAGE_MARGIN_LEFT", page->Margins.left());
+				ss->set("PAGE_MARGIN_RIGHT", page->Margins.right());
+				ss->set("PAGE_INITIAL_MARGIN_TOP", page->initialMargins.top());
+				ss->set("PAGE_INITIAL_MARGIN_BOTTOM", page->initialMargins.bottom());
+				ss->set("PAGE_INITIAL_MARGIN_LEFT", page->initialMargins.left());
+				ss->set("PAGE_INITIAL_MARGIN_RIGHT", page->initialMargins.right());
+			}
 			// replace the deleted page in the undostack by a dummy object that will
 			// replaced with the "undone" page if user choose to undo the action
 			auto *duo = new DummyUndoObject();
@@ -7961,8 +7997,28 @@
 		{
 			slotNewMasterPage(wo, pageName);
 			const QUuid restoredId(state->get("MASTER_PAGE_ID"));
+			ScPage* restoredPage = doc->Pages->at(pagenr - 1);
 			if (!restoredId.isNull())
-				doc->Pages->at(pagenr - 1)->setMasterPageId(restoredId);
+				restoredPage->setMasterPageId(restoredId);
+			if (state->contains("PAGE_WIDTH"))
+			{
+				restoredPage->setInitialWidth(state->getDouble("PAGE_INITIAL_WIDTH"));
+				restoredPage->setInitialHeight(state->getDouble("PAGE_INITIAL_HEIGHT"));
+				restoredPage->setWidth(state->getDouble("PAGE_WIDTH"));
+				restoredPage->setHeight(state->getDouble("PAGE_HEIGHT"));
+				restoredPage->setOrientation(state->getInt("PAGE_ORIENTATION"));
+				restoredPage->setSize(state->get("PAGE_SIZE"));
+				restoredPage->LeftPg = state->getInt("PAGE_LEFT");
+				restoredPage->marginPreset = state->getInt("PAGE_MARGIN_PRESET");
+				restoredPage->Margins.setTop(state->getDouble("PAGE_MARGIN_TOP"));
+				restoredPage->Margins.setBottom(state->getDouble("PAGE_MARGIN_BOTTOM"));
+				restoredPage->Margins.setLeft(state->getDouble("PAGE_MARGIN_LEFT"));
+				restoredPage->Margins.setRight(state->getDouble("PAGE_MARGIN_RIGHT"));
+				restoredPage->initialMargins.setTop(state->getDouble("PAGE_INITIAL_MARGIN_TOP"));
+				restoredPage->initialMargins.setBottom(state->getDouble("PAGE_INITIAL_MARGIN_BOTTOM"));
+				restoredPage->initialMargins.setLeft(state->getDouble("PAGE_INITIAL_MARGIN_LEFT"));
+				restoredPage->initialMargins.setRight(state->getDouble("PAGE_INITIAL_MARGIN_RIGHT"));
+			}
 		}
 		else
 		{
@@ -7986,6 +8042,8 @@
 	if (currMasterPageMode != savedMasterPageMode)
 		doc->setMasterPageMode(currMasterPageMode);
 	doc->rebuildMasterNames();
+	if (savedMasterPageMode)
+		view->reformPages();
 	pagePalette->updateMasterPageList();
 	pagePalette->rebuildPages();
 	if (outlinePalette->isVisible())
Index: scribus/scribusdoc.cpp
===================================================================
--- scribus/scribusdoc.cpp	(revision 27861)
+++ scribus/scribusdoc.cpp	(working copy)
@@ -273,6 +273,7 @@
 	Q_CHECK_PTR(autoSaveTimer);
 
 	m_uuid = QUuid::createUuid();
+	m_defaultMasterPageIds.clear();
 
 	m_docPrefsData.colorPrefs.DCMSset.CMSinUse = false;
 
@@ -2797,7 +2798,7 @@
 bool ScribusDoc::renameMasterPage(const QUuid& masterId, const QString& newPageName)
 {
 	const int number = masterPageIndex(masterId);
-	if (number < 0 || MasterNames.contains(newPageName))
+	if (number < 0 || isDefaultMasterPage(masterId) || MasterNames.contains(newPageName))
 		return false;
 
 	const QString oldPageName = MasterPages.at(number)->pageName();
@@ -2837,6 +2838,7 @@
 	assert( Pages->count() > 1 && Pages->count() > pageNumber );
 	setCurrentPage(Pages->at(0));
 	ScPage* page = Pages->takeAt(pageNumber);
+	m_defaultMasterPageIds.remove(page->masterPageId());
 	delete page;
 	// #10658 : renumber masterpages and masterpage objects
 	// in order to avoid crash after masterpage deletion
@@ -2886,48 +2888,115 @@
 	return it == MasterPageIds.constEnd() ? -1 : it.value();
 }
 
+bool ScribusDoc::isDefaultMasterPage(const QUuid& masterId) const
+{
+	return !masterId.isNull() && m_defaultMasterPageIds.contains(masterId);
+}
 
-void ScribusDoc::replaceMasterPage(const QString& oldMasterPage)
+bool ScribusDoc::canDeleteMasterPage(const QUuid& masterId) const
 {
-	uint pageIndex = 0;
-	QMap<QString,int>::Iterator it = MasterNames.begin();
-	QListIterator<ScPage *> dpIt(DocPages);
-	ScPage* docPage = nullptr;
-	while (dpIt.hasNext())
-	{
-		docPage = dpIt.next();
-		if (docPage->masterPageName() == oldMasterPage)
-		{
-			PageLocation pageLoc = locationOfPage(pageIndex);
-			if (pageLoc == LeftPage)
-			{
-				if (MasterNames.contains(CommonStrings::trMasterPageNormalLeft))
-					docPage->setMasterPageName(CommonStrings::trMasterPageNormalLeft);
-				else if (MasterNames.contains(CommonStrings::trMasterPageNormal))
-					docPage->setMasterPageNameNormal();
-				else
-					docPage->setMasterPageName(it.key());
-			}
-			else if (pageLoc == RightPage)
-			{
-				if (MasterNames.contains(CommonStrings::trMasterPageNormalRight))
-					docPage->setMasterPageName(CommonStrings::trMasterPageNormalRight);
-				else if (MasterNames.contains(CommonStrings::trMasterPageNormal))
-					docPage->setMasterPageNameNormal();
-				else
-					docPage->setMasterPageName(it.key());
-			}
-			else
-			{
-				if (MasterNames.contains(CommonStrings::trMasterPageNormalMiddle))
-					docPage->setMasterPageName(CommonStrings::trMasterPageNormalMiddle);
-				else if (MasterNames.contains(CommonStrings::trMasterPageNormal))
-					docPage->setMasterPageNameNormal();
-				else
-					docPage->setMasterPageName(it.key());
-			}
+	return MasterPages.count() > 1 && masterPageIndex(masterId) >= 0 && !isDefaultMasterPage(masterId);
+}
+
+void ScribusDoc::setDefaultMasterPage(const QUuid& masterId, bool isDefault)
+{
+	if (masterId.isNull())
+		return;
+	if (isDefault)
+		m_defaultMasterPageIds.insert(masterId);
+	else
+		m_defaultMasterPageIds.remove(masterId);
+}
+
+void ScribusDoc::ensureDefaultMasterPages()
+{
+	if (!m_defaultMasterPageIds.isEmpty())
+		return;
+
+	const int columns = pageSets()[m_docPrefsData.docSetupPrefs.pagePositioning].Columns;
+	for (ScPage* master : std::as_const(MasterPages))
+	{
+		const QString& name = master->pageName();
+		const bool normal = name == CommonStrings::masterPageNormal || name == CommonStrings::trMasterPageNormal;
+		const bool normalLeft = name == CommonStrings::masterPageNormalLeft || name == CommonStrings::trMasterPageNormalLeft;
+		const bool normalMiddle = name == CommonStrings::masterPageNormalMiddle || name == CommonStrings::trMasterPageNormalMiddle;
+		const bool normalRight = name == CommonStrings::masterPageNormalRight || name == CommonStrings::trMasterPageNormalRight;
+		bool legacyDefault = columns == 1 && normal;
+		legacyDefault |= columns > 1 && normalLeft && master->LeftPg == 1;
+		legacyDefault |= columns > 1 && normalRight && master->LeftPg == 0;
+		legacyDefault |= columns > 2 && normalMiddle && master->LeftPg > 1;
+		if (legacyDefault)
+			m_defaultMasterPageIds.insert(master->masterPageId());
+	}
+
+	// Older files may contain translated default names from another UI language.
+	// Default masters have historically been created first, so use that ordering
+	// only when name-based migration found nothing.
+	if (m_defaultMasterPageIds.isEmpty() && !MasterPages.isEmpty())
+	{
+		const int expectedDefaults = columns == 1 ? 1 : (columns == 2 ? 2 : 3);
+		for (int i = 0; i < qMin(expectedDefaults, MasterPages.count()); ++i)
+			m_defaultMasterPageIds.insert(MasterPages.at(i)->masterPageId());
+	}
+}
+
+QUuid ScribusDoc::defaultMasterPageId(int pageIndex) const
+{
+	if (m_defaultMasterPageIds.isEmpty())
+		return QUuid();
+
+	if (pagePositioning() == singlePage)
+	{
+		for (ScPage* master : MasterPages)
+		{
+			if (isDefaultMasterPage(master->masterPageId()))
+				return master->masterPageId();
 		}
-		pageIndex++;
+		return QUuid();
+	}
+
+	const PageLocation pageLoc = locationOfPage(pageIndex);
+	for (ScPage* master : MasterPages)
+	{
+		if (!isDefaultMasterPage(master->masterPageId()))
+			continue;
+		if (pageLoc == LeftPage && master->LeftPg == 1)
+			return master->masterPageId();
+		if (pageLoc == RightPage && master->LeftPg == 0)
+			return master->masterPageId();
+		if (pageLoc == MiddlePage && master->LeftPg > 1)
+			return master->masterPageId();
+	}
+
+	for (ScPage* master : MasterPages)
+	{
+		if (isDefaultMasterPage(master->masterPageId()))
+			return master->masterPageId();
+	}
+	return QUuid();
+}
+
+
+void ScribusDoc::replaceMasterPage(const QString& oldMasterPage)
+{
+	replaceMasterPage(masterPageId(oldMasterPage));
+}
+
+void ScribusDoc::replaceMasterPage(const QUuid& oldMasterPageId)
+{
+	if (oldMasterPageId.isNull())
+		return;
+	const QString oldMasterPageName = masterPageName(oldMasterPageId);
+
+	for (int pageIndex = 0; pageIndex < DocPages.count(); ++pageIndex)
+	{
+		ScPage* docPage = DocPages.at(pageIndex);
+		if (docPage->appliedMasterPageId() != oldMasterPageId && docPage->masterPageName() != oldMasterPageName)
+			continue;
+
+		const QUuid replacementId = defaultMasterPageId(pageIndex);
+		if (!replacementId.isNull() && replacementId != oldMasterPageId)
+			applyMasterPage(replacementId, pageIndex);
 	}
 }
 
@@ -14710,15 +14779,18 @@
 	int setcol = pageSets()[m_docPrefsData.docSetupPrefs.pagePositioning].Columns;
 	if (setcol == 1)
 	{
-		addMasterPage(0, CommonStrings::trMasterPageNormal);
+		ScPage* mp = addMasterPage(0, CommonStrings::trMasterPageNormal);
+		setDefaultMasterPage(mp->masterPageId());
 	}
 	else if (setcol == 2)
 	{
 		ScPage *lp = addMasterPage(0, CommonStrings::trMasterPageNormalLeft);
+		setDefaultMasterPage(lp->masterPageId());
 		lp->LeftPg = 1;
 		lp->Margins.setLeft(lp->initialMargins.right());
 		lp->Margins.setRight(lp->initialMargins.left());
 		lp = addMasterPage(1, CommonStrings::trMasterPageNormalRight);
+		setDefaultMasterPage(lp->masterPageId());
 		lp->LeftPg = 0;
 		lp->Margins.setRight(lp->initialMargins.right());
 		lp->Margins.setLeft(lp->initialMargins.left());
@@ -14726,14 +14798,17 @@
 	else if ((setcol == 3) || (setcol == 4))
 	{
 		ScPage *lp = addMasterPage(0, CommonStrings::trMasterPageNormalLeft);
+		setDefaultMasterPage(lp->masterPageId());
 		lp->LeftPg = 1;
 		lp->Margins.setLeft(lp->initialMargins.right());
 		lp->Margins.setRight(lp->initialMargins.left());
 		lp = addMasterPage(1, CommonStrings::trMasterPageNormalRight);
+		setDefaultMasterPage(lp->masterPageId());
 		lp->LeftPg = 0;
 		lp->Margins.setRight(lp->initialMargins.right());
 		lp->Margins.setLeft(lp->initialMargins.left());
 		lp = addMasterPage(2, CommonStrings::trMasterPageNormalMiddle);
+		setDefaultMasterPage(lp->masterPageId());
 		lp->LeftPg = 2;
 		lp->Margins.setLeft(lp->initialMargins.left());
 		lp->Margins.setRight(lp->initialMargins.left());
Index: scribus/scribusdoc.h
===================================================================
--- scribus/scribusdoc.h	(revision 27861)
+++ scribus/scribusdoc.h	(working copy)
@@ -36,6 +36,7 @@
 #include <QObject>
 #include <QPixmap>
 #include <QRectF>
+#include <QSet>
 #include <QStringList>
 #include <QTimer>
 #include <QUuid>
@@ -268,8 +269,14 @@
 		QUuid masterPageId(const QString& pageName) const;
 		QString masterPageName(const QUuid& masterId) const;
 		int masterPageIndex(const QUuid& masterId) const;
+		bool isDefaultMasterPage(const QUuid& masterId) const;
+		bool canDeleteMasterPage(const QUuid& masterId) const;
+		void setDefaultMasterPage(const QUuid& masterId, bool isDefault = true);
+		void ensureDefaultMasterPages();
+		QUuid defaultMasterPageId(int pageIndex) const;
 		//! @brief Replace a master page by default one
 		void replaceMasterPage(const QString& oldMasterPage);
+		void replaceMasterPage(const QUuid& oldMasterPageId);
 		//! @brief Rename a master page
 		bool renameMasterPage(const QString& oldPageName, const QString& newPageName);
 		bool renameMasterPage(const QUuid& masterId, const QString& newPageName);
@@ -1553,6 +1560,7 @@
 		void itemResizeToMargin(PageItem* item, int direction); //direction reflect enum numbers from Canvas::FrameHandle
 
 	private:
+		QSet<QUuid> m_defaultMasterPageIds;
 		UndoTransaction m_itemCreationTransaction;
 		UndoTransaction m_alignTransaction;
 
Index: scribus/ui/pagepalette_masterpages.cpp
===================================================================
--- scribus/ui/pagepalette_masterpages.cpp	(revision 27861)
+++ scribus/ui/pagepalette_masterpages.cpp	(working copy)
@@ -105,7 +105,9 @@
 	connect(deleteButton   , SIGNAL(clicked()), this, SLOT(deleteMasterPage()));
 	connect(newButton      , SIGNAL(clicked()), this, SLOT(newMasterPage()));
 	connect(importButton   , SIGNAL(clicked()), this, SLOT(importPage()));
-	connect(masterPageListBox, SIGNAL(itemClicked(QListWidgetItem*)), this, SLOT(selectMasterPage(QListWidgetItem*)));
+	connect(masterPageListBox, SIGNAL(currentItemChanged(QListWidgetItem*,QListWidgetItem*)),
+			this, SLOT(currentMasterPageChanged(QListWidgetItem*,QListWidgetItem*)));
+	connect(masterPageListBox, SIGNAL(itemSelectionChanged()), this, SLOT(updateDeleteButton()));
 	connect(masterPageListBox, SIGNAL(itemChanged(QListWidgetItem*)), this, SLOT(renameMasterPage(QListWidgetItem*)));
 	connect(finishButton   , SIGNAL(released()), this, SIGNAL(finished()));
 }
@@ -116,8 +118,9 @@
 	disconnect(deleteButton   , SIGNAL(clicked()), this, SLOT(deleteMasterPage()));
 	disconnect(newButton      , SIGNAL(clicked()), this, SLOT(newMasterPage()));
 	disconnect(importButton   , SIGNAL(clicked()), this, SLOT(importPage()));
-	disconnect(masterPageListBox, SIGNAL(itemClicked(QListWidgetItem*)),
-			   this, SLOT(selectMasterPage(QListWidgetItem*)));
+	disconnect(masterPageListBox, SIGNAL(currentItemChanged(QListWidgetItem*,QListWidgetItem*)),
+			   this, SLOT(currentMasterPageChanged(QListWidgetItem*,QListWidgetItem*)));
+	disconnect(masterPageListBox, SIGNAL(itemSelectionChanged()), this, SLOT(updateDeleteButton()));
 	disconnect(masterPageListBox, SIGNAL(itemChanged(QListWidgetItem*)),
 			   this, SLOT(renameMasterPage( QListWidgetItem*)));
 	disconnect(finishButton   , SIGNAL(released()), this, SIGNAL(finished()));
@@ -154,14 +157,22 @@
 void PagePalette_MasterPages::deleteMasterPage()
 {
 	bool forceDelete = false;
+	const QUuid displayedMasterId = m_doc->masterPageId(m_currentPage);
+	QList<QUuid> selectedMasterIds;
+	for (QListWidgetItem* item : masterPageListBox->selectedItems())
+	{
+		const QUuid masterId = m_doc->masterPageId(item->data(Qt::UserRole).toString());
+		if (m_doc->canDeleteMasterPage(masterId))
+			selectedMasterIds.append(masterId);
+	}
 
 	// allow to delete multiple pages in one step
-	foreach (QListWidgetItem * delItem, masterPageListBox->selectedItems())
+	for (const QUuid& masterId : selectedMasterIds)
 	{
-		m_currentPage = delItem->text();
-
-		if (isReservedName(m_currentPage))
+		const QString masterPageName = m_doc->masterPageName(masterId);
+		if (masterPageName.isEmpty() || !m_doc->canDeleteMasterPage(masterId))
 			continue;
+		m_currentPage = masterPageName;
 
 		QString extraWarn;
 
@@ -189,17 +200,18 @@
 
 		if (m_doc->appMode == modeEditClip)
 			m_view->requestMode(submodeEndNodeEdit);
-		m_doc->scMW()->deletePage2(m_doc->MasterNames[m_currentPage]);
-		//<<CB TODO Move back into ScribusDoc::deleteMasterPage();
-		//This must happen after the pages have been reformed (view/doc)
-		m_doc->rebuildMasterNames();
-		// Fix up any pages that refer to the deleted master page
-		m_doc->replaceMasterPage(m_currentPage);
+		m_doc->scMW()->deletePage2(m_doc->masterPageIndex(masterId));
 	}
 
-	// set the 1st MP for the other slots
-	auto it = m_doc->MasterNames.cbegin();
-	m_currentPage = it.key();
+	if (m_doc->masterPageIndex(displayedMasterId) >= 0)
+		m_currentPage = m_doc->masterPageName(displayedMasterId);
+	else
+	{
+		const QUuid defaultId = m_doc->defaultMasterPageId(0);
+		m_currentPage = m_doc->masterPageName(defaultId);
+		if (m_currentPage.isEmpty() && !m_doc->MasterPages.isEmpty())
+			m_currentPage = m_doc->MasterPages.first()->pageName();
+	}
 	updateMasterPageList(m_currentPage);
 	selectMasterPage(m_currentPage);
 }
@@ -388,13 +400,10 @@
 
 void PagePalette_MasterPages::selectMasterPage(QListWidgetItem *item)
 {
+	if (!item)
+		return;
 	m_currentPage = item->data(Qt::UserRole).toString();
-
-	bool deleteEnabled = true;
-	deleteEnabled &= (m_doc->MasterNames.count() > 1);
-	deleteEnabled &= (m_currentPage != CommonStrings::masterPageNormal);
-	deleteEnabled &= (m_currentPage != CommonStrings::trMasterPageNormal);
-	deleteButton->setEnabled(deleteEnabled);
+	updateDeleteButton();
 
 	if (m_doc->appMode == modeEditClip)
 		m_view->requestMode(submodeEndNodeEdit);
@@ -405,40 +414,63 @@
 	this->setEnabled(true);
 }
 
+void PagePalette_MasterPages::currentMasterPageChanged(QListWidgetItem *current, QListWidgetItem *previous)
+{
+	Q_UNUSED(previous);
+	selectMasterPage(current);
+}
+
+void PagePalette_MasterPages::updateDeleteButton()
+{
+	bool deleteEnabled = false;
+	if (m_doc)
+	{
+		for (QListWidgetItem* item : masterPageListBox->selectedItems())
+		{
+			const QUuid masterId = m_doc->masterPageId(item->data(Qt::UserRole).toString());
+			if (m_doc->canDeleteMasterPage(masterId))
+			{
+				deleteEnabled = true;
+				break;
+			}
+		}
+	}
+	deleteButton->setEnabled(deleteEnabled);
+}
+
 void PagePalette_MasterPages::selectMasterPage(const QString& name)
 {
 	if (!m_doc || !m_view)
 		return;
 
 	m_currentPage.clear();
+	QListWidgetItem* targetItem = nullptr;
 	for (int i = 0; i < masterPageListBox->count(); ++i)
 	{
 		QListWidgetItem* item = masterPageListBox->item(i);
 		QString realPageName  = item->data(Qt::UserRole).toString();
-		if (item->text() == name)
+		if (item->text() == name || realPageName == name)
 		{
 			m_currentPage = realPageName;
-			break;
-		}
-		if (realPageName == name)
-		{
-			m_currentPage = name;
+			targetItem = item;
 			break;
 		}
 	}
-	
+
 	if (m_currentPage.isEmpty())
 		m_currentPage = m_doc->MasterPages.at(0)->pageName();
 
-	bool deleteEnabled = true;
-	deleteEnabled &= (m_doc->MasterNames.count() > 1);
-	deleteEnabled &= (m_currentPage != CommonStrings::masterPageNormal);
-	deleteEnabled &= (m_currentPage != CommonStrings::trMasterPageNormal);
-	deleteButton->setEnabled(deleteEnabled);
+	if (targetItem)
+	{
+		masterPageListBox->clearSelection();
+		masterPageListBox->setCurrentItem(targetItem);
+		targetItem->setSelected(true);
+	}
 
 	if (m_doc->appMode == modeEditClip)
 		m_view->requestMode(submodeEndNodeEdit);
 	m_view->showMasterPage(m_doc->MasterNames[m_currentPage]);
+	updateDeleteButton();
 
 	this->setEnabled(true);
 }
@@ -463,7 +495,7 @@
 			mpName = CommonStrings::trMasterPageNormal;
 		QListWidgetItem* mpItem = new QListWidgetItem(mpName);
 
-		if (!isReservedName(mpName))
+		if (!m_doc->isDefaultMasterPage(m_doc->masterPageId(it.key())))
 			mpItem->setFlags(mpItem->flags() |= Qt::ItemIsEditable);
 		else
 			mpItem->setFlags(mpItem->flags() &= ~Qt::ItemIsEditable);
@@ -471,15 +503,15 @@
 		mpItem->setData(Qt::UserRole, it.key());
 		masterPageListBox->addItem(mpItem);
 	}
-	deleteButton->setEnabled(m_doc->MasterNames.count() != 1);
 	if (masterPageName == CommonStrings::masterPageNormal)
-	{
 		masterPageName = CommonStrings::trMasterPageNormal;
-		deleteButton->setEnabled(false);
-	}
 	QList<QListWidgetItem *> itL = masterPageListBox->findItems(masterPageName, Qt::MatchExactly);
 	if (itL.count() != 0)
+	{
+		masterPageListBox->setCurrentItem(itL.at(0));
 		itL.at(0)->setSelected(true);
+	}
+	updateDeleteButton();
 }
 
 void PagePalette_MasterPages::renameMasterPage(QListWidgetItem * item)
Index: scribus/ui/pagepalette_masterpages.h
===================================================================
--- scribus/ui/pagepalette_masterpages.h	(revision 27861)
+++ scribus/ui/pagepalette_masterpages.h	(working copy)
@@ -68,6 +68,8 @@
 	void renameMasterPage(QListWidgetItem * item);
 	void importPage();
 	void selectMasterPage(QListWidgetItem *item);
+	void currentMasterPageChanged(QListWidgetItem *current, QListWidgetItem *previous);
+	void updateDeleteButton();
 
 signals:
 	void removePage(int);
Index: scribus/ui/pagepalette_pages.cpp
===================================================================
--- scribus/ui/pagepalette_pages.cpp	(revision 27861)
+++ scribus/ui/pagepalette_pages.cpp	(working copy)
@@ -86,7 +86,8 @@
 
 void PagePalette_Pages::deleteMasterPage(const QString &tmp)
 {
-	if (tmp == CommonStrings::trMasterPageNormal)
+	const QUuid masterId = currView->m_doc->masterPageId(tmp);
+	if (!currView->m_doc->canDeleteMasterPage(masterId))
 		return;
 	QString extraWarn;
 	for (int i = 0; i < currView->m_doc->DocPages.count(); ++i)
@@ -109,12 +110,7 @@
 		int storedViewYCoor = currView->verticalScrollBar()->value();
 
 		currView->m_doc->setMasterPageMode(true);
-		currView->m_doc->scMW()->deletePage2(currView->m_doc->MasterNames[tmp]);
-		//<<CB TODO Move back into ScribusDoc::deleteMasterPage();
-		//This must happen after the pages have been reformed (view/doc)
-		currView->m_doc->rebuildMasterNames();
-		// Fix up any pages that refer to the deleted master page
-		currView->m_doc->replaceMasterPage(tmp);
+		currView->m_doc->scMW()->deletePage2(currView->m_doc->masterPageIndex(masterId));
 		currView->m_doc->setMasterPageMode(oldMPMode);
 		currView->m_doc->setModified(true);
 
Index: scribus/ui/pagepalette_widgets.cpp
===================================================================
--- scribus/ui/pagepalette_widgets.cpp	(revision 27861)
+++ scribus/ui/pagepalette_widgets.cpp	(working copy)
@@ -117,7 +117,7 @@
 		if (currentItem())
 		{
 			e->accept();
-			emit delMasterRequest(currentItem()->text());
+			emit delMasterRequest(currentItem()->data(Qt::UserRole).toString());
 			return;
 		}
 	}
mdu2-delete-unused-v1.33.patch (47,161 bytes)   
Index: scribus/CMakeLists_Sources.txt
===================================================================
--- scribus/CMakeLists_Sources.txt	(revision 27861)
+++ scribus/CMakeLists_Sources.txt	(working copy)
@@ -412,6 +412,7 @@
 	ui/pageitemattributes.cpp
 	ui/pagepalette.cpp
 	ui/pagepalette_pages.cpp
+	ui/masterpagedeletedialog.cpp
 	ui/pagepalette_masterpages.cpp
 	ui/pagepalette_widgets.cpp
 	ui/pagepropertiesdialog.cpp
Index: scribus/scribus.cpp
===================================================================
--- scribus/scribus.cpp	(revision 27861)
+++ scribus/scribus.cpp	(working copy)
@@ -6111,6 +6111,67 @@
 	deletePage(pg + 1, pg + 1);
 }
 
+void ScribusMainWindow::deleteMasterPages(const QList<QUuid>& masterIds, const QMap<int, QUuid>& replacementMasterIds)
+{
+	if (!HaveDoc || !doc->masterPageMode())
+		return;
+
+	QList<QUuid> deletableMasterIds;
+	for (const QUuid& masterId : masterIds)
+	{
+		if (!deletableMasterIds.contains(masterId) && doc->canDeleteMasterPage(masterId))
+			deletableMasterIds.append(masterId);
+	}
+	if (deletableMasterIds.isEmpty())
+		return;
+
+	QMap<int, QUuid> validReplacements;
+	for (auto it = replacementMasterIds.constBegin(); it != replacementMasterIds.constEnd(); ++it)
+	{
+		const int pageIndex = it.key();
+		const QUuid replacementId = it.value();
+		if (pageIndex < 0 || pageIndex >= doc->DocPages.count())
+			continue;
+		if (replacementId.isNull() || deletableMasterIds.contains(replacementId) || doc->masterPageIndex(replacementId) < 0)
+			continue;
+
+		const ScPage* page = doc->DocPages.at(pageIndex);
+		const QUuid currentMasterId = page->appliedMasterPageId();
+		if (!deletableMasterIds.contains(currentMasterId))
+		{
+			bool deletingLegacyAssignment = false;
+			for (const QUuid& masterId : deletableMasterIds)
+			{
+				if (page->masterPageName() == doc->masterPageName(masterId))
+				{
+					deletingLegacyAssignment = true;
+					break;
+				}
+			}
+			if (!deletingLegacyAssignment)
+				continue;
+		}
+		validReplacements.insert(pageIndex, replacementId);
+	}
+
+	UndoTransaction transaction;
+	if (UndoManager::undoEnabled() && (deletableMasterIds.count() > 1 || !validReplacements.isEmpty()))
+		transaction = m_undoManager->beginTransaction(doc->documentFileName(), Um::IDocument, Um::DeletePages, QString(), Um::IDelete);
+
+	for (auto it = validReplacements.constBegin(); it != validReplacements.constEnd(); ++it)
+		doc->applyMasterPage(it.value(), it.key());
+
+	for (const QUuid& masterId : deletableMasterIds)
+	{
+		const int masterIndex = doc->masterPageIndex(masterId);
+		if (masterIndex >= 0 && doc->canDeleteMasterPage(masterId))
+			deletePage2(masterIndex);
+	}
+
+	if (transaction)
+		transaction.commit();
+}
+
 void ScribusMainWindow::deletePage()
 {
 	if (HaveDoc && doc->appMode == modeEditClip)
Index: scribus/scribus.h
===================================================================
--- scribus/scribus.h	(revision 27861)
+++ scribus/scribus.h	(working copy)
@@ -450,6 +450,7 @@
 	 */
 	void deletePage(int from, int to);
 	void deletePage2(int pg);
+	void deleteMasterPages(const QList<QUuid>& masterIds, const QMap<int, QUuid>& replacementMasterIds = QMap<int, QUuid>());
 	/** \brief Verschiebt Seiten */
 	void movePage();
 	void copyPage();
Index: scribus/scribusdoc.cpp
===================================================================
--- scribus/scribusdoc.cpp	(revision 27861)
+++ scribus/scribusdoc.cpp	(working copy)
@@ -2898,6 +2898,33 @@
 	return MasterPages.count() > 1 && masterPageIndex(masterId) >= 0 && !isDefaultMasterPage(masterId);
 }
 
+QList<int> ScribusDoc::masterPageUsage(const QUuid& masterId) const
+{
+	QList<int> pages;
+	if (masterId.isNull())
+		return pages;
+	const QString masterName = masterPageName(masterId);
+	for (int pageIndex = 0; pageIndex < DocPages.count(); ++pageIndex)
+	{
+		const ScPage* page = DocPages.at(pageIndex);
+		if (page->appliedMasterPageId() == masterId || (!masterName.isEmpty() && page->masterPageName() == masterName))
+			pages.append(pageIndex);
+	}
+	return pages;
+}
+
+QList<QUuid> ScribusDoc::unusedCustomMasterPageIds() const
+{
+	QList<QUuid> masterIds;
+	for (const ScPage* master : MasterPages)
+	{
+		const QUuid masterId = master->masterPageId();
+		if (canDeleteMasterPage(masterId) && masterPageUsage(masterId).isEmpty())
+			masterIds.append(masterId);
+	}
+	return masterIds;
+}
+
 void ScribusDoc::setDefaultMasterPage(const QUuid& masterId, bool isDefault)
 {
 	if (masterId.isNull())
Index: scribus/scribusdoc.h
===================================================================
--- scribus/scribusdoc.h	(revision 27861)
+++ scribus/scribusdoc.h	(working copy)
@@ -271,6 +271,8 @@
 		int masterPageIndex(const QUuid& masterId) const;
 		bool isDefaultMasterPage(const QUuid& masterId) const;
 		bool canDeleteMasterPage(const QUuid& masterId) const;
+		QList<int> masterPageUsage(const QUuid& masterId) const;
+		QList<QUuid> unusedCustomMasterPageIds() const;
 		void setDefaultMasterPage(const QUuid& masterId, bool isDefault = true);
 		void ensureDefaultMasterPages();
 		QUuid defaultMasterPageId(int pageIndex) const;
Index: scribus/ui/masterpagedeletedialog.cpp
===================================================================
--- scribus/ui/masterpagedeletedialog.cpp	(nonexistent)
+++ scribus/ui/masterpagedeletedialog.cpp	(working copy)
@@ -0,0 +1,814 @@
+/*
+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 "masterpagedeletedialog.h"
+
+#include <algorithm>
+
+#include <QStringList>
+
+#include <QAbstractScrollArea>
+#include <QCheckBox>
+#include <QComboBox>
+#include <QDialogButtonBox>
+#include <QHBoxLayout>
+#include <QHeaderView>
+#include <QIcon>
+#include <QLabel>
+#include <QPushButton>
+#include <QResizeEvent>
+#include <QShowEvent>
+#include <QSignalBlocker>
+#include <QStyle>
+#include <QTableWidget>
+#include <QVBoxLayout>
+
+#include "commonstrings.h"
+#include "iconmanager.h"
+#include "pagestructs.h"
+#include "scpage.h"
+#include "scribusdoc.h"
+
+MasterPageDeleteDialog::MasterPageDeleteDialog(QWidget* parent, ScribusDoc* doc, const QList<QUuid>& masterIds)
+	: QDialog(parent),
+	  m_doc(doc)
+{
+	setWindowTitle(tr("Delete Master Pages"));
+	setModal(true);
+
+	int protectedCount = 0;
+	QList<QUuid> seenIds;
+	for (const QUuid& masterId : masterIds)
+	{
+		if (!m_doc || masterId.isNull() || seenIds.contains(masterId) || m_doc->masterPageIndex(masterId) < 0)
+			continue;
+		seenIds.append(masterId);
+		if (m_doc->isDefaultMasterPage(masterId))
+		{
+			++protectedCount;
+			continue;
+		}
+		if (!m_doc->canDeleteMasterPage(masterId))
+			continue;
+
+		MasterInfo info;
+		info.id = masterId;
+		info.name = m_doc->masterPageName(masterId);
+		info.pages = m_doc->masterPageUsage(masterId);
+		m_masterInfo.append(info);
+		m_deleteIds.append(masterId);
+	}
+
+	for (MasterInfo& info : m_masterInfo)
+	{
+		if (m_doc->pagePositioning() == singlePage)
+			info.allPages = info.pages;
+		else
+		{
+			for (int pageIndex : info.pages)
+			{
+				switch (m_doc->locationOfPage(pageIndex))
+				{
+					case LeftPage:
+						info.leftPages.append(pageIndex);
+						break;
+					case RightPage:
+						info.rightPages.append(pageIndex);
+						break;
+					case MiddlePage:
+						info.middlePages.append(pageIndex);
+						break;
+				}
+			}
+		}
+
+		if (!info.allPages.isEmpty())
+			info.allReplacementId = m_doc->defaultMasterPageId(info.allPages.first());
+		if (!info.leftPages.isEmpty())
+			info.leftReplacementId = m_doc->defaultMasterPageId(info.leftPages.first());
+		if (!info.rightPages.isEmpty())
+			info.rightReplacementId = m_doc->defaultMasterPageId(info.rightPages.first());
+	}
+
+	int usedCount = 0;
+	int totalUsage = 0;
+	for (const MasterInfo& info : m_masterInfo)
+	{
+		if (!info.pages.isEmpty())
+			++usedCount;
+		totalUsage += info.pages.count();
+	}
+	const int unusedCount = m_masterInfo.count() - usedCount;
+
+	auto* layout = new QVBoxLayout(this);
+
+	const bool showReplacementColumn = usedCount > 0;
+	m_table = new QTableWidget(m_masterInfo.count(), showReplacementColumn ? 3 : 2, this);
+	if (showReplacementColumn)
+		m_table->setHorizontalHeaderLabels({ tr("Master page"), tr("Usage"), tr("Replacement") });
+	else
+		m_table->setHorizontalHeaderLabels({ tr("Master page"), tr("Usage") });
+	m_table->setSelectionBehavior(QAbstractItemView::SelectRows);
+	m_table->setSelectionMode(QAbstractItemView::NoSelection);
+	m_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
+	m_table->setAlternatingRowColors(true);
+	m_table->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContentsOnFirstShow);
+	m_table->verticalHeader()->hide();
+	m_table->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive);
+	m_table->horizontalHeader()->setStretchLastSection(false);
+	connect(m_table->horizontalHeader(), &QHeaderView::sectionResized, this, [this](int, int, int)
+	{
+		if (!m_adjustingColumns)
+			m_columnsUserAdjusted = true;
+		updateElidedToolTips();
+		for (const ReplacementControl& control : m_replacementControls)
+			updateComboToolTip(control.combo);
+	});
+
+	for (int row = 0; row < m_masterInfo.count(); ++row)
+	{
+		const MasterInfo& info = m_masterInfo.at(row);
+		auto* nameItem = new QTableWidgetItem(displayMasterName(info.name));
+		nameItem->setData(Qt::UserRole, info.id.toString(QUuid::WithoutBraces));
+		m_table->setItem(row, 0, nameItem);
+
+		QString usage;
+		if (info.pages.isEmpty())
+			usage = tr("Unused");
+		else if (info.pages.count() == 1)
+			usage = tr("1 page");
+		else
+			usage = tr("%1 pages").arg(info.pages.count());
+		auto* usageItem = new QTableWidgetItem(usage);
+		if (!info.pages.isEmpty())
+			usageItem->setToolTip(tr("Applied to pages: %1").arg(formatPageRanges(info.pages)));
+		m_table->setItem(row, 1, usageItem);
+
+		if (showReplacementColumn)
+		{
+			if (info.pages.isEmpty())
+				m_table->setItem(row, 2, new QTableWidgetItem(QString(QChar(0x2014))));
+			else
+			{
+				QWidget* replacementCell = createReplacementCell(row);
+				if (replacementCell)
+					m_table->setCellWidget(row, 2, replacementCell);
+				else
+					m_table->setItem(row, 2, new QTableWidgetItem(tr("Matching default")));
+			}
+		}
+	}
+	m_adjustingColumns = true;
+	m_table->resizeRowsToContents();
+	int uniformRowHeight = 0;
+	for (int row = 0; row < m_table->rowCount(); ++row)
+		uniformRowHeight = qMax(uniformRowHeight, m_table->rowHeight(row));
+	for (int row = 0; row < m_table->rowCount(); ++row)
+		m_table->setRowHeight(row, uniformRowHeight);
+	m_table->resizeColumnsToContents();
+	m_adjustingColumns = false;
+	layout->addWidget(m_table);
+
+	if (QWidget* bulkControls = createBulkReplacementControls(usedCount))
+		layout->addWidget(bulkControls);
+
+	if (protectedCount > 0)
+	{
+		auto* protectedLayout = new QHBoxLayout;
+		auto* infoIcon = new QLabel(this);
+		infoIcon->setPixmap(IconManager::instance().loadPixmap("information", 16));
+		infoIcon->setAlignment(Qt::AlignTop);
+		protectedLayout->addWidget(infoIcon);
+		auto* protectedLabel = new QLabel(this);
+		protectedLabel->setWordWrap(true);
+		if (protectedCount == 1)
+			protectedLabel->setText(tr("1 protected default master page will be kept."));
+		else
+			protectedLabel->setText(tr("%1 protected default master pages will be kept.").arg(protectedCount));
+		protectedLayout->addWidget(protectedLabel, 1);
+		layout->addLayout(protectedLayout);
+	}
+
+	QString summary;
+	if (m_masterInfo.count() == 1 && usedCount == 0)
+		summary = tr("Delete this unused master page?");
+	else if (m_masterInfo.count() == 1)
+	{
+		if (totalUsage == 1)
+			summary = tr("Delete this master page? Used on 1 document page.");
+		else
+			summary = tr("Delete this master page? Used on %1 document pages.").arg(totalUsage);
+	}
+	else if (usedCount == 0)
+		summary = tr("Delete %1 unused master pages?").arg(m_masterInfo.count());
+	else if (unusedCount == 0)
+		summary = tr("Delete %1 master pages? All are used.").arg(m_masterInfo.count());
+	else
+	{
+		const QString usedText = usedCount == 1 ? tr("1 used") : tr("%1 used").arg(usedCount);
+		const QString unusedText = unusedCount == 1 ? tr("1 unused") : tr("%1 unused").arg(unusedCount);
+		summary = tr("Delete %1 master pages? %2, %3.").arg(m_masterInfo.count()).arg(usedText).arg(unusedText);
+	}
+
+	layout->addSpacing(12);
+	auto* actionLayout = new QHBoxLayout;
+	auto* summaryLabel = new QLabel(summary, this);
+	summaryLabel->setWordWrap(true);
+	actionLayout->addWidget(summaryLabel, 1, Qt::AlignVCenter);
+
+	auto* buttonBox = new QDialogButtonBox(this);
+	QPushButton* deleteButton = buttonBox->addButton(tr("Delete"), QDialogButtonBox::DestructiveRole);
+	deleteButton->setIcon(IconManager::instance().loadIcon("delete"));
+	QPushButton* cancelButton = buttonBox->addButton(QDialogButtonBox::Cancel);
+	cancelButton->setIcon(QIcon());
+	cancelButton->setDefault(true);
+	deleteButton->setAutoDefault(false);
+	connect(deleteButton, &QPushButton::clicked, this, &QDialog::accept);
+	connect(cancelButton, &QPushButton::clicked, this, &QDialog::reject);
+	actionLayout->addWidget(buttonBox, 0, Qt::AlignBottom);
+	layout->addLayout(actionLayout);
+
+	setMinimumSize(500, 220);
+	QSize initialSize = sizeHint();
+	initialSize.setWidth(qBound(showReplacementColumn ? 600 : 500, initialSize.width(), 760));
+	initialSize.setHeight(qBound(240, initialSize.height(), 560));
+	resize(initialSize);
+	layout->activate();
+	m_columnsUserAdjusted = false;
+	fitTableColumns();
+	updateElidedToolTips();
+}
+
+QList<QUuid> MasterPageDeleteDialog::masterPageIds() const
+{
+	QList<QUuid> ids;
+	for (const MasterInfo& info : m_masterInfo)
+		ids.append(info.id);
+	return ids;
+}
+
+QMap<int, QUuid> MasterPageDeleteDialog::replacementMasterIds() const
+{
+	QMap<int, QUuid> replacements;
+	for (const MasterInfo& info : m_masterInfo)
+	{
+		for (int pageIndex : info.allPages)
+		{
+			if (!info.allReplacementId.isNull())
+				replacements.insert(pageIndex, info.allReplacementId);
+		}
+		for (int pageIndex : info.leftPages)
+		{
+			if (!info.leftReplacementId.isNull())
+				replacements.insert(pageIndex, info.leftReplacementId);
+		}
+		for (int pageIndex : info.rightPages)
+		{
+			if (!info.rightReplacementId.isNull())
+				replacements.insert(pageIndex, info.rightReplacementId);
+		}
+	}
+	return replacements;
+}
+
+void MasterPageDeleteDialog::showEvent(QShowEvent* event)
+{
+	QDialog::showEvent(event);
+	if (!m_columnsUserAdjusted)
+		fitTableColumns();
+}
+
+void MasterPageDeleteDialog::resizeEvent(QResizeEvent* event)
+{
+	QDialog::resizeEvent(event);
+	if (!m_columnsUserAdjusted)
+		fitTableColumns();
+	updateElidedToolTips();
+	for (const ReplacementControl& control : m_replacementControls)
+		updateComboToolTip(control.combo);
+	updateComboToolTip(m_bulkAllCombo);
+	updateComboToolTip(m_bulkLeftCombo);
+	updateComboToolTip(m_bulkRightCombo);
+}
+
+QString MasterPageDeleteDialog::displayMasterName(const QString& name) const
+{
+	if (name == CommonStrings::masterPageNormal)
+		return CommonStrings::trMasterPageNormal;
+	if (name == CommonStrings::masterPageNormalLeft)
+		return CommonStrings::trMasterPageNormalLeft;
+	if (name == CommonStrings::masterPageNormalMiddle)
+		return CommonStrings::trMasterPageNormalMiddle;
+	if (name == CommonStrings::masterPageNormalRight)
+		return CommonStrings::trMasterPageNormalRight;
+	return name;
+}
+
+QString MasterPageDeleteDialog::formatPageRanges(const QList<int>& pages) const
+{
+	if (pages.isEmpty())
+		return QString();
+	QList<int> sortedPages = pages;
+	std::sort(sortedPages.begin(), sortedPages.end());
+	QStringList ranges;
+	int start = sortedPages.first();
+	int previous = start;
+	for (int i = 1; i < sortedPages.count(); ++i)
+	{
+		const int current = sortedPages.at(i);
+		if (current == previous + 1)
+		{
+			previous = current;
+			continue;
+		}
+		ranges.append(start == previous ? QString::number(start + 1) : tr("%1-%2").arg(start + 1).arg(previous + 1));
+		start = previous = current;
+	}
+	ranges.append(start == previous ? QString::number(start + 1) : tr("%1-%2").arg(start + 1).arg(previous + 1));
+	return ranges.join(tr(", "));
+}
+
+QList<QUuid> MasterPageDeleteDialog::replacementCandidates(ReplacementSide side) const
+{
+	QList<QUuid> candidates;
+	if (!m_doc)
+		return candidates;
+
+	for (const ScPage* master : m_doc->MasterPages)
+	{
+		const QUuid masterId = master->masterPageId();
+		if (masterId.isNull() || m_deleteIds.contains(masterId))
+			continue;
+		if (side == ReplacementSide::Left && master->LeftPg != 1)
+			continue;
+		if (side == ReplacementSide::Right && master->LeftPg != 0)
+			continue;
+		candidates.append(masterId);
+	}
+	return candidates;
+}
+
+QUuid MasterPageDeleteDialog::replacementId(const MasterInfo& info, ReplacementSide side) const
+{
+	switch (side)
+	{
+		case ReplacementSide::All:
+			return info.allReplacementId;
+		case ReplacementSide::Left:
+			return info.leftReplacementId;
+		case ReplacementSide::Right:
+			return info.rightReplacementId;
+		case ReplacementSide::None:
+			break;
+	}
+	return QUuid();
+}
+
+void MasterPageDeleteDialog::setReplacementId(MasterInfo& info, ReplacementSide side, const QUuid& masterId)
+{
+	switch (side)
+	{
+		case ReplacementSide::All:
+			info.allReplacementId = masterId;
+			break;
+		case ReplacementSide::Left:
+			info.leftReplacementId = masterId;
+			break;
+		case ReplacementSide::Right:
+			info.rightReplacementId = masterId;
+			break;
+		case ReplacementSide::None:
+			break;
+	}
+}
+
+bool MasterPageDeleteDialog::usesReplacementSide(const MasterInfo& info, ReplacementSide side) const
+{
+	switch (side)
+	{
+		case ReplacementSide::All:
+			return !info.allPages.isEmpty();
+		case ReplacementSide::Left:
+			return !info.leftPages.isEmpty();
+		case ReplacementSide::Right:
+			return !info.rightPages.isEmpty();
+		case ReplacementSide::None:
+			break;
+	}
+	return false;
+}
+
+QString MasterPageDeleteDialog::sideLabel(ReplacementSide side) const
+{
+	switch (side)
+	{
+		case ReplacementSide::All:
+			return tr("Pages");
+		case ReplacementSide::Left:
+			return tr("Left pages");
+		case ReplacementSide::Right:
+			return tr("Right pages");
+		case ReplacementSide::None:
+			break;
+	}
+	return QString();
+}
+
+QComboBox* MasterPageDeleteDialog::createReplacementCombo(int row, ReplacementSide side, QWidget* parent)
+{
+	auto* combo = new QComboBox(parent);
+	combo->setMinimumWidth(135);
+	populateReplacementCombo(combo, side, replacementId(m_masterInfo[row], side));
+	if (combo->currentIndex() >= 0)
+		setReplacementId(m_masterInfo[row], side, QUuid(combo->currentData().toString()));
+	connect(combo, qOverload<int>(&QComboBox::currentIndexChanged), this, [this, row, side, combo](int)
+	{
+		rowReplacementChanged(row, side, combo);
+	});
+	ReplacementControl control;
+	control.row = row;
+	control.side = side;
+	control.combo = combo;
+	m_replacementControls.append(control);
+	updateComboToolTip(combo);
+	return combo;
+}
+
+QWidget* MasterPageDeleteDialog::createReplacementControl(int row, ReplacementSide side, QWidget* parent)
+{
+	auto* control = new QWidget(parent);
+	auto* controlLayout = new QHBoxLayout(control);
+	controlLayout->setContentsMargins(0, 0, 0, 0);
+	controlLayout->setSpacing(0);
+
+	QComboBox* combo = createReplacementCombo(row, side, control);
+	combo->setAccessibleName(sideLabel(side));
+	controlLayout->addWidget(combo);
+	return control;
+}
+
+QWidget* MasterPageDeleteDialog::createReplacementCell(int row)
+{
+	if (row < 0 || row >= m_masterInfo.count())
+		return nullptr;
+
+	const MasterInfo& info = m_masterInfo.at(row);
+	QList<ReplacementSide> sides;
+	if (!info.allPages.isEmpty())
+		sides.append(ReplacementSide::All);
+	else
+	{
+		if (!info.leftPages.isEmpty())
+			sides.append(ReplacementSide::Left);
+		if (!info.rightPages.isEmpty())
+			sides.append(ReplacementSide::Right);
+	}
+	if (sides.isEmpty())
+		return nullptr;
+
+	auto* cell = new QWidget(m_table);
+	auto* cellLayout = new QHBoxLayout(cell);
+	cellLayout->setContentsMargins(4, 2, 4, 2);
+	cellLayout->setSpacing(8);
+	for (ReplacementSide side : sides)
+		cellLayout->addWidget(createReplacementControl(row, side, cell));
+	cellLayout->addStretch(1);
+	return cell;
+}
+
+QWidget* MasterPageDeleteDialog::createBulkReplacementControls(int usedCount)
+{
+	if (usedCount < 2)
+		return nullptr;
+
+	QList<ReplacementSide> sides;
+	if (m_doc->pagePositioning() == singlePage)
+	{
+		for (const MasterInfo& info : m_masterInfo)
+		{
+			if (!info.allPages.isEmpty())
+			{
+				sides.append(ReplacementSide::All);
+				break;
+			}
+		}
+	}
+	else
+	{
+		bool hasLeft = false;
+		bool hasRight = false;
+		for (const MasterInfo& info : m_masterInfo)
+		{
+			hasLeft |= !info.leftPages.isEmpty();
+			hasRight |= !info.rightPages.isEmpty();
+		}
+		if (hasLeft)
+			sides.append(ReplacementSide::Left);
+		if (hasRight)
+			sides.append(ReplacementSide::Right);
+	}
+	if (sides.isEmpty())
+		return nullptr;
+
+	auto* widget = new QWidget(this);
+	auto* widgetLayout = new QHBoxLayout(widget);
+	widgetLayout->setContentsMargins(0, 4, 0, 0);
+	widgetLayout->setSpacing(8);
+
+	m_bulkReplacementCheckBox = new QCheckBox(tr("Set all replacements"), widget);
+	widgetLayout->addWidget(m_bulkReplacementCheckBox, 0, Qt::AlignTop);
+
+	m_bulkReplacementControls = new QWidget(widget);
+	auto* controlsLayout = new QHBoxLayout(m_bulkReplacementControls);
+	controlsLayout->setContentsMargins(0, 0, 0, 0);
+	controlsLayout->setSpacing(8);
+	for (ReplacementSide side : sides)
+		controlsLayout->addWidget(createBulkReplacementControl(side, m_bulkReplacementControls));
+	m_bulkReplacementControls->setEnabled(false);
+	widgetLayout->addWidget(m_bulkReplacementControls);
+	widgetLayout->addStretch(1);
+
+	connect(m_bulkReplacementCheckBox, &QCheckBox::toggled, this, &MasterPageDeleteDialog::setBulkReplacementEnabled);
+	return widget;
+}
+
+QWidget* MasterPageDeleteDialog::createBulkReplacementControl(ReplacementSide side, QWidget* parent)
+{
+	auto* control = new QWidget(parent);
+	auto* controlLayout = new QHBoxLayout(control);
+	controlLayout->setContentsMargins(0, 0, 0, 0);
+	controlLayout->setSpacing(4);
+
+	QString labelText;
+	switch (side)
+	{
+		case ReplacementSide::All:
+			labelText = tr("Pages:");
+			break;
+		case ReplacementSide::Left:
+			labelText = tr("Left:");
+			break;
+		case ReplacementSide::Right:
+			labelText = tr("Right:");
+			break;
+		case ReplacementSide::None:
+			break;
+	}
+	controlLayout->addWidget(new QLabel(labelText, control));
+
+	auto* combo = new QComboBox(control);
+	combo->setMinimumWidth(135);
+	populateBulkReplacementCombo(combo, side);
+	connect(combo, qOverload<int>(&QComboBox::currentIndexChanged), this, [this, side, combo](int)
+	{
+		bulkReplacementChanged(side, combo);
+	});
+	controlLayout->addWidget(combo);
+
+	switch (side)
+	{
+		case ReplacementSide::All:
+			m_bulkAllCombo = combo;
+			break;
+		case ReplacementSide::Left:
+			m_bulkLeftCombo = combo;
+			break;
+		case ReplacementSide::Right:
+			m_bulkRightCombo = combo;
+			break;
+		case ReplacementSide::None:
+			break;
+	}
+	updateComboToolTip(combo);
+	return control;
+}
+
+void MasterPageDeleteDialog::populateReplacementCombo(QComboBox* combo, ReplacementSide side, const QUuid& selectedId)
+{
+	combo->clear();
+	const QList<QUuid> candidates = replacementCandidates(side);
+	for (const QUuid& masterId : candidates)
+	{
+		const QString name = displayMasterName(m_doc->masterPageName(masterId));
+		combo->addItem(name, masterId.toString(QUuid::WithoutBraces));
+	}
+
+	int selectedIndex = combo->findData(selectedId.toString(QUuid::WithoutBraces));
+	if (selectedIndex < 0 && combo->count() > 0)
+		selectedIndex = 0;
+	combo->setCurrentIndex(selectedIndex);
+	combo->setEnabled(combo->count() > 0);
+}
+
+void MasterPageDeleteDialog::populateBulkReplacementCombo(QComboBox* combo, ReplacementSide side)
+{
+	if (!combo)
+		return;
+
+	m_updatingBulkControls = true;
+	combo->clear();
+
+	QUuid selectedId;
+	for (const MasterInfo& info : m_masterInfo)
+	{
+		if (!usesReplacementSide(info, side))
+			continue;
+		selectedId = replacementId(info, side);
+		if (!selectedId.isNull())
+			break;
+	}
+
+	const QList<QUuid> candidates = replacementCandidates(side);
+	for (const QUuid& masterId : candidates)
+		combo->addItem(displayMasterName(m_doc->masterPageName(masterId)), masterId.toString(QUuid::WithoutBraces));
+
+	if (!selectedId.isNull())
+		setComboMasterId(combo, selectedId);
+	if (combo->currentIndex() < 0 && combo->count() > 0)
+		combo->setCurrentIndex(0);
+	combo->setEnabled(!candidates.isEmpty());
+	m_updatingBulkControls = false;
+	updateComboToolTip(combo);
+}
+
+void MasterPageDeleteDialog::rowReplacementChanged(int row, ReplacementSide side, QComboBox* combo)
+{
+	if (row < 0 || row >= m_masterInfo.count() || !combo || combo->currentIndex() < 0)
+		return;
+	const QUuid masterId(combo->currentData().toString());
+	if (masterId.isNull())
+		return;
+	setReplacementId(m_masterInfo[row], side, masterId);
+	updateComboToolTip(combo);
+	updateBulkReplacementCombo(side);
+}
+
+void MasterPageDeleteDialog::bulkReplacementChanged(ReplacementSide side, QComboBox* combo)
+{
+	if (!m_bulkReplacementEnabled || m_updatingBulkControls || !combo || combo->currentIndex() < 0)
+		return;
+	const QUuid masterId(combo->currentData().toString());
+	if (masterId.isNull())
+		return;
+
+	for (int row = 0; row < m_masterInfo.count(); ++row)
+	{
+		if (usesReplacementSide(m_masterInfo[row], side))
+			setReplacementId(m_masterInfo[row], side, masterId);
+	}
+	for (const ReplacementControl& control : m_replacementControls)
+	{
+		if (control.side != side || !control.combo)
+			continue;
+		QSignalBlocker blocker(control.combo);
+		setComboMasterId(control.combo, masterId);
+		updateComboToolTip(control.combo);
+	}
+}
+
+void MasterPageDeleteDialog::setBulkReplacementEnabled(bool enabled)
+{
+	if (enabled == m_bulkReplacementEnabled)
+		return;
+
+	if (enabled)
+	{
+		m_individualReplacementSnapshot = m_masterInfo;
+		m_bulkReplacementEnabled = true;
+		if (m_bulkReplacementControls)
+			m_bulkReplacementControls->setEnabled(true);
+		setRowReplacementControlsEnabled(false);
+		if (m_bulkAllCombo)
+			bulkReplacementChanged(ReplacementSide::All, m_bulkAllCombo);
+		if (m_bulkLeftCombo)
+			bulkReplacementChanged(ReplacementSide::Left, m_bulkLeftCombo);
+		if (m_bulkRightCombo)
+			bulkReplacementChanged(ReplacementSide::Right, m_bulkRightCombo);
+	}
+	else
+	{
+		m_bulkReplacementEnabled = false;
+		if (m_bulkReplacementControls)
+			m_bulkReplacementControls->setEnabled(false);
+		restoreIndividualReplacements();
+		setRowReplacementControlsEnabled(true);
+	}
+}
+
+void MasterPageDeleteDialog::setRowReplacementControlsEnabled(bool enabled)
+{
+	for (const ReplacementControl& control : m_replacementControls)
+	{
+		if (control.combo)
+			control.combo->setEnabled(enabled && control.combo->count() > 0);
+	}
+}
+
+void MasterPageDeleteDialog::restoreIndividualReplacements()
+{
+	if (m_individualReplacementSnapshot.count() != m_masterInfo.count())
+	{
+		m_individualReplacementSnapshot.clear();
+		return;
+	}
+
+	for (int row = 0; row < m_masterInfo.count(); ++row)
+	{
+		m_masterInfo[row].allReplacementId = m_individualReplacementSnapshot.at(row).allReplacementId;
+		m_masterInfo[row].leftReplacementId = m_individualReplacementSnapshot.at(row).leftReplacementId;
+		m_masterInfo[row].rightReplacementId = m_individualReplacementSnapshot.at(row).rightReplacementId;
+	}
+	for (const ReplacementControl& control : m_replacementControls)
+	{
+		if (!control.combo || control.row < 0 || control.row >= m_masterInfo.count())
+			continue;
+		QSignalBlocker blocker(control.combo);
+		setComboMasterId(control.combo, replacementId(m_masterInfo.at(control.row), control.side));
+		updateComboToolTip(control.combo);
+	}
+	m_individualReplacementSnapshot.clear();
+	updateBulkReplacementCombo(ReplacementSide::All);
+	updateBulkReplacementCombo(ReplacementSide::Left);
+	updateBulkReplacementCombo(ReplacementSide::Right);
+}
+
+void MasterPageDeleteDialog::updateBulkReplacementCombo(ReplacementSide side)
+{
+	QComboBox* combo = bulkReplacementCombo(side);
+	if (!combo)
+		return;
+	populateBulkReplacementCombo(combo, side);
+}
+
+QComboBox* MasterPageDeleteDialog::bulkReplacementCombo(ReplacementSide side) const
+{
+	switch (side)
+	{
+		case ReplacementSide::All:
+			return m_bulkAllCombo;
+		case ReplacementSide::Left:
+			return m_bulkLeftCombo;
+		case ReplacementSide::Right:
+			return m_bulkRightCombo;
+		case ReplacementSide::None:
+			break;
+	}
+	return nullptr;
+}
+
+void MasterPageDeleteDialog::setComboMasterId(QComboBox* combo, const QUuid& masterId)
+{
+	if (!combo)
+		return;
+	const int index = combo->findData(masterId.toString(QUuid::WithoutBraces));
+	if (index >= 0)
+		combo->setCurrentIndex(index);
+}
+
+void MasterPageDeleteDialog::fitTableColumns()
+{
+	if (!m_table || m_table->columnCount() < 2)
+		return;
+
+	m_adjustingColumns = true;
+	const int availableWidth = qMax(0, m_table->horizontalHeader()->width());
+	const int usageWidth = qBound(90, m_table->horizontalHeader()->sectionSizeHint(1), 140);
+	int replacementWidth = 0;
+	if (m_table->columnCount() > 2)
+		replacementWidth = qBound(150, m_table->horizontalHeader()->sectionSizeHint(2), 300);
+	const int minimumNameWidth = qBound(170, m_table->horizontalHeader()->sectionSizeHint(0), 300);
+	const int nameWidth = qMax(minimumNameWidth, availableWidth - usageWidth - replacementWidth);
+
+	m_table->setColumnWidth(0, nameWidth);
+	m_table->setColumnWidth(1, usageWidth);
+	if (m_table->columnCount() > 2)
+		m_table->setColumnWidth(2, replacementWidth);
+	m_adjustingColumns = false;
+}
+
+void MasterPageDeleteDialog::updateElidedToolTips()
+{
+	if (!m_table)
+		return;
+	const int availableWidth = qMax(0, m_table->columnWidth(0) - 12);
+	for (int row = 0; row < m_table->rowCount(); ++row)
+	{
+		QTableWidgetItem* item = m_table->item(row, 0);
+		if (!item)
+			continue;
+		const bool elided = m_table->fontMetrics().horizontalAdvance(item->text()) > availableWidth;
+		item->setToolTip(elided ? item->text() : QString());
+	}
+}
+
+void MasterPageDeleteDialog::updateComboToolTip(QComboBox* combo)
+{
+	if (!combo || !combo->isVisible() || combo->currentIndex() < 0)
+		return;
+	const int availableWidth = qMax(0, combo->width() - combo->style()->pixelMetric(QStyle::PM_ComboBoxFrameWidth, nullptr, combo) * 2 - 28);
+	const bool elided = combo->fontMetrics().horizontalAdvance(combo->currentText()) > availableWidth;
+	combo->setToolTip(elided ? combo->currentText() : QString());
+}
Index: scribus/ui/masterpagedeletedialog.h
===================================================================
--- scribus/ui/masterpagedeletedialog.h	(nonexistent)
+++ scribus/ui/masterpagedeletedialog.h	(working copy)
@@ -0,0 +1,115 @@
+/*
+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 MASTERPAGEDELETEDIALOG_H
+#define MASTERPAGEDELETEDIALOG_H
+
+#include <QDialog>
+#include <QList>
+#include <QMap>
+#include <QString>
+#include <QUuid>
+
+#include "scribusapi.h"
+
+class QCheckBox;
+class QComboBox;
+class QLabel;
+class QResizeEvent;
+class QShowEvent;
+class QTableWidget;
+class QWidget;
+class ScribusDoc;
+
+class SCRIBUS_API MasterPageDeleteDialog : public QDialog
+{
+	Q_OBJECT
+
+public:
+	MasterPageDeleteDialog(QWidget* parent, ScribusDoc* doc, const QList<QUuid>& masterIds);
+
+	QList<QUuid> masterPageIds() const;
+	QMap<int, QUuid> replacementMasterIds() const;
+
+protected:
+	void resizeEvent(QResizeEvent* event) override;
+	void showEvent(QShowEvent* event) override;
+
+private:
+	enum class ReplacementSide
+	{
+		None,
+		All,
+		Left,
+		Right
+	};
+
+	struct MasterInfo
+	{
+		QUuid id;
+		QString name;
+		QList<int> pages;
+		QList<int> allPages;
+		QList<int> leftPages;
+		QList<int> rightPages;
+		QList<int> middlePages;
+		QUuid allReplacementId;
+		QUuid leftReplacementId;
+		QUuid rightReplacementId;
+	};
+
+	struct ReplacementControl
+	{
+		int row { -1 };
+		ReplacementSide side { ReplacementSide::None };
+		QComboBox* combo { nullptr };
+	};
+
+	QString displayMasterName(const QString& name) const;
+	QString formatPageRanges(const QList<int>& pages) const;
+	QList<QUuid> replacementCandidates(ReplacementSide side) const;
+	QUuid replacementId(const MasterInfo& info, ReplacementSide side) const;
+	void setReplacementId(MasterInfo& info, ReplacementSide side, const QUuid& masterId);
+	bool usesReplacementSide(const MasterInfo& info, ReplacementSide side) const;
+	QString sideLabel(ReplacementSide side) const;
+
+	QComboBox* createReplacementCombo(int row, ReplacementSide side, QWidget* parent);
+	QWidget* createReplacementControl(int row, ReplacementSide side, QWidget* parent);
+	QWidget* createReplacementCell(int row);
+	QWidget* createBulkReplacementControls(int usedCount);
+	QWidget* createBulkReplacementControl(ReplacementSide side, QWidget* parent);
+	void populateReplacementCombo(QComboBox* combo, ReplacementSide side, const QUuid& selectedId);
+	void populateBulkReplacementCombo(QComboBox* combo, ReplacementSide side);
+	void rowReplacementChanged(int row, ReplacementSide side, QComboBox* combo);
+	void bulkReplacementChanged(ReplacementSide side, QComboBox* combo);
+	void setBulkReplacementEnabled(bool enabled);
+	void setRowReplacementControlsEnabled(bool enabled);
+	void restoreIndividualReplacements();
+	void updateBulkReplacementCombo(ReplacementSide side);
+	QComboBox* bulkReplacementCombo(ReplacementSide side) const;
+	void setComboMasterId(QComboBox* combo, const QUuid& masterId);
+	void fitTableColumns();
+	void updateElidedToolTips();
+	void updateComboToolTip(QComboBox* combo);
+
+	ScribusDoc* m_doc { nullptr };
+	QList<MasterInfo> m_masterInfo;
+	QList<QUuid> m_deleteIds;
+	QTableWidget* m_table { nullptr };
+	QList<ReplacementControl> m_replacementControls;
+	QList<MasterInfo> m_individualReplacementSnapshot;
+	QCheckBox* m_bulkReplacementCheckBox { nullptr };
+	QWidget* m_bulkReplacementControls { nullptr };
+	QComboBox* m_bulkAllCombo { nullptr };
+	QComboBox* m_bulkLeftCombo { nullptr };
+	QComboBox* m_bulkRightCombo { nullptr };
+	bool m_updatingBulkControls { false };
+	bool m_bulkReplacementEnabled { false };
+	bool m_adjustingColumns { false };
+	bool m_columnsUserAdjusted { false };
+};
+
+#endif
Index: scribus/ui/pagepalette_masterpages.cpp
===================================================================
--- scribus/ui/pagepalette_masterpages.cpp	(revision 27861)
+++ scribus/ui/pagepalette_masterpages.cpp	(working copy)
@@ -6,6 +6,7 @@
 */
 #include "pagepalette_masterpages.h"
 
+#include <QAction>
 #include <QApplication>
 #include <QCloseEvent>
 #include <QCursor>
@@ -13,6 +14,7 @@
 #include <QInputDialog>
 #include <QListWidget>
 #include <QListWidgetItem>
+#include <QMenu>
 #include <QMessageBox>
 #include <QScopedPointer>
 #include <QSpacerItem>
@@ -26,6 +28,7 @@
 #include "canvasmode.h"
 #include "commonstrings.h"
 #include "iconmanager.h"
+#include "masterpagedeletedialog.h"
 #include "mergedoc.h"
 #include "newtemp.h"
 #include "pagestructs.h"
@@ -47,6 +50,11 @@
 	m_view = pCurrentView;
 
 	setupUi(this);
+	m_deleteMenu = new QMenu(deleteOptionsButton);
+	m_deleteUnusedAction = m_deleteMenu->addAction(QString());
+	m_selectUnusedAction = m_deleteMenu->addAction(QString());
+	deleteOptionsButton->setMenu(m_deleteMenu);
+	deleteOptionsButton->setPopupMode(QToolButton::InstantPopup);
 	iconSetChange();
 
 	masterPageListBox->setSelectionMode(QAbstractItemView::ExtendedSelection);
@@ -103,6 +111,8 @@
 {
 	connect(duplicateButton, SIGNAL(clicked()), this, SLOT(duplicateMasterPage()));
 	connect(deleteButton   , SIGNAL(clicked()), this, SLOT(deleteMasterPage()));
+	connect(m_deleteUnusedAction, SIGNAL(triggered()), this, SLOT(deleteUnusedMasterPages()));
+	connect(m_selectUnusedAction, SIGNAL(triggered()), this, SLOT(selectUnusedMasterPages()));
 	connect(newButton      , SIGNAL(clicked()), this, SLOT(newMasterPage()));
 	connect(importButton   , SIGNAL(clicked()), this, SLOT(importPage()));
 	connect(masterPageListBox, SIGNAL(currentItemChanged(QListWidgetItem*,QListWidgetItem*)),
@@ -116,6 +126,8 @@
 {
 	disconnect(duplicateButton, SIGNAL(clicked()), this, SLOT(duplicateMasterPage()));
 	disconnect(deleteButton   , SIGNAL(clicked()), this, SLOT(deleteMasterPage()));
+	disconnect(m_deleteUnusedAction, SIGNAL(triggered()), this, SLOT(deleteUnusedMasterPages()));
+	disconnect(m_selectUnusedAction, SIGNAL(triggered()), this, SLOT(selectUnusedMasterPages()));
 	disconnect(newButton      , SIGNAL(clicked()), this, SLOT(newMasterPage()));
 	disconnect(importButton   , SIGNAL(clicked()), this, SLOT(importPage()));
 	disconnect(masterPageListBox, SIGNAL(currentItemChanged(QListWidgetItem*,QListWidgetItem*)),
@@ -140,7 +152,10 @@
 {
 	setWindowTitle( tr( "Edit Master Pages" ) );
 	duplicateButton->setToolTip( tr( "Duplicate the selected master page" ) );
-	deleteButton->setToolTip( tr( "Delete the selected master page" ) );
+	deleteButton->setToolTip( tr( "Delete the selected master pages" ) );
+	deleteOptionsButton->setToolTip(tr("Master page deletion options"));
+	m_deleteUnusedAction->setText(tr("Delete Unused..."));
+	m_selectUnusedAction->setText(tr("Select Unused"));
 	newButton->setToolTip( tr( "Add a new master page" ) );
 	importButton->setToolTip( tr( "Import master pages from another document" ) );
 }
@@ -156,53 +171,55 @@
 
 void PagePalette_MasterPages::deleteMasterPage()
 {
-	bool forceDelete = false;
 	const QUuid displayedMasterId = m_doc->masterPageId(m_currentPage);
 	QList<QUuid> selectedMasterIds;
 	for (QListWidgetItem* item : masterPageListBox->selectedItems())
 	{
 		const QUuid masterId = m_doc->masterPageId(item->data(Qt::UserRole).toString());
-		if (m_doc->canDeleteMasterPage(masterId))
+		if (!masterId.isNull() && !selectedMasterIds.contains(masterId))
 			selectedMasterIds.append(masterId);
 	}
 
-	// allow to delete multiple pages in one step
-	for (const QUuid& masterId : selectedMasterIds)
-	{
-		const QString masterPageName = m_doc->masterPageName(masterId);
-		if (masterPageName.isEmpty() || !m_doc->canDeleteMasterPage(masterId))
-			continue;
-		m_currentPage = masterPageName;
+	MasterPageDeleteDialog dialog(this, m_doc, selectedMasterIds);
+	if (dialog.masterPageIds().isEmpty() || dialog.exec() != QDialog::Accepted)
+		return;
 
-		QString extraWarn;
+	if (m_doc->appMode == modeEditClip)
+		m_view->requestMode(submodeEndNodeEdit);
+	m_doc->scMW()->deleteMasterPages(dialog.masterPageIds(), dialog.replacementMasterIds());
 
-		if (!forceDelete)
-		{
-			for (int i=0; i < m_doc->DocPages.count(); ++i )
-			{
-				if (m_doc->DocPages[i]->masterPageName() == m_currentPage)
-					extraWarn = tr("This master page is used at least once in the document.");
-			}
-			int exit = ScMessageBox::warning(this,
-										CommonStrings::trWarning,
-										tr("Do you really want to delete master page \"%1\"?").arg(m_currentPage)+"\n"+extraWarn,
-										QMessageBox::Yes | QMessageBox::YesToAll | QMessageBox::No | QMessageBox::Cancel,
-										QMessageBox::NoButton,	// GUI default
-										QMessageBox::YesToAll);	// batch default
-
-			if (exit == QMessageBox::YesToAll)
-				forceDelete = true;
-			if (exit == QMessageBox::No)
-				continue;
-			if (exit == QMessageBox::Cancel)
-				break;
-		}
+	if (m_doc->masterPageIndex(displayedMasterId) >= 0)
+		m_currentPage = m_doc->masterPageName(displayedMasterId);
+	else
+	{
+		const QUuid defaultId = m_doc->defaultMasterPageId(0);
+		m_currentPage = m_doc->masterPageName(defaultId);
+		if (m_currentPage.isEmpty() && !m_doc->MasterPages.isEmpty())
+			m_currentPage = m_doc->MasterPages.first()->pageName();
+	}
+	updateMasterPageList(m_currentPage);
+	selectMasterPage(m_currentPage);
+}
 
-		if (m_doc->appMode == modeEditClip)
-			m_view->requestMode(submodeEndNodeEdit);
-		m_doc->scMW()->deletePage2(m_doc->masterPageIndex(masterId));
+void PagePalette_MasterPages::deleteUnusedMasterPages()
+{
+	const QList<QUuid> unusedMasterIds = m_doc->unusedCustomMasterPageIds();
+	if (unusedMasterIds.isEmpty())
+	{
+		QMessageBox::information(this, tr("Delete Unused Master Pages"),
+			tr("No unused custom master pages found. Default master pages are kept."));
+		return;
 	}
 
+	const QUuid displayedMasterId = m_doc->masterPageId(m_currentPage);
+	MasterPageDeleteDialog dialog(this, m_doc, unusedMasterIds);
+	if (dialog.exec() != QDialog::Accepted)
+		return;
+
+	if (m_doc->appMode == modeEditClip)
+		m_view->requestMode(submodeEndNodeEdit);
+	m_doc->scMW()->deleteMasterPages(dialog.masterPageIds(), dialog.replacementMasterIds());
+
 	if (m_doc->masterPageIndex(displayedMasterId) >= 0)
 		m_currentPage = m_doc->masterPageName(displayedMasterId);
 	else
@@ -216,6 +233,27 @@
 	selectMasterPage(m_currentPage);
 }
 
+void PagePalette_MasterPages::selectUnusedMasterPages()
+{
+	const QList<QUuid> unusedMasterIds = m_doc->unusedCustomMasterPageIds();
+	if (unusedMasterIds.isEmpty())
+	{
+		QMessageBox::information(this, tr("Select Unused Master Pages"),
+			tr("No unused custom master pages found. Default master pages are kept."));
+		return;
+	}
+
+	masterPageListBox->clearSelection();
+	for (int row = 0; row < masterPageListBox->count(); ++row)
+	{
+		QListWidgetItem* item = masterPageListBox->item(row);
+		const QUuid masterId = m_doc->masterPageId(item->data(Qt::UserRole).toString());
+		if (unusedMasterIds.contains(masterId))
+			item->setSelected(true);
+	}
+	updateDeleteButton();
+}
+
 void PagePalette_MasterPages::duplicateMasterPage()
 {
 	QString name = getUniqueName(m_currentPage, m_doc->MasterNames);
@@ -436,6 +474,7 @@
 		}
 	}
 	deleteButton->setEnabled(deleteEnabled);
+	deleteOptionsButton->setEnabled(m_doc != nullptr);
 }
 
 void PagePalette_MasterPages::selectMasterPage(const QString& name)
Index: scribus/ui/pagepalette_masterpages.h
===================================================================
--- scribus/ui/pagepalette_masterpages.h	(revision 27861)
+++ scribus/ui/pagepalette_masterpages.h	(working copy)
@@ -7,10 +7,12 @@
 #ifndef PAGEPALETTE_MASTERPAGES_H
 #define PAGEPALETTE_MASTERPAGES_H
 
+class QAction;
 class QCloseEvent;
 class QHBoxLayout;
 class QListWidget;
 class QListWidgetItem;
+class QMenu;
 class QToolButton;
 class QVBoxLayout;
 
@@ -47,6 +49,9 @@
 	QString      m_currentPage;
 	ScribusDoc  *m_doc;
 	ScribusView *m_view;
+	QMenu *m_deleteMenu { nullptr };
+	QAction *m_deleteUnusedAction { nullptr };
+	QAction *m_selectUnusedAction { nullptr };
 
 protected:
 	void connectSignals();
@@ -64,6 +69,8 @@
 private slots:
 	void duplicateMasterPage();
 	void deleteMasterPage();
+	void deleteUnusedMasterPages();
+	void selectUnusedMasterPages();
 	void newMasterPage();
 	void renameMasterPage(QListWidgetItem * item);
 	void importPage();
Index: scribus/ui/pagepalette_masterpagesbase.ui
===================================================================
--- scribus/ui/pagepalette_masterpagesbase.ui	(revision 27861)
+++ scribus/ui/pagepalette_masterpagesbase.ui	(working copy)
@@ -69,6 +69,16 @@
       </widget>
      </item>
      <item>
+      <widget class="QToolButton" name="deleteOptionsButton">
+       <property name="text">
+        <string/>
+       </property>
+       <property name="arrowType">
+        <enum>Qt::ArrowType::DownArrow</enum>
+       </property>
+      </widget>
+     </item>
+     <item>
       <spacer name="horizontalSpacer">
        <property name="orientation">
         <enum>Qt::Horizontal</enum>
Index: scribus/ui/pagepalette_pages.cpp
===================================================================
--- scribus/ui/pagepalette_pages.cpp	(revision 27861)
+++ scribus/ui/pagepalette_pages.cpp	(working copy)
@@ -22,6 +22,7 @@
 
 #include "commonstrings.h"
 #include "iconmanager.h"
+#include "masterpagedeletedialog.h"
 #include "ui/widgets/pagelayout.h"
 #include "pagepalette_pages.h"
 #include "pagepalette_widgets.h"
@@ -89,36 +90,26 @@
 	const QUuid masterId = currView->m_doc->masterPageId(tmp);
 	if (!currView->m_doc->canDeleteMasterPage(masterId))
 		return;
-	QString extraWarn;
-	for (int i = 0; i < currView->m_doc->DocPages.count(); ++i)
-	{
-		if (currView->m_doc->DocPages[i]->masterPageName() == tmp)
-			extraWarn = tr("This master page is used at least once in the document.");
-	}
-	int exit = ScMessageBox::warning(this,
-									 CommonStrings::trWarning,
-									 tr("Do you really want to delete this master page?") + "\n"
-									 + extraWarn,
-									 QMessageBox::Yes | QMessageBox::No,
-									 QMessageBox::NoButton, // GUI default
-									 QMessageBox::Yes);     // batch default
-	if (exit == QMessageBox::Yes)
-	{
-		bool oldMPMode = currView->m_doc->masterPageMode();
-		int storedPageNum = currView->m_doc->currentPageNumber();
-		int storedViewXCoor = currView->horizontalScrollBar()->value();
-		int storedViewYCoor = currView->verticalScrollBar()->value();
-
-		currView->m_doc->setMasterPageMode(true);
-		currView->m_doc->scMW()->deletePage2(currView->m_doc->masterPageIndex(masterId));
-		currView->m_doc->setMasterPageMode(oldMPMode);
-		currView->m_doc->setModified(true);
-
-		currView->m_doc->setCurrentPage(currView->m_doc->DocPages.at(storedPageNum));
-		currView->reformPages(false);
-		currView->setContentsPos(storedViewXCoor, storedViewYCoor);
-		currView->DrawNew();
-	}
+
+	MasterPageDeleteDialog dialog(this, currView->m_doc, { masterId });
+	if (dialog.exec() != QDialog::Accepted)
+		return;
+
+	bool oldMPMode = currView->m_doc->masterPageMode();
+	int storedPageNum = currView->m_doc->currentPageNumber();
+	int storedViewXCoor = currView->horizontalScrollBar()->value();
+	int storedViewYCoor = currView->verticalScrollBar()->value();
+
+	currView->m_doc->setMasterPageMode(true);
+	currView->m_doc->scMW()->deleteMasterPages(dialog.masterPageIds(), dialog.replacementMasterIds());
+	currView->m_doc->setMasterPageMode(oldMPMode);
+	currView->m_doc->setModified(true);
+
+	if (!currView->m_doc->DocPages.isEmpty())
+		currView->m_doc->setCurrentPage(currView->m_doc->DocPages.at(qBound(0, storedPageNum, currView->m_doc->DocPages.count() - 1)));
+	currView->reformPages(false);
+	currView->setContentsPos(storedViewXCoor, storedViewYCoor);
+	currView->DrawNew();
 }
 
 void PagePalette_Pages::pageView_applyMasterPage(const QString &masterpageName, int pageIndex)
mdu2-delete-unused-v1.33.patch (47,161 bytes)   

buttar

2026-09-23 12:52

reporter   ~0054491

A few screenshots showing the proposed feature/changes.
delete0unused-PROPOSED.png (20,078 bytes)   
delete0unused-PROPOSED.png (20,078 bytes)   
delete-mixed-PROPOSED.png (152,523 bytes)   
delete-mixed-PROPOSED.png (152,523 bytes)   

Issue History

Date Modified Username Field Change
2026-09-18 14:27 buttar New Issue
2026-09-23 12:50 buttar Note Added: 0054490
2026-09-23 12:50 buttar File Added: mdu1-deletion-hardening-v1.0.patch
2026-09-23 12:50 buttar File Added: mdu2-delete-unused-v1.33.patch
2026-09-23 12:52 buttar Note Added: 0054491
2026-09-23 12:52 buttar File Added: delete0unused-PROPOSED.png
2026-09-23 12:52 buttar File Added: delete-mixed-PROPOSED.png
2026-09-23 12:52 buttar File Added: delete-or-select-unused-PROPOSED.png
2026-09-23 16:41 buttar Tag Attached: #please_test