View Issue Details

IDProjectCategoryView StatusLast Update
0017970ScribusTablespublic2026-09-06 09:34
Reporterbuttar Assigned To 
PrioritynormalSeverityfeatureReproducibilityN/A
Status newResolutionopen 
PlatformLinuxOSFedora WorkstationOS Version44
Product Version1.7.4.svn 
Summary0017970: [patch] Tables: On-Canvas Contextual Merge/Unmerge Controls
Description(Second in the series, it applies cleanly after the one in 0017969)

* Adds a direct Merge/Unmerge affordance to Table Edit mode.
* A multi-cell selection shows a Merge control at the top-right of the selected cell area.
* A merged cell shows an Unmerge control at the same contextual position.
* Also exposes Unmerge for the active merged cell when there is no explicit multi-cell selection.
* Reuses Scribus’s existing:
-- `itemSelection_MergeTableCells()`
-- `itemSelection_SplitTableCells()`
-- table merge/split logic
-- Undo behaviour
-- document/table refresh machinery
* Uses Scribus’s built-in `chain-closed` / `chain-open` icons rather than adding new assets.
* Keeps the control compact:
-- 14 px base button;
-- 10 px glyph;
-- 18 px hit target;
-- additionally capped against the actual anchor-cell size so it does not overwhelm small cells.
* No table model or file-format changes.
Additional Informationtux = table UX
Tags#please_test
Attached Files
tux2-table-merge-unmerge-v1.07.patch (8,852 bytes)   
Index: scribus/canvasmode_edittable.cpp
===================================================================
--- scribus/canvasmode_edittable.cpp	(revision 27816)
+++ scribus/canvasmode_edittable.cpp	(working copy)
@@ -14,6 +14,7 @@
 #include <QDebug>
 #include <QPainter>
 #include <QPointF>
+#include <QPixmap>
 #include <QScopedValueRollback>
 #include <QSet>
 #include <QTimer>
@@ -46,6 +47,10 @@
 constexpr double ResizeGripOffsetPx = 5.0;
 constexpr double InsertControlOffsetPx = 22.0;
 constexpr double DeleteControlOffsetPx = 18.0;
+constexpr double CellActionButtonPx = 14.0;
+constexpr double CellActionHitPx = 18.0;
+constexpr double CellActionIconPx = 10.0;
+constexpr double CellActionMarginPx = 2.0;
 constexpr double CanvasControlExtentPx = 36.0;
 constexpr double CanvasControlZoomMin = 0.6;
 constexpr double CanvasControlZoomMax = 1.4;
@@ -333,6 +338,8 @@
 		case TableCanvasControl::InsertColumn:
 		case TableCanvasControl::DeleteRows:
 		case TableCanvasControl::DeleteColumns:
+		case TableCanvasControl::MergeCells:
+		case TableCanvasControl::UnmergeCells:
 			cursor = Qt::PointingHandCursor;
 			break;
 		case TableCanvasControl::None:
@@ -439,6 +446,22 @@
 					setHoveredTableCanvasControl(TableCanvasControl());
 					updateCanvas(true);
 					return;
+				case TableCanvasControl::MergeCells:
+					m_tableCanvasControlPressed = true;
+					m_doc->itemSelection_MergeTableCells();
+					m_dragSelectionMode = CellSelect::Cells;
+					resetSelectionAnchor();
+					setHoveredTableCanvasControl(TableCanvasControl());
+					updateCanvas(true);
+					return;
+				case TableCanvasControl::UnmergeCells:
+					m_tableCanvasControlPressed = true;
+					m_doc->itemSelection_SplitTableCells();
+					m_dragSelectionMode = CellSelect::Cells;
+					resetSelectionAnchor();
+					setHoveredTableCanvasControl(TableCanvasControl());
+					updateCanvas(true);
+					return;
 				case TableCanvasControl::None:
 					break;
 			}
@@ -706,6 +729,78 @@
 	return CellSelect::Cells;
 }
 
+bool CanvasMode_EditTable::cellActionControl(TableCanvasControl& control, QPointF& center,
+										double& buttonSize, double& hitSize) const
+{
+	if (!m_table || !m_canvas)
+		return false;
+
+	QRectF area;
+	QRectF anchorCellRect;
+	const QSet<TableCell>& selectedCells = m_table->selectedCells();
+	if (selectedCells.size() > 1)
+	{
+		control.type = TableCanvasControl::MergeCells;
+		bool firstCell = true;
+		int anchorRow = -1;
+		int anchorEndColumn = -1;
+		for (const TableCell& cell : selectedCells)
+		{
+			if (!cell.isValid())
+				continue;
+
+			const QRectF cellRect = cell.boundingRect();
+			if (firstCell)
+			{
+				area = cellRect;
+				firstCell = false;
+			}
+			else
+				area = area.united(cellRect);
+
+			const int cellEndColumn = cell.column() + cell.columnSpan();
+			if (!anchorCellRect.isValid() || cell.row() < anchorRow ||
+				(cell.row() == anchorRow && cellEndColumn > anchorEndColumn))
+			{
+				anchorCellRect = cellRect;
+				anchorRow = cell.row();
+				anchorEndColumn = cellEndColumn;
+			}
+		}
+	}
+	else
+	{
+		TableCell cell;
+		if (selectedCells.size() == 1)
+			cell = *selectedCells.constBegin();
+		else
+			cell = m_table->activeCell();
+
+		if (!cell.isValid() || (cell.rowSpan() == 1 && cell.columnSpan() == 1))
+			return false;
+
+		control.type = TableCanvasControl::UnmergeCells;
+		area = cell.boundingRect();
+		anchorCellRect = area;
+	}
+
+	if (!area.isValid() || !anchorCellRect.isValid())
+		return false;
+
+	const double controlScale = canvasControlPixelScale(m_canvas->scale());
+	const double anchorMinSize = qMin(anchorCellRect.width(), anchorCellRect.height());
+	buttonSize = qMin(CellActionButtonPx * controlScale, anchorMinSize * 0.5);
+	if (buttonSize <= 0.0)
+		return false;
+
+	hitSize = qMax(buttonSize, qMin(CellActionHitPx * controlScale, anchorMinSize * 0.75));
+	const double inset = buttonSize / 2.0 + CellActionMarginPx * controlScale;
+	const QPointF offset = m_table->gridOffset();
+	center = QPointF(offset.x() + area.right() - inset,
+					 offset.y() + area.top() + inset);
+	return true;
+}
+
 CanvasMode_EditTable::TableCanvasControl CanvasMode_EditTable::hitTestTableCanvasControls(const QPointF& canvasPoint) const
 {
 	TableCanvasControl control;
@@ -720,6 +815,19 @@
 	const QPointF offset = m_table->gridOffset();
 	const double tableW = m_table->tableWidth();
 	const double tableH = m_table->tableHeight();
+
+	// Merge/unmerge is a contextual control inside the top-right corner of the
+	// selected area, or of the active merged cell when there is no selection.
+	TableCanvasControl cellControl;
+	QPointF cellControlCenter;
+	double cellActionButtonSize = 0.0;
+	double cellActionHitSize = 0.0;
+	if (cellActionControl(cellControl, cellControlCenter, cellActionButtonSize, cellActionHitSize) &&
+		QRectF(cellControlCenter.x() - cellActionHitSize / 2.0, cellControlCenter.y() - cellActionHitSize / 2.0,
+			   cellActionHitSize, cellActionHitSize).contains(framePoint))
+	{
+		return cellControl;
+	}
 
 	// Contextual delete controls are visible whenever the selected cells cover
 	// complete rows/columns, regardless of which selection gesture produced them.
@@ -884,6 +992,33 @@
 			p->drawEllipse(QPointF(center.x(), center.y() + i * 3.5 * controlScale), dotRadius, dotRadius);
 	};
 
+	auto drawCellActionButton = [&](const QPointF& center, TableCanvasControl::Type type, double buttonSize)
+	{
+		const QRectF buttonRect(center.x() - buttonSize / 2.0, center.y() - buttonSize / 2.0,
+								 buttonSize, buttonSize);
+		p->setPen(Qt::NoPen);
+		p->setBrush(controlColor);
+		const double cornerRadius = qMin(4.0 * controlScale, buttonSize / 3.0);
+		p->drawRoundedRect(buttonRect, cornerRadius, cornerRadius);
+
+		const QString iconName = (type == TableCanvasControl::MergeCells)
+			? QStringLiteral("chain-closed")
+			: QStringLiteral("chain-open");
+		QPixmap icon = IconManager::instance().loadPixmap(iconName, 16);
+		if (!icon.isNull())
+		{
+			QPainter iconPainter(&icon);
+			iconPainter.setCompositionMode(QPainter::CompositionMode_SourceIn);
+			iconPainter.fillRect(icon.rect(), glyphColor);
+			iconPainter.end();
+
+			const double iconSize = buttonSize * CellActionIconPx / CellActionButtonPx;
+			const QRectF iconRect(center.x() - iconSize / 2.0, center.y() - iconSize / 2.0,
+								  iconSize, iconSize);
+			p->drawPixmap(iconRect, icon, QRectF(icon.rect()));
+		}
+	};
+
 	// Hovering either part of a boundary control reveals both the drag grip
 	// and the one-click insertion control for that boundary.
 	if (m_hoverTableCanvasControl.type == TableCanvasControl::ColumnResize ||
@@ -910,6 +1045,13 @@
 			drawRoundButton(QPointF(offset.x() - InsertControlOffsetPx * controlScale, offset.y() + y), true);
 		}
 	}
+
+	TableCanvasControl cellControl;
+	QPointF cellControlCenter;
+	double cellActionButtonSize = 0.0;
+	double cellActionHitSize = 0.0;
+	if (cellActionControl(cellControl, cellControlCenter, cellActionButtonSize, cellActionHitSize))
+		drawCellActionButton(cellControlCenter, cellControl.type, cellActionButtonSize);
 
 	// A minus control is persistent while the selected cells cover complete
 	// rows/columns. It disappears when deleting the selection would remove the
Index: scribus/canvasmode_edittable.h
===================================================================
--- scribus/canvasmode_edittable.h	(revision 27816)
+++ scribus/canvasmode_edittable.h	(working copy)
@@ -78,7 +78,9 @@
 			InsertRow,
 			InsertColumn,
 			DeleteRows,
-			DeleteColumns
+			DeleteColumns,
+			MergeCells,
+			UnmergeCells
 		};
 
 		Type type {None};
@@ -90,9 +92,11 @@
 
 	/// Returns whether the current cell selection covers complete rows or columns.
 	CellSelect::SelectionMode structuralSelectionMode() const;
-	/// Returns a direct-manipulation control outside the table, if any.
+	/// Returns the contextual merge/unmerge control, center and cell-capped sizes.
+	bool cellActionControl(TableCanvasControl& control, QPointF& center, double& buttonSize, double& hitSize) const;
+	/// Returns a direct-manipulation control under the canvas point, if any.
 	TableCanvasControl hitTestTableCanvasControls(const QPointF& canvasPoint) const;
-	/// Paints hover controls and contextual row/column deletion controls.
+	/// Paints direct table controls and contextual cell/row/column actions.
 	void drawTableCanvasControls(QPainter* p);
 	/// Updates the currently hovered direct-manipulation control.
 	void setHoveredTableCanvasControl(const TableCanvasControl& control);
@@ -165,7 +169,7 @@
 
 	/// Direct table control currently under the pointer.
 	TableCanvasControl m_hoverTableCanvasControl;
-	/// Prevents a click on +/- from turning into a cell-selection drag.
+	/// Prevents a canvas-control click from turning into a cell-selection drag.
 	bool m_tableCanvasControlPressed {false};
 };
 
PatchYes

Activities

Issue History

Date Modified Username Field Change
2026-09-06 09:34 buttar New Issue
2026-09-06 09:34 buttar Tag Attached: #please_test
2026-09-06 09:34 buttar File Added: table--on-canvas-cell-merge-unmerge.jpg
2026-09-06 09:34 buttar File Added: tux2-table-merge-unmerge-v1.07.patch