View Issue Details

IDProjectCategoryView StatusLast Update
0017972ScribusTablespublic2026-09-07 11:53
Reporterbuttar Assigned To 
PrioritynormalSeverityfeatureReproducibilityN/A
Status newResolutionopen 
PlatformLinuxOSFedora WorkstationOS Version44
Product Version1.7.4.svn 
Summary0017972: [patch] Tables: Proportional Row/Column Resize
Description(Third in the series, it applies cleanly after the one in 0017970)

* Adds a Ctrl+drag proportional resize mode for table row and column dividers.
* Resizing one row/column redistributes the inverse size change proportionally across all following rows/columns while keeping the overall table dimension fixed.
* Preserves the relative proportions of the following rows/columns during redistribution.
* Keeps existing resize behaviour unchanged:
-- plain drag → existing resize behaviour;
-- Shift+drag → existing move-following behaviour;
-- Shift takes priority if used together with Ctrl.
* Ctrl on the final row/column boundary falls back to the existing resize behaviour because there is no following geometry to redistribute.
* Respects existing minimum-size constraints:
-- column minimum widths;
-- content-aware minimum row heights.
* Supports pressing/releasing Ctrl during an active resize gesture, with the preview updating immediately.
* Updates the resize HUD to indicate proportional mode while Ctrl is active.
* Commits the proportional resize as a single grouped Undo operation.
* Uses the existing table resize gesture infrastructure; no table-model or file-format changes.
* Applies on top of TUX1 and TUX2 without modifying their behaviour.
Additional Informationtux = tables UX
Tags#please_test
Attached Files
tux3-table-proportional-resize-v1.01.patch (15,992 bytes)   
Index: scribus/canvasgesture_table.cpp
===================================================================
--- scribus/canvasgesture_table.cpp	(revision 27816)
+++ scribus/canvasgesture_table.cpp	(working copy)
@@ -23,6 +23,68 @@
 
 #include "canvasgesture_table.h"
 
+QList<double> TableGesture::proportionalDistribution(const QList<double>& sizes,
+	const QList<double>& minimumSizes, double targetTotal)
+{
+	Q_ASSERT(sizes.size() == minimumSizes.size());
+
+	QList<double> result = sizes;
+	if (sizes.isEmpty())
+		return result;
+
+	QList<int> active;
+	double minimumTotal = 0.0;
+	for (int i = 0; i < sizes.size(); ++i)
+	{
+		active.append(i);
+		minimumTotal += minimumSizes.at(i);
+	}
+
+	double remaining = qMax(targetTotal, minimumTotal);
+	while (!active.isEmpty())
+	{
+		double sizeTotal = 0.0;
+		for (int index : active)
+			sizeTotal += sizes.at(index);
+
+		if (sizeTotal <= 0.0)
+			break;
+
+		const double factor = remaining / sizeTotal;
+		bool clamped = false;
+		for (int i = active.size() - 1; i >= 0; --i)
+		{
+			const int index = active.at(i);
+			if (sizes.at(index) * factor < minimumSizes.at(index))
+			{
+				result[index] = minimumSizes.at(index);
+				remaining -= result.at(index);
+				active.removeAt(i);
+				clamped = true;
+			}
+		}
+
+		if (clamped)
+			continue;
+
+		double assigned = 0.0;
+		for (int i = 0; i < active.size(); ++i)
+		{
+			const int index = active.at(i);
+			if (i == active.size() - 1)
+				result[index] = qMax(minimumSizes.at(index), remaining - assigned);
+			else
+			{
+				result[index] = sizes.at(index) * factor;
+				assigned += result.at(index);
+			}
+		}
+		break;
+	}
+
+	return result;
+}
+
 QPointF TableGesture::globalToTableGrid(const QPointF& point, bool snap)
 {
 	FPoint canvasPoint = m_canvas->globalToCanvas(point);
Index: scribus/canvasgesture_table.h
===================================================================
--- scribus/canvasgesture_table.h	(revision 27816)
+++ scribus/canvasgesture_table.h	(working copy)
@@ -67,6 +67,14 @@
 	 */
 	void paintCellSelection(QPainter* p);
 
+	/**
+	 * Returns @a sizes scaled proportionally to sum to @a targetTotal while
+	 * respecting the corresponding @a minimumSizes. Values that hit their
+	 * minimum are held there and the remaining space is redistributed.
+	 */
+	static QList<double> proportionalDistribution(const QList<double>& sizes,
+		const QList<double>& minimumSizes, double targetTotal);
+
 private:
 	/// The currently edited table.
 	PageItem_Table* m_table;
Index: scribus/canvasgesture_columnresize.cpp
===================================================================
--- scribus/canvasgesture_columnresize.cpp	(revision 27816)
+++ scribus/canvasgesture_columnresize.cpp	(working copy)
@@ -29,9 +29,9 @@
 		event->accept();
 		m_view->stopGesture();
 	}
-	if (event->key() == Qt::Key_Shift)
+	if (event->key() == Qt::Key_Shift || event->key() == Qt::Key_Control)
 	{
-		// Re-copy column geometires since resizing behavior will change.
+		// Re-copy column geometries since resizing behavior will change.
 		m_columnWidths = table()->columnWidths();
 		m_columnPositions = table()->columnPositions();
 	}
@@ -39,9 +39,9 @@
 
 void ColumnResize::keyReleaseEvent(QKeyEvent* event)
 {
-	if (event->key() == Qt::Key_Shift)
+	if (event->key() == Qt::Key_Shift || event->key() == Qt::Key_Control)
 	{
-		// Re-copy column geometires since resizing behavior will change.
+		// Re-copy column geometries since resizing behavior will change.
 		m_columnWidths = table()->columnWidths();
 		m_columnPositions = table()->columnPositions();
 	}
@@ -52,24 +52,33 @@
 	event->accept();
 
 	QPointF gridPoint = globalToTableGrid(event->globalPosition());
-
-	// Perform the actual resize of the row.
-	PageItem_Table::ResizeStrategy strategy;
-	if (event->modifiers() & Qt::ShiftModifier)
-		strategy = PageItem_Table::MoveFollowing;
-	else
-		strategy = PageItem_Table::ResizeFollowing;
+	const double requestedWidth = gridPoint.x() - table()->columnPosition(m_column);
+	const bool moveFollowing = event->modifiers() & Qt::ShiftModifier;
+	const bool proportional = !moveFollowing
+		&& (event->modifiers() & Qt::ControlModifier)
+		&& m_column < table()->columns() - 1;
 
 	UndoTransaction activeTransaction;
 	if (UndoManager::undoEnabled())
 		activeTransaction = UndoManager::instance()->beginTransaction(table()->getUName(), table()->getUPixmap(), Um::TableColumnWidth, QString(), Um::ITable);
 
 	table()->doc()->dontResize = true;
-	table()->resizeColumn(m_column, gridPoint.x() - table()->columnPosition(m_column), strategy);
-	if (strategy == PageItem_Table::MoveFollowing)
+	if (proportional)
+	{
+		resizeColumnProportionally(requestedWidth);
+		for (int column = m_column; column < table()->columns(); ++column)
+			table()->resizeColumn(column, m_columnWidths.at(column), PageItem_Table::MoveFollowing);
+	}
+	else
 	{
-		table()->adjustTableToFrame();
-		table()->adjustFrameToTable();
+		const PageItem_Table::ResizeStrategy strategy = moveFollowing
+			? PageItem_Table::MoveFollowing : PageItem_Table::ResizeFollowing;
+		table()->resizeColumn(m_column, requestedWidth, strategy);
+		if (strategy == PageItem_Table::MoveFollowing)
+		{
+			table()->adjustTableToFrame();
+			table()->adjustFrameToTable();
+		}
 	}
 	table()->doc()->dontResize = false;
 	table()->adjustFrameToTable();
@@ -90,14 +99,21 @@
 
 	double requestedWidth = gridPoint.x() - m_columnPositions.at(m_column);
 	double actualWidth = 0.0;
+	const bool moveFollowing = event->modifiers() & Qt::ShiftModifier;
+	const bool proportional = !moveFollowing
+		&& (event->modifiers() & Qt::ControlModifier)
+		&& m_column < table()->columns() - 1;
 
-	if (event->modifiers() & Qt::ShiftModifier)
+	if (moveFollowing)
 		actualWidth = resizeColumnMoveFollowing(requestedWidth);
+	else if (proportional)
+		actualWidth = resizeColumnProportionally(requestedWidth);
 	else
 		actualWidth = resizeColumnResizeFollowing(requestedWidth);
 
 	// Display width tooltip.
-	m_canvas->displayDoubleHUD(event->globalPosition(), tr("Width"), actualWidth);
+	const QString label = proportional ? tr("Width (Proportional)") : tr("Width");
+	m_canvas->displayDoubleHUD(event->globalPosition(), label, actualWidth);
 
 	// Update canvas.
 	m_canvas->update();
@@ -168,3 +184,47 @@
 
 	return newWidth;
 }
+
+double ColumnResize::resizeColumnProportionally(double width)
+{
+	// Always calculate from the table's committed geometry so repeated mouse
+	// moves preserve the proportions that existed when the drag began.
+	m_columnWidths = table()->columnWidths();
+	m_columnPositions = table()->columnPositions();
+
+	if (m_column >= table()->columns() - 1)
+		return resizeColumnResizeFollowing(width);
+
+	double rangeWidth = 0.0;
+	for (int column = m_column; column < table()->columns(); ++column)
+		rangeWidth += m_columnWidths.at(column);
+
+	const int followingCount = table()->columns() - m_column - 1;
+	const double followingMinimum = followingCount * PageItem_Table::MinimumColumnWidth;
+	const double maximumWidth = qMax(PageItem_Table::MinimumColumnWidth, rangeWidth - followingMinimum);
+	const double newWidth = qBound(PageItem_Table::MinimumColumnWidth, width, maximumWidth);
+
+	QList<double> followingWidths;
+	QList<double> minimumWidths;
+	for (int column = m_column + 1; column < table()->columns(); ++column)
+	{
+		followingWidths.append(m_columnWidths.at(column));
+		minimumWidths.append(PageItem_Table::MinimumColumnWidth);
+	}
+
+	const QList<double> distributed = proportionalDistribution(
+		followingWidths, minimumWidths, rangeWidth - newWidth);
+	m_columnWidths[m_column] = newWidth;
+	for (int i = 0; i < distributed.size(); ++i)
+		m_columnWidths[m_column + 1 + i] = distributed.at(i);
+
+	double columnPosition = m_columnPositions.at(m_column);
+	for (int column = m_column; column < m_columnPositions.size(); ++column)
+	{
+		m_columnPositions[column] = columnPosition;
+		columnPosition += m_columnWidths.at(column);
+	}
+
+	return newWidth;
+}
+
Index: scribus/canvasgesture_columnresize.h
===================================================================
--- scribus/canvasgesture_columnresize.h	(revision 27816)
+++ scribus/canvasgesture_columnresize.h	(working copy)
@@ -58,6 +58,12 @@
 	 */
 	double resizeColumnResizeFollowing(double width);
 
+	/**
+	 * Resizes the column to @a width and redistributes the remaining width
+	 * proportionally over all following columns.
+	 */
+	double resizeColumnProportionally(double width);
+
 private:
 	/// Column being resized.
 	int m_column;
Index: scribus/canvasgesture_rowresize.cpp
===================================================================
--- scribus/canvasgesture_rowresize.cpp	(revision 27816)
+++ scribus/canvasgesture_rowresize.cpp	(working copy)
@@ -29,9 +29,9 @@
 		event->accept();
 		m_view->stopGesture();
 	}
-	if (event->key() == Qt::Key_Shift)
+	if (event->key() == Qt::Key_Shift || event->key() == Qt::Key_Control)
 	{
-		// Re-copy row geometires since resizing behavior will change.
+		// Re-copy row geometries since resizing behavior will change.
 		m_rowHeights = table()->rowHeights();
 		m_rowPositions = table()->rowPositions();
 	}
@@ -39,9 +39,9 @@
 
 void RowResize::keyReleaseEvent(QKeyEvent* event)
 {
-	if (event->key() == Qt::Key_Shift)
+	if (event->key() == Qt::Key_Shift || event->key() == Qt::Key_Control)
 	{
-		// Re-copy row geometires since resizing behavior will change.
+		// Re-copy row geometries since resizing behavior will change.
 		m_rowHeights = table()->rowHeights();
 		m_rowPositions = table()->rowPositions();
 	}
@@ -52,27 +52,35 @@
 	event->accept();
 
 	QPointF gridPoint = globalToTableGrid(event->globalPosition());
-
-	// Perform the actual resize of the row.
-	PageItem_Table::ResizeStrategy strategy;
-	if (event->modifiers() & Qt::ShiftModifier)
-		strategy = PageItem_Table::MoveFollowing;
-	else
-		strategy = PageItem_Table::ResizeFollowing;
+	const double requestedHeight = gridPoint.y() - table()->rowPosition(m_row);
+	const bool moveFollowing = event->modifiers() & Qt::ShiftModifier;
+	const bool proportional = !moveFollowing
+		&& (event->modifiers() & Qt::ControlModifier)
+		&& m_row < table()->rows() - 1;
 
 	UndoTransaction activeTransaction;
 	if (UndoManager::undoEnabled())
 		activeTransaction = UndoManager::instance()->beginTransaction(table()->getUName(), table()->getUPixmap(), Um::TableRowHeight, QString(), Um::ITable);
 
 	table()->doc()->dontResize = true;
-	double requestedHeight = gridPoint.y() - table()->rowPosition(m_row);
-	requestedHeight = qMax(requestedHeight, m_minRowHeight);
-	table()->resizeRow(m_row, requestedHeight, strategy);
-
-	if (strategy == PageItem_Table::MoveFollowing)
+	if (proportional)
+	{
+		resizeRowProportionally(requestedHeight);
+		for (int row = m_row; row < table()->rows(); ++row)
+			table()->resizeRow(row, m_rowHeights.at(row), PageItem_Table::MoveFollowing);
+	}
+	else
 	{
-		table()->adjustTableToFrame();
-		table()->adjustFrameToTable();
+		const PageItem_Table::ResizeStrategy strategy = moveFollowing
+			? PageItem_Table::MoveFollowing : PageItem_Table::ResizeFollowing;
+		const double boundedHeight = qMax(requestedHeight, m_minRowHeight);
+		table()->resizeRow(m_row, boundedHeight, strategy);
+
+		if (strategy == PageItem_Table::MoveFollowing)
+		{
+			table()->adjustTableToFrame();
+			table()->adjustFrameToTable();
+		}
 	}
 	table()->doc()->dontResize = false;
 	table()->adjustFrameToTable();
@@ -93,14 +101,21 @@
 
 	double requestedHeight = gridPoint.y() - m_rowPositions.at(m_row);
 	double actualHeight = 0.0;
+	const bool moveFollowing = event->modifiers() & Qt::ShiftModifier;
+	const bool proportional = !moveFollowing
+		&& (event->modifiers() & Qt::ControlModifier)
+		&& m_row < table()->rows() - 1;
 
-	if (event->modifiers() & Qt::ShiftModifier)
+	if (moveFollowing)
 		actualHeight = resizeRowMoveFollowing(requestedHeight);
+	else if (proportional)
+		actualHeight = resizeRowProportionally(requestedHeight);
 	else
 		actualHeight = resizeRowResizeFollowing(requestedHeight);
 
 	// Display height tooltip.
-	m_canvas->displayDoubleHUD(event->globalPosition(), tr("Height"), actualHeight);
+	const QString label = proportional ? tr("Height (Proportional)") : tr("Height");
+	m_canvas->displayDoubleHUD(event->globalPosition(), label, actualHeight);
 
 	// Update canvas.
 	m_canvas->update();
@@ -140,6 +155,28 @@
 	else
 		m_nextMinRowHeight = PageItem_Table::MinimumRowHeight;
 
+	// Cache feasible content-aware floors for proportional redistribution.
+	// If a row is already smaller than its natural content height, keep its
+	// current height as the floor so entering proportional mode does not make
+	// the table jump before the pointer moves.
+	m_proportionalMinRowHeights.clear();
+	m_proportionalMinRowHeights.reserve(table->rows());
+	for (int index = 0; index < table->rows(); ++index)
+	{
+		if (index < row)
+		{
+			m_proportionalMinRowHeights.append(PageItem_Table::MinimumRowHeight);
+			continue;
+		}
+
+		bool rowHasContent = false;
+		const double rowNatural = table->naturalRowHeight(index, &rowHasContent);
+		const double contentFloor = rowHasContent
+			? qMax(PageItem_Table::MinimumRowHeight, rowNatural)
+			: PageItem_Table::MinimumRowHeight;
+		m_proportionalMinRowHeights.append(qMin(table->rowHeight(index), contentFloor));
+	}
+
 	// Make copies of the row geometries to be used during resize.
 	m_rowHeights = table->rowHeights();
 	m_rowPositions = table->rowPositions();
@@ -189,3 +226,50 @@
 
 	return newHeight;
 }
+
+double RowResize::resizeRowProportionally(double height)
+{
+	// Always calculate from the table's committed geometry so repeated mouse
+	// moves preserve the proportions that existed when the drag began.
+	m_rowHeights = table()->rowHeights();
+	m_rowPositions = table()->rowPositions();
+
+	if (m_row >= table()->rows() - 1)
+		return resizeRowResizeFollowing(height);
+
+	double rangeHeight = 0.0;
+	for (int row = m_row; row < table()->rows(); ++row)
+		rangeHeight += m_rowHeights.at(row);
+
+	double followingMinimum = 0.0;
+	for (int row = m_row + 1; row < table()->rows(); ++row)
+		followingMinimum += m_proportionalMinRowHeights.at(row);
+
+	const double minimumHeight = m_proportionalMinRowHeights.at(m_row);
+	const double maximumHeight = qMax(minimumHeight, rangeHeight - followingMinimum);
+	const double newHeight = qBound(minimumHeight, height, maximumHeight);
+
+	QList<double> followingHeights;
+	QList<double> minimumHeights;
+	for (int row = m_row + 1; row < table()->rows(); ++row)
+	{
+		followingHeights.append(m_rowHeights.at(row));
+		minimumHeights.append(m_proportionalMinRowHeights.at(row));
+	}
+
+	const QList<double> distributed = proportionalDistribution(
+		followingHeights, minimumHeights, rangeHeight - newHeight);
+	m_rowHeights[m_row] = newHeight;
+	for (int i = 0; i < distributed.size(); ++i)
+		m_rowHeights[m_row + 1 + i] = distributed.at(i);
+
+	double rowPosition = m_rowPositions.at(m_row);
+	for (int row = m_row; row < m_rowPositions.size(); ++row)
+	{
+		m_rowPositions[row] = rowPosition;
+		rowPosition += m_rowHeights.at(row);
+	}
+
+	return newHeight;
+}
+
Index: scribus/canvasgesture_rowresize.h
===================================================================
--- scribus/canvasgesture_rowresize.h	(revision 27816)
+++ scribus/canvasgesture_rowresize.h	(working copy)
@@ -58,6 +58,12 @@
 	 */
 	double resizeRowResizeFollowing(double height);
 
+	/**
+	 * Resizes the row to @a height and redistributes the remaining height
+	 * proportionally over all following rows.
+	 */
+	double resizeRowProportionally(double height);
+
 private:
 	/// Row being resized.
 	int m_row;
@@ -69,6 +75,8 @@
 	double m_minRowHeight { 0.0 };
 	/// Smallest height the following row may be squeezed to (cached at setup).
 	double m_nextMinRowHeight { 0.0 };
+	/// Content-aware floors used when distributing height proportionally.
+	QList<double> m_proportionalMinRowHeights;
 };
 
 #endif // CANVASGESTURE_ROWRESIZE_H
PatchYes

Activities

Issue History

Date Modified Username Field Change
2026-09-07 11:53 buttar New Issue
2026-09-07 11:53 buttar Tag Attached: #please_test
2026-09-07 11:53 buttar File Added: tux3-table-proportional-resize-v1.01.patch