View Issue Details

IDProjectCategoryView StatusLast Update
0014648ScribusUser Interfacepublic2026-09-12 17:31
ReporterJLuc Assigned To 
PrioritynormalSeverityfeatureReproducibilityN/A
Status newResolutionopen 
PlatformLinuxOSUbuntuOS Version14.04
Summary0014648: Allow to delete multiple points in curve
DescriptionWhen in shape edit mode, its possible to select many points at once (using lasso for example)
and it's possible to move them all at once (using the mouse or the keyboard arrow keys)
BUT it's not possible to delete them all at once.

It's not an issue on rectangular shapes where there is only few points
but it becomes a pain on curve shapes, which can sometime contain hundreds of points depending on their source.

This request is to allow the use of DEL key so as to delete ALL selected points.
Tags#please_test
PatchNo

Activities

buttar

2026-09-12 17:31

reporter   ~0054459

Here is the patch that fixes this a more closely related fixes.

* Deletes all selected anchor nodes in one operation.
* Excludes Bézier control handles from node Nexus deletion.
* Handles duplicated anchors and compound subpaths safely.
* Rejects deletions that would leave an invalid open or closed path.
* Supports both shape and contour editing.
* Records the complete deletion as one Undo action.
* Immediately refreshes contour-node rendering.
* Keeps modified Delete shortcuts, such as Clear Contents, separate from node deletion.
nodeedit-multiple-node-deletion-v1.01.patch (8,470 bytes)   
Index: scribus/canvasmode_nodeedit.cpp
===================================================================
--- scribus/canvasmode_nodeedit.cpp	(revision 27829)
+++ scribus/canvasmode_nodeedit.cpp	(working copy)
@@ -14,9 +14,13 @@
 ***************************************************************************/
 
 
+#include <algorithm>
+#include <functional>
+
 #include <QMenu>
 #include <QMouseEvent>
 #include <QPainter>
+#include <QSet>
 
 #include "canvas.h"
 #include "canvasgesture_pan.h"
@@ -581,19 +585,21 @@
 
 void CanvasMode_NodeEdit::keyPressEvent(QKeyEvent *e)
 {
-	commonkeyPressEvent_NormalNodeEdit(e);
-
 	int kk = e->key();
 	PageItem* currItem = m_doc->m_Selection->itemAt(0);
 
-	if (kk == Qt::Key_Delete || kk == Qt::Key_Backspace)
+	const auto commandModifiers = e->modifiers() & (Qt::ShiftModifier | Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier);
+	if ((kk == Qt::Key_Delete || kk == Qt::Key_Backspace) && commandModifiers == Qt::NoModifier)
 	{
 		if (currItem && m_doc->nodeEdit.hasNodeSelected())
 		{
-			// delete Nodes
+			e->accept();
 			refreshPath(deleteNodes());
+			return;
 		}
 	}
+
+	commonkeyPressEvent_NormalNodeEdit(e);
 }
 
 void CanvasMode_NodeEdit::keyReleaseEvent(QKeyEvent *e)
@@ -1349,96 +1355,154 @@
 bool CanvasMode_NodeEdit::deleteNodes()
 {
 	PageItem* currItem = m_doc->m_Selection->itemAt(0);
-	FPointArray Clip = m_doc->nodeEdit.beginTransaction(currItem);
+	if (!currItem)
+		return false;
 
-	if (!currItem || Clip.size() == 0)
+	FPointArray clip = m_doc->nodeEdit.clipFromItem(currItem);
+	if (clip.empty())
 		return false;
 
-	FPointArray cli;
-	int EndInd = Clip.size();
-	int StartInd = 0;
-	if (m_doc->nodeEdit.clre() > 0)
+	struct Subpath
 	{
-		for (int n = m_doc->nodeEdit.clre(); n < Clip.size(); ++n)
-		{
-			if (Clip.isMarker(n))
-			{
-				EndInd = n;
-				break;
-			}
+		int start { 0 };
+		int end { 0 };
+		bool closed { false };
+		QSet<int> anchors;
+	};
+
+	QList<Subpath> subpaths;
+	const bool pathsAreClosed = m_doc->nodeEdit.isContourLine()
+		|| (!currItem->isLine() && !currItem->isPathText() && !currItem->isPolyLine());
+	for (int start = 0; start < clip.size();)
+	{
+		while (start < clip.size() && clip.isMarker(start))
+			++start;
+		if (start >= clip.size())
+			break;
+
+		int end = start;
+		while (end < clip.size() && !clip.isMarker(end))
+			++end;
+		if (end > start)
+		{
+			Subpath subpath;
+			subpath.start = start;
+			subpath.end = end;
+			subpath.closed = pathsAreClosed;
+			subpaths.append(subpath);
 		}
-		for (int n2 = m_doc->nodeEdit.clre(); n2 > 0; n2--)
+		start = end;
+	}
+
+	// Map both internal representations of a shared anchor to one index.
+	for (int selected : m_doc->nodeEdit.selNode())
+	{
+		if (!NodeEditContext::isNode(selected) || selected < 0 || selected >= clip.size() || clip.isMarker(selected))
+			continue;
+
+		for (Subpath& subpath : subpaths)
 		{
-			if (Clip.isMarker(n2))
-			{
-				StartInd = n2 + 1;
-				break;
-			}
+			if (selected < subpath.start || selected >= subpath.end)
+				continue;
+
+			int anchor = selected;
+			if (subpath.closed && selected == subpath.end - 2)
+				anchor = subpath.start;
+			else if (selected > subpath.start && ((selected - subpath.start) % 4) == 0)
+				anchor = selected - 2;
+
+			if (anchor == subpath.start || ((anchor - subpath.start - 2) % 4) == 0)
+				subpath.anchors.insert(anchor);
+			break;
 		}
 	}
 
-	if (m_doc->nodeEdit.hasNodeSelected())
+	int affectedSubpaths = 0;
+	int removedSubpaths = 0;
+	for (const Subpath& subpath : subpaths)
 	{
-		int minClipSize = 4;
-		if (!currItem->isLine() && !currItem->isPathText() && !currItem->isPolyLine())
-			minClipSize = 12;
+		if (subpath.anchors.isEmpty())
+			continue;
+		++affectedSubpaths;
 
-		if (Clip.size() <= minClipSize)
+		const int segments = (subpath.end - subpath.start) / 4;
+		const int anchorCount = segments + (subpath.closed ? 0 : 1);
+		const int remaining = anchorCount - subpath.anchors.size();
+		if (remaining == 0)
 		{
-			m_doc->nodeEdit.deselect();
-			return false;
+			++removedSubpaths;
+			continue;
 		}
 
-		int numPoints = (StartInd != 0) ? (StartInd-4) : 0;
-		numPoints += (Clip.size() - EndInd);
-		if ((currItem->Segments.count() > 0) && ((EndInd - StartInd) <= minClipSize) && (numPoints > 0))
-		{
-			if (StartInd != 0)
-				cli.putPoints(0, StartInd - 4, Clip);
-			cli.putPoints(cli.size(), Clip.size() - EndInd, Clip, EndInd);
-		}
-		else if (m_doc->nodeEdit.clre() == StartInd)
-		{
-			if (!currItem->isLine() && !currItem->isPathText() && !currItem->isPolyLine())
-			{
-				const FPoint& kp(Clip.point(EndInd - 3));
-				cli.putPoints(0, StartInd, Clip);
-				cli.putPoints(cli.size(), EndInd - StartInd - 4, Clip, StartInd);
-				cli.setPoint(StartInd, cli.point(cli.size() - 2));
-				cli.setPoint(StartInd + 1, kp);
-				cli.putPoints(cli.size(), Clip.size() - EndInd, Clip, EndInd);
-			}
-			else
+		const int minimum = subpath.closed ? 3 : 2;
+		if (remaining < minimum)
+			return false;
+	}
+
+	if (affectedSubpaths == 0 || removedSubpaths == subpaths.size())
+		return false;
+
+	// Work on a copy and commit only after every selected deletion is valid.
+	FPointArray result = clip.copy();
+	for (int subpathIndex = subpaths.size() - 1; subpathIndex >= 0; --subpathIndex)
+	{
+		const Subpath& subpath = subpaths.at(subpathIndex);
+		if (subpath.anchors.isEmpty())
+			continue;
+
+		const int segments = (subpath.end - subpath.start) / 4;
+		const int anchorCount = segments + (subpath.closed ? 0 : 1);
+		if (subpath.anchors.size() == anchorCount)
+		{
+			int removeStart = subpath.start;
+			int removeCount = subpath.end - subpath.start;
+			if (subpath.end < result.size() && result.isMarker(subpath.end))
+				removeCount += 4;
+			else if (removeStart >= 4 && result.isMarker(removeStart - 1))
 			{
-				cli.putPoints(0, StartInd, Clip);
-				cli.putPoints(cli.size(), EndInd - StartInd - 4, Clip, StartInd+4);
-				cli.putPoints(cli.size(), Clip.size() - EndInd, Clip, EndInd);
+				removeStart -= 4;
+				removeCount += 4;
 			}
+			result.remove(removeStart, removeCount);
+			continue;
 		}
-		else if (m_doc->nodeEdit.clre() == (EndInd - 2))
-		{
-			Clip.remove(m_doc->nodeEdit.clre() - 2, 4);
-			cli = Clip.copy();
-		}
-		else
+
+		QList<int> anchors = subpath.anchors.values();
+		std::sort(anchors.begin(), anchors.end(), std::greater<int>());
+		for (int anchor : anchors)
 		{
-			if (m_doc->nodeEdit.clre() != 0)
-				cli.putPoints(0, m_doc->nodeEdit.clre(), Clip);
-			cli.putPoints(cli.size(), Clip.size() - (m_doc->nodeEdit.clre() + 4), Clip, m_doc->nodeEdit.clre() + 4);
+			int end = subpath.end;
+			// Higher-index removals in this subpath have already shortened it.
+			for (int higher : anchors)
+			{
+				if (higher > anchor)
+					end -= 4;
+			}
+
+			if (anchor == subpath.start && subpath.closed)
+			{
+				const FPoint incomingControl = result.point(end - 3);
+				result.remove(end - 4, 4);
+				result.setPoint(subpath.start, result.point(end - 6));
+				result.setPoint(subpath.start + 1, incomingControl);
+			}
+			else if (anchor == subpath.start)
+				result.remove(subpath.start, 4);
+			else if (anchor == end - 2)
+				result.remove(anchor - 2, 4);
+			else
+				result.remove(anchor, 4);
 		}
-		while (cli.isMarker(0))
-			cli.remove(0, 4);
-		if (m_doc->nodeEdit.isContourLine())
-			currItem->ContourLine = cli.copy();
-		else
-			currItem->PoLine = cli.copy();
-		m_doc->nodeEdit.deselect();
-		currItem->ClipEdited = true;
-		return true;
 	}
 
-
-	return false;
+	m_doc->nodeEdit.beginTransaction(currItem);
+	if (m_doc->nodeEdit.isContourLine())
+		currItem->ContourLine = result.copy();
+	else
+		currItem->PoLine = result.copy();
+	m_doc->nodeEdit.deselect();
+	currItem->ClipEdited = true;
+	return true;
 }
 
 void CanvasMode_NodeEdit::refreshPath(bool edited)
@@ -1451,6 +1515,14 @@
 	if (edited)
 	{
 		currItem->FrameType = 3;
+		if (m_doc->nodeEdit.isContourLine())
+		{
+			currItem->update();
+			m_view->DrawNew();
+			emit m_view->PolyStatus(currItem->itemType(), currItem->ContourLine.size());
+			return;
+		}
+
 		double xp = currItem->xPos();
 		double yp = currItem->yPos();
 		double w = currItem->width();

Issue History

Date Modified Username Field Change
2017-02-27 10:57 JLuc New Issue
2026-09-12 17:31 buttar Note Added: 0054459
2026-09-12 17:31 buttar File Added: nodeedit-multiple-node-deletion-v1.01.patch
2026-09-12 17:31 buttar Tag Attached: #please_test