View Issue Details

IDProjectCategoryView StatusLast Update
0017831ScribusText Frames / Story Editorpublic2026-06-12 07:37
Reportersoerendanielkarch Assigned To 
PrioritynormalSeverityminorReproducibilityalways
Status newResolutionopen 
Product Version1.7.3 
Summary0017831: Mouse text selection does not work properly across columns in a multi-column text frame
Descriptionn a multi-column text frame, it is not possible to reliably select text with the mouse across column boundaries.

When text flows from one column into the next column, I often need to select a specific range of text that starts in one column and continues into the next column. However, dragging the mouse selection across columns does not work as expected. The selection cannot be continued naturally over the column break.

The only reliable alternative is to use Ctrl+A / Cmd+A to select all text, but that is not useful when the text contains different existing formatting or when only a specific text range should be edited.
Steps To ReproduceCreate a text frame with two or more columns.
Add enough text so that it flows from the first column into the next.
Switch to text editing mode.
Attempt to select text with the mouse by starting at the end of a column and dragging into the next column.
Observe whether the selection continues correctly across the column boundary. In my case, the selection cuts off immediately.
Additional InformationActual result:

The text selection with the mouse does not continue properly across columns. It is not possible to select the intended text range in a natural and precise way.

Expected result:

Mouse selection should follow the logical text flow across columns. The user should be able to click and drag from one column into the next and select exactly the intended text range, just like in other text editing and layout applications.

Why this matters:

Multi-column text frames are common in layout work. When text reflows between columns, users often need to select and edit only part of the text. If mouse selection does not work across columns, editing becomes slow and frustrating.

Workaround:

Use Ctrl+A / Cmd+A to select all text, or edit the text in smaller parts. This is not a practical workaround when the story contains mixed formatting or only a specific section should be changed.
TagsNo tags attached.
PatchNo

Activities

soerendanielkarch

2026-06-07 06:27

reporter   ~0053786

I am new to contributing code and not an experienced programmer, but I have ideas and would like to help improve Scribus.

This patch attempts to preserve the active character style when inserting text into empty paragraphs or after paragraph breaks.

I developed and tested this patch with assistance from AI. On my macOS Apple Silicon system with Scribus 1.7.3, it works very well and resolves the reported problem.

Please consider this a proposed patch for review and testing. I would appreciate it if experienced developers could verify the implementation and confirm that it does not cause regressions on other platforms or in other text-editing situations.

Thank you for reviewing my contribution.

soerendanielkarch

2026-06-07 06:31

reporter   ~0053788

As mentioned in another post, I am quite new to contributing to open-source projects and am not an experienced programmer. However, I have many ideas and would like to help develop and improve Scribus.

This patch aims to improve—or make possible—the selection of text with the mouse across multiple columns and linked text frames. The selection can continue across column boundaries, beyond the edge of a text frame, and into the next linked text frame.

This improvement is particularly important for practical work with Scribus, as this limitation has existed for a long time and makes editing extensive, multi-column text considerably more difficult.

I developed and tested this patch with assistance from AI. It works very well on my Apple Silicon Mac with Scribus 1.7.3 and resolves the issue I reported.

Please consider this contribution a proposed patch submitted for review and testing. I would appreciate it if experienced developers could verify whether the implementation also works reliably on other platforms and in other text-editing scenarios without causing unintended side effects.

Thank you very much for reviewing my contribution.
preserve-mouse-selection-across-columns.diff (6,406 bytes)   
diff --git a/scribus/canvasmode_edit.cpp b/scribus/canvasmode_edit.cpp
index dc7749246..305dd3f35 100644
--- a/scribus/canvasmode_edit.cpp
+++ b/scribus/canvasmode_edit.cpp
@@ -14,6 +14,7 @@
 ***************************************************************************/
 
 #include <chrono>
+#include <limits>
 
 #include "canvasmode_edit.h"
 
@@ -68,6 +69,41 @@ inline bool CanvasMode_Edit::GetItem(PageItem** pi)
 	return (*pi) != nullptr;
 }
 
+PageItem_TextFrame* CanvasMode_Edit::textFrameAtPoint(PageItem_TextFrame* startFrame, const QPointF& canvasPoint) const
+{
+	PageItem_TextFrame* nearestFrame = startFrame;
+	double nearestDistance = std::numeric_limits<double>::max();
+	PageItem* chainFrame = startFrame->firstInChain();
+	while (chainFrame)
+	{
+		PageItem_TextFrame* textFrame = chainFrame->asTextFrame();
+		bool invertible = false;
+		QTransform inverseTransform = textFrame->getTransform().inverted(&invertible);
+		if (invertible)
+		{
+			QPointF localPoint = inverseTransform.map(canvasPoint);
+			QRectF frameRect(0.0, 0.0, textFrame->width(), textFrame->height());
+			if (frameRect.contains(localPoint))
+				return textFrame;
+
+			QPointF nearestLocalPoint(
+				qBound(frameRect.left(), localPoint.x(), frameRect.right()),
+				qBound(frameRect.top(), localPoint.y(), frameRect.bottom()));
+			QPointF nearestCanvasPoint = textFrame->getTransform().map(nearestLocalPoint);
+			double distanceX = canvasPoint.x() - nearestCanvasPoint.x();
+			double distanceY = canvasPoint.y() - nearestCanvasPoint.y();
+			double distance = distanceX * distanceX + distanceY * distanceY;
+			if (distance < nearestDistance)
+			{
+				nearestFrame = textFrame;
+				nearestDistance = distance;
+			}
+		}
+		chainFrame = chainFrame->nextInChain();
+	}
+	return nearestFrame;
+}
+
 
 void CanvasMode_Edit::blinkTextCursor()
 {
@@ -469,11 +505,17 @@ void CanvasMode_Edit::mouseMoveEvent(QMouseEvent *m)
 			}
 			if (currItem->isTextFrame())
 			{
-				int refStartSel(currItem->asTextFrame()->itemText.startOfSelection());
-				int refEndSel(currItem->asTextFrame()->itemText.endOfSelection());
+				PageItem_TextFrame* currentTextFrame = currItem->asTextFrame();
+				int refStartSel(currentTextFrame->itemText.startOfSelection());
+				int refEndSel(currentTextFrame->itemText.endOfSelection());
 				currItem->itemText.deselectAll();
 				currItem->HasSel = false;
-				m_view->slotSetCurs(globalPos.x(), globalPos.y());
+				PageItem_TextFrame* targetTextFrame = textFrameAtPoint(currentTextFrame, mousePointDoc.toQPointF());
+				if (targetTextFrame->invalid)
+					targetTextFrame->layout();
+				int textPosition = targetTextFrame->textPositionFromPoint(mousePointDoc.toQPointF());
+				if (textPosition >= 0)
+					currItem->itemText.setCursorPosition(textPosition);
 				//Make sure we don't go here if the old cursor position was not set
 				if (oldCp!=-1 && currItem->itemText.isNotEmpty())
 				{
@@ -492,11 +534,16 @@ void CanvasMode_Edit::mouseMoveEvent(QMouseEvent *m)
 				if (currItem->HasSel)
 				{
 					m_canvas->m_viewMode.operTextSelecting = true;
-					if ((refStartSel != currItem->asTextFrame()->itemText.startOfSelection()) ||
-						(refEndSel   != currItem->asTextFrame()->itemText.endOfSelection()))
+					if ((refStartSel != currentTextFrame->itemText.startOfSelection()) ||
+						(refEndSel   != currentTextFrame->itemText.endOfSelection()))
 					{
-						QRectF br(currItem->getBoundingRect());
-						m_canvas->update(QRectF(m_canvas->canvasToLocal(br.topLeft()), br.size() * m_canvas->scale()).toRect());
+						PageItem* chainFrame = currentTextFrame->firstInChain();
+						while (chainFrame)
+						{
+							QRectF br(chainFrame->getBoundingRect());
+							m_canvas->update(QRectF(m_canvas->canvasToLocal(br.topLeft()), br.size() * m_canvas->scale()).toRect());
+							chainFrame = chainFrame->nextInChain();
+						}
 					}
 					// We have to call this unconditionally because slotSetCurs() doesn't know selection
 					// when it is called
diff --git a/scribus/canvasmode_edit.h b/scribus/canvasmode_edit.h
index c3aa9ae1a..91894e2f3 100644
--- a/scribus/canvasmode_edit.h
+++ b/scribus/canvasmode_edit.h
@@ -61,6 +61,7 @@ public:
 private:
 	inline bool GetItem(PageItem** pi);
 	void drawTextCursor(QPainter* p, PageItem_TextFrame* textframe);
+	PageItem_TextFrame* textFrameAtPoint(PageItem_TextFrame* startFrame, const QPointF& canvasPoint) const;
 	bool SeleItem(QMouseEvent *m);
 	void createContextMenu(PageItem *currItem, double mx, double my);
 
diff --git a/scribus/scribusview.cpp b/scribus/scribusview.cpp
index 1bbfa64b6..82c2a2282 100644
--- a/scribus/scribusview.cpp
+++ b/scribus/scribusview.cpp
@@ -1558,7 +1558,8 @@ bool ScribusView::slotSetCurs(int x, int y)
 				textFrame->itemText.setCursorPosition(result);
 		}
 		*/
-		textFrame->itemText.setCursorPosition(textPosition);
+		if (textPosition >= 0)
+			textFrame->itemText.setCursorPosition(textPosition);
 		if (textFrame->itemText.isNotEmpty())
 		{
 			int pos = qMax(0, qMin(textFrame->itemText.cursorPosition(), textFrame->itemText.length()));
diff --git a/scribus/text/boxes.cpp b/scribus/text/boxes.cpp
index 1ff8d4dc2..1b196aa17 100644
--- a/scribus/text/boxes.cpp
+++ b/scribus/text/boxes.cpp
@@ -6,6 +6,7 @@
  */
 
 #include <cmath>
+#include <limits>
 #include <unicode/brkiter.h>
 
 #include "pageitem.h"
@@ -42,6 +43,34 @@ int GroupBox::pointToPosition(const QPointF& coord, const StoryText &story) cons
 			return lastLine->lastChar() + 1;
 	}
 
+	if ((m_type == T_Block) && !m_boxes.isEmpty())
+	{
+		const Box* nearestBox = nullptr;
+		QPointF nearestPoint;
+		double nearestDistance = std::numeric_limits<double>::max();
+		for (const Box* box : boxes())
+		{
+			if ((box->type() == T_Block) && box->isEmpty())
+				continue;
+
+			const QRectF boxRect = box->bbox();
+			const QPointF candidatePoint(
+				qBound(boxRect.left(), rel.x(), boxRect.right()),
+				qBound(boxRect.top(), rel.y(), boxRect.bottom()));
+			const double distanceX = rel.x() - candidatePoint.x();
+			const double distanceY = rel.y() - candidatePoint.y();
+			const double distance = distanceX * distanceX + distanceY * distanceY;
+			if (distance < nearestDistance)
+			{
+				nearestBox = box;
+				nearestPoint = candidatePoint;
+				nearestDistance = distance;
+			}
+		}
+		if (nearestBox)
+			return nearestBox->pointToPosition(nearestPoint, story);
+	}
+
 	return -1;
 }
 

ale

2026-06-07 08:03

manager   ~0053789

Last edited: 2026-06-07 08:04

Hi

Thank you for you contributions.

I think that you're the first "external" person providing AI based patches for Scribus.
The Scribus project will need to setup some rules, to avoid frustration on all sides.

Currently, the team has a hard time keeping up with reviewing patches proposed by humans, I fear that if several people who are not (C++) programmers start uploading AI patches, there is no chance that the team can keep up with the new contributions.

As a first "measure", I'd like to suggest four requirements:

- AI patches are only accepted for features and bugs that are already recorded in the bug tracker.
- If possible (and not too messy), you should attach the prompt that generated the patch.
- You should also always attach an explanation of the patch, generated by your AI.
- List what you have tested, to make sure that the patch works as expected (if possible / useful with test .sla documents also attached)

In this specific case, you opened a new ticket, but we already have old requests for this feature, so you're on the safe side.

It now would help, if you could let your AI explain the patch it produced.
And list what you did, to make sure that your patch works as expected.

soerendanielkarch

2026-06-07 11:43

reporter   ~0053790

Hello,

First of all, I would like to apologize for submitting an AI-assisted patch before it had been clarified how such contributions should be handled within the Scribus project. It was never my intention to create additional work, bypass experienced programmers, or upset anyone.

I understand the concern that unreviewed or mass-generated AI patches could place an additional burden on the team’s already limited resources. I therefore consider the proposed rules reasonable and will follow them for future contributions.

Nevertheless, I believe that such tools can benefit the open-source community when they are used transparently, responsibly, and purposefully. New tools can accelerate processes and help people with ideas overcome technical barriers. What matters to me is that the results are not accepted without review, but are documented clearly, tested, and subsequently evaluated by experienced developers.

Most importantly, in cases such as these two patches, the tools may help close gaps that have existed for a long time and have, to some extent, hindered professional work with Scribus.

## Starting Point and Prompt

As the starting point, I copied the complete text of my bug report into Codex without modification. It included the description, reproduction steps, expected result, and actual result.

The additional instruction was essentially:

> Analyze this problem in the Scribus source code, identify the cause, and implement a possible fix. Then compile Scribus 1.7.3 so that I can test the change in practice.

Development took place in several stages. The first attempts only partially solved the problem. Through subsequent practical testing, I was able to describe more precisely where the selection still stopped.

The original problem was that the selection already stopped as soon as the pointer left the text frame while the mouse button remained pressed. The patch was then adjusted again, after which this part worked.

## Analysis of the Cause

Text selection during mouse movement is handled mainly in `CanvasMode_Edit::mouseMoveEvent()`.

Two related problems were identified:

1. The mouse position was converted into a text position through `ScribusView::slotSetCurs()`. Under normal circumstances, however, this function only accepts positions within the available text region of a frame.

2. When the pointer was positioned in the column gap or outside the text frame, the layout hit test could not determine a valid text position and returned `-1`.

This invalid position could subsequently be passed to `StoryText::setCursorPosition()`, where it was clamped to the beginning of the text. As a result, the selection either jumped or could not continue naturally.

After this behavior had been corrected, another test showed that the selection could continue outside an individual frame but could not move into the next linked text frame.

The reason was that the text layout of the originally selected frame was used throughout the entire mouse movement. The layout of the next linked text frame was not considered, even though all frames in the same text chain share the same `StoryText`.

## Changes Made by the Patch

The patch essentially makes the following changes:

- Invalid text positions are no longer used as new cursor positions without validation.
- During an active text selection, the position is calculated directly through the text layout, without allowing the normal frame hit test to stop the selection at the frame boundary.
- For points within column gaps or outside a text line, the geometrically nearest valid text position is determined.
- A new helper function searches the complete chain of linked text frames.
- When the pointer is positioned over a linked frame, that frame’s text layout is used to calculate the text position.
- When the pointer is positioned between frames, the geometrically nearest frame in the text chain is used.
- When the selection changes, all text frames in the chain are redrawn so that the selection also becomes visible in the other frames.

As a result, a selection can now continue across:

- column boundaries,
- column gaps,
- the boundary of a text frame, and
- multiple linked text frames.

## Tests Performed

I tested the patch with Scribus 1.7.3 on an Apple Silicon Mac.

In particular, I tested:

- selection within a normal line of text,
- selection across multiple lines,
- selection from the text area into a column gap,
- selection across the boundary between two columns,
- selection outside the text frame while holding down the mouse button,
- selection from the first frame into the next linked text frame,
- selection across several text sections containing existing formatting, and
- selection in both directions, as far as I was able to test it practically.

Additionally:

- the modified source code was successfully compiled as a native ARM64 build,
- the resulting macOS application was launched,
- the application bundle’s code signature was verified, and
- the application bundle was checked for unintended absolute Homebrew dependencies.

In my practical tests so far, the change works very well. However, I cannot provide a complete assessment for other platforms, more complex documents, or possible side effects. I am therefore submitting this patch expressly as a proposal for technical review and further testing.

A dedicated automated regression test or separate `.sla` test document is not yet included with this patch. If requested, I can create and attach a small example document containing multi-column and linked text frames, although it may be preferable for reviewers to create their own test document. For now, I will attach an image illustrating the result.

Thank you for your guidance and for the time you invest in reviewing this contribution. I will follow the stated requirements for future submissions.
spalten.jpg (684,265 bytes)   
spalten.jpg (684,265 bytes)   

ale

2026-06-11 17:52

manager   ~0053811

I've locally applied your patch and:

- it works as you describe
- I miss the shift-click to extend the selection

Now, it's time to check the quality of the code : - )

soerendanielkarch

2026-06-12 07:18

reporter   ~0053822

You mean clicking at one spot, holding down Shift, and clicking at another spot to make a selection? That still works... just not across columns yet :-) ... good idea, even though I don't use it that way myself. I'll see what we can do.

soerendanielkarch

2026-06-12 07:33

reporter   ~0053823

Okay, that was quick. Feel free to test it out. I can now select across columns using the mouse, AND by using Shift-start-click and Shift-end-click.
preserve-mouse-selection-across-columns-2.diff (11,281 bytes)   
diff --git a/scribus/canvasmode_edit.cpp b/scribus/canvasmode_edit.cpp
index dc7749246..8895a82a0 100644
--- a/scribus/canvasmode_edit.cpp
+++ b/scribus/canvasmode_edit.cpp
@@ -14,6 +14,7 @@
 ***************************************************************************/
 
 #include <chrono>
+#include <limits>
 
 #include "canvasmode_edit.h"
 
@@ -68,6 +69,88 @@ inline bool CanvasMode_Edit::GetItem(PageItem** pi)
 	return (*pi) != nullptr;
 }
 
+void CanvasMode_Edit::extendTextSelection(PageItem_TextFrame* textFrame, int oldPosition, Qt::KeyboardModifiers modifiers)
+{
+	StoryText& story = textFrame->itemText;
+	if (story.hasSelection())
+	{
+		int selectionAnchor;
+		if (story.cursorPosition() < (story.startOfSelection() + story.endOfSelection()) / 2)
+		{
+			if (modifiers & Qt::ControlModifier)
+				story.setCursorPosition(story.startOfParagraph());
+			selectionAnchor = story.startOfSelection();
+		}
+		else
+		{
+			if (modifiers & Qt::ControlModifier)
+				story.setCursorPosition(story.endOfParagraph());
+			selectionAnchor = story.endOfSelection();
+		}
+		story.extendSelection(selectionAnchor, story.cursorPosition());
+		oldCp = story.cursorPosition();
+	}
+	else
+	{
+		int direction = (oldCp > story.cursorPosition()) ? -1 : 1;
+		if (modifiers & Qt::ControlModifier)
+		{
+			if (direction == 1)
+				story.setCursorPosition(story.endOfParagraph());
+			else
+				story.setCursorPosition(story.startOfParagraph());
+		}
+		textFrame->ExpandSel(oldPosition);
+		oldCp = oldPosition;
+	}
+}
+
+PageItem_TextFrame* CanvasMode_Edit::textFrameAtPoint(PageItem_TextFrame* startFrame, const QPointF& canvasPoint, bool findNearest) const
+{
+	PageItem_TextFrame* nearestFrame = startFrame;
+	double nearestDistance = std::numeric_limits<double>::max();
+	PageItem* chainFrame = startFrame->firstInChain();
+	while (chainFrame)
+	{
+		PageItem_TextFrame* textFrame = chainFrame->asTextFrame();
+		bool invertible = false;
+		QTransform inverseTransform = textFrame->getTransform().inverted(&invertible);
+		if (invertible)
+		{
+			QPointF localPoint = inverseTransform.map(canvasPoint);
+			QRectF frameRect(0.0, 0.0, textFrame->width(), textFrame->height());
+			if (frameRect.contains(localPoint))
+				return textFrame;
+
+			QPointF nearestLocalPoint(
+				qBound(frameRect.left(), localPoint.x(), frameRect.right()),
+				qBound(frameRect.top(), localPoint.y(), frameRect.bottom()));
+			QPointF nearestCanvasPoint = textFrame->getTransform().map(nearestLocalPoint);
+			double distanceX = canvasPoint.x() - nearestCanvasPoint.x();
+			double distanceY = canvasPoint.y() - nearestCanvasPoint.y();
+			double distance = distanceX * distanceX + distanceY * distanceY;
+			if (distance < nearestDistance)
+			{
+				nearestFrame = textFrame;
+				nearestDistance = distance;
+			}
+		}
+		chainFrame = chainFrame->nextInChain();
+	}
+	return findNearest ? nearestFrame : nullptr;
+}
+
+void CanvasMode_Edit::updateTextSelectionFrames(PageItem_TextFrame* textFrame) const
+{
+	PageItem* chainFrame = textFrame->firstInChain();
+	while (chainFrame)
+	{
+		QRectF boundingRect(chainFrame->getBoundingRect());
+		m_canvas->update(QRectF(m_canvas->canvasToLocal(boundingRect.topLeft()), boundingRect.size() * m_canvas->scale()).toRect());
+		chainFrame = chainFrame->nextInChain();
+	}
+}
+
 
 void CanvasMode_Edit::blinkTextCursor()
 {
@@ -469,11 +552,17 @@ void CanvasMode_Edit::mouseMoveEvent(QMouseEvent *m)
 			}
 			if (currItem->isTextFrame())
 			{
-				int refStartSel(currItem->asTextFrame()->itemText.startOfSelection());
-				int refEndSel(currItem->asTextFrame()->itemText.endOfSelection());
+				PageItem_TextFrame* currentTextFrame = currItem->asTextFrame();
+				int refStartSel(currentTextFrame->itemText.startOfSelection());
+				int refEndSel(currentTextFrame->itemText.endOfSelection());
 				currItem->itemText.deselectAll();
 				currItem->HasSel = false;
-				m_view->slotSetCurs(globalPos.x(), globalPos.y());
+				PageItem_TextFrame* targetTextFrame = textFrameAtPoint(currentTextFrame, mousePointDoc.toQPointF(), true);
+				if (targetTextFrame->invalid)
+					targetTextFrame->layout();
+				int textPosition = targetTextFrame->textPositionFromPoint(mousePointDoc.toQPointF());
+				if (textPosition >= 0)
+					currItem->itemText.setCursorPosition(textPosition);
 				//Make sure we don't go here if the old cursor position was not set
 				if (oldCp!=-1 && currItem->itemText.isNotEmpty())
 				{
@@ -492,12 +581,9 @@ void CanvasMode_Edit::mouseMoveEvent(QMouseEvent *m)
 				if (currItem->HasSel)
 				{
 					m_canvas->m_viewMode.operTextSelecting = true;
-					if ((refStartSel != currItem->asTextFrame()->itemText.startOfSelection()) ||
-						(refEndSel   != currItem->asTextFrame()->itemText.endOfSelection()))
-					{
-						QRectF br(currItem->getBoundingRect());
-						m_canvas->update(QRectF(m_canvas->canvasToLocal(br.topLeft()), br.size() * m_canvas->scale()).toRect());
-					}
+					if ((refStartSel != currentTextFrame->itemText.startOfSelection()) ||
+						(refEndSel   != currentTextFrame->itemText.endOfSelection()))
+						updateTextSelectionFrames(currentTextFrame);
 					// We have to call this unconditionally because slotSetCurs() doesn't know selection
 					// when it is called
 					m_doc->scMW()->setTBvals(currItem);
@@ -597,11 +683,33 @@ void CanvasMode_Edit::mousePressEvent(QMouseEvent *m)
 	}
 
 	frameResizeHandle = 0;
-	int oldP = 0;
 
 	PageItem* currItem { nullptr };
 	if (GetItem(&currItem))
 	{
+		if (currItem->isTextFrame() && (m->button() == Qt::LeftButton) && (m->modifiers() & Qt::ShiftModifier))
+		{
+			PageItem_TextFrame* currentTextFrame = currItem->asTextFrame();
+			PageItem_TextFrame* targetTextFrame = textFrameAtPoint(currentTextFrame, mousePointDoc.toQPointF(), false);
+			if (targetTextFrame)
+			{
+				if (targetTextFrame->invalid)
+					targetTextFrame->layout();
+				int oldPosition = currentTextFrame->itemText.cursorPosition();
+				int textPosition = targetTextFrame->textPositionFromPoint(mousePointDoc.toQPointF());
+				if (textPosition >= 0)
+				{
+					currentTextFrame->itemText.setCursorPosition(textPosition);
+					extendTextSelection(currentTextFrame, oldPosition, m->modifiers());
+					currentTextFrame->HasSel = currentTextFrame->itemText.hasSelection();
+					m_ScMW->setCopyCutEnabled(currentTextFrame->HasSel);
+					updateTextSelectionFrames(currentTextFrame);
+					currentTextFrame->emitAllToGUI();
+					return;
+				}
+			}
+		}
+
 //		m_view->slotDoCurs(false);
 		if ((!currItem->locked() || currItem->isTextFrame()) && !currItem->isLine())
 		{
@@ -644,7 +752,7 @@ void CanvasMode_Edit::mousePressEvent(QMouseEvent *m)
 				return;
 			}
 		}
-		oldP = currItem->itemText.cursorPosition();
+		int oldPosition = currItem->itemText.cursorPosition();
 		//CB Where we set the cursor for a click in text frame
 		if (currItem->isTextFrame())
 		{
@@ -665,40 +773,7 @@ void CanvasMode_Edit::mousePressEvent(QMouseEvent *m)
 				//currItem->asTextFrame()->deselectAll();
 				//<<CB Add in shift select to text frames
 				if (m->modifiers() & Qt::ShiftModifier)
-				{
-					if (currItem->itemText.hasSelection())
-					{
-						if (currItem->itemText.cursorPosition() < (currItem->itemText.startOfSelection() + currItem->itemText.endOfSelection()) / 2)
-						{
-							if (m->modifiers() & Qt::ControlModifier)
-								currItem->itemText.setCursorPosition(currItem->itemText.startOfParagraph());
-							oldP = currItem->itemText.startOfSelection();
-						}
-						else
-						{
-							if (m->modifiers() & Qt::ControlModifier)
-								currItem->itemText.setCursorPosition(currItem->itemText.endOfParagraph());
-							oldP = currItem->itemText.endOfSelection();
-						}
-						currItem->asTextFrame()->itemText.extendSelection(oldP, currItem->itemText.cursorPosition());
-						oldCp = currItem->itemText.cursorPosition();
-					}
-					else
-					{
-						int dir=1;
-						if (oldCp > currItem->itemText.cursorPosition())
-							dir=-1;
-						if (m->modifiers() & Qt::ControlModifier) //no selection but Ctrl+Shift+click still select paragraphs
-						{
-							if (dir == 1)
-								currItem->itemText.setCursorPosition(currItem->itemText.endOfParagraph());
-							else
-								currItem->itemText.setCursorPosition(currItem->itemText.startOfParagraph());
-						}
-						currItem->asTextFrame()->ExpandSel(oldP);
-						oldCp = oldP;
-					}
-				}
+					extendTextSelection(currItem->asTextFrame(), oldPosition, m->modifiers());
 				else //>>CB
 				{
 					oldCp = currItem->itemText.cursorPosition();
diff --git a/scribus/canvasmode_edit.h b/scribus/canvasmode_edit.h
index c3aa9ae1a..5fae13da6 100644
--- a/scribus/canvasmode_edit.h
+++ b/scribus/canvasmode_edit.h
@@ -61,6 +61,9 @@ public:
 private:
 	inline bool GetItem(PageItem** pi);
 	void drawTextCursor(QPainter* p, PageItem_TextFrame* textframe);
+	void extendTextSelection(PageItem_TextFrame* textFrame, int oldPosition, Qt::KeyboardModifiers modifiers);
+	PageItem_TextFrame* textFrameAtPoint(PageItem_TextFrame* startFrame, const QPointF& canvasPoint, bool findNearest) const;
+	void updateTextSelectionFrames(PageItem_TextFrame* textFrame) const;
 	bool SeleItem(QMouseEvent *m);
 	void createContextMenu(PageItem *currItem, double mx, double my);
 
diff --git a/scribus/scribusview.cpp b/scribus/scribusview.cpp
index 1bbfa64b6..82c2a2282 100644
--- a/scribus/scribusview.cpp
+++ b/scribus/scribusview.cpp
@@ -1558,7 +1558,8 @@ bool ScribusView::slotSetCurs(int x, int y)
 				textFrame->itemText.setCursorPosition(result);
 		}
 		*/
-		textFrame->itemText.setCursorPosition(textPosition);
+		if (textPosition >= 0)
+			textFrame->itemText.setCursorPosition(textPosition);
 		if (textFrame->itemText.isNotEmpty())
 		{
 			int pos = qMax(0, qMin(textFrame->itemText.cursorPosition(), textFrame->itemText.length()));
diff --git a/scribus/text/boxes.cpp b/scribus/text/boxes.cpp
index 1ff8d4dc2..1b196aa17 100644
--- a/scribus/text/boxes.cpp
+++ b/scribus/text/boxes.cpp
@@ -6,6 +6,7 @@
  */
 
 #include <cmath>
+#include <limits>
 #include <unicode/brkiter.h>
 
 #include "pageitem.h"
@@ -42,6 +43,34 @@ int GroupBox::pointToPosition(const QPointF& coord, const StoryText &story) cons
 			return lastLine->lastChar() + 1;
 	}
 
+	if ((m_type == T_Block) && !m_boxes.isEmpty())
+	{
+		const Box* nearestBox = nullptr;
+		QPointF nearestPoint;
+		double nearestDistance = std::numeric_limits<double>::max();
+		for (const Box* box : boxes())
+		{
+			if ((box->type() == T_Block) && box->isEmpty())
+				continue;
+
+			const QRectF boxRect = box->bbox();
+			const QPointF candidatePoint(
+				qBound(boxRect.left(), rel.x(), boxRect.right()),
+				qBound(boxRect.top(), rel.y(), boxRect.bottom()));
+			const double distanceX = rel.x() - candidatePoint.x();
+			const double distanceY = rel.y() - candidatePoint.y();
+			const double distance = distanceX * distanceX + distanceY * distanceY;
+			if (distance < nearestDistance)
+			{
+				nearestBox = box;
+				nearestPoint = candidatePoint;
+				nearestDistance = distance;
+			}
+		}
+		if (nearestBox)
+			return nearestBox->pointToPosition(nearestPoint, story);
+	}
+
 	return -1;
 }
 

soerendanielkarch

2026-06-12 07:37

reporter   ~0053824

Previously, Shift-click was handled only after the normal frame hit test. When clicking another column or linked text frame, Scribus first activated the clicked object. This caused the original text cursor or selection anchor to be lost.

The following changes were made:
Shift-click is now detected before the normal object-selection logic.
Scribus searches the complete chain of linked text frames.
It determines the text frame under the pointer and the precise text position within it.
The original cursor position is preserved as the selection anchor.
The existing selection is extended to the clicked position using StoryText::extendSelection().
All linked text frames are redrawn so that the selection is visible throughout the chain.
Shift-click within one frame and across linked frames now share the same extendTextSelection() helper function.
Normal clicks and mouse-drag selection remain unaffected.

Issue History

Date Modified Username Field Change
2026-06-03 07:20 soerendanielkarch New Issue
2026-06-07 06:27 soerendanielkarch Note Added: 0053786
2026-06-07 06:27 soerendanielkarch File Added: 0017830-preserve-character-style-in-empty-paragraphs.diff
2026-06-07 06:27 soerendanielkarch File Deleted: preserve-character-style-in-empty-paragraphs.diff
2026-06-07 06:31 soerendanielkarch Note Added: 0053788
2026-06-07 06:31 soerendanielkarch File Added: 0017831-preserve-mouse-selection-across-columns.diff
2026-06-07 08:03 ale Note Added: 0053789
2026-06-07 08:04 ale Note Edited: 0053789
2026-06-07 11:43 soerendanielkarch Note Added: 0053790
2026-06-07 11:43 soerendanielkarch File Added: spalten.jpg
2026-06-11 17:52 ale Note Added: 0053811
2026-06-12 07:18 soerendanielkarch Note Added: 0053822
2026-06-12 07:33 soerendanielkarch Note Added: 0053823
2026-06-12 07:33 soerendanielkarch File Added: Bildschirmfoto 2026-06-12 um 09.31.03.png
2026-06-12 07:33 soerendanielkarch File Added: 0017831-preserve-mouse-selection-across-columns-2.diff
2026-06-12 07:37 soerendanielkarch Note Added: 0053824