View Issue Details

IDProjectCategoryView StatusLast Update
0000162ScribusText Frames / Story Editorpublic2026-09-14 08:06
Reporterestebandido Assigned To 
PrioritylowSeverityfeatureReproducibilityhave not tried
Status acknowledgedResolutionopen 
Product Version1.6 milestone 
Summary0000162: unequal columns in a textframe
DescriptionI'd love to see a way create columns of unequal width in a single textframe.
Additional InformationI realize there's no elegant way to regulate widths (and gaps) individually. After all, OOo couldn't come up with a tidy way to do it. All I can think of is making the Properties palette extremely context-sensitive (knowing which column it's in, in a textframe), and binding the gap to the column on its left. In other words, one column and its gap appear at a time on the palette for changes (except the last column, which has no gap). Changes would then ripple to the right (which would mean the last column width would not be editable, because there is no place to pass the change to).

If this is implemented - and I consider it a low priority - making the gaps of equal width should also be optional.

A further problem would be how to recalculate the column widths if the textframe is resized.
Tags#please_test
Patch

Relationships

related to 0002098 closedcbradney Metabug: New File Format 
related to 0000212 acknowledged Ability to create initial vertical space at the beginning of a column 

Activities

cbradney

2004-01-14 11:38

administrator   ~0000216

Marking this as low priority and 1.3+ because of above issues. May change if Franz gets a brilliant idea.

louisdesjardins

2005-02-19 16:35

developer   ~0003746

The unequal columns could be set as percentage of the text frame width minus the (total) column gap(s).

The columns would then keep their relative widths as text frame gets changed.

Add'nl note : Could set the gap *or* the column width to change when frame is modified.

If this feature is implemented, we'd have to make sure the option is there throughout ["New" dialog, Settings>Prefs>Tools, Properties]. Also consider the option of mirroring the effect in facing pages.

ringerc

2005-06-16 03:37

reporter   ~0005051

What about supporting two different ways to set up columns? We could either put the number of columns in the columns field (like normal) or let the user do the layout manually by grabbing column positioning grips on the text frame on the canvas. Maybe, similar to "edit shape", we could have these only available in a "change columns" mode to avoid accidental movement of columns.

The column position data could be initially displayed in the properties palette as a series of column sizes and gaps, eg:

5cm;1cm;4cm;0.5cm;5cm

and some sort of "layout columns" widget could then be grafted on later. Sound reasonable?

louisdesjardins

2005-06-16 12:31

developer   ~0005053

I agree this approach would allow more precision than the "percentage" approach I suggested. But what happens when a text frame is resized in width? We'd have to find a way to establish which value has precedence? Which value will remain the same, which will not.

Anyway, the idea is appealing. I can imagine being able to simply grab the column gap and displace it...

Instead of putting these values in the Properties palette which is quite full imo, could we consider:
1. Adding this to the "New document" dialog (maybe as an option tab or a check box option: "Unequal columns")
2. Using a tab-kind slider (would save space I think) to set the column values (either fixed values or percentages - still to be discussed!)
3. Add this to the guide management dialog???

More thoughts?

ringerc

2005-06-16 13:08

reporter   ~0005054

Something like a tab slider is probably a sensible way to do the UI in the properties dialog (or it could be added to the rulers), yes.

As for scaling, probably a toggle defaulting to proportional scaling, maybe with an option of just expanding the last column instead ("manual").

buttar

2026-09-14 05:39

reporter   ~0054465

I once asked the same. Here is my attempt on this as to what I could imagine as of now.

## cc1-custom-text-column-geometry-v1.02.patch

- Adds a normalized text-column layout model supporting equal and custom-width columns.
- Calculates per-column positions and widths while preserving existing equal-column behaviour.
- Uses custom geometry throughout text layout, frame drawing and available-width calculations.
- Supports physical LTR/RTL column traversal.
- Adds undo/redo support for complete column-layout changes.
- Preserves custom layouts when PageItems are copied.
- Validates ratios, gaps and column counts; malformed layouts safely fall back to Equal.
- Keeps existing scripting setters compatible by converting Custom layouts back to Equal when used.

## cc2-custom-text-column-persistence-v1.01.patch

- Depends on `cc1.patch`.
- Saves custom width ratios and individual gaps in the Scribus 1.7.1 document format.
- Restores custom layouts for ordinary text frames and table-cell text frames.
- Uses locale-independent, round-trip-safe floating-point serialization.
- Rejects malformed serialized lists through `cc1.patch` layout validation.
- Extends Copy Properties to copy the complete text-column layout.
- Produces no additional attributes for Equal layouts, preserving existing files unchanged.

## cc3-custom-text-column-editor-v1.04.patch

- Depends on `cc1.patch` and `cc2.patch`.
- Adds Equal and Custom layout controls to Text Properties → Columns & Indentation.
- Provides an interactive, horizontally scrollable column preview with mouse and keyboard selection.
- Allows editing individual column widths and following gaps.
- Supports Adjacent and All others width-adjustment strategies.
- Adds an Equal gaps option for synchronizing all custom gaps.
- Keeps Equal mode compact with only Columns and Gap controls.
- Uses theme-aware column, selection and gap highlighting.
- Adds accessible descriptions, tooltips and keyboard navigation.
- Updates the horizontal ruler to use actual unequal-column geometry in LTR and RTL text.
- Adds a re-colorable column-gap icon for light and dark themes.
- Blocks Width and Gap signals during unit conversion to prevent unintended document or undo changes.
- Introduces no further document-format changes.
cc1-custom-text-column-geometry-v1.02.patch (15,738 bytes)   
Index: scribus/pageitem.h
===================================================================
--- scribus/pageitem.h	(revision 27829)
+++ scribus/pageitem.h	(working copy)
@@ -95,6 +95,32 @@
 #define _unlink unlink
 #endif
 
+struct SCRIBUS_API TextColumnLayout
+{
+	int columnCount { 1 };
+	double columnGap { 0.0 };
+	bool custom { false };
+	QVector<double> widthRatios;
+	QVector<double> gaps;
+
+	bool operator==(const TextColumnLayout& other) const
+	{
+		return columnCount == other.columnCount
+			&& columnGap == other.columnGap
+			&& custom == other.custom
+			&& widthRatios == other.widthRatios
+			&& gaps == other.gaps;
+	}
+
+	bool operator!=(const TextColumnLayout& other) const { return !(*this == other); }
+};
+
+struct SCRIBUS_API TextColumnGeometry
+{
+	double left { 0.0 };
+	double width { 0.0 };
+};
+
 class SCRIBUS_API PageItem : public QObject, public UndoObject, public SaxIO, public SingleObservable<PageItem>, public TextContext
 {
 	Q_OBJECT
@@ -793,6 +819,8 @@
 	double textToFrameDistBottom() const { return m_textDistanceMargins.bottom(); }
 	int columns() const { return m_columns; }
 	double columnGap() const { return m_columnGap; }
+	TextColumnLayout textColumnLayout() const;
+	bool hasCustomTextColumns() const { return m_customColumnLayout; }
 	double gridOffset() const;
 	double gridDistance() const;
 	int verticalAlignment() const;
@@ -802,6 +830,7 @@
 	void setTextToFrameDistBottom(double);
 	void setColumns(int);
 	void setColumnGap(double);
+	void setTextColumnLayout(const TextColumnLayout& layout);
 	void setGridOffset(double);
 	void setGridDistance(double);
 	void setVerticalAlignment(int);
@@ -1349,6 +1378,9 @@
 	bool snapToPatchGrid {false};
 	int m_columns;
 	double m_columnGap;
+	bool m_customColumnLayout { false };
+	QVector<double> m_columnWidthRatios;
+	QVector<double> m_columnGaps;
 	Qt::PenStyle PLineArt; ///< Linestyle
 	Qt::PenCapStyle PLineEnd;
 	Qt::PenJoinStyle PLineJoin;
@@ -1566,6 +1598,7 @@
 	void restoreClearImage(UndoState *state, bool isUndo);
 	void restoreColumns(SimpleState *state, bool isUndo);
 	void restoreColumnsGap(SimpleState *state, bool isUndo);
+	void restoreTextColumnLayout(SimpleState *state, bool isUndo);
 	void restoreConnectPath(SimpleState *state, bool isUndo);
 	void restoreContourLine(SimpleState *state, bool isUndo);
 	void restoreCornerRadius(SimpleState *state, bool isUndo);
Index: scribus/pageitem.cpp
===================================================================
--- scribus/pageitem.cpp	(revision 27829)
+++ scribus/pageitem.cpp	(working copy)
@@ -137,6 +137,9 @@
 	snapToPatchGrid(other.snapToPatchGrid),
 	m_columns(other.m_columns),
 	m_columnGap(other.m_columnGap),
+	m_customColumnLayout(other.m_customColumnLayout),
+	m_columnWidthRatios(other.m_columnWidthRatios),
+	m_columnGaps(other.m_columnGaps),
 	PLineArt(other.PLineArt),
 	PLineEnd(other.PLineEnd),
 	PLineJoin(other.PLineJoin),
@@ -1514,6 +1517,16 @@
 
 void PageItem::setColumns(int newColumnCount)
 {
+	if (m_customColumnLayout)
+	{
+		TextColumnLayout layout = textColumnLayout();
+		layout.columnCount = newColumnCount;
+		layout.custom = false;
+		layout.widthRatios.clear();
+		layout.gaps.clear();
+		setTextColumnLayout(layout);
+		return;
+	}
 	if (m_columns == newColumnCount)
 		return;
 	if (UndoManager::undoEnabled())
@@ -1529,6 +1542,16 @@
 
 void PageItem::setColumnGap(double newColumnGap)
 {
+	if (m_customColumnLayout)
+	{
+		TextColumnLayout layout = textColumnLayout();
+		layout.columnGap = newColumnGap;
+		layout.custom = false;
+		layout.widthRatios.clear();
+		layout.gaps.clear();
+		setTextColumnLayout(layout);
+		return;
+	}
 	if (m_columnGap == newColumnGap)
 		return;
 	if (UndoManager::undoEnabled())
@@ -1542,6 +1565,80 @@
 	m_columnGap = newColumnGap;
 }
 
+TextColumnLayout PageItem::textColumnLayout() const
+{
+	TextColumnLayout layout;
+	layout.columnCount = m_columns;
+	layout.columnGap = m_columnGap;
+	layout.custom = m_customColumnLayout;
+	layout.widthRatios = m_columnWidthRatios;
+	layout.gaps = m_columnGaps;
+	return layout;
+}
+
+void PageItem::setTextColumnLayout(const TextColumnLayout& newLayout)
+{
+	TextColumnLayout layout = newLayout;
+	layout.columnCount = qMax(1, layout.columnCount);
+	layout.columnGap = qIsFinite(layout.columnGap) ? qMax(0.0, layout.columnGap) : 0.0;
+
+	bool validCustomLayout = layout.custom
+		&& layout.columnCount >= 2
+		&& layout.widthRatios.size() == layout.columnCount
+		&& layout.gaps.size() == layout.columnCount - 1;
+	double ratioSum = 0.0;
+	if (validCustomLayout)
+	{
+		for (double ratio : std::as_const(layout.widthRatios))
+		{
+			if (!qIsFinite(ratio) || ratio <= 0.0)
+			{
+				validCustomLayout = false;
+				break;
+			}
+			ratioSum += ratio;
+		}
+		for (double& gap : layout.gaps)
+		{
+			if (!qIsFinite(gap))
+			{
+				validCustomLayout = false;
+				break;
+			}
+			gap = qMax(0.0, gap);
+		}
+		validCustomLayout &= qIsFinite(ratioSum) && ratioSum > 0.0;
+	}
+
+	if (validCustomLayout)
+	{
+		for (double& ratio : layout.widthRatios)
+			ratio /= ratioSum;
+	}
+	else
+	{
+		layout.custom = false;
+		layout.widthRatios.clear();
+		layout.gaps.clear();
+	}
+
+	const TextColumnLayout oldLayout = textColumnLayout();
+	if (oldLayout == layout)
+		return;
+	if (UndoManager::undoEnabled())
+	{
+		auto *state = new ScOldNewState<TextColumnLayout>(Um::Columns, QString(), Um::IBorder);
+		state->set("TEXT_COLUMN_LAYOUT");
+		state->setStates(oldLayout, layout);
+		undoManager->action(this, state);
+	}
+	m_columns = layout.columnCount;
+	m_columnGap = layout.columnGap;
+	m_customColumnLayout = layout.custom;
+	m_columnWidthRatios = layout.widthRatios;
+	m_columnGaps = layout.gaps;
+}
+
 int PageItem::verticalAlignment() const
 {
 	return verticalAlign;
@@ -4911,6 +5008,8 @@
 			restoreLineColor(ss, isUndo);
 		else if (ss->contains("VERTICAL_ALIGN"))
 			restoreVerticalAlign(ss, isUndo);
+		else if (ss->contains("TEXT_COLUMN_LAYOUT"))
+			restoreTextColumnLayout(ss, isUndo);
 		else if (ss->contains("COLUMNS"))
 			restoreColumns(ss, isUndo);
 		else if (ss->contains("COLUMNSGAP"))
@@ -6926,6 +7025,23 @@
 	update();
 }
 
+void PageItem::restoreTextColumnLayout(SimpleState *ss, bool isUndo)
+{
+	const auto *state = dynamic_cast<ScOldNewState<TextColumnLayout>*>(ss);
+	if (!state)
+	{
+		qFatal("PageItem::restoreTextColumnLayout: dynamic cast failed");
+		return;
+	}
+	const TextColumnLayout& layout = isUndo ? state->getOldState() : state->getNewState();
+	m_columns = layout.columnCount;
+	m_columnGap = layout.columnGap;
+	m_customColumnLayout = layout.custom;
+	m_columnWidthRatios = layout.widthRatios;
+	m_columnGaps = layout.gaps;
+	update();
+}
+
 void PageItem::restoreLeftTextFrameDist(SimpleState *ss, bool isUndo)
 {
 	if (isUndo)
Index: scribus/pageitem_textframe.h
===================================================================
--- scribus/pageitem_textframe.h	(revision 27829)
+++ scribus/pageitem_textframe.h	(working copy)
@@ -114,7 +114,8 @@
 	bool isValidChainFromBegin();
 	void setTextAnnotationOpen(bool open);
 
-	double columnWidth();
+	double columnWidth(int physicalColumn = 0) const;
+	QVector<TextColumnGeometry> columnGeometry() const;
 
 	//enable/disable marks inserting actions depending on editMode
 	void toggleEditModeActions();
Index: scribus/pageitem_textframe.cpp
===================================================================
--- scribus/pageitem_textframe.cpp	(revision 27829)
+++ scribus/pageitem_textframe.cpp	(working copy)
@@ -23,6 +23,8 @@
 
 #include "pageitem_textframe.h"
 
+#include <utility>
+
 #include <QDebug>
 #include <QList>
 #include <QTransform>
@@ -373,10 +375,8 @@
 	QList<GlyphCluster> glyphs;
 	bool     isEmpty { true };
 	int      hyphenCount { 0 };
-	double   colWidth;
-	double   colGap;
-	double   colLeft;
-	double   colRight;
+	double   colLeft { 0.0 };
+	double   colRight { 0.0 };
 	int      column { 0 };
 	bool     startOfCol { true };
 	bool     hasDropCap;
@@ -406,33 +406,38 @@
 	double   maxStretch { 0.0 };
 	ScribusDoc* doc { nullptr };
 	ITextContext* context { nullptr };
+	QVector<TextColumnGeometry> columns;
 
 	/// remember frame dimensions and offsets
-	LineControl(double w, double h, const MarginStruct& extra, double lCorr, ScribusDoc* d, ITextContext* ctx, double colwidth, double colgap)
+	LineControl(double w, double h, const MarginStruct& extra, double lCorr, ScribusDoc* d, ITextContext* ctx, const QVector<TextColumnGeometry>& columnGeometry)
 		: hasDropCap(false)
-		, doc(d), context(ctx)
+		, doc(d), context(ctx), columns(columnGeometry)
 	{
 		insets = extra;
 		lineCorr = lCorr;
 		frameWidth = w;
 		frameHeight = h;
-		colWidth = colwidth;
-		colGap = colgap;
-		colLeft = insets.left() + lineCorr;
-		colRight = colLeft + colWidth;
+		if (!columns.isEmpty())
+		{
+			colLeft = columns.first().left;
+			colRight = colLeft + columns.first().width;
+		}
 	}
 
 	/// move position to next column
 	void nextColumn(TextLayout &textLayout)
 	{
 		startOfCol = true;
-		if (textLayout.story()->defaultStyle().direction() == ParagraphStyle::RTL)
-			colLeft = textLayout.frame()->width() - insets.right() - ((colWidth * (column + 1)) + (colGap * column));
-		else
-			colLeft = (colWidth + colGap) * column + insets.left() + lineCorr;
-		textLayout.addColumn(colLeft, colWidth);
+		const bool rtl = textLayout.story()->defaultStyle().direction() == ParagraphStyle::RTL;
+		const int columnCount = static_cast<int>(columns.size());
+		const int physicalColumn = rtl ? columnCount - column - 1 : column;
+		if (physicalColumn < 0 || physicalColumn >= columnCount)
+			return;
+		const TextColumnGeometry& geometry = columns.at(physicalColumn);
+		colLeft = geometry.left;
+		textLayout.addColumn(colLeft, geometry.width);
 		//now colRight is REAL column right edge
-		colRight = colLeft + colWidth;
+		colRight = colLeft + geometry.width;
 		if (legacy)
 			colRight += lineCorr;
 		xPos = colLeft;
@@ -1341,7 +1346,7 @@
 		QList<GlyphCluster> glyphClusters; // = textShaper.shape();
 		// std::sort(glyphClusters.begin(), glyphClusters.end(), logicalGlyphRunComp);
 
-		LineControl current(m_width, m_height, m_textDistanceMargins, lineCorr, m_Doc, context, columnWidth(), m_columnGap);
+		LineControl current(m_width, m_height, m_textDistanceMargins, lineCorr, m_Doc, context, columnGeometry());
 		current.nextColumn(textLayout);
 
 		lastLineY = m_textDistanceMargins.top();
@@ -5109,15 +5114,64 @@
 	m_Doc->scMW()->setCopyCutEnabled(false);
 }
 
-double PageItem_TextFrame::columnWidth()
+QVector<TextColumnGeometry> PageItem_TextFrame::columnGeometry() const
 {
-	double lineCorr;
+	double lineCorr = 0.0;
 	if (lineColor() != CommonStrings::None)
 		lineCorr = m_lineWidth / 2.0;
-	else
-		lineCorr = 0;
-	return (m_width - (m_columnGap * (m_columns - 1)) - m_textDistanceMargins.left() - m_textDistanceMargins.right() - 2 * lineCorr) / m_columns;
-//	return (Width - (ColGap * (Cols - 1)) - m_textDistanceMargins.left() - m_textDistanceMargins.right() - lineCorr) / Cols;
+
+	const int columnCount = qMax(1, m_columns);
+	const double contentLeft = m_textDistanceMargins.left() + lineCorr;
+	const double rawContentWidth = m_width - m_textDistanceMargins.left()
+		- m_textDistanceMargins.right() - 2.0 * lineCorr;
+	const bool useCustomLayout = m_customColumnLayout
+		&& m_columnWidthRatios.size() == columnCount
+		&& m_columnGaps.size() == columnCount - 1;
+	QVector<TextColumnGeometry> result;
+	result.reserve(columnCount);
+	if (!useCustomLayout)
+	{
+		const double width = (rawContentWidth - m_columnGap * (columnCount - 1)) / columnCount;
+		for (int column = 0; column < columnCount; ++column)
+			result.append(TextColumnGeometry { contentLeft + (width + m_columnGap) * column, width });
+		return result;
+	}
+
+	const double contentWidth = qMax(0.0, rawContentWidth);
+	const QVector<double>& ratios = m_columnWidthRatios;
+	const QVector<double>& gaps = m_columnGaps;
+
+	double gapTotal = 0.0;
+	for (double gap : std::as_const(gaps))
+		gapTotal += qMax(0.0, gap);
+	const double minimumColumnWidth = qMin(0.01, contentWidth / columnCount);
+	const double maximumGapTotal = qMax(0.0, contentWidth - minimumColumnWidth * columnCount);
+	const double gapScale = gapTotal > maximumGapTotal && gapTotal > 0.0
+		? maximumGapTotal / gapTotal : 1.0;
+	const double widthsTotal = qMax(0.0, contentWidth - gapTotal * gapScale);
+
+	double left = contentLeft;
+	double usedWidth = 0.0;
+	for (int column = 0; column < columnCount; ++column)
+	{
+		double width = widthsTotal * ratios.at(column);
+		if (column == columnCount - 1)
+			width = qMax(0.0, widthsTotal - usedWidth);
+		result.append(TextColumnGeometry { left, width });
+		usedWidth += width;
+		left += width;
+		if (column < gaps.size())
+			left += qMax(0.0, gaps.at(column)) * gapScale;
+	}
+	return result;
+}
+
+double PageItem_TextFrame::columnWidth(int physicalColumn) const
+{
+	const QVector<TextColumnGeometry> geometry = columnGeometry();
+	if (geometry.isEmpty())
+		return 0.0;
+	return geometry.at(qBound(0, physicalColumn, static_cast<int>(geometry.size()) - 1)).width;
 }
 
 /*
@@ -5159,9 +5213,6 @@
 	p->setFillMode(ScPainter::Solid);
 	p->setupPolygon(&PoLine);
 	p->setClipPath();
-	double colWidth = columnWidth();
-	double colLeft=0;
-	int curCol=0;
 	double lineCorr=0;
 	if (lineColor() != CommonStrings::None)
 		lineCorr = m_lineWidth / 2.0;
@@ -5169,14 +5220,13 @@
 		p->drawSharpLine(FPoint(m_textDistanceMargins.left() + lineCorr, m_textDistanceMargins.top() + lineCorr), FPoint(m_width - m_textDistanceMargins.right() - lineCorr, m_textDistanceMargins.top() + lineCorr));
 	if (m_textDistanceMargins.bottom() + lineCorr!=0.0)
 		p->drawSharpLine(FPoint(m_textDistanceMargins.left() + lineCorr, m_height - m_textDistanceMargins.bottom() - lineCorr), FPoint(m_width - m_textDistanceMargins.right() - lineCorr, m_height - m_textDistanceMargins.bottom() - lineCorr));
-	while (curCol < m_columns)
+	const QVector<TextColumnGeometry> geometry = columnGeometry();
+	for (const TextColumnGeometry& column : geometry)
 	{
-		colLeft=(colWidth + m_columnGap) * curCol + m_textDistanceMargins.left() + lineCorr;
-		if (colLeft != 0.0)
-			p->drawSharpLine(FPoint(colLeft, m_textDistanceMargins.top() + lineCorr), FPoint(colLeft, m_height - m_textDistanceMargins.bottom() - lineCorr));
-		if (colLeft + colWidth != m_width)
-			p->drawSharpLine(FPoint(colLeft + colWidth, m_textDistanceMargins.top() + lineCorr), FPoint(colLeft + colWidth, m_height - m_textDistanceMargins.bottom() - lineCorr));
-		++curCol;
+		if (column.left != 0.0)
+			p->drawSharpLine(FPoint(column.left, m_textDistanceMargins.top() + lineCorr), FPoint(column.left, m_height - m_textDistanceMargins.bottom() - lineCorr));
+		if (column.left + column.width != m_width)
+			p->drawSharpLine(FPoint(column.left + column.width, m_textDistanceMargins.top() + lineCorr), FPoint(column.left + column.width, m_height - m_textDistanceMargins.bottom() - lineCorr));
 	}
 
 }
Index: scribus/plugins/scriptplugin/cmdtext.cpp
===================================================================
--- scribus/plugins/scriptplugin/cmdtext.cpp	(revision 27829)
+++ scribus/plugins/scriptplugin/cmdtext.cpp	(working copy)
@@ -1142,7 +1142,7 @@
 		PyErr_SetString(WrongFrameTypeError, QObject::tr("Cannot set column gap on a non-text frame.","python error").toUtf8().constData());
 		return nullptr;
 	}
-	item->m_columnGap = ValueToPoint(w);
+	item->setColumnGap(ValueToPoint(w));
 
 	Py_RETURN_NONE;
 }
@@ -1168,7 +1168,7 @@
 		PyErr_SetString(WrongFrameTypeError, QObject::tr("Cannot set number of columns on a non-text frame.","python error").toUtf8().constData());
 		return nullptr;
 	}
-	item->m_columns = w;
+	item->setColumns(w);
 
 	Py_RETURN_NONE;
 }
cc2-custom-text-column-persistence-v1.01.patch (7,151 bytes)   
Index: scribus/pageitem.h
===================================================================
--- scribus/pageitem.h	(revision 27829)
+++ scribus/pageitem.h	(working copy)
@@ -830,7 +830,7 @@
 	void setTextToFrameDistBottom(double);
 	void setColumns(int);
 	void setColumnGap(double);
-	void setTextColumnLayout(const TextColumnLayout& layout);
+	void setTextColumnLayout(const TextColumnLayout& layout, bool recordUndo = true);
 	void setGridOffset(double);
 	void setGridDistance(double);
 	void setVerticalAlignment(int);
Index: scribus/pageitem.cpp
===================================================================
--- scribus/pageitem.cpp	(revision 27829)
+++ scribus/pageitem.cpp	(working copy)
@@ -1576,7 +1576,7 @@
 	return layout;
 }
 
-void PageItem::setTextColumnLayout(const TextColumnLayout& newLayout)
+void PageItem::setTextColumnLayout(const TextColumnLayout& newLayout, bool recordUndo)
 {
 	TextColumnLayout layout = newLayout;
 	layout.columnCount = qMax(1, layout.columnCount);
@@ -1624,7 +1624,7 @@
 	const TextColumnLayout oldLayout = textColumnLayout();
 	if (oldLayout == layout)
 		return;
-	if (UndoManager::undoEnabled())
+	if (recordUndo && UndoManager::undoEnabled())
 	{
 		auto *state = new ScOldNewState<TextColumnLayout>(Um::Columns, QString(), Um::IBorder);
 		state->set("TEXT_COLUMN_LAYOUT");
Index: scribus/canvasmode_copyproperties.cpp
===================================================================
--- scribus/canvasmode_copyproperties.cpp	(revision 27829)
+++ scribus/canvasmode_copyproperties.cpp	(working copy)
@@ -159,8 +159,7 @@
 	if (GetItem(&currItem))
 	{
 		double sx, sy, ex, ey, fx, fy, ss, sk;
-		currItem->m_columnGap = m_doc->ElemToLink->m_columnGap;
-		currItem->m_columns = m_doc->ElemToLink->m_columns;
+		currItem->setTextColumnLayout(m_doc->ElemToLink->textColumnLayout());
 		currItem->setTextToFrameDist(m_doc->ElemToLink->textToFrameDistLeft(), m_doc->ElemToLink->textToFrameDistRight(), m_doc->ElemToLink->textToFrameDistTop(), m_doc->ElemToLink->textToFrameDistBottom());
 		// Stroke Properties
 		currItem->setLineStyle(m_doc->ElemToLink->lineStyle());
Index: scribus/plugins/fileloader/scribus171format/scribus171format.cpp
===================================================================
--- scribus/plugins/fileloader/scribus171format/scribus171format.cpp	(revision 27829)
+++ scribus/plugins/fileloader/scribus171format/scribus171format.cpp	(working copy)
@@ -18,6 +18,7 @@
 #include <QList>
 #include <QRegularExpression>
 #include <QScopedPointer>
+#include <QStringList>
 #include <QStringView>
 
 #include "../../formatidlist.h"
@@ -62,6 +63,38 @@
 // Please don't implement the functionality of your plugin here; do that
 // in scribus171formatimpl.h and scribus171formatimpl.cpp .
 
+namespace
+{
+	QVector<double> parseTextColumnDoubleList(const QString& values)
+	{
+		QVector<double> result;
+		const QStringList parts = values.split(QLatin1Char(' '), Qt::SkipEmptyParts);
+		result.reserve(parts.size());
+		for (const QString& part : parts)
+		{
+			bool ok = false;
+			const double value = part.toDouble(&ok);
+			if (!ok)
+				return {};
+			result.append(value);
+		}
+		return result;
+	}
+
+	void readCustomTextColumns(PageItem* item, const ScXmlStreamAttributes& attrs,
+							   const char* customAttribute, const char* widthsAttribute, const char* gapsAttribute)
+	{
+		if (!attrs.valueAsBool(customAttribute, false))
+			return;
+
+		TextColumnLayout layout = item->textColumnLayout();
+		layout.custom = true;
+		layout.widthRatios = parseTextColumnDoubleList(attrs.valueAsString(widthsAttribute));
+		layout.gaps = parseTextColumnDoubleList(attrs.valueAsString(gapsAttribute));
+		item->setTextColumnLayout(layout, false);
+	}
+}
+
 Scribus171Format::Scribus171Format()
 {
 	// Set action info in languageChange, so we only have to do
@@ -7103,6 +7136,7 @@
 		currItem->m_columns = attrs.valueAsInt("Columns", 1);
 		currItem->m_columnGap = attrs.valueAsDouble("ColumnGap", 0.0);
 	}
+	readCustomTextColumns(currItem, attrs, "CustomColumns", "ColumnWidthRatios", "ColumnGaps");
 	//Remove uppercase in 1.8 format
 	if (attrs.hasAttribute("LAYER") && (attrs.valueAsInt("LAYER", 0) != -1))
 	{
@@ -7924,6 +7958,7 @@
 		PageItem* newItem = item->cellAt(row, col).textFrame();
 		newItem->m_columns = tAtt.valueAsInt("TextColumns", 1);
 		newItem->m_columnGap = tAtt.valueAsDouble("TextColGap", 0.0);
+		readCustomTextColumns(newItem, tAtt, "CustomTextColumns", "TextColumnWidthRatios", "TextColumnGaps");
 		newItem->setTextToFrameDist(tAtt.valueAsDouble("TextDistLeft", 0.0),
 							tAtt.valueAsDouble("TextDistRight", 0.0),
 							tAtt.valueAsDouble("TextDistTop", 0.0),
Index: scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp
===================================================================
--- scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp	(revision 27829)
+++ scribus/plugins/fileloader/scribus171format/scribus171format_save.cpp	(working copy)
@@ -16,6 +16,7 @@
 #include <QList>
 #include <QDataStream>
 #include <QScopedPointer>
+#include <QStringList>
 
 #include "../../formatidlist.h"
 
@@ -49,6 +50,30 @@
 #include "util_color.h"
 #include "util_text.h"
 
+namespace
+{
+	QString serializeDoubleList(const QVector<double>& values)
+	{
+		QStringList result;
+		result.reserve(values.size());
+		for (double value : values)
+			result.append(QString::number(value, 'g', 17));
+		return result.join(QLatin1Char(' '));
+	}
+
+	void writeCustomTextColumns(ScXmlStreamWriter& writer, const PageItem* item,
+								const char* customAttribute, const char* widthsAttribute, const char* gapsAttribute)
+	{
+		const TextColumnLayout layout = item->textColumnLayout();
+		if (!layout.custom)
+			return;
+
+		writer.writeAttribute(customAttribute, 1);
+		writer.writeAttribute(widthsAttribute, serializeDoubleList(layout.widthRatios));
+		writer.writeAttribute(gapsAttribute, serializeDoubleList(layout.gaps));
+	}
+}
+
 QString Scribus171Format::saveElements(double xp, double yp, double wp, double hp, Selection* selection, QByteArray &prevData)
 {
 	ResourceCollection lists;
@@ -2580,6 +2605,7 @@
 					docu.writeAttribute("Style", cell.styleName());
 					docu.writeAttribute("TextColumns", textFrame->columns());
 					docu.writeAttribute("TextColGap", textFrame->columnGap());
+					writeCustomTextColumns(docu, textFrame, "CustomTextColumns", "TextColumnWidthRatios", "TextColumnGaps");
 					docu.writeAttribute("TextDistLeft", textFrame->textToFrameDistLeft());
 					docu.writeAttribute("TextDistTop", textFrame->textToFrameDistTop());
 					docu.writeAttribute("TextDistBottom", textFrame->textToFrameDistBottom());
@@ -2851,6 +2877,7 @@
 	{
 		docu.writeAttribute("Columns", item->columns());
 		docu.writeAttribute("ColumnGap", item->columnGap());
+		writeCustomTextColumns(docu, item, "CustomColumns", "ColumnWidthRatios", "ColumnGaps");
 		docu.writeAttribute("AutomaticTextFrame", item->isAutoText ? 1 : 0);
 		docu.writeAttribute("TextToFrameDistanceLeft", item->textToFrameDistLeft());
 		docu.writeAttribute("TextToFrameDistanceTop", item->textToFrameDistTop());
cc3-custom-text-column-editor-v1.04.patch (59,824 bytes)   
Index: scribus/CMakeLists_Sources.txt
===================================================================
--- scribus/CMakeLists_Sources.txt	(revision 27829)
+++ scribus/CMakeLists_Sources.txt	(working copy)
@@ -11,6 +11,7 @@
 	)
 endif()
 
+
 if (GMAGICK_FOUND)
 	set(SCRIBUS_GMAGICK_SRC imagedataloaders/scimgdataloader_gmagick.cpp)
 else()
@@ -473,6 +474,7 @@
 	ui/propertywidget_advanced.cpp
 	ui/propertywidget_alignment.cpp
 	ui/propertywidget_distance.cpp
+	ui/textcolumnpreview.cpp
 	ui/propertywidget_paragraphstyle.cpp
 	ui/propertywidget_pareffect.cpp
 	ui/propertywidget_hyphenation.cpp
@@ -626,5 +628,3 @@
 	set(SCRIBUS_WIN32_ONLY_SOURCES)
 	set(SCRIBUS_MAIN_CPP main.cpp)
 endif()
-
-
Index: scribus/ui/propertywidget_distancebase.ui
===================================================================
--- scribus/ui/propertywidget_distancebase.ui	(revision 27829)
+++ scribus/ui/propertywidget_distancebase.ui	(working copy)
@@ -47,13 +47,36 @@
        </property>
       </spacer>
      </item>
-    </layout>
+   </layout>
    </item>
    <item>
-    <layout class="QHBoxLayout" name="horizontalLayout">
+    <widget class="QWidget" name="columnControlsWidget" native="true">
+     <layout class="QHBoxLayout" name="horizontalLayout">
      <property name="spacing">
-      <number>4</number>
+      <number>8</number>
      </property>
+     <property name="leftMargin"><number>0</number></property>
+     <property name="topMargin"><number>0</number></property>
+     <property name="rightMargin"><number>0</number></property>
+     <property name="bottomMargin"><number>0</number></property>
+     <item>
+      <widget class="FormWidget" name="columnLayoutLabel">
+       <property name="label" stdset="0"><string>&amp;Layout</string></property>
+       <layout class="QHBoxLayout" name="columnLayoutControlLayout">
+        <property name="leftMargin"><number>0</number></property>
+        <property name="topMargin"><number>0</number></property>
+        <property name="rightMargin"><number>0</number></property>
+        <property name="bottomMargin"><number>0</number></property>
+        <item>
+         <widget class="QComboBox" name="columnLayout">
+          <property name="sizeAdjustPolicy"><enum>QComboBox::SizeAdjustPolicy::AdjustToContents</enum></property>
+          <item><property name="text"><string>Equal</string></property></item>
+          <item><property name="text"><string>Custom</string></property></item>
+         </widget>
+        </item>
+       </layout>
+      </widget>
+     </item>
      <item>
       <widget class="FormWidget" name="columnsLabel">
        <property name="label" stdset="0">
@@ -89,90 +112,32 @@
       </widget>
      </item>
      <item>
-      <spacer name="horizontalSpacer_4">
-       <property name="orientation">
-        <enum>Qt::Orientation::Horizontal</enum>
-       </property>
-       <property name="sizeType">
-        <enum>QSizePolicy::Policy::Fixed</enum>
-       </property>
-       <property name="sizeHint" stdset="0">
-        <size>
-         <width>8</width>
-         <height>0</height>
-        </size>
-       </property>
-      </spacer>
-     </item>
-     <item>
-      <widget class="FormWidget" name="columnGapLabel">
-       <property name="label" stdset="0">
-        <string/>
-       </property>
-       <property name="preserveLabelSpace">
-        <bool>true</bool>
-       </property>
-       <layout class="QHBoxLayout" name="horizontalLayout_8">
-        <property name="leftMargin">
-         <number>0</number>
-        </property>
-        <property name="topMargin">
-         <number>0</number>
-        </property>
-        <property name="rightMargin">
-         <number>0</number>
-        </property>
-        <property name="bottomMargin">
-         <number>0</number>
-        </property>
+      <widget class="FormWidget" name="widthAdjustmentLabel">
+       <property name="label" stdset="0"><string>Width adjustment</string></property>
+       <layout class="QHBoxLayout" name="widthAdjustmentLayout">
+        <property name="leftMargin"><number>0</number></property>
+        <property name="topMargin"><number>0</number></property>
+        <property name="rightMargin"><number>0</number></property>
+        <property name="bottomMargin"><number>0</number></property>
         <item>
-         <widget class="QComboBox" name="columnGapCombo">
-          <property name="toolTip">
-           <string>Switches between Gap or Column width</string>
-          </property>
-          <property name="sizeAdjustPolicy">
-           <enum>QComboBox::SizeAdjustPolicy::AdjustToContents</enum>
-          </property>
-          <item>
-           <property name="text">
-            <string>Gap:</string>
-           </property>
-          </item>
-          <item>
-           <property name="text">
-            <string>Width:</string>
-           </property>
-          </item>
+         <widget class="QComboBox" name="widthAdjustment">
+          <property name="sizeAdjustPolicy"><enum>QComboBox::SizeAdjustPolicy::AdjustToContents</enum></property>
+          <item><property name="text"><string>Adjacent</string></property></item>
+          <item><property name="text"><string>All others</string></property></item>
          </widget>
         </item>
        </layout>
       </widget>
      </item>
      <item>
-      <widget class="FormWidget" name="columnGapSizeLabel">
-       <property name="label" stdset="0">
-        <string>Size</string>
-       </property>
-       <layout class="QHBoxLayout" name="horizontalLayout_7">
-        <property name="leftMargin">
-         <number>0</number>
-        </property>
-        <property name="topMargin">
-         <number>0</number>
-        </property>
-        <property name="rightMargin">
-         <number>0</number>
-        </property>
-        <property name="bottomMargin">
-         <number>0</number>
-        </property>
-        <item>
-         <widget class="ScrSpinBox" name="columnGap">
-          <property name="toolTip">
-           <string>Distance between columns</string>
-          </property>
-         </widget>
-        </item>
+      <widget class="FormWidget" name="columnGapLabel">
+       <property name="label" stdset="0"><string>Gap</string></property>
+       <layout class="QHBoxLayout" name="columnGapLayout">
+        <property name="leftMargin"><number>0</number></property>
+        <property name="topMargin"><number>0</number></property>
+        <property name="rightMargin"><number>0</number></property>
+        <property name="bottomMargin"><number>0</number></property>
+        <item><widget class="ScrSpinBox" name="columnGap"/></item>
        </layout>
       </widget>
      </item>
@@ -189,7 +154,106 @@
        </property>
       </spacer>
      </item>
-    </layout>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <widget class="TextColumnPreview" name="columnPreview"/>
+   </item>
+   <item>
+    <widget class="QWidget" name="customColumnsWidget" native="true">
+     <layout class="QGridLayout" name="customColumnsLayout">
+      <property name="horizontalSpacing">
+       <number>8</number>
+      </property>
+      <property name="verticalSpacing">
+       <number>4</number>
+      </property>
+      <property name="leftMargin">
+       <number>0</number>
+      </property>
+      <property name="topMargin">
+       <number>0</number>
+      </property>
+      <property name="rightMargin">
+       <number>0</number>
+      </property>
+      <property name="bottomMargin">
+       <number>0</number>
+      </property>
+      <item row="1" column="0">
+       <widget class="FormWidget" name="customColumnLabel">
+        <property name="visible"><bool>false</bool></property>
+        <property name="label" stdset="0">
+         <string>Column</string>
+        </property>
+        <layout class="QHBoxLayout" name="customColumnControlLayout">
+         <property name="leftMargin"><number>0</number></property>
+         <property name="topMargin"><number>0</number></property>
+         <property name="rightMargin"><number>0</number></property>
+         <property name="bottomMargin"><number>0</number></property>
+         <item>
+          <widget class="ScrSpinBox" name="customColumn">
+           <property name="minimum"><double>1.000000000000000</double></property>
+	           <property name="decimals"><number>0</number></property>
+          </widget>
+         </item>
+        </layout>
+       </widget>
+      </item>
+      <item row="0" column="0">
+       <widget class="FormWidget" name="customWidthLabel">
+        <property name="label" stdset="0">
+         <string>Width</string>
+        </property>
+        <layout class="QHBoxLayout" name="customWidthControlLayout">
+         <property name="leftMargin"><number>0</number></property>
+         <property name="topMargin"><number>0</number></property>
+         <property name="rightMargin"><number>0</number></property>
+         <property name="bottomMargin"><number>0</number></property>
+         <item><widget class="ScrSpinBox" name="customWidth"/></item>
+        </layout>
+       </widget>
+      </item>
+      <item row="0" column="1">
+       <widget class="FormWidget" name="customGapLabel">
+        <property name="label" stdset="0">
+         <string>Gap</string>
+        </property>
+        <layout class="QHBoxLayout" name="customGapControlLayout">
+         <property name="leftMargin"><number>0</number></property>
+         <property name="topMargin"><number>0</number></property>
+         <property name="rightMargin"><number>0</number></property>
+         <property name="bottomMargin"><number>0</number></property>
+         <item><widget class="ScrSpinBox" name="customGap"/></item>
+        </layout>
+       </widget>
+      </item>
+      <item row="0" column="2">
+       <widget class="FormWidget" name="equalGapsLabel">
+        <property name="label" stdset="0"><string/></property>
+        <property name="preserveLabelSpace"><bool>true</bool></property>
+        <layout class="QHBoxLayout" name="equalGapsLayout">
+         <property name="leftMargin"><number>0</number></property>
+         <property name="topMargin"><number>0</number></property>
+         <property name="rightMargin"><number>0</number></property>
+         <property name="bottomMargin"><number>0</number></property>
+         <item>
+          <widget class="QCheckBox" name="equalGaps">
+           <property name="text"><string>Equal gaps</string></property>
+          </widget>
+         </item>
+        </layout>
+       </widget>
+      </item>
+      <item row="0" column="3">
+       <spacer name="customColumnsSpacer">
+        <property name="orientation"><enum>Qt::Orientation::Horizontal</enum></property>
+        <property name="sizeHint" stdset="0"><size><width>0</width><height>0</height></size></property>
+       </spacer>
+      </item>
+     </layout>
+    </widget>
    </item>
    <item>
     <layout class="QGridLayout" name="gridLayout">
@@ -376,6 +440,11 @@
  </widget>
  <customwidgets>
   <customwidget>
+   <class>TextColumnPreview</class>
+   <extends>QAbstractScrollArea</extends>
+   <header>ui/textcolumnpreview.h</header>
+  </customwidget>
+  <customwidget>
    <class>FormWidget</class>
    <extends>QWidget</extends>
    <header>ui/widgets/form_widget.h</header>
Index: scribus/ui/propertywidget_distance.h
===================================================================
--- scribus/ui/propertywidget_distance.h	(revision 27829)
+++ scribus/ui/propertywidget_distance.h	(working copy)
@@ -14,6 +14,7 @@
 
 class ScribusDoc;
 class ScribusMainWindow;
+class PageItem_TextFrame;
 
 class PropertyWidget_Distance : public QFrame, public Ui::PropertyWidget_DistanceBase,
                                 public PropertyWidgetBase
@@ -31,10 +32,15 @@
 	double m_unitRatio {1.0};
 	int m_unitIndex {SC_PT};
 	PageItem *m_item {nullptr};
+	PageItem_TextFrame *m_columnItem {nullptr};
+	bool m_initializeGapMode {true};
 	ScribusMainWindow* m_ScMW {nullptr};
 
 	void configureWidgets();
 	void setCurrentItem(PageItem *item);
+	PageItem_TextFrame* currentTextFrame() const;
+	void updateColumnControls(PageItem_TextFrame* textItem);
+	void updateTextFrame(PageItem_TextFrame* textItem);
 
 	void changeEvent(QEvent *e) override;
 
@@ -59,7 +65,12 @@
 private slots:
 	void handleColumns();
 	void handleColumnGap();
-	void handleGapSwitch();
+	void handleColumnLayout(int index);
+	void handleCustomColumn();
+	void handlePreviewColumn(int column);
+	void handleCustomWidth();
+	void handleCustomGap();
+	void handleEqualGaps(bool enabled);
 	void handleTabs();
 	void handleTextDistances();
 	void handleIndents();
Index: scribus/ui/propertywidget_distance.cpp
===================================================================
--- scribus/ui/propertywidget_distance.cpp	(revision 27829)
+++ scribus/ui/propertywidget_distance.cpp	(working copy)
@@ -7,6 +7,10 @@
 
 #include "propertywidget_distance.h"
 
+#include <algorithm>
+#include <numeric>
+#include <utility>
+
 #include <QSignalBlocker>
 
 #include "appmodehelper.h"
@@ -18,9 +22,35 @@
 #include "scribus.h"
 #include "scribusapp.h"
 #include "scribusdoc.h"
+#include "scribusview.h"
 #include "selection.h"
+#include "hruler.h"
 #include "tabmanager.h"
 
+namespace
+{
+	bool gapsAreEqual(const QVector<double>& gaps)
+	{
+		if (gaps.size() < 2)
+			return true;
+		for (int i = 1; i < gaps.size(); ++i)
+		{
+			const double tolerance = 1e-7 * qMax(1.0, qMax(qAbs(gaps.at(0)), qAbs(gaps.at(i))));
+			if (qAbs(gaps.at(i) - gaps.at(0)) > tolerance)
+				return false;
+		}
+		return true;
+	}
+
+	double minimumTextColumnWidth(const PageItem_TextFrame* item)
+	{
+		double width = 0.0;
+		for (const TextColumnGeometry& column : item->columnGeometry())
+			width = width == 0.0 ? column.width : qMin(width, column.width);
+		return width;
+	}
+}
+
 PropertyWidget_Distance::PropertyWidget_Distance(QWidget* parent) : QFrame(parent)
 {
 	setupUi(this);
@@ -30,6 +60,24 @@
 	columnGap->setValues(0, 300, 2, 0);
 	columns->setDecimals(0);
 	columns->setSuffix("");
+	customColumn->setValues(1, 3000, 0, 1);
+	customColumn->setSuffix("");
+	customWidth->setValues(0.01, 1000000, 2, 0);
+	customGap->setValues(0, 1000000, 2, 0);
+	customWidth->setToolTip(tr("Width of the selected column"));
+	customGap->setToolTip(tr("Space after the selected column"));
+	customColumnsWidget->setVisible(false);
+	columnPreview->setVisible(false);
+	widthAdjustmentLabel->setVisible(false);
+	equalGaps->setVisible(false);
+	setTabOrder(columnLayout, columns);
+	setTabOrder(columns, widthAdjustment);
+	setTabOrder(widthAdjustment, columnGap);
+	setTabOrder(columnGap, columnPreview);
+	setTabOrder(columnPreview, customWidth);
+	setTabOrder(customWidth, customGap);
+	setTabOrder(customGap, equalGaps);
+	setTabOrder(equalGaps, tabsButton);
 
 	MarginStruct distances;
 
@@ -48,7 +96,7 @@
 	iconSetChange();
 	languageChange();
 
-	columnGapCombo->setCurrentIndex(0);
+	columnLayout->setCurrentIndex(0);
 
 	connect(ScQApp, SIGNAL(iconSetChanged()), this, SLOT(iconSetChange()));
 	connect(ScQApp, SIGNAL(localeChanged()), this, SLOT(localeChange()));
@@ -88,6 +136,9 @@
 
 	columns->setDecimals(0);
 	columnGap->setDecimals(2);
+	customColumn->setDecimals(0);
+	customWidth->setDecimals(2);
+	customGap->setDecimals(2);
 
 	connect(m_doc->m_Selection, SIGNAL(selectionChanged()), this, SLOT(handleSelectionChanged()));
 	connect(m_doc             , SIGNAL(docChanged())      , this, SLOT(handleSelectionChanged()));
@@ -136,25 +187,18 @@
 	if (m_doc->appMode == modeEditTable)
 		textItem = m_item->asTable()->activeCell().textFrame();
 	if (!textItem) return;
+	if (textItem != m_columnItem)
+	{
+		m_columnItem = textItem;
+		m_initializeGapMode = true;
+		customColumn->setValue(1);
+	}
 
 	ParagraphStyle parStyle =  m_item->itemText.defaultStyle();
 	if (m_doc->appMode == modeEdit || m_doc->appMode == modeEditTable)
 		m_item->currentTextProps(parStyle);
 
-	//#14427: columns->setMaximum(qMax(qRound(textItem->width() / qMax(textItem->ColGap, 10.0)), 1));
-	columns->setMinimum(1);
-	columns->setValue(textItem->m_columns);
-	columnGap->setMinimum(0);
-	if (columnGapCombo->currentIndex() == 0)
-	{
-		columnGap->setMaximum(qMax((textItem->width() / textItem->m_columns - textItem->textToFrameDistLeft() - textItem->textToFrameDistRight()) * m_unitRatio, 0.0));
-		columnGap->setValue(textItem->m_columnGap * m_unitRatio);
-	}
-	else
-	{
-		columnGap->setMaximum(qMax((textItem->width() / textItem->m_columns) * m_unitRatio, 0.0));
-		columnGap->setValue(textItem->columnWidth() * m_unitRatio);
-	}
+	updateColumnControls(textItem);
 
 	MarginStruct distances;
 	distances.set(
@@ -167,9 +211,6 @@
 	distanceWidget->setPageWidth(m_item->width());
 	distanceWidget->setNewValues(distances);
 
-	columnGapLabel->setVisible(columns->value() != 1);
-	columnGapSizeLabel->setVisible(columns->value() != 1);
-
 	updateStyle(parStyle);
 
 	connectSignals();
@@ -179,7 +220,13 @@
 {
 	connect(columns       , SIGNAL(valueChanged(double))   , this, SLOT(handleColumns()), Qt::UniqueConnection);
 	connect(columnGap     , SIGNAL(valueChanged(double)), this, SLOT(handleColumnGap()), Qt::UniqueConnection);
-	connect(columnGapCombo, SIGNAL(activated(int))      , this, SLOT(handleGapSwitch()), Qt::UniqueConnection);
+	connect(columnLayout  , SIGNAL(activated(int))      , this, SLOT(handleColumnLayout(int)), Qt::UniqueConnection);
+	connect(customColumn  , SIGNAL(valueChanged(double)), this, SLOT(handleCustomColumn()), Qt::UniqueConnection);
+	connect(columnPreview , SIGNAL(selectedColumnChanged(int)), this, SLOT(handlePreviewColumn(int)), Qt::UniqueConnection);
+	connect(customWidth   , SIGNAL(valueChanged(double)), this, SLOT(handleCustomWidth()), Qt::UniqueConnection);
+	connect(customGap     , SIGNAL(valueChanged(double)), this, SLOT(handleCustomGap()), Qt::UniqueConnection);
+	connect(equalGaps     , SIGNAL(toggled(bool))        , this, SLOT(handleEqualGaps(bool)), Qt::UniqueConnection);
+	connect(widthAdjustment, SIGNAL(activated(int))      , this, SLOT(handleCustomColumn()), Qt::UniqueConnection);
 	connect(distanceWidget, SIGNAL(valuesChanged(MarginStruct)), this, SLOT(handleTextDistances()), Qt::UniqueConnection);
 	connect(tabsButton    , SIGNAL(clicked())           , this, SLOT(handleTabs()), Qt::UniqueConnection);
 	connect(firstLineIndentSpin, SIGNAL(valueChanged(double)), this, SLOT(handleIndents()), Qt::UniqueConnection);
@@ -194,7 +241,13 @@
 {
 	disconnect(columns       , SIGNAL(valueChanged(double))   , this, SLOT(handleColumns()));
 	disconnect(columnGap     , SIGNAL(valueChanged(double)), this, SLOT(handleColumnGap()));
-	disconnect(columnGapCombo, SIGNAL(activated(int))      , this, SLOT(handleGapSwitch()));
+	disconnect(columnLayout  , SIGNAL(activated(int))      , this, SLOT(handleColumnLayout(int)));
+	disconnect(customColumn  , SIGNAL(valueChanged(double)), this, SLOT(handleCustomColumn()));
+	disconnect(columnPreview , SIGNAL(selectedColumnChanged(int)), this, SLOT(handlePreviewColumn(int)));
+	disconnect(customWidth   , SIGNAL(valueChanged(double)), this, SLOT(handleCustomWidth()));
+	disconnect(customGap     , SIGNAL(valueChanged(double)), this, SLOT(handleCustomGap()));
+	disconnect(equalGaps     , SIGNAL(toggled(bool))        , this, SLOT(handleEqualGaps(bool)));
+	disconnect(widthAdjustment, SIGNAL(activated(int))      , this, SLOT(handleCustomColumn()));
 	disconnect(distanceWidget, SIGNAL(valuesChanged(MarginStruct)), this, SLOT(handleTextDistances()));
 	disconnect(tabsButton    , SIGNAL(clicked())           , this, SLOT(handleTabs()));
 	disconnect(firstLineIndentSpin, SIGNAL(valueChanged(double)), this, SLOT(handleIndents()));
@@ -221,12 +274,127 @@
 			int numCols = textItem->m_columns;
 			
 			columnGapLabel->setVisible(numCols != 1);
-			columnGapSizeLabel->setVisible(numCols != 1);
 		}
 	}
 	setEnabled(enabled);
 }
 
+PageItem_TextFrame* PropertyWidget_Distance::currentTextFrame() const
+{
+	if (!m_item || !m_doc)
+		return nullptr;
+	if (m_doc->appMode == modeEditTable && m_item->isTable())
+		return m_item->asTable()->activeCell().textFrame();
+	return m_item->asTextFrame();
+}
+
+void PropertyWidget_Distance::updateColumnControls(PageItem_TextFrame* textItem)
+{
+	if (!textItem)
+		return;
+
+	QSignalBlocker columnsBlocker(columns);
+	QSignalBlocker gapBlocker(columnGap);
+	QSignalBlocker layoutBlocker(columnLayout);
+	QSignalBlocker customColumnBlocker(customColumn);
+	QSignalBlocker customWidthBlocker(customWidth);
+	QSignalBlocker customGapBlocker(customGap);
+	QSignalBlocker equalGapsBlocker(equalGaps);
+
+	const TextColumnLayout layout = textItem->textColumnLayout();
+	const bool custom = layout.custom && layout.columnCount > 1;
+	const int columnCount = qMax(1, layout.columnCount);
+	const int column = qBound(0, static_cast<int>(customColumn->value()) - 1, columnCount - 1);
+
+	columns->setMinimum(1);
+	columns->setValue(columnCount);
+	columnLayout->setCurrentIndex(custom ? 1 : 0);
+	columnLayout->setEnabled(columnCount > 1);
+
+	columnGap->setMinimum(0);
+	columnGap->setMaximum(qMax((textItem->width() / columnCount - textItem->textToFrameDistLeft() - textItem->textToFrameDistRight()) * m_unitRatio, 0.0));
+	columnGap->setValue(layout.columnGap * m_unitRatio);
+
+	customColumn->setMaximum(columnCount);
+	customColumn->setValue(column + 1);
+	const QVector<TextColumnGeometry> geometry = textItem->columnGeometry();
+	QVector<double> effectiveWidths;
+	QVector<double> effectiveGaps;
+	for (const TextColumnGeometry& value : geometry)
+		effectiveWidths.append(value.width);
+	for (int i = 0; i + 1 < geometry.size(); ++i)
+		effectiveGaps.append(qMax(0.0, geometry.at(i + 1).left - geometry.at(i).left - geometry.at(i).width));
+	if (m_initializeGapMode)
+	{
+		equalGaps->setChecked(gapsAreEqual(effectiveGaps));
+		m_initializeGapMode = false;
+	}
+	else if (equalGaps->isChecked() && !gapsAreEqual(effectiveGaps))
+		equalGaps->setChecked(false);
+	columnPreview->setGeometry(effectiveWidths, effectiveGaps);
+	columnPreview->setSelectedColumn(column);
+	columnPreview->setEqualGaps(equalGaps->isChecked());
+	if (column < geometry.size())
+	{
+		const int neighbour = (column + 1 < columnCount) ? column + 1 : column - 1;
+		double maximumWidth = geometry.at(column).width;
+		if (widthAdjustment->currentIndex() == 1)
+		{
+			for (int i = 0; i < geometry.size(); ++i)
+				if (i != column)
+					maximumWidth += qMax(0.0, geometry.at(i).width - 0.01);
+		}
+		else if (neighbour >= 0)
+			maximumWidth += qMax(0.0, geometry.at(neighbour).width - 0.01);
+		customWidth->setMaximum(qMax(0.01, maximumWidth) * m_unitRatio);
+		customWidth->setValue(geometry.at(column).width * m_unitRatio);
+	}
+	const bool editEqualGaps = custom && equalGaps->isChecked() && !effectiveGaps.isEmpty();
+	customGap->setEnabled(custom && (column < columnCount - 1 || editEqualGaps));
+	customGap->setToolTip(editEqualGaps ? tr("Space between all columns")
+		: (column < columnCount - 1 ? tr("Space after the selected column")
+		: tr("The last column has no following gap")));
+	if (editEqualGaps)
+	{
+		const double contentWidth = std::accumulate(effectiveWidths.cbegin(), effectiveWidths.cend(), 0.0)
+			+ std::accumulate(effectiveGaps.cbegin(), effectiveGaps.cend(), 0.0);
+		customGap->setMaximum(qMax(0.0, (contentWidth - 0.01 * columnCount) / effectiveGaps.size()) * m_unitRatio);
+	}
+	if (custom && column < effectiveGaps.size() && column + 1 < geometry.size())
+	{
+		if (!editEqualGaps)
+			customGap->setMaximum((effectiveGaps.at(column) + qMax(0.0, geometry.at(column + 1).width - 0.01)) * m_unitRatio);
+		customGap->setValue(effectiveGaps.at(column) * m_unitRatio);
+	}
+	else
+		customGap->setValue(0.0);
+	if (editEqualGaps && column >= effectiveGaps.size())
+		customGap->setValue(effectiveGaps.constFirst() * m_unitRatio);
+
+	columnGapLabel->setVisible(!custom && columnCount > 1);
+	customColumnsWidget->setVisible(custom);
+	columnPreview->setVisible(custom);
+	widthAdjustmentLabel->setVisible(custom);
+	widthAdjustment->setEnabled(columnCount > 2);
+	equalGaps->setVisible(custom && columnCount > 2);
+}
+
+void PropertyWidget_Distance::updateTextFrame(PageItem_TextFrame* textItem)
+{
+	if (!textItem)
+		return;
+	updateColumnControls(textItem);
+	textItem->update();
+	if (m_doc->appMode == modeEditTable)
+		m_item->asTable()->update();
+	if (m_ScMW->view && m_doc->inAnEditMode())
+	{
+		m_ScMW->view->horizRuler->setItem(textItem);
+		m_ScMW->view->horizRuler->update();
+	}
+	m_doc->regionsChanged()->update(QRect());
+}
+
 void PropertyWidget_Distance::handleAppModeChanged(int oldMode, int mode)
 {
 	if (oldMode == modeEditTable || mode == modeEditTable)
@@ -255,6 +423,9 @@
 	IconManager &im = IconManager::instance();
 
 	columnsLabel->setPixmap(im.loadPixmap("paragraph-columns"));
+	columnGapLabel->setPixmap(im.loadPixmap("paragraph-column-gap"));
+	customWidthLabel->setPixmap(im.loadPixmap("scale-width"));
+	customGapLabel->setPixmap(im.loadPixmap("paragraph-column-gap"));
 	firstLineIndentLabel->setPixmap(im.loadPixmap("paragraph-indent-firstline"));
 	leftIndentLabel->setPixmap(im.loadPixmap("paragraph-indent-left"));
 	rightIndentLabel->setPixmap(im.loadPixmap("paragraph-indent-right"));
@@ -262,47 +433,68 @@
 	gapAfterLabel->setPixmap(im.loadPixmap("paragraph-space-below") );
 }
 
-void PropertyWidget_Distance::showColumns(int r, double g)
+void PropertyWidget_Distance::showColumns(int /*columnCount*/, double /*columnGapValue*/)
 {
 	if (!m_ScMW || m_ScMW->scriptIsRunning())
 		return;
+	if (PageItem_TextFrame* textItem = currentTextFrame())
+		updateColumnControls(textItem);
+}
+
+void PropertyWidget_Distance::handleColumns()
+{
+	if (!m_doc || !m_item || !m_ScMW || m_ScMW->scriptIsRunning())
+		return;
 
-	bool cSigWasBlocked    = columns->blockSignals(true);
-	bool cGapSigWasBlocked = columnGap->blockSignals(true);
+	PageItem_TextFrame* textItem = currentTextFrame();
+	if (!textItem)
+		return;
 
-	columns->setValue(r);
-	columnGap->setValue(g * m_unitRatio);
-	if (m_item)
+	const int newCount = qMax(1, static_cast<int>(columns->value()));
+	TextColumnLayout layout = textItem->textColumnLayout();
+	if (!layout.custom)
+		textItem->setColumns(newCount);
+	else if (newCount == 1)
+	{
+		layout.columnCount = 1;
+		layout.custom = false;
+		layout.widthRatios.clear();
+		layout.gaps.clear();
+		textItem->setTextColumnLayout(layout);
+	}
+	else if (newCount > layout.columnCount)
 	{
-		PageItem_TextFrame *textItem = m_item->asTextFrame();
-		if (m_doc->appMode == modeEditTable)
-			textItem = m_item->asTable()->activeCell().textFrame();
-
-		if (textItem != nullptr)
+		while (layout.columnCount < newCount)
 		{
-//#14427: columns->setMaximum(qMax(qRound(textItem->width() / qMax(textItem->ColGap, 10.0)), 1));
-			if (columnGapCombo->currentIndex() == 0)
-			{
-				columnGap->setMaximum(qMax((textItem->width() / textItem->m_columns - textItem->textToFrameDistLeft() - textItem->textToFrameDistRight()) * m_unitRatio, 0.0));
-				columnGap->setValue(textItem->m_columnGap * m_unitRatio);
-			}
-			else
+			const int oldCount = layout.columnCount;
+			const double newShare = 1.0 / (oldCount + 1.0);
+			for (double& ratio : layout.widthRatios)
+				ratio *= 1.0 - newShare;
+			layout.widthRatios.append(newShare);
+			double averageGap = layout.columnGap;
+			if (!layout.gaps.isEmpty())
 			{
-				columnGap->setMaximum(qMax((textItem->width() / textItem->m_columns) * m_unitRatio, 0.0));
-				columnGap->setValue(textItem->columnWidth() * m_unitRatio);
+				averageGap = 0.0;
+				for (double gap : std::as_const(layout.gaps))
+					averageGap += gap;
+				averageGap /= layout.gaps.size();
 			}
+			layout.gaps.append(averageGap);
+			layout.columnCount++;
 		}
+		textItem->setTextColumnLayout(layout);
 	}
-	columns->setMinimum(1);
-	columnGap->setMinimum(0);
-	columnGapLabel->setVisible(columns->value() != 1);
-	columnGapSizeLabel->setVisible(columns->value() != 1);
-
-	columns->blockSignals(cSigWasBlocked);
-	columnGap->blockSignals(cGapSigWasBlocked);
+	else if (newCount < layout.columnCount)
+	{
+		layout.columnCount = newCount;
+		layout.widthRatios.resize(newCount);
+		layout.gaps.resize(newCount - 1);
+		textItem->setTextColumnLayout(layout);
+	}
+	updateTextFrame(textItem);
 }
 
-void PropertyWidget_Distance::handleColumns()
+void PropertyWidget_Distance::handleColumnGap()
 {
 	if (!m_doc || !m_item || !m_ScMW || m_ScMW->scriptIsRunning())
 		return;
@@ -310,59 +502,199 @@
 	PageItem *textItem = m_item;
 	if (m_doc->appMode == modeEditTable)
 		textItem = m_item->asTable()->activeCell().textFrame();
+	if (!textItem)
+		return;
 
-	if (textItem)
-	{
-		textItem->setColumns(static_cast<int>(columns->value()));
-		showColumns(textItem->m_columns, textItem->m_columnGap);
-		textItem->update();
-		if (m_doc->appMode == modeEditTable)
-			m_item->asTable()->update();
-		m_doc->regionsChanged()->update(QRect());
-	}
+	textItem->setColumnGap(columnGap->value() / m_unitRatio);
+	updateTextFrame(textItem->asTextFrame());
 }
 
-void PropertyWidget_Distance::handleColumnGap()
+void PropertyWidget_Distance::handleColumnLayout(int index)
 {
 	if (!m_doc || !m_item || !m_ScMW || m_ScMW->scriptIsRunning())
 		return;
-
-	PageItem *textItem = m_item;
-	if (m_doc->appMode == modeEditTable)
-		textItem = m_item->asTable()->activeCell().textFrame();
-	if (!textItem)
+	PageItem_TextFrame* textItem = currentTextFrame();
+	if (!textItem || textItem->columns() < 2)
 		return;
 
-	if (columnGapCombo->currentIndex() == 0)
-		textItem->setColumnGap(columnGap->value() / m_unitRatio);
-	else
+	TextColumnLayout layout = textItem->textColumnLayout();
+	if (index == 1 && !layout.custom)
 	{
-		double lineCorr=0.0;
-		if ((textItem->lineColor() != CommonStrings::None) || (!textItem->strokePattern().isEmpty()))
-			lineCorr = textItem->lineWidth();
-		double newWidth = columnGap->value() / m_unitRatio;
-		double newGap = qMax(((textItem->width() - textItem->textToFrameDistLeft() - textItem->textToFrameDistRight() - lineCorr) - (newWidth * textItem->m_columns)) / (textItem->m_columns - 1), 0.0);
-		textItem->setColumnGap(newGap);
+		const QVector<TextColumnGeometry> geometry = textItem->columnGeometry();
+		double widthTotal = 0.0;
+		for (const TextColumnGeometry& column : geometry)
+			widthTotal += column.width;
+		if (widthTotal <= 0.0)
+		{
+			updateColumnControls(textItem);
+			return;
+		}
+		layout.custom = true;
+		layout.widthRatios.clear();
+		layout.gaps.clear();
+		for (const TextColumnGeometry& column : geometry)
+			layout.widthRatios.append(column.width / widthTotal);
+		for (int column = 1; column < layout.columnCount; ++column)
+			layout.gaps.append(layout.columnGap);
 	}
-	textItem->update();
-	if (m_doc->appMode == modeEditTable)
-		m_item->asTable()->update();
-	m_doc->regionsChanged()->update(QRect());
+	else if (index == 0 && layout.custom)
+	{
+		if (!layout.gaps.isEmpty())
+		{
+			layout.columnGap = 0.0;
+			for (double gap : std::as_const(layout.gaps))
+				layout.columnGap += gap;
+			layout.columnGap /= layout.gaps.size();
+		}
+		layout.custom = false;
+		layout.widthRatios.clear();
+		layout.gaps.clear();
+	}
+	textItem->setTextColumnLayout(layout);
+	updateTextFrame(textItem);
 }
 
-void PropertyWidget_Distance::handleGapSwitch()
+void PropertyWidget_Distance::handleCustomColumn()
 {
-	if (!m_doc || !m_item || !m_ScMW || m_ScMW->scriptIsRunning())
+	if (PageItem_TextFrame* textItem = currentTextFrame())
+		updateColumnControls(textItem);
+}
+
+void PropertyWidget_Distance::handlePreviewColumn(int column)
+{
+	QSignalBlocker blocker(customColumn);
+	customColumn->setValue(column + 1);
+	if (PageItem_TextFrame* textItem = currentTextFrame())
+		updateColumnControls(textItem);
+}
+
+void PropertyWidget_Distance::handleCustomWidth()
+{
+	PageItem_TextFrame* textItem = currentTextFrame();
+	if (!textItem || !textItem->hasCustomTextColumns())
 		return;
 
-	const PageItem *textItem = m_item;
-	if (m_doc->appMode == modeEditTable)
-		textItem = m_item->asTable()->activeCell().textFrame();
-	if (textItem != nullptr)
-		showColumns(textItem->m_columns, textItem->m_columnGap);
+	TextColumnLayout layout = textItem->textColumnLayout();
+	const int column = qBound(0, static_cast<int>(customColumn->value()) - 1, layout.columnCount - 1);
+	const int neighbour = (column + 1 < layout.columnCount) ? column + 1 : column - 1;
+	if (neighbour < 0)
+		return;
 
-	int index = columnGapCombo->currentIndex();
-	columnGap->setToolTip((index == 0) ? tr( "Distance between columns" ) : tr( "Column width" ));
+	const QVector<TextColumnGeometry> geometry = textItem->columnGeometry();
+	QVector<double> widths;
+	QVector<double> gaps;
+	widths.reserve(geometry.size());
+	for (const TextColumnGeometry& value : geometry)
+		widths.append(value.width);
+	for (int i = 0; i + 1 < geometry.size(); ++i)
+		gaps.append(qMax(0.0, geometry.at(i + 1).left - geometry.at(i).left - geometry.at(i).width));
+	layout.gaps = gaps;
+	if (widthAdjustment->currentIndex() == 1 && widths.size() > 2)
+	{
+		double widthTotal = 0.0;
+		for (double width : std::as_const(widths))
+			widthTotal += width;
+		const double newWidth = qBound(0.01, customWidth->value() / m_unitRatio,
+			qMax(0.01, widthTotal - 0.01 * (widths.size() - 1)));
+		const double otherWidth = (widthTotal - newWidth) / (widths.size() - 1);
+		for (int i = 0; i < widths.size(); ++i)
+			widths[i] = (i == column) ? newWidth : otherWidth;
+	}
+	else
+	{
+		const double available = widths.at(column) + widths.at(neighbour);
+		const double newWidth = qBound(0.01, customWidth->value() / m_unitRatio, qMax(0.01, available - 0.01));
+		widths[neighbour] = available - newWidth;
+		widths[column] = newWidth;
+	}
+	double widthTotal = 0.0;
+	for (double width : std::as_const(widths))
+		widthTotal += width;
+	for (int i = 0; i < widths.size(); ++i)
+		layout.widthRatios[i] = widths.at(i) / widthTotal;
+	textItem->setTextColumnLayout(layout);
+	updateTextFrame(textItem);
+}
+
+void PropertyWidget_Distance::handleCustomGap()
+{
+	PageItem_TextFrame* textItem = currentTextFrame();
+	if (!textItem || !textItem->hasCustomTextColumns())
+		return;
+
+	TextColumnLayout layout = textItem->textColumnLayout();
+	const int column = qBound(0, static_cast<int>(customColumn->value()) - 1, layout.columnCount - 1);
+	if (column >= layout.gaps.size() && !equalGaps->isChecked())
+		return;
+
+	const QVector<TextColumnGeometry> geometry = textItem->columnGeometry();
+	QVector<double> widths;
+	QVector<double> gaps;
+	widths.reserve(geometry.size());
+	for (const TextColumnGeometry& value : geometry)
+		widths.append(value.width);
+	for (int i = 0; i + 1 < geometry.size(); ++i)
+		gaps.append(qMax(0.0, geometry.at(i + 1).left - geometry.at(i).left - geometry.at(i).width));
+	layout.gaps = gaps;
+	if (equalGaps->isChecked())
+	{
+		double widthTotal = 0.0;
+		for (double width : std::as_const(widths))
+			widthTotal += width;
+		for (int i = 0; i < widths.size(); ++i)
+			layout.widthRatios[i] = widths.at(i) / widthTotal;
+		const double contentWidth = widthTotal + std::accumulate(gaps.cbegin(), gaps.cend(), 0.0);
+		const double maximumGap = qMax(0.0, (contentWidth - 0.01 * widths.size()) / gaps.size());
+		const double newGap = qBound(0.0, customGap->value() / m_unitRatio, maximumGap);
+		std::fill(layout.gaps.begin(), layout.gaps.end(), newGap);
+		textItem->setTextColumnLayout(layout);
+		updateTextFrame(textItem);
+		return;
+	}
+	const double oldGap = layout.gaps.at(column);
+	const double newGap = qBound(0.0, customGap->value() / m_unitRatio,
+		oldGap + qMax(0.0, widths.at(column + 1) - 0.01));
+	widths[column + 1] -= newGap - oldGap;
+	layout.gaps[column] = newGap;
+	double widthTotal = 0.0;
+	for (double width : std::as_const(widths))
+		widthTotal += width;
+	for (int i = 0; i < widths.size(); ++i)
+		layout.widthRatios[i] = widths.at(i) / widthTotal;
+	textItem->setTextColumnLayout(layout);
+	updateTextFrame(textItem);
+}
+
+void PropertyWidget_Distance::handleEqualGaps(bool enabled)
+{
+	PageItem_TextFrame* textItem = currentTextFrame();
+	if (!textItem || !textItem->hasCustomTextColumns())
+		return;
+	if (!enabled)
+	{
+		updateColumnControls(textItem);
+		return;
+	}
+
+	TextColumnLayout layout = textItem->textColumnLayout();
+	const QVector<TextColumnGeometry> geometry = textItem->columnGeometry();
+	if (geometry.size() < 3)
+		return;
+	QVector<double> widths;
+	QVector<double> gaps;
+	for (const TextColumnGeometry& value : geometry)
+		widths.append(value.width);
+	for (int i = 0; i + 1 < geometry.size(); ++i)
+		gaps.append(qMax(0.0, geometry.at(i + 1).left - geometry.at(i).left - geometry.at(i).width));
+	const int column = qBound(0, static_cast<int>(customColumn->value()) - 1, static_cast<int>(geometry.size()) - 1);
+	const double gap = gaps.at(qMin(column, static_cast<int>(gaps.size()) - 1));
+	std::fill(gaps.begin(), gaps.end(), gap);
+	double widthTotal = std::accumulate(widths.cbegin(), widths.cend(), 0.0);
+	for (int i = 0; i < widths.size(); ++i)
+		layout.widthRatios[i] = widths.at(i) / widthTotal;
+	layout.gaps = gaps;
+	textItem->setTextColumnLayout(layout);
+	updateTextFrame(textItem);
 }
 
 void PropertyWidget_Distance::handleTabs()
@@ -374,7 +706,7 @@
 	if (tItem == nullptr)
 		return;
 	const ParagraphStyle& style(m_doc->appMode == modeEdit ? tItem->currentStyle() : tItem->itemText.defaultStyle());
-	TabManager *dia = new TabManager(this, m_doc->unitIndex(), style.tabValues(), tItem->columnWidth());
+	TabManager *dia = new TabManager(this, m_doc->unitIndex(), style.tabValues(), minimumTextColumnWidth(tItem));
 	if (dia->exec())
 	{
 		if (m_doc->appMode != modeEdit)
@@ -467,17 +799,12 @@
 {
 	retranslateUi(this);
 
-	QSignalBlocker columnGapLabelBlocker(columnGapCombo);
-	int oldColGapLabel = columnGapCombo->currentIndex();
-	columnGapCombo->clear();
-	columnGapCombo->addItem( tr("Gap:"));
-	columnGapCombo->addItem( tr("Width:"));
-	columnGapCombo->setCurrentIndex(oldColGapLabel);
-
 	QString ptSuffix = tr(" pt");
 	QString suffix = (m_doc) ? unitGetSuffixFromIndex(m_doc->unitIndex()) : ptSuffix;
 
 	columnGap->setSuffix(suffix);
+	customWidth->setSuffix(suffix);
+	customGap->setSuffix(suffix);
 	firstLineIndentSpin->setSuffix(suffix);
 	leftIndentSpin->setSuffix(suffix);
 	rightIndentSpin->setSuffix(suffix);
@@ -489,6 +816,21 @@
 	leftIndentLabel->setToolTip(leftIndentSpin->toolTip());
 	rightIndentLabel->setToolTip(rightIndentSpin->toolTip());
 	tabsButton->setToolTip(tr("Edit tab settings of text frame..."));
+	columnLayout->setToolTip(tr("Use equal or individually sized columns"));
+	columnLayoutLabel->setToolTip(columnLayout->toolTip());
+	widthAdjustment->setToolTip(tr("How width changes are distributed"));
+	widthAdjustment->setItemData(0, tr("Take or return space from the adjacent column"), Qt::ToolTipRole);
+	widthAdjustment->setItemData(1, tr("Divide the remaining width equally among all other columns"), Qt::ToolTipRole);
+	widthAdjustmentLabel->setToolTip(widthAdjustment->toolTip());
+	equalGaps->setToolTip(tr("Use the selected gap value between all columns"));
+	columnGap->setToolTip(tr("Space between columns"));
+	columnGapLabel->setToolTip(columnGap->toolTip());
+	customColumn->setToolTip(tr("Physical column to edit, numbered from left to right"));
+	customColumnLabel->setToolTip(customColumn->toolTip());
+	customWidth->setToolTip(tr("Width of the selected column"));
+	customWidthLabel->setToolTip(customWidth->toolTip());
+	customGap->setToolTip(tr("Space after the selected column"));
+	customGapLabel->setToolTip(customGap->toolTip());
 	gapBeforeLabel->setToolTip(tr("Space above a paragraph"));
 	gapAfterLabel->setToolTip(tr("Space below a paragraph"));
 }
@@ -498,8 +840,10 @@
 	if (!m_doc)
 		return;
 
 	QSignalBlocker columnGapBlocker(columnGap);
+	QSignalBlocker customWidthBlocker(customWidth);
+	QSignalBlocker customGapBlocker(customGap);
 	QSignalBlocker distancesBlocker(distanceWidget);
 	QSignalBlocker numberFirstLineIndentBlocker(firstLineIndentSpin);
 	QSignalBlocker numberLeftIndentBlocker(leftIndentSpin);
 	QSignalBlocker numberRightIndentBlocker(rightIndentSpin);
@@ -508,6 +852,8 @@
 	m_unitIndex = m_doc->unitIndex();
 
 	columnGap->setNewUnit(m_unitIndex);
+	customWidth->setNewUnit(m_unitIndex);
+	customGap->setNewUnit(m_unitIndex);
 	distanceWidget->setNewUnit(m_unitIndex);
 	firstLineIndentSpin->setNewUnit(m_unitIndex);
 	leftIndentSpin->setNewUnit(m_unitIndex);
@@ -518,6 +862,9 @@
 {
 	const QLocale& l(LocaleManager::instance().userPreferredLocale());
 	columnGap->setLocale(l);
+	customColumn->setLocale(l);
+	customWidth->setLocale(l);
+	customGap->setLocale(l);
 	distanceWidget->setLocale(l);
 	firstLineIndentSpin->setLocale(l);
 	leftIndentSpin->setLocale(l);
@@ -528,9 +875,14 @@
 
 void PropertyWidget_Distance::toggleLabelVisibility(bool v)
 {
-	columnGapSizeLabel->setLabelVisibility(v);
 	columnGapLabel->setLabelVisibility(v);
 	columnsLabel->setLabelVisibility(v);
+	columnLayoutLabel->setLabelVisibility(v);
+	widthAdjustmentLabel->setLabelVisibility(v);
+	customColumnLabel->setLabelVisibility(v);
+	customWidthLabel->setLabelVisibility(v);
+	customGapLabel->setLabelVisibility(v);
+	equalGapsLabel->setLabelVisibility(v);
 	firstLineIndentLabel->setLabelVisibility(v);
 	leftIndentLabel->setLabelVisibility(v);
 	rightIndentLabel->setLabelVisibility(v);
Index: scribus/ui/textcolumnpreview.h
===================================================================
--- scribus/ui/textcolumnpreview.h	(nonexistent)
+++ scribus/ui/textcolumnpreview.h	(working copy)
@@ -0,0 +1,46 @@
+#ifndef TEXTCOLUMNPREVIEW_H
+#define TEXTCOLUMNPREVIEW_H
+
+#include <QAbstractScrollArea>
+#include <QVector>
+
+#include "scribusapi.h"
+
+class SCRIBUS_API TextColumnPreview : public QAbstractScrollArea
+{
+	Q_OBJECT
+
+public:
+	explicit TextColumnPreview(QWidget* parent = nullptr);
+	void setGeometry(const QVector<double>& widths, const QVector<double>& gaps);
+	int selectedColumn() const { return m_selectedColumn; }
+	void setSelectedColumn(int column);
+	void setEqualGaps(bool enabled);
+
+signals:
+	void selectedColumnChanged(int column);
+
+protected:
+	void keyPressEvent(QKeyEvent* event) override;
+	void mousePressEvent(QMouseEvent* event) override;
+	void mouseMoveEvent(QMouseEvent* event) override;
+	void leaveEvent(QEvent* event) override;
+	void paintEvent(QPaintEvent* event) override;
+	void resizeEvent(QResizeEvent* event) override;
+	void wheelEvent(QWheelEvent* event) override;
+
+private:
+	void updateScrollBar();
+	QRectF columnRect(int column) const;
+	int columnAt(const QPointF& position) const;
+	double contentScale() const;
+	int contentWidth() const;
+
+	QVector<double> m_widths;
+	QVector<double> m_gaps;
+	int m_selectedColumn { 0 };
+	int m_hoveredColumn { -1 };
+	bool m_equalGaps {false};
+};
+
+#endif
Index: scribus/ui/textcolumnpreview.cpp
===================================================================
--- scribus/ui/textcolumnpreview.cpp	(nonexistent)
+++ scribus/ui/textcolumnpreview.cpp	(working copy)
@@ -0,0 +1,183 @@
+#include "textcolumnpreview.h"
+
+#include <QKeyEvent>
+#include <QMouseEvent>
+#include <QPainter>
+#include <QScrollBar>
+#include <QWheelEvent>
+
+TextColumnPreview::TextColumnPreview(QWidget* parent) : QAbstractScrollArea(parent)
+{
+	setFocusPolicy(Qt::StrongFocus);
+	setMouseTracking(true);
+	setFrameShape(QFrame::StyledPanel);
+	setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
+	setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+	setMinimumHeight(58);
+	setAccessibleName(tr("Column layout preview"));
+}
+
+void TextColumnPreview::setEqualGaps(bool enabled)
+{
+	if (m_equalGaps == enabled)
+		return;
+	m_equalGaps = enabled;
+	viewport()->update();
+}
+
+void TextColumnPreview::setGeometry(const QVector<double>& widths, const QVector<double>& gaps)
+{
+	m_widths = widths;
+	m_gaps = gaps;
+	m_selectedColumn = qBound(0, m_selectedColumn, qMax(0, m_widths.size() - 1));
+	updateScrollBar();
+	viewport()->update();
+}
+
+void TextColumnPreview::setSelectedColumn(int column)
+{
+	column = qBound(0, column, qMax(0, m_widths.size() - 1));
+	const bool changed = column != m_selectedColumn;
+	m_selectedColumn = column;
+	setAccessibleDescription(tr("Column %1 of %2 selected").arg(column + 1).arg(m_widths.size()));
+	const QRectF rect = columnRect(column);
+	horizontalScrollBar()->setValue(qBound(horizontalScrollBar()->minimum(),
+		static_cast<int>(rect.center().x() - viewport()->width() / 2.0), horizontalScrollBar()->maximum()));
+	viewport()->update();
+	if (changed)
+		emit selectedColumnChanged(column);
+}
+
+int TextColumnPreview::contentWidth() const
+{
+	return qMax(viewport()->width() - 12, m_widths.size() * 32);
+}
+
+double TextColumnPreview::contentScale() const
+{
+	double total = 0.0;
+	for (double width : m_widths) total += width;
+	for (double gap : m_gaps) total += gap;
+	return total > 0.0 ? contentWidth() / total : 1.0;
+}
+
+QRectF TextColumnPreview::columnRect(int column) const
+{
+	double x = 6.0 - horizontalScrollBar()->value();
+	const double scale = contentScale();
+	for (int i = 0; i < column; ++i)
+		x += (m_widths.value(i) + m_gaps.value(i)) * scale;
+	return QRectF(x, 7, m_widths.value(column) * scale, qMax(1, viewport()->height() - 14));
+}
+
+int TextColumnPreview::columnAt(const QPointF& position) const
+{
+	for (int i = 0; i < m_widths.size(); ++i)
+	{
+		const QRectF rect = columnRect(i);
+		if (rect.adjusted(-3, 0, 3, 0).contains(position))
+			return i;
+		if (i < m_gaps.size())
+		{
+			const double gapWidth = qMax(2.0, m_gaps.at(i) * contentScale());
+			if (QRectF(rect.right(), rect.top(), gapWidth, rect.height()).contains(position))
+				return i;
+		}
+	}
+	return -1;
+}
+
+void TextColumnPreview::updateScrollBar()
+{
+	const int overflow = qMax(0, contentWidth() - qMax(0, viewport()->width() - 12));
+	horizontalScrollBar()->setRange(0, overflow);
+	horizontalScrollBar()->setPageStep(viewport()->width());
+}
+
+void TextColumnPreview::paintEvent(QPaintEvent*)
+{
+	QPainter painter(viewport());
+	painter.fillRect(viewport()->rect(), palette().brush(QPalette::Window));
+	for (int i = 0; i < m_widths.size(); ++i)
+	{
+		QRectF rect = columnRect(i);
+		painter.fillRect(rect, palette().brush(QPalette::Base));
+		if (i == m_selectedColumn)
+		{
+			QColor fill = palette().color(QPalette::Highlight); fill.setAlphaF(0.28);
+			painter.fillRect(rect, fill);
+		}
+		QColor outline = palette().color(i == m_selectedColumn || i == m_hoveredColumn ? QPalette::Highlight : QPalette::Text);
+		if (i == m_selectedColumn)
+			outline.setAlphaF(0.58);
+		else if (i == m_hoveredColumn)
+			outline.setAlphaF(0.30);
+		else
+			outline.setAlphaF(0.28);
+		painter.setPen(outline);
+		painter.drawRect(rect.adjusted(0, 0, -1, -1));
+		if (i < m_gaps.size() && (m_equalGaps || i == m_selectedColumn))
+		{
+			QColor fill = palette().color(QPalette::Highlight);
+			fill.setAlphaF(i == m_selectedColumn ? 0.34 : 0.19);
+			const double gapWidth = qMax(2.0, m_gaps.at(i) * contentScale());
+			painter.fillRect(QRectF(rect.right(), rect.top(), gapWidth, rect.height()), fill);
+		}
+	}
+}
+
+void TextColumnPreview::mousePressEvent(QMouseEvent* event)
+{
+	const int column = columnAt(event->position());
+	if (column >= 0)
+		setSelectedColumn(column);
+	setFocus();
+}
+
+void TextColumnPreview::mouseMoveEvent(QMouseEvent* event)
+{
+	const int column = columnAt(event->position());
+	if (column != m_hoveredColumn)
+	{
+		m_hoveredColumn = column;
+		viewport()->setCursor(column >= 0 ? Qt::PointingHandCursor : Qt::ArrowCursor);
+		setToolTip(column >= 0 ? tr("Column %1 — click to edit").arg(column + 1) : QString());
+		viewport()->update();
+	}
+}
+
+void TextColumnPreview::leaveEvent(QEvent* event)
+{
+	m_hoveredColumn = -1;
+	viewport()->setCursor(Qt::ArrowCursor);
+	viewport()->update();
+	QAbstractScrollArea::leaveEvent(event);
+}
+
+void TextColumnPreview::keyPressEvent(QKeyEvent* event)
+{
+	int column = m_selectedColumn;
+	if (event->key() == Qt::Key_Left) --column;
+	else if (event->key() == Qt::Key_Right) ++column;
+	else if (event->key() == Qt::Key_Home) column = 0;
+	else if (event->key() == Qt::Key_End) column = m_widths.size() - 1;
+	else { QAbstractScrollArea::keyPressEvent(event); return; }
+	setSelectedColumn(column);
+}
+
+void TextColumnPreview::wheelEvent(QWheelEvent* event)
+{
+	if (horizontalScrollBar()->maximum() <= 0)
+	{
+		event->ignore();
+		return;
+	}
+	horizontalScrollBar()->setValue(horizontalScrollBar()->value() - event->angleDelta().y());
+	event->accept();
+}
+
+void TextColumnPreview::resizeEvent(QResizeEvent* event)
+{
+	QAbstractScrollArea::resizeEvent(event);
+	updateScrollBar();
+}
Index: scribus/ui/hruler.h
===================================================================
--- scribus/ui/hruler.h	(revision 27829)
+++ scribus/ui/hruler.h	(working copy)
@@ -26,6 +26,7 @@
 
 #include <QWidget>
 
+#include "pageitem.h"
 #include "scribusapi.h"
 #include "styles/paragraphstyle.h"
 
@@ -110,7 +111,7 @@
 	bool m_reverse {false};
 	bool m_rtl {false};
 	bool m_textEditMode {false};
-	double m_colGap {0.0};
+	QVector<TextColumnGeometry> m_columnGeometry;
 	double m_cor {0.0};
 	double m_distLeft {0.0};
 	double m_distRight {0.0};
@@ -125,7 +126,6 @@
 	double m_offset {0.0};
 	double m_rightMargin {0.0};
 	double m_scaling {0.0};
-	int m_cols {0};
 	int m_currCol {1};
 	int m_currTab {0};
 	int m_mouseX {0};
Index: scribus/ui/hruler.cpp
===================================================================
--- scribus/ui/hruler.cpp	(revision 27829)
+++ scribus/ui/hruler.cpp	(working copy)
@@ -22,6 +22,7 @@
  ***************************************************************************/
 
 #include <cmath>
+#include <utility>
 
 #include <QCursor>
 #include <QMouseEvent>
@@ -100,11 +101,12 @@
 double Hruler::localToTextPos(int x) const
 {
 	double canvasPos = (x + m_view->contentsX()) / m_scaling;
-	if (m_rtl && m_currItem)
+	if (m_currItem && !m_columnGeometry.isEmpty() && m_currCol > 0)
 	{
-		double colWidth = (textWidth() - m_colGap * (m_cols - 1)) / m_cols;
-		double colEnd = textBase() + (colWidth + m_colGap) * (m_currCol - 1.0) + colWidth;
-		return colEnd - canvasPos;
+		const TextColumnGeometry& column = m_columnGeometry.at(qBound(0, m_currCol - 1, static_cast<int>(m_columnGeometry.size()) - 1));
+		if (m_rtl)
+			return textBase() + column.left + column.width - canvasPos;
+		return canvasPos - textBase() - column.left;
 	}
 	return canvasPos - textBase();
 }
@@ -139,13 +141,12 @@
 	if (pos + 1 < (mx + grabRadius) && pos + 1 > (mx - grabRadius))
 		result = rc_rightFrameDist;
 	
-	double colWidth = (textWidth() - m_colGap * (m_cols - 1)) / m_cols;
-	
 	m_currCol = 0;
-	for (int currCol = 0; currCol < m_cols; ++currCol)
+	for (int currCol = 0; currCol < m_columnGeometry.size(); ++currCol)
 	{
-		pos = textPosToLocal((colWidth + m_colGap) * currCol);
-		fpo = QRect(pos, topline, static_cast<int>(colWidth * m_scaling), rulerheight);
+		const TextColumnGeometry& column = m_columnGeometry.at(currCol);
+		pos = textPosToLocal(column.left);
+		fpo = QRect(pos, topline, static_cast<int>(column.width * m_scaling), rulerheight);
 		if (fpo.contains(mp))
 		{
 			m_currCol = currCol + 1;
@@ -153,12 +154,11 @@
 		}
 	}
 	if (m_currCol == 0)
-	{
-		m_currCol = 1;
 		return result;
-	}
 	
-	double colOffset = (colWidth + m_colGap) * (m_currCol - 1.0);
+	const TextColumnGeometry& column = m_columnGeometry.at(m_currCol - 1);
+	double colOffset = column.left;
+	double colWidth = column.width;
 	double colEnd    = colOffset + colWidth;
 
 	// Map a logical value (distance from logical start) to canvas text position
@@ -179,7 +179,7 @@
 		return rc_leftMargin;
 
 	// Right Margin handle — rightMargin is always from the logical end side
-	pos = textPosToLocal(colOffset + m_rightMargin);
+	pos = textPosToLocal(logicalToCanvas(colWidth - m_rightMargin));
 	fpo = QRect(m_rtl ? (pos - 1) : (pos - grabRadius), midline, grabRadius, rulerheight / 2);
 	if (fpo.contains(mp))
 		return rc_rightMargin;
@@ -254,7 +254,6 @@
 		{
 			bool mustApplyStyle = false;
 			ParagraphStyle paraStyle;
-			double colWidth = (textWidth() - m_colGap * (m_cols - 1)) / m_cols;
 			switch (m_rulerCode)
 			{
 				case rc_leftFrameDist:
@@ -277,7 +276,7 @@
 					emit DocChanged(false);
 					break;
 				case rc_rightMargin:
-					paraStyle.setRightMargin(colWidth - m_rightMargin);
+					paraStyle.setRightMargin(m_rightMargin);
 					mustApplyStyle = true;
 					emit DocChanged(false);
 					break;
@@ -344,7 +343,8 @@
 	if (m_textEditMode)
 	{
 		int colStart, colEnd;
-		double colWidth = (textWidth() - m_colGap * (m_cols - 1)) / m_cols;
+		const TextColumnGeometry& column = m_columnGeometry.at(qBound(0, m_currCol - 1, static_cast<int>(m_columnGeometry.size()) - 1));
+		double colWidth = column.width;
 		double oldInd;
 		if (m_rulerCode == rc_leftFrameDist || m_rulerCode == rc_rightFrameDist)
 		{
@@ -353,15 +353,19 @@
 		}
 		else
 		{
-			colStart = textPosToLocal((colWidth + m_colGap) * (m_currCol - 1.0));
-			colEnd   = textPosToLocal((colWidth + m_colGap) * (m_currCol - 1.0) + colWidth);
+			colStart = textPosToLocal(column.left);
+			colEnd   = textPosToLocal(column.left + column.width);
 		}
 		QPointF mousePos = m->position();
 		if (m_mousePressed && (mousePos.y() < height()) && (mousePos.y() > 0) && (mousePos.x() > colStart - m_doc->guidesPrefs().grabRadius) && (mousePos.x() < colEnd + m_doc->guidesPrefs().grabRadius))
 		{
 			QApplication::changeOverrideCursor(QCursor(Qt::SizeHorCursor));
-			double toplimit = textWidth() + m_distRight - (m_colGap * (m_cols - 1.0)) - 1.0;
-			double toplimit2 = textWidth() + m_distLeft - (m_colGap * (m_cols - 1.0)) - 1.0;
+			double totalColumnWidth = 0.0;
+			for (const TextColumnGeometry& value : std::as_const(m_columnGeometry))
+				totalColumnWidth += value.width;
+			const double totalGap = qMax(0.0, textWidth() - totalColumnWidth);
+			double toplimit = textWidth() + m_distRight - totalGap - 1.0;
+			double toplimit2 = textWidth() + m_distLeft - totalGap - 1.0;
 			// In RTL dragging right moves away from the logical start (right edge),
 			// so negate the delta for all logical-position markers.
 			double dx = (m_mouseX - mousePos.x()) / m_scaling;
@@ -370,11 +374,11 @@
 			// canvas-relative position (distance from textBase) for MarkerMoved.
 			// In LTR: canvasRel = logicalValue (distance from left edge)
 			// In RTL:  canvasRel = colEnd - textBase() - logicalValue
-			double colOffset = (colWidth + m_colGap) * (m_currCol - 1.0);
+			double colOffset = column.left;
 			auto toCanvasRel = [&](double logicalValue) -> double {
 				if (m_rtl)
 					return colOffset + colWidth - logicalValue;
-				return logicalValue;
+				return colOffset + logicalValue;
 			};
 			switch (m_rulerCode)
 			{
@@ -417,12 +421,12 @@
 					repaint();
 					break;
 				case rc_rightMargin:
-					m_rightMargin -= logicalDx;
+					m_rightMargin += logicalDx;
 					if (m_rightMargin < 0)
 						m_rightMargin = 0;
 					if (m_rightMargin > colWidth - 1)
 						m_rightMargin  = colWidth - 1;
-					emit MarkerMoved(textBase(), toCanvasRel(m_rightMargin));
+					emit MarkerMoved(textBase(), toCanvasRel(colWidth - m_rightMargin));
 					repaint();
 					break;
 				case rc_tab:
@@ -499,7 +503,10 @@
 			return;
 
 		const ParagraphStyle& style(m_doc->appMode == modeEdit ? tItem->currentStyle() : tItem->itemText.defaultStyle());
-		TabManager *dia = new TabManager(this, m_doc->unitIndex(), style.tabValues(), tItem->columnWidth());
+		double minimumColumnWidth = 0.0;
+		for (const TextColumnGeometry& column : tItem->columnGeometry())
+			minimumColumnWidth = minimumColumnWidth == 0.0 ? column.width : qMin(minimumColumnWidth, column.width);
+		TabManager *dia = new TabManager(this, m_doc->unitIndex(), style.tabValues(), minimumColumnWidth);
 		dia->setRtl(m_rtl);
 		if (dia->exec())
 		{
@@ -551,10 +558,10 @@
 			p.translate( textPosToLocal(m_distLeft) - textPosToLocal(m_itemEndPos - m_itemPos - m_distRight), 0);
 			p.translate(-textPosToLocal(0), 0);
 		}
-		for (int currCol = 0; currCol < m_cols; ++currCol)
+		for (const TextColumnGeometry& column : std::as_const(m_columnGeometry))
 		{
-			double colWidth = (textWidth() - m_colGap * (m_cols - 1.0)) / m_cols;
-			double pos = (colWidth + m_colGap) * currCol;
+			double colWidth = column.width;
+			double pos = column.left;
 			double endPos = pos + colWidth;
 
 			p.fillRect(QRect(QPoint(textPosToLocal(pos), 0), QPoint(textPosToLocal(endPos), bottomline)), backgroundColor);
@@ -587,7 +594,7 @@
 				p.drawPolygon(cr2);
 
 				// Right Margin (logical end = left side), triangle opens right
-				xPos = textPosToLocal(endPos - m_rightMargin);
+				xPos = textPosToLocal(pos + m_rightMargin);
 				QPolygon cr3;
 				cr3.setPoints(3, xPos, tabline, xPos + 8, tabline, xPos, bottomline);
 				p.drawPolygon(cr3);
@@ -661,7 +668,7 @@
 				p.drawPolygon(cr2);
 
 				// Right Margin
-				xPos = textPosToLocal(pos + m_rightMargin);
+				xPos = textPosToLocal(endPos - m_rightMargin);
 				QPolygon cr3;
 				cr3.setPoints(3, xPos - 8, tabline, xPos, tabline, xPos, bottomline);
 				p.drawPolygon(cr3);
@@ -972,18 +979,23 @@
 		m_lineCorr = item->lineWidth() / 2.0;
 	else
 		m_lineCorr = 0;
-	m_colGap = item->m_columnGap;
-	m_cols = item->m_columns;
 	m_distLeft  = item->textToFrameDistLeft();
 	m_distRight = item->textToFrameDistRight();
+	m_columnGeometry.clear();
+	if (const PageItem_TextFrame* textFrame = item->asTextFrame())
+	{
+		m_columnGeometry = textFrame->columnGeometry();
+		const double contentLeft = m_distLeft + m_lineCorr;
+		for (TextColumnGeometry& column : m_columnGeometry)
+			column.left -= contentLeft;
+	}
+	if (m_columnGeometry.isEmpty())
+		m_columnGeometry.append(TextColumnGeometry { 0.0, textWidth() });
 
 	const ParagraphStyle& currentStyle = item->currentStyle();
 	m_firstIndent = currentStyle.firstIndent();
 	m_leftMargin = currentStyle.leftMargin();
-	double columnWidth = (item->width() - (item->columnGap() * (item->columns() - 1.0))
-						  - item->textToFrameDistLeft() - item->textToFrameDistLeft()
-						  - 2 * m_lineCorr) / item->columns();
-	m_rightMargin = columnWidth - currentStyle.rightMargin();
+	m_rightMargin = currentStyle.rightMargin();
 	m_reverse = item->imageFlippedH();
 	setRtl(item->currentStyle().direction() == ParagraphStyle::RTL);
 	m_textEditMode = true;
Index: resources/iconsets/1_7_0/1_7_0.xml
===================================================================
--- resources/iconsets/1_7_0/1_7_0.xml	(revision 27829)
+++ resources/iconsets/1_7_0/1_7_0.xml	(working copy)
@@ -545,6 +545,7 @@
 		<icon id="paragraph-align-top" file="16/paragraph-align-top.svg" />
 		<icon id="paragraph-align-vertical-center" file="16/paragraph-align-vertical-center.svg" />
 		<icon id="paragraph-columns" file="16/paragraph-columns.svg" />
+		<icon id="paragraph-column-gap" file="16/paragraph-column-gap.svg" />
 		<icon id="paragraph-effects-style" file="16/paragraph-effects-style.svg" />
 		<icon id="paragraph-indent-firstline" file="16/paragraph-firstline.svg" />
 		<icon id="paragraph-indent-left" file="16/paragraph-indent-left.svg" />	
Index: resources/iconsets/1_7_0/16/paragraph-column-gap.svg
===================================================================
--- resources/iconsets/1_7_0/16/paragraph-column-gap.svg	(nonexistent)
+++ resources/iconsets/1_7_0/16/paragraph-column-gap.svg	(working copy)
@@ -0,0 +1,4 @@
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><style>@import '../colors.css';</style>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M1 2h5v12H1V2Zm1 1v10h3V3H2Zm8-1h5v12h-5V2Zm1 1v10h3V3h-3Z"/>
+<path d="M7 7h2v2H7V7Z" class="error"/>
+</svg>

buttar

2026-09-14 05:41

reporter   ~0054466

Here is a small demo

@cbradney, @ale, @jghali, please test.

buttar

2026-09-14 05:44

reporter   ~0054467

Sorry on the demo, cannot upload file larger than 2MB.

buttar

2026-09-14 08:01

reporter   ~0054469

A tiny change to fix the preview height.
cc3-custom-text-column-editor-v1.06.patch (59,822 bytes)   
Index: scribus/CMakeLists_Sources.txt
===================================================================
--- scribus/CMakeLists_Sources.txt	(revision 27829)
+++ scribus/CMakeLists_Sources.txt	(working copy)
@@ -11,6 +11,7 @@
 	)
 endif()
 
+
 if (GMAGICK_FOUND)
 	set(SCRIBUS_GMAGICK_SRC imagedataloaders/scimgdataloader_gmagick.cpp)
 else()
@@ -473,6 +474,7 @@
 	ui/propertywidget_advanced.cpp
 	ui/propertywidget_alignment.cpp
 	ui/propertywidget_distance.cpp
+	ui/textcolumnpreview.cpp
 	ui/propertywidget_paragraphstyle.cpp
 	ui/propertywidget_pareffect.cpp
 	ui/propertywidget_hyphenation.cpp
@@ -626,5 +628,3 @@
 	set(SCRIBUS_WIN32_ONLY_SOURCES)
 	set(SCRIBUS_MAIN_CPP main.cpp)
 endif()
-
-
Index: scribus/ui/propertywidget_distancebase.ui
===================================================================
--- scribus/ui/propertywidget_distancebase.ui	(revision 27829)
+++ scribus/ui/propertywidget_distancebase.ui	(working copy)
@@ -47,13 +47,36 @@
        </property>
       </spacer>
      </item>
-    </layout>
+   </layout>
    </item>
    <item>
-    <layout class="QHBoxLayout" name="horizontalLayout">
+    <widget class="QWidget" name="columnControlsWidget" native="true">
+     <layout class="QHBoxLayout" name="horizontalLayout">
      <property name="spacing">
-      <number>4</number>
+      <number>8</number>
      </property>
+     <property name="leftMargin"><number>0</number></property>
+     <property name="topMargin"><number>0</number></property>
+     <property name="rightMargin"><number>0</number></property>
+     <property name="bottomMargin"><number>0</number></property>
+     <item>
+      <widget class="FormWidget" name="columnLayoutLabel">
+       <property name="label" stdset="0"><string>&amp;Layout</string></property>
+       <layout class="QHBoxLayout" name="columnLayoutControlLayout">
+        <property name="leftMargin"><number>0</number></property>
+        <property name="topMargin"><number>0</number></property>
+        <property name="rightMargin"><number>0</number></property>
+        <property name="bottomMargin"><number>0</number></property>
+        <item>
+         <widget class="QComboBox" name="columnLayout">
+          <property name="sizeAdjustPolicy"><enum>QComboBox::SizeAdjustPolicy::AdjustToContents</enum></property>
+          <item><property name="text"><string>Equal</string></property></item>
+          <item><property name="text"><string>Custom</string></property></item>
+         </widget>
+        </item>
+       </layout>
+      </widget>
+     </item>
      <item>
       <widget class="FormWidget" name="columnsLabel">
        <property name="label" stdset="0">
@@ -89,90 +112,32 @@
       </widget>
      </item>
      <item>
-      <spacer name="horizontalSpacer_4">
-       <property name="orientation">
-        <enum>Qt::Orientation::Horizontal</enum>
-       </property>
-       <property name="sizeType">
-        <enum>QSizePolicy::Policy::Fixed</enum>
-       </property>
-       <property name="sizeHint" stdset="0">
-        <size>
-         <width>8</width>
-         <height>0</height>
-        </size>
-       </property>
-      </spacer>
-     </item>
-     <item>
-      <widget class="FormWidget" name="columnGapLabel">
-       <property name="label" stdset="0">
-        <string/>
-       </property>
-       <property name="preserveLabelSpace">
-        <bool>true</bool>
-       </property>
-       <layout class="QHBoxLayout" name="horizontalLayout_8">
-        <property name="leftMargin">
-         <number>0</number>
-        </property>
-        <property name="topMargin">
-         <number>0</number>
-        </property>
-        <property name="rightMargin">
-         <number>0</number>
-        </property>
-        <property name="bottomMargin">
-         <number>0</number>
-        </property>
+      <widget class="FormWidget" name="widthAdjustmentLabel">
+       <property name="label" stdset="0"><string>Width adjustment</string></property>
+       <layout class="QHBoxLayout" name="widthAdjustmentLayout">
+        <property name="leftMargin"><number>0</number></property>
+        <property name="topMargin"><number>0</number></property>
+        <property name="rightMargin"><number>0</number></property>
+        <property name="bottomMargin"><number>0</number></property>
         <item>
-         <widget class="QComboBox" name="columnGapCombo">
-          <property name="toolTip">
-           <string>Switches between Gap or Column width</string>
-          </property>
-          <property name="sizeAdjustPolicy">
-           <enum>QComboBox::SizeAdjustPolicy::AdjustToContents</enum>
-          </property>
-          <item>
-           <property name="text">
-            <string>Gap:</string>
-           </property>
-          </item>
-          <item>
-           <property name="text">
-            <string>Width:</string>
-           </property>
-          </item>
+         <widget class="QComboBox" name="widthAdjustment">
+          <property name="sizeAdjustPolicy"><enum>QComboBox::SizeAdjustPolicy::AdjustToContents</enum></property>
+          <item><property name="text"><string>Adjacent</string></property></item>
+          <item><property name="text"><string>All others</string></property></item>
          </widget>
         </item>
        </layout>
       </widget>
      </item>
      <item>
-      <widget class="FormWidget" name="columnGapSizeLabel">
-       <property name="label" stdset="0">
-        <string>Size</string>
-       </property>
-       <layout class="QHBoxLayout" name="horizontalLayout_7">
-        <property name="leftMargin">
-         <number>0</number>
-        </property>
-        <property name="topMargin">
-         <number>0</number>
-        </property>
-        <property name="rightMargin">
-         <number>0</number>
-        </property>
-        <property name="bottomMargin">
-         <number>0</number>
-        </property>
-        <item>
-         <widget class="ScrSpinBox" name="columnGap">
-          <property name="toolTip">
-           <string>Distance between columns</string>
-          </property>
-         </widget>
-        </item>
+      <widget class="FormWidget" name="columnGapLabel">
+       <property name="label" stdset="0"><string>Gap</string></property>
+       <layout class="QHBoxLayout" name="columnGapLayout">
+        <property name="leftMargin"><number>0</number></property>
+        <property name="topMargin"><number>0</number></property>
+        <property name="rightMargin"><number>0</number></property>
+        <property name="bottomMargin"><number>0</number></property>
+        <item><widget class="ScrSpinBox" name="columnGap"/></item>
        </layout>
       </widget>
      </item>
@@ -189,7 +154,106 @@
        </property>
       </spacer>
      </item>
-    </layout>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <widget class="TextColumnPreview" name="columnPreview"/>
+   </item>
+   <item>
+    <widget class="QWidget" name="customColumnsWidget" native="true">
+     <layout class="QGridLayout" name="customColumnsLayout">
+      <property name="horizontalSpacing">
+       <number>8</number>
+      </property>
+      <property name="verticalSpacing">
+       <number>4</number>
+      </property>
+      <property name="leftMargin">
+       <number>0</number>
+      </property>
+      <property name="topMargin">
+       <number>0</number>
+      </property>
+      <property name="rightMargin">
+       <number>0</number>
+      </property>
+      <property name="bottomMargin">
+       <number>0</number>
+      </property>
+      <item row="1" column="0">
+       <widget class="FormWidget" name="customColumnLabel">
+        <property name="visible"><bool>false</bool></property>
+        <property name="label" stdset="0">
+         <string>Column</string>
+        </property>
+        <layout class="QHBoxLayout" name="customColumnControlLayout">
+         <property name="leftMargin"><number>0</number></property>
+         <property name="topMargin"><number>0</number></property>
+         <property name="rightMargin"><number>0</number></property>
+         <property name="bottomMargin"><number>0</number></property>
+         <item>
+          <widget class="ScrSpinBox" name="customColumn">
+           <property name="minimum"><double>1.000000000000000</double></property>
+	           <property name="decimals"><number>0</number></property>
+          </widget>
+         </item>
+        </layout>
+       </widget>
+      </item>
+      <item row="0" column="0">
+       <widget class="FormWidget" name="customWidthLabel">
+        <property name="label" stdset="0">
+         <string>Width</string>
+        </property>
+        <layout class="QHBoxLayout" name="customWidthControlLayout">
+         <property name="leftMargin"><number>0</number></property>
+         <property name="topMargin"><number>0</number></property>
+         <property name="rightMargin"><number>0</number></property>
+         <property name="bottomMargin"><number>0</number></property>
+         <item><widget class="ScrSpinBox" name="customWidth"/></item>
+        </layout>
+       </widget>
+      </item>
+      <item row="0" column="1">
+       <widget class="FormWidget" name="customGapLabel">
+        <property name="label" stdset="0">
+         <string>Gap</string>
+        </property>
+        <layout class="QHBoxLayout" name="customGapControlLayout">
+         <property name="leftMargin"><number>0</number></property>
+         <property name="topMargin"><number>0</number></property>
+         <property name="rightMargin"><number>0</number></property>
+         <property name="bottomMargin"><number>0</number></property>
+         <item><widget class="ScrSpinBox" name="customGap"/></item>
+        </layout>
+       </widget>
+      </item>
+      <item row="0" column="2">
+       <widget class="FormWidget" name="equalGapsLabel">
+        <property name="label" stdset="0"><string/></property>
+        <property name="preserveLabelSpace"><bool>true</bool></property>
+        <layout class="QHBoxLayout" name="equalGapsLayout">
+         <property name="leftMargin"><number>0</number></property>
+         <property name="topMargin"><number>0</number></property>
+         <property name="rightMargin"><number>0</number></property>
+         <property name="bottomMargin"><number>0</number></property>
+         <item>
+          <widget class="QCheckBox" name="equalGaps">
+           <property name="text"><string>Equal gaps</string></property>
+          </widget>
+         </item>
+        </layout>
+       </widget>
+      </item>
+      <item row="0" column="3">
+       <spacer name="customColumnsSpacer">
+        <property name="orientation"><enum>Qt::Orientation::Horizontal</enum></property>
+        <property name="sizeHint" stdset="0"><size><width>0</width><height>0</height></size></property>
+       </spacer>
+      </item>
+     </layout>
+    </widget>
    </item>
    <item>
     <layout class="QGridLayout" name="gridLayout">
@@ -376,6 +440,11 @@
  </widget>
  <customwidgets>
   <customwidget>
+   <class>TextColumnPreview</class>
+   <extends>QAbstractScrollArea</extends>
+   <header>ui/textcolumnpreview.h</header>
+  </customwidget>
+  <customwidget>
    <class>FormWidget</class>
    <extends>QWidget</extends>
    <header>ui/widgets/form_widget.h</header>
Index: scribus/ui/propertywidget_distance.h
===================================================================
--- scribus/ui/propertywidget_distance.h	(revision 27829)
+++ scribus/ui/propertywidget_distance.h	(working copy)
@@ -14,6 +14,7 @@
 
 class ScribusDoc;
 class ScribusMainWindow;
+class PageItem_TextFrame;
 
 class PropertyWidget_Distance : public QFrame, public Ui::PropertyWidget_DistanceBase,
                                 public PropertyWidgetBase
@@ -31,10 +32,15 @@
 	double m_unitRatio {1.0};
 	int m_unitIndex {SC_PT};
 	PageItem *m_item {nullptr};
+	PageItem_TextFrame *m_columnItem {nullptr};
+	bool m_initializeGapMode {true};
 	ScribusMainWindow* m_ScMW {nullptr};
 
 	void configureWidgets();
 	void setCurrentItem(PageItem *item);
+	PageItem_TextFrame* currentTextFrame() const;
+	void updateColumnControls(PageItem_TextFrame* textItem);
+	void updateTextFrame(PageItem_TextFrame* textItem);
 
 	void changeEvent(QEvent *e) override;
 
@@ -59,7 +65,12 @@
 private slots:
 	void handleColumns();
 	void handleColumnGap();
-	void handleGapSwitch();
+	void handleColumnLayout(int index);
+	void handleCustomColumn();
+	void handlePreviewColumn(int column);
+	void handleCustomWidth();
+	void handleCustomGap();
+	void handleEqualGaps(bool enabled);
 	void handleTabs();
 	void handleTextDistances();
 	void handleIndents();
Index: scribus/ui/propertywidget_distance.cpp
===================================================================
--- scribus/ui/propertywidget_distance.cpp	(revision 27829)
+++ scribus/ui/propertywidget_distance.cpp	(working copy)
@@ -7,6 +7,10 @@
 
 #include "propertywidget_distance.h"
 
+#include <algorithm>
+#include <numeric>
+#include <utility>
+
 #include <QSignalBlocker>
 
 #include "appmodehelper.h"
@@ -18,9 +22,35 @@
 #include "scribus.h"
 #include "scribusapp.h"
 #include "scribusdoc.h"
+#include "scribusview.h"
 #include "selection.h"
+#include "hruler.h"
 #include "tabmanager.h"
 
+namespace
+{
+	bool gapsAreEqual(const QVector<double>& gaps)
+	{
+		if (gaps.size() < 2)
+			return true;
+		for (int i = 1; i < gaps.size(); ++i)
+		{
+			const double tolerance = 1e-7 * qMax(1.0, qMax(qAbs(gaps.at(0)), qAbs(gaps.at(i))));
+			if (qAbs(gaps.at(i) - gaps.at(0)) > tolerance)
+				return false;
+		}
+		return true;
+	}
+
+	double minimumTextColumnWidth(const PageItem_TextFrame* item)
+	{
+		double width = 0.0;
+		for (const TextColumnGeometry& column : item->columnGeometry())
+			width = width == 0.0 ? column.width : qMin(width, column.width);
+		return width;
+	}
+}
+
 PropertyWidget_Distance::PropertyWidget_Distance(QWidget* parent) : QFrame(parent)
 {
 	setupUi(this);
@@ -30,6 +60,24 @@
 	columnGap->setValues(0, 300, 2, 0);
 	columns->setDecimals(0);
 	columns->setSuffix("");
+	customColumn->setValues(1, 3000, 0, 1);
+	customColumn->setSuffix("");
+	customWidth->setValues(0.01, 1000000, 2, 0);
+	customGap->setValues(0, 1000000, 2, 0);
+	customWidth->setToolTip(tr("Width of the selected column"));
+	customGap->setToolTip(tr("Space after the selected column"));
+	customColumnsWidget->setVisible(false);
+	columnPreview->setVisible(false);
+	widthAdjustmentLabel->setVisible(false);
+	equalGaps->setVisible(false);
+	setTabOrder(columnLayout, columns);
+	setTabOrder(columns, widthAdjustment);
+	setTabOrder(widthAdjustment, columnGap);
+	setTabOrder(columnGap, columnPreview);
+	setTabOrder(columnPreview, customWidth);
+	setTabOrder(customWidth, customGap);
+	setTabOrder(customGap, equalGaps);
+	setTabOrder(equalGaps, tabsButton);
 
 	MarginStruct distances;
 
@@ -48,7 +96,7 @@
 	iconSetChange();
 	languageChange();
 
-	columnGapCombo->setCurrentIndex(0);
+	columnLayout->setCurrentIndex(0);
 
 	connect(ScQApp, SIGNAL(iconSetChanged()), this, SLOT(iconSetChange()));
 	connect(ScQApp, SIGNAL(localeChanged()), this, SLOT(localeChange()));
@@ -88,6 +136,9 @@
 
 	columns->setDecimals(0);
 	columnGap->setDecimals(2);
+	customColumn->setDecimals(0);
+	customWidth->setDecimals(2);
+	customGap->setDecimals(2);
 
 	connect(m_doc->m_Selection, SIGNAL(selectionChanged()), this, SLOT(handleSelectionChanged()));
 	connect(m_doc             , SIGNAL(docChanged())      , this, SLOT(handleSelectionChanged()));
@@ -136,25 +187,18 @@
 	if (m_doc->appMode == modeEditTable)
 		textItem = m_item->asTable()->activeCell().textFrame();
 	if (!textItem) return;
+	if (textItem != m_columnItem)
+	{
+		m_columnItem = textItem;
+		m_initializeGapMode = true;
+		customColumn->setValue(1);
+	}
 
 	ParagraphStyle parStyle =  m_item->itemText.defaultStyle();
 	if (m_doc->appMode == modeEdit || m_doc->appMode == modeEditTable)
 		m_item->currentTextProps(parStyle);
 
-	//#14427: columns->setMaximum(qMax(qRound(textItem->width() / qMax(textItem->ColGap, 10.0)), 1));
-	columns->setMinimum(1);
-	columns->setValue(textItem->m_columns);
-	columnGap->setMinimum(0);
-	if (columnGapCombo->currentIndex() == 0)
-	{
-		columnGap->setMaximum(qMax((textItem->width() / textItem->m_columns - textItem->textToFrameDistLeft() - textItem->textToFrameDistRight()) * m_unitRatio, 0.0));
-		columnGap->setValue(textItem->m_columnGap * m_unitRatio);
-	}
-	else
-	{
-		columnGap->setMaximum(qMax((textItem->width() / textItem->m_columns) * m_unitRatio, 0.0));
-		columnGap->setValue(textItem->columnWidth() * m_unitRatio);
-	}
+	updateColumnControls(textItem);
 
 	MarginStruct distances;
 	distances.set(
@@ -167,9 +211,6 @@
 	distanceWidget->setPageWidth(m_item->width());
 	distanceWidget->setNewValues(distances);
 
-	columnGapLabel->setVisible(columns->value() != 1);
-	columnGapSizeLabel->setVisible(columns->value() != 1);
-
 	updateStyle(parStyle);
 
 	connectSignals();
@@ -179,7 +220,13 @@
 {
 	connect(columns       , SIGNAL(valueChanged(double))   , this, SLOT(handleColumns()), Qt::UniqueConnection);
 	connect(columnGap     , SIGNAL(valueChanged(double)), this, SLOT(handleColumnGap()), Qt::UniqueConnection);
-	connect(columnGapCombo, SIGNAL(activated(int))      , this, SLOT(handleGapSwitch()), Qt::UniqueConnection);
+	connect(columnLayout  , SIGNAL(activated(int))      , this, SLOT(handleColumnLayout(int)), Qt::UniqueConnection);
+	connect(customColumn  , SIGNAL(valueChanged(double)), this, SLOT(handleCustomColumn()), Qt::UniqueConnection);
+	connect(columnPreview , SIGNAL(selectedColumnChanged(int)), this, SLOT(handlePreviewColumn(int)), Qt::UniqueConnection);
+	connect(customWidth   , SIGNAL(valueChanged(double)), this, SLOT(handleCustomWidth()), Qt::UniqueConnection);
+	connect(customGap     , SIGNAL(valueChanged(double)), this, SLOT(handleCustomGap()), Qt::UniqueConnection);
+	connect(equalGaps     , SIGNAL(toggled(bool))        , this, SLOT(handleEqualGaps(bool)), Qt::UniqueConnection);
+	connect(widthAdjustment, SIGNAL(activated(int))      , this, SLOT(handleCustomColumn()), Qt::UniqueConnection);
 	connect(distanceWidget, SIGNAL(valuesChanged(MarginStruct)), this, SLOT(handleTextDistances()), Qt::UniqueConnection);
 	connect(tabsButton    , SIGNAL(clicked())           , this, SLOT(handleTabs()), Qt::UniqueConnection);
 	connect(firstLineIndentSpin, SIGNAL(valueChanged(double)), this, SLOT(handleIndents()), Qt::UniqueConnection);
@@ -194,7 +241,13 @@
 {
 	disconnect(columns       , SIGNAL(valueChanged(double))   , this, SLOT(handleColumns()));
 	disconnect(columnGap     , SIGNAL(valueChanged(double)), this, SLOT(handleColumnGap()));
-	disconnect(columnGapCombo, SIGNAL(activated(int))      , this, SLOT(handleGapSwitch()));
+	disconnect(columnLayout  , SIGNAL(activated(int))      , this, SLOT(handleColumnLayout(int)));
+	disconnect(customColumn  , SIGNAL(valueChanged(double)), this, SLOT(handleCustomColumn()));
+	disconnect(columnPreview , SIGNAL(selectedColumnChanged(int)), this, SLOT(handlePreviewColumn(int)));
+	disconnect(customWidth   , SIGNAL(valueChanged(double)), this, SLOT(handleCustomWidth()));
+	disconnect(customGap     , SIGNAL(valueChanged(double)), this, SLOT(handleCustomGap()));
+	disconnect(equalGaps     , SIGNAL(toggled(bool))        , this, SLOT(handleEqualGaps(bool)));
+	disconnect(widthAdjustment, SIGNAL(activated(int))      , this, SLOT(handleCustomColumn()));
 	disconnect(distanceWidget, SIGNAL(valuesChanged(MarginStruct)), this, SLOT(handleTextDistances()));
 	disconnect(tabsButton    , SIGNAL(clicked())           , this, SLOT(handleTabs()));
 	disconnect(firstLineIndentSpin, SIGNAL(valueChanged(double)), this, SLOT(handleIndents()));
@@ -221,12 +274,127 @@
 			int numCols = textItem->m_columns;
 			
 			columnGapLabel->setVisible(numCols != 1);
-			columnGapSizeLabel->setVisible(numCols != 1);
 		}
 	}
 	setEnabled(enabled);
 }
 
+PageItem_TextFrame* PropertyWidget_Distance::currentTextFrame() const
+{
+	if (!m_item || !m_doc)
+		return nullptr;
+	if (m_doc->appMode == modeEditTable && m_item->isTable())
+		return m_item->asTable()->activeCell().textFrame();
+	return m_item->asTextFrame();
+}
+
+void PropertyWidget_Distance::updateColumnControls(PageItem_TextFrame* textItem)
+{
+	if (!textItem)
+		return;
+
+	QSignalBlocker columnsBlocker(columns);
+	QSignalBlocker gapBlocker(columnGap);
+	QSignalBlocker layoutBlocker(columnLayout);
+	QSignalBlocker customColumnBlocker(customColumn);
+	QSignalBlocker customWidthBlocker(customWidth);
+	QSignalBlocker customGapBlocker(customGap);
+	QSignalBlocker equalGapsBlocker(equalGaps);
+
+	const TextColumnLayout layout = textItem->textColumnLayout();
+	const bool custom = layout.custom && layout.columnCount > 1;
+	const int columnCount = qMax(1, layout.columnCount);
+	const int column = qBound(0, static_cast<int>(customColumn->value()) - 1, columnCount - 1);
+
+	columns->setMinimum(1);
+	columns->setValue(columnCount);
+	columnLayout->setCurrentIndex(custom ? 1 : 0);
+	columnLayout->setEnabled(columnCount > 1);
+
+	columnGap->setMinimum(0);
+	columnGap->setMaximum(qMax((textItem->width() / columnCount - textItem->textToFrameDistLeft() - textItem->textToFrameDistRight()) * m_unitRatio, 0.0));
+	columnGap->setValue(layout.columnGap * m_unitRatio);
+
+	customColumn->setMaximum(columnCount);
+	customColumn->setValue(column + 1);
+	const QVector<TextColumnGeometry> geometry = textItem->columnGeometry();
+	QVector<double> effectiveWidths;
+	QVector<double> effectiveGaps;
+	for (const TextColumnGeometry& value : geometry)
+		effectiveWidths.append(value.width);
+	for (int i = 0; i + 1 < geometry.size(); ++i)
+		effectiveGaps.append(qMax(0.0, geometry.at(i + 1).left - geometry.at(i).left - geometry.at(i).width));
+	if (m_initializeGapMode)
+	{
+		equalGaps->setChecked(gapsAreEqual(effectiveGaps));
+		m_initializeGapMode = false;
+	}
+	else if (equalGaps->isChecked() && !gapsAreEqual(effectiveGaps))
+		equalGaps->setChecked(false);
+	columnPreview->setGeometry(effectiveWidths, effectiveGaps);
+	columnPreview->setSelectedColumn(column);
+	columnPreview->setEqualGaps(equalGaps->isChecked());
+	if (column < geometry.size())
+	{
+		const int neighbour = (column + 1 < columnCount) ? column + 1 : column - 1;
+		double maximumWidth = geometry.at(column).width;
+		if (widthAdjustment->currentIndex() == 1)
+		{
+			for (int i = 0; i < geometry.size(); ++i)
+				if (i != column)
+					maximumWidth += qMax(0.0, geometry.at(i).width - 0.01);
+		}
+		else if (neighbour >= 0)
+			maximumWidth += qMax(0.0, geometry.at(neighbour).width - 0.01);
+		customWidth->setMaximum(qMax(0.01, maximumWidth) * m_unitRatio);
+		customWidth->setValue(geometry.at(column).width * m_unitRatio);
+	}
+	const bool editEqualGaps = custom && equalGaps->isChecked() && !effectiveGaps.isEmpty();
+	customGap->setEnabled(custom && (column < columnCount - 1 || editEqualGaps));
+	customGap->setToolTip(editEqualGaps ? tr("Space between all columns")
+		: (column < columnCount - 1 ? tr("Space after the selected column")
+		: tr("The last column has no following gap")));
+	if (editEqualGaps)
+	{
+		const double contentWidth = std::accumulate(effectiveWidths.cbegin(), effectiveWidths.cend(), 0.0)
+			+ std::accumulate(effectiveGaps.cbegin(), effectiveGaps.cend(), 0.0);
+		customGap->setMaximum(qMax(0.0, (contentWidth - 0.01 * columnCount) / effectiveGaps.size()) * m_unitRatio);
+	}
+	if (custom && column < effectiveGaps.size() && column + 1 < geometry.size())
+	{
+		if (!editEqualGaps)
+			customGap->setMaximum((effectiveGaps.at(column) + qMax(0.0, geometry.at(column + 1).width - 0.01)) * m_unitRatio);
+		customGap->setValue(effectiveGaps.at(column) * m_unitRatio);
+	}
+	else
+		customGap->setValue(0.0);
+	if (editEqualGaps && column >= effectiveGaps.size())
+		customGap->setValue(effectiveGaps.constFirst() * m_unitRatio);
+
+	columnGapLabel->setVisible(!custom && columnCount > 1);
+	customColumnsWidget->setVisible(custom);
+	columnPreview->setVisible(custom);
+	widthAdjustmentLabel->setVisible(custom);
+	widthAdjustment->setEnabled(columnCount > 2);
+	equalGaps->setVisible(custom && columnCount > 2);
+}
+
+void PropertyWidget_Distance::updateTextFrame(PageItem_TextFrame* textItem)
+{
+	if (!textItem)
+		return;
+	updateColumnControls(textItem);
+	textItem->update();
+	if (m_doc->appMode == modeEditTable)
+		m_item->asTable()->update();
+	if (m_ScMW->view && m_doc->inAnEditMode())
+	{
+		m_ScMW->view->horizRuler->setItem(textItem);
+		m_ScMW->view->horizRuler->update();
+	}
+	m_doc->regionsChanged()->update(QRect());
+}
+
 void PropertyWidget_Distance::handleAppModeChanged(int oldMode, int mode)
 {
 	if (oldMode == modeEditTable || mode == modeEditTable)
@@ -255,6 +423,9 @@
 	IconManager &im = IconManager::instance();
 
 	columnsLabel->setPixmap(im.loadPixmap("paragraph-columns"));
+	columnGapLabel->setPixmap(im.loadPixmap("paragraph-column-gap"));
+	customWidthLabel->setPixmap(im.loadPixmap("scale-width"));
+	customGapLabel->setPixmap(im.loadPixmap("paragraph-column-gap"));
 	firstLineIndentLabel->setPixmap(im.loadPixmap("paragraph-indent-firstline"));
 	leftIndentLabel->setPixmap(im.loadPixmap("paragraph-indent-left"));
 	rightIndentLabel->setPixmap(im.loadPixmap("paragraph-indent-right"));
@@ -262,47 +433,68 @@
 	gapAfterLabel->setPixmap(im.loadPixmap("paragraph-space-below") );
 }
 
-void PropertyWidget_Distance::showColumns(int r, double g)
+void PropertyWidget_Distance::showColumns(int /*columnCount*/, double /*columnGapValue*/)
 {
 	if (!m_ScMW || m_ScMW->scriptIsRunning())
 		return;
+	if (PageItem_TextFrame* textItem = currentTextFrame())
+		updateColumnControls(textItem);
+}
+
+void PropertyWidget_Distance::handleColumns()
+{
+	if (!m_doc || !m_item || !m_ScMW || m_ScMW->scriptIsRunning())
+		return;
 
-	bool cSigWasBlocked    = columns->blockSignals(true);
-	bool cGapSigWasBlocked = columnGap->blockSignals(true);
+	PageItem_TextFrame* textItem = currentTextFrame();
+	if (!textItem)
+		return;
 
-	columns->setValue(r);
-	columnGap->setValue(g * m_unitRatio);
-	if (m_item)
+	const int newCount = qMax(1, static_cast<int>(columns->value()));
+	TextColumnLayout layout = textItem->textColumnLayout();
+	if (!layout.custom)
+		textItem->setColumns(newCount);
+	else if (newCount == 1)
+	{
+		layout.columnCount = 1;
+		layout.custom = false;
+		layout.widthRatios.clear();
+		layout.gaps.clear();
+		textItem->setTextColumnLayout(layout);
+	}
+	else if (newCount > layout.columnCount)
 	{
-		PageItem_TextFrame *textItem = m_item->asTextFrame();
-		if (m_doc->appMode == modeEditTable)
-			textItem = m_item->asTable()->activeCell().textFrame();
-
-		if (textItem != nullptr)
+		while (layout.columnCount < newCount)
 		{
-//#14427: columns->setMaximum(qMax(qRound(textItem->width() / qMax(textItem->ColGap, 10.0)), 1));
-			if (columnGapCombo->currentIndex() == 0)
-			{
-				columnGap->setMaximum(qMax((textItem->width() / textItem->m_columns - textItem->textToFrameDistLeft() - textItem->textToFrameDistRight()) * m_unitRatio, 0.0));
-				columnGap->setValue(textItem->m_columnGap * m_unitRatio);
-			}
-			else
+			const int oldCount = layout.columnCount;
+			const double newShare = 1.0 / (oldCount + 1.0);
+			for (double& ratio : layout.widthRatios)
+				ratio *= 1.0 - newShare;
+			layout.widthRatios.append(newShare);
+			double averageGap = layout.columnGap;
+			if (!layout.gaps.isEmpty())
 			{
-				columnGap->setMaximum(qMax((textItem->width() / textItem->m_columns) * m_unitRatio, 0.0));
-				columnGap->setValue(textItem->columnWidth() * m_unitRatio);
+				averageGap = 0.0;
+				for (double gap : std::as_const(layout.gaps))
+					averageGap += gap;
+				averageGap /= layout.gaps.size();
 			}
+			layout.gaps.append(averageGap);
+			layout.columnCount++;
 		}
+		textItem->setTextColumnLayout(layout);
 	}
-	columns->setMinimum(1);
-	columnGap->setMinimum(0);
-	columnGapLabel->setVisible(columns->value() != 1);
-	columnGapSizeLabel->setVisible(columns->value() != 1);
-
-	columns->blockSignals(cSigWasBlocked);
-	columnGap->blockSignals(cGapSigWasBlocked);
+	else if (newCount < layout.columnCount)
+	{
+		layout.columnCount = newCount;
+		layout.widthRatios.resize(newCount);
+		layout.gaps.resize(newCount - 1);
+		textItem->setTextColumnLayout(layout);
+	}
+	updateTextFrame(textItem);
 }
 
-void PropertyWidget_Distance::handleColumns()
+void PropertyWidget_Distance::handleColumnGap()
 {
 	if (!m_doc || !m_item || !m_ScMW || m_ScMW->scriptIsRunning())
 		return;
@@ -310,59 +502,199 @@
 	PageItem *textItem = m_item;
 	if (m_doc->appMode == modeEditTable)
 		textItem = m_item->asTable()->activeCell().textFrame();
+	if (!textItem)
+		return;
 
-	if (textItem)
-	{
-		textItem->setColumns(static_cast<int>(columns->value()));
-		showColumns(textItem->m_columns, textItem->m_columnGap);
-		textItem->update();
-		if (m_doc->appMode == modeEditTable)
-			m_item->asTable()->update();
-		m_doc->regionsChanged()->update(QRect());
-	}
+	textItem->setColumnGap(columnGap->value() / m_unitRatio);
+	updateTextFrame(textItem->asTextFrame());
 }
 
-void PropertyWidget_Distance::handleColumnGap()
+void PropertyWidget_Distance::handleColumnLayout(int index)
 {
 	if (!m_doc || !m_item || !m_ScMW || m_ScMW->scriptIsRunning())
 		return;
-
-	PageItem *textItem = m_item;
-	if (m_doc->appMode == modeEditTable)
-		textItem = m_item->asTable()->activeCell().textFrame();
-	if (!textItem)
+	PageItem_TextFrame* textItem = currentTextFrame();
+	if (!textItem || textItem->columns() < 2)
 		return;
 
-	if (columnGapCombo->currentIndex() == 0)
-		textItem->setColumnGap(columnGap->value() / m_unitRatio);
-	else
+	TextColumnLayout layout = textItem->textColumnLayout();
+	if (index == 1 && !layout.custom)
 	{
-		double lineCorr=0.0;
-		if ((textItem->lineColor() != CommonStrings::None) || (!textItem->strokePattern().isEmpty()))
-			lineCorr = textItem->lineWidth();
-		double newWidth = columnGap->value() / m_unitRatio;
-		double newGap = qMax(((textItem->width() - textItem->textToFrameDistLeft() - textItem->textToFrameDistRight() - lineCorr) - (newWidth * textItem->m_columns)) / (textItem->m_columns - 1), 0.0);
-		textItem->setColumnGap(newGap);
+		const QVector<TextColumnGeometry> geometry = textItem->columnGeometry();
+		double widthTotal = 0.0;
+		for (const TextColumnGeometry& column : geometry)
+			widthTotal += column.width;
+		if (widthTotal <= 0.0)
+		{
+			updateColumnControls(textItem);
+			return;
+		}
+		layout.custom = true;
+		layout.widthRatios.clear();
+		layout.gaps.clear();
+		for (const TextColumnGeometry& column : geometry)
+			layout.widthRatios.append(column.width / widthTotal);
+		for (int column = 1; column < layout.columnCount; ++column)
+			layout.gaps.append(layout.columnGap);
 	}
-	textItem->update();
-	if (m_doc->appMode == modeEditTable)
-		m_item->asTable()->update();
-	m_doc->regionsChanged()->update(QRect());
+	else if (index == 0 && layout.custom)
+	{
+		if (!layout.gaps.isEmpty())
+		{
+			layout.columnGap = 0.0;
+			for (double gap : std::as_const(layout.gaps))
+				layout.columnGap += gap;
+			layout.columnGap /= layout.gaps.size();
+		}
+		layout.custom = false;
+		layout.widthRatios.clear();
+		layout.gaps.clear();
+	}
+	textItem->setTextColumnLayout(layout);
+	updateTextFrame(textItem);
 }
 
-void PropertyWidget_Distance::handleGapSwitch()
+void PropertyWidget_Distance::handleCustomColumn()
 {
-	if (!m_doc || !m_item || !m_ScMW || m_ScMW->scriptIsRunning())
+	if (PageItem_TextFrame* textItem = currentTextFrame())
+		updateColumnControls(textItem);
+}
+
+void PropertyWidget_Distance::handlePreviewColumn(int column)
+{
+	QSignalBlocker blocker(customColumn);
+	customColumn->setValue(column + 1);
+	if (PageItem_TextFrame* textItem = currentTextFrame())
+		updateColumnControls(textItem);
+}
+
+void PropertyWidget_Distance::handleCustomWidth()
+{
+	PageItem_TextFrame* textItem = currentTextFrame();
+	if (!textItem || !textItem->hasCustomTextColumns())
 		return;
 
-	const PageItem *textItem = m_item;
-	if (m_doc->appMode == modeEditTable)
-		textItem = m_item->asTable()->activeCell().textFrame();
-	if (textItem != nullptr)
-		showColumns(textItem->m_columns, textItem->m_columnGap);
+	TextColumnLayout layout = textItem->textColumnLayout();
+	const int column = qBound(0, static_cast<int>(customColumn->value()) - 1, layout.columnCount - 1);
+	const int neighbour = (column + 1 < layout.columnCount) ? column + 1 : column - 1;
+	if (neighbour < 0)
+		return;
 
-	int index = columnGapCombo->currentIndex();
-	columnGap->setToolTip((index == 0) ? tr( "Distance between columns" ) : tr( "Column width" ));
+	const QVector<TextColumnGeometry> geometry = textItem->columnGeometry();
+	QVector<double> widths;
+	QVector<double> gaps;
+	widths.reserve(geometry.size());
+	for (const TextColumnGeometry& value : geometry)
+		widths.append(value.width);
+	for (int i = 0; i + 1 < geometry.size(); ++i)
+		gaps.append(qMax(0.0, geometry.at(i + 1).left - geometry.at(i).left - geometry.at(i).width));
+	layout.gaps = gaps;
+	if (widthAdjustment->currentIndex() == 1 && widths.size() > 2)
+	{
+		double widthTotal = 0.0;
+		for (double width : std::as_const(widths))
+			widthTotal += width;
+		const double newWidth = qBound(0.01, customWidth->value() / m_unitRatio,
+			qMax(0.01, widthTotal - 0.01 * (widths.size() - 1)));
+		const double otherWidth = (widthTotal - newWidth) / (widths.size() - 1);
+		for (int i = 0; i < widths.size(); ++i)
+			widths[i] = (i == column) ? newWidth : otherWidth;
+	}
+	else
+	{
+		const double available = widths.at(column) + widths.at(neighbour);
+		const double newWidth = qBound(0.01, customWidth->value() / m_unitRatio, qMax(0.01, available - 0.01));
+		widths[neighbour] = available - newWidth;
+		widths[column] = newWidth;
+	}
+	double widthTotal = 0.0;
+	for (double width : std::as_const(widths))
+		widthTotal += width;
+	for (int i = 0; i < widths.size(); ++i)
+		layout.widthRatios[i] = widths.at(i) / widthTotal;
+	textItem->setTextColumnLayout(layout);
+	updateTextFrame(textItem);
+}
+
+void PropertyWidget_Distance::handleCustomGap()
+{
+	PageItem_TextFrame* textItem = currentTextFrame();
+	if (!textItem || !textItem->hasCustomTextColumns())
+		return;
+
+	TextColumnLayout layout = textItem->textColumnLayout();
+	const int column = qBound(0, static_cast<int>(customColumn->value()) - 1, layout.columnCount - 1);
+	if (column >= layout.gaps.size() && !equalGaps->isChecked())
+		return;
+
+	const QVector<TextColumnGeometry> geometry = textItem->columnGeometry();
+	QVector<double> widths;
+	QVector<double> gaps;
+	widths.reserve(geometry.size());
+	for (const TextColumnGeometry& value : geometry)
+		widths.append(value.width);
+	for (int i = 0; i + 1 < geometry.size(); ++i)
+		gaps.append(qMax(0.0, geometry.at(i + 1).left - geometry.at(i).left - geometry.at(i).width));
+	layout.gaps = gaps;
+	if (equalGaps->isChecked())
+	{
+		double widthTotal = 0.0;
+		for (double width : std::as_const(widths))
+			widthTotal += width;
+		for (int i = 0; i < widths.size(); ++i)
+			layout.widthRatios[i] = widths.at(i) / widthTotal;
+		const double contentWidth = widthTotal + std::accumulate(gaps.cbegin(), gaps.cend(), 0.0);
+		const double maximumGap = qMax(0.0, (contentWidth - 0.01 * widths.size()) / gaps.size());
+		const double newGap = qBound(0.0, customGap->value() / m_unitRatio, maximumGap);
+		std::fill(layout.gaps.begin(), layout.gaps.end(), newGap);
+		textItem->setTextColumnLayout(layout);
+		updateTextFrame(textItem);
+		return;
+	}
+	const double oldGap = layout.gaps.at(column);
+	const double newGap = qBound(0.0, customGap->value() / m_unitRatio,
+		oldGap + qMax(0.0, widths.at(column + 1) - 0.01));
+	widths[column + 1] -= newGap - oldGap;
+	layout.gaps[column] = newGap;
+	double widthTotal = 0.0;
+	for (double width : std::as_const(widths))
+		widthTotal += width;
+	for (int i = 0; i < widths.size(); ++i)
+		layout.widthRatios[i] = widths.at(i) / widthTotal;
+	textItem->setTextColumnLayout(layout);
+	updateTextFrame(textItem);
+}
+
+void PropertyWidget_Distance::handleEqualGaps(bool enabled)
+{
+	PageItem_TextFrame* textItem = currentTextFrame();
+	if (!textItem || !textItem->hasCustomTextColumns())
+		return;
+	if (!enabled)
+	{
+		updateColumnControls(textItem);
+		return;
+	}
+
+	TextColumnLayout layout = textItem->textColumnLayout();
+	const QVector<TextColumnGeometry> geometry = textItem->columnGeometry();
+	if (geometry.size() < 3)
+		return;
+	QVector<double> widths;
+	QVector<double> gaps;
+	for (const TextColumnGeometry& value : geometry)
+		widths.append(value.width);
+	for (int i = 0; i + 1 < geometry.size(); ++i)
+		gaps.append(qMax(0.0, geometry.at(i + 1).left - geometry.at(i).left - geometry.at(i).width));
+	const int column = qBound(0, static_cast<int>(customColumn->value()) - 1, static_cast<int>(geometry.size()) - 1);
+	const double gap = gaps.at(qMin(column, static_cast<int>(gaps.size()) - 1));
+	std::fill(gaps.begin(), gaps.end(), gap);
+	double widthTotal = std::accumulate(widths.cbegin(), widths.cend(), 0.0);
+	for (int i = 0; i < widths.size(); ++i)
+		layout.widthRatios[i] = widths.at(i) / widthTotal;
+	layout.gaps = gaps;
+	textItem->setTextColumnLayout(layout);
+	updateTextFrame(textItem);
 }
 
 void PropertyWidget_Distance::handleTabs()
@@ -374,7 +706,7 @@
 	if (tItem == nullptr)
 		return;
 	const ParagraphStyle& style(m_doc->appMode == modeEdit ? tItem->currentStyle() : tItem->itemText.defaultStyle());
-	TabManager *dia = new TabManager(this, m_doc->unitIndex(), style.tabValues(), tItem->columnWidth());
+	TabManager *dia = new TabManager(this, m_doc->unitIndex(), style.tabValues(), minimumTextColumnWidth(tItem));
 	if (dia->exec())
 	{
 		if (m_doc->appMode != modeEdit)
@@ -467,17 +799,12 @@
 {
 	retranslateUi(this);
 
-	QSignalBlocker columnGapLabelBlocker(columnGapCombo);
-	int oldColGapLabel = columnGapCombo->currentIndex();
-	columnGapCombo->clear();
-	columnGapCombo->addItem( tr("Gap:"));
-	columnGapCombo->addItem( tr("Width:"));
-	columnGapCombo->setCurrentIndex(oldColGapLabel);
-
 	QString ptSuffix = tr(" pt");
 	QString suffix = (m_doc) ? unitGetSuffixFromIndex(m_doc->unitIndex()) : ptSuffix;
 
 	columnGap->setSuffix(suffix);
+	customWidth->setSuffix(suffix);
+	customGap->setSuffix(suffix);
 	firstLineIndentSpin->setSuffix(suffix);
 	leftIndentSpin->setSuffix(suffix);
 	rightIndentSpin->setSuffix(suffix);
@@ -489,6 +816,21 @@
 	leftIndentLabel->setToolTip(leftIndentSpin->toolTip());
 	rightIndentLabel->setToolTip(rightIndentSpin->toolTip());
 	tabsButton->setToolTip(tr("Edit tab settings of text frame..."));
+	columnLayout->setToolTip(tr("Use equal or individually sized columns"));
+	columnLayoutLabel->setToolTip(columnLayout->toolTip());
+	widthAdjustment->setToolTip(tr("How width changes are distributed"));
+	widthAdjustment->setItemData(0, tr("Take or return space from the adjacent column"), Qt::ToolTipRole);
+	widthAdjustment->setItemData(1, tr("Divide the remaining width equally among all other columns"), Qt::ToolTipRole);
+	widthAdjustmentLabel->setToolTip(widthAdjustment->toolTip());
+	equalGaps->setToolTip(tr("Use the selected gap value between all columns"));
+	columnGap->setToolTip(tr("Space between columns"));
+	columnGapLabel->setToolTip(columnGap->toolTip());
+	customColumn->setToolTip(tr("Physical column to edit, numbered from left to right"));
+	customColumnLabel->setToolTip(customColumn->toolTip());
+	customWidth->setToolTip(tr("Width of the selected column"));
+	customWidthLabel->setToolTip(customWidth->toolTip());
+	customGap->setToolTip(tr("Space after the selected column"));
+	customGapLabel->setToolTip(customGap->toolTip());
 	gapBeforeLabel->setToolTip(tr("Space above a paragraph"));
 	gapAfterLabel->setToolTip(tr("Space below a paragraph"));
 }
@@ -498,8 +840,10 @@
 	if (!m_doc)
 		return;
 
 	QSignalBlocker columnGapBlocker(columnGap);
+	QSignalBlocker customWidthBlocker(customWidth);
+	QSignalBlocker customGapBlocker(customGap);
 	QSignalBlocker distancesBlocker(distanceWidget);
 	QSignalBlocker numberFirstLineIndentBlocker(firstLineIndentSpin);
 	QSignalBlocker numberLeftIndentBlocker(leftIndentSpin);
 	QSignalBlocker numberRightIndentBlocker(rightIndentSpin);
@@ -508,6 +852,8 @@
 	m_unitIndex = m_doc->unitIndex();
 
 	columnGap->setNewUnit(m_unitIndex);
+	customWidth->setNewUnit(m_unitIndex);
+	customGap->setNewUnit(m_unitIndex);
 	distanceWidget->setNewUnit(m_unitIndex);
 	firstLineIndentSpin->setNewUnit(m_unitIndex);
 	leftIndentSpin->setNewUnit(m_unitIndex);
@@ -518,6 +862,9 @@
 {
 	const QLocale& l(LocaleManager::instance().userPreferredLocale());
 	columnGap->setLocale(l);
+	customColumn->setLocale(l);
+	customWidth->setLocale(l);
+	customGap->setLocale(l);
 	distanceWidget->setLocale(l);
 	firstLineIndentSpin->setLocale(l);
 	leftIndentSpin->setLocale(l);
@@ -528,9 +875,14 @@
 
 void PropertyWidget_Distance::toggleLabelVisibility(bool v)
 {
-	columnGapSizeLabel->setLabelVisibility(v);
 	columnGapLabel->setLabelVisibility(v);
 	columnsLabel->setLabelVisibility(v);
+	columnLayoutLabel->setLabelVisibility(v);
+	widthAdjustmentLabel->setLabelVisibility(v);
+	customColumnLabel->setLabelVisibility(v);
+	customWidthLabel->setLabelVisibility(v);
+	customGapLabel->setLabelVisibility(v);
+	equalGapsLabel->setLabelVisibility(v);
 	firstLineIndentLabel->setLabelVisibility(v);
 	leftIndentLabel->setLabelVisibility(v);
 	rightIndentLabel->setLabelVisibility(v);
Index: scribus/ui/textcolumnpreview.h
===================================================================
--- scribus/ui/textcolumnpreview.h	(nonexistent)
+++ scribus/ui/textcolumnpreview.h	(working copy)
@@ -0,0 +1,46 @@
+#ifndef TEXTCOLUMNPREVIEW_H
+#define TEXTCOLUMNPREVIEW_H
+
+#include <QAbstractScrollArea>
+#include <QVector>
+
+#include "scribusapi.h"
+
+class SCRIBUS_API TextColumnPreview : public QAbstractScrollArea
+{
+	Q_OBJECT
+
+public:
+	explicit TextColumnPreview(QWidget* parent = nullptr);
+	void setGeometry(const QVector<double>& widths, const QVector<double>& gaps);
+	int selectedColumn() const { return m_selectedColumn; }
+	void setSelectedColumn(int column);
+	void setEqualGaps(bool enabled);
+
+signals:
+	void selectedColumnChanged(int column);
+
+protected:
+	void keyPressEvent(QKeyEvent* event) override;
+	void mousePressEvent(QMouseEvent* event) override;
+	void mouseMoveEvent(QMouseEvent* event) override;
+	void leaveEvent(QEvent* event) override;
+	void paintEvent(QPaintEvent* event) override;
+	void resizeEvent(QResizeEvent* event) override;
+	void wheelEvent(QWheelEvent* event) override;
+
+private:
+	void updateScrollBar();
+	QRectF columnRect(int column) const;
+	int columnAt(const QPointF& position) const;
+	double contentScale() const;
+	int contentWidth() const;
+
+	QVector<double> m_widths;
+	QVector<double> m_gaps;
+	int m_selectedColumn { 0 };
+	int m_hoveredColumn { -1 };
+	bool m_equalGaps {false};
+};
+
+#endif
Index: scribus/ui/textcolumnpreview.cpp
===================================================================
--- scribus/ui/textcolumnpreview.cpp	(nonexistent)
+++ scribus/ui/textcolumnpreview.cpp	(working copy)
@@ -0,0 +1,183 @@
+#include "textcolumnpreview.h"
+
+#include <QKeyEvent>
+#include <QMouseEvent>
+#include <QPainter>
+#include <QScrollBar>
+#include <QWheelEvent>
+
+TextColumnPreview::TextColumnPreview(QWidget* parent) : QAbstractScrollArea(parent)
+{
+	setFocusPolicy(Qt::StrongFocus);
+	setMouseTracking(true);
+	setFrameShape(QFrame::StyledPanel);
+	setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
+	setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+	setFixedHeight(70);
+	setAccessibleName(tr("Column layout preview"));
+}
+
+void TextColumnPreview::setEqualGaps(bool enabled)
+{
+	if (m_equalGaps == enabled)
+		return;
+	m_equalGaps = enabled;
+	viewport()->update();
+}
+
+void TextColumnPreview::setGeometry(const QVector<double>& widths, const QVector<double>& gaps)
+{
+	m_widths = widths;
+	m_gaps = gaps;
+	m_selectedColumn = qBound(0, m_selectedColumn, qMax(0, m_widths.size() - 1));
+	updateScrollBar();
+	viewport()->update();
+}
+
+void TextColumnPreview::setSelectedColumn(int column)
+{
+	column = qBound(0, column, qMax(0, m_widths.size() - 1));
+	const bool changed = column != m_selectedColumn;
+	m_selectedColumn = column;
+	setAccessibleDescription(tr("Column %1 of %2 selected").arg(column + 1).arg(m_widths.size()));
+	const QRectF rect = columnRect(column);
+	horizontalScrollBar()->setValue(qBound(horizontalScrollBar()->minimum(),
+		static_cast<int>(rect.center().x() - viewport()->width() / 2.0), horizontalScrollBar()->maximum()));
+	viewport()->update();
+	if (changed)
+		emit selectedColumnChanged(column);
+}
+
+int TextColumnPreview::contentWidth() const
+{
+	return qMax(viewport()->width() - 12, m_widths.size() * 32);
+}
+
+double TextColumnPreview::contentScale() const
+{
+	double total = 0.0;
+	for (double width : m_widths) total += width;
+	for (double gap : m_gaps) total += gap;
+	return total > 0.0 ? contentWidth() / total : 1.0;
+}
+
+QRectF TextColumnPreview::columnRect(int column) const
+{
+	double x = 6.0 - horizontalScrollBar()->value();
+	const double scale = contentScale();
+	for (int i = 0; i < column; ++i)
+		x += (m_widths.value(i) + m_gaps.value(i)) * scale;
+	return QRectF(x, 7, m_widths.value(column) * scale, qMax(1, viewport()->height() - 14));
+}
+
+int TextColumnPreview::columnAt(const QPointF& position) const
+{
+	for (int i = 0; i < m_widths.size(); ++i)
+	{
+		const QRectF rect = columnRect(i);
+		if (rect.adjusted(-3, 0, 3, 0).contains(position))
+			return i;
+		if (i < m_gaps.size())
+		{
+			const double gapWidth = qMax(2.0, m_gaps.at(i) * contentScale());
+			if (QRectF(rect.right(), rect.top(), gapWidth, rect.height()).contains(position))
+				return i;
+		}
+	}
+	return -1;
+}
+
+void TextColumnPreview::updateScrollBar()
+{
+	const int overflow = qMax(0, contentWidth() - qMax(0, viewport()->width() - 12));
+	horizontalScrollBar()->setRange(0, overflow);
+	horizontalScrollBar()->setPageStep(viewport()->width());
+}
+
+void TextColumnPreview::paintEvent(QPaintEvent*)
+{
+	QPainter painter(viewport());
+	painter.fillRect(viewport()->rect(), palette().brush(QPalette::Window));
+	for (int i = 0; i < m_widths.size(); ++i)
+	{
+		QRectF rect = columnRect(i);
+		painter.fillRect(rect, palette().brush(QPalette::Base));
+		if (i == m_selectedColumn)
+		{
+			QColor fill = palette().color(QPalette::Highlight); fill.setAlphaF(0.28);
+			painter.fillRect(rect, fill);
+		}
+		QColor outline = palette().color(i == m_selectedColumn || i == m_hoveredColumn ? QPalette::Highlight : QPalette::Text);
+		if (i == m_selectedColumn)
+			outline.setAlphaF(0.58);
+		else if (i == m_hoveredColumn)
+			outline.setAlphaF(0.30);
+		else
+			outline.setAlphaF(0.28);
+		painter.setPen(outline);
+		painter.drawRect(rect.adjusted(0, 0, -1, -1));
+		if (i < m_gaps.size() && (m_equalGaps || i == m_selectedColumn))
+		{
+			QColor fill = palette().color(QPalette::Highlight);
+			fill.setAlphaF(i == m_selectedColumn ? 0.34 : 0.19);
+			const double gapWidth = qMax(2.0, m_gaps.at(i) * contentScale());
+			painter.fillRect(QRectF(rect.right(), rect.top(), gapWidth, rect.height()), fill);
+		}
+	}
+}
+
+void TextColumnPreview::mousePressEvent(QMouseEvent* event)
+{
+	const int column = columnAt(event->position());
+	if (column >= 0)
+		setSelectedColumn(column);
+	setFocus();
+}
+
+void TextColumnPreview::mouseMoveEvent(QMouseEvent* event)
+{
+	const int column = columnAt(event->position());
+	if (column != m_hoveredColumn)
+	{
+		m_hoveredColumn = column;
+		viewport()->setCursor(column >= 0 ? Qt::PointingHandCursor : Qt::ArrowCursor);
+		setToolTip(column >= 0 ? tr("Column %1 — click to edit").arg(column + 1) : QString());
+		viewport()->update();
+	}
+}
+
+void TextColumnPreview::leaveEvent(QEvent* event)
+{
+	m_hoveredColumn = -1;
+	viewport()->setCursor(Qt::ArrowCursor);
+	viewport()->update();
+	QAbstractScrollArea::leaveEvent(event);
+}
+
+void TextColumnPreview::keyPressEvent(QKeyEvent* event)
+{
+	int column = m_selectedColumn;
+	if (event->key() == Qt::Key_Left) --column;
+	else if (event->key() == Qt::Key_Right) ++column;
+	else if (event->key() == Qt::Key_Home) column = 0;
+	else if (event->key() == Qt::Key_End) column = m_widths.size() - 1;
+	else { QAbstractScrollArea::keyPressEvent(event); return; }
+	setSelectedColumn(column);
+}
+
+void TextColumnPreview::wheelEvent(QWheelEvent* event)
+{
+	if (horizontalScrollBar()->maximum() <= 0)
+	{
+		event->ignore();
+		return;
+	}
+	horizontalScrollBar()->setValue(horizontalScrollBar()->value() - event->angleDelta().y());
+	event->accept();
+}
+
+void TextColumnPreview::resizeEvent(QResizeEvent* event)
+{
+	QAbstractScrollArea::resizeEvent(event);
+	updateScrollBar();
+}
Index: scribus/ui/hruler.h
===================================================================
--- scribus/ui/hruler.h	(revision 27829)
+++ scribus/ui/hruler.h	(working copy)
@@ -26,6 +26,7 @@
 
 #include <QWidget>
 
+#include "pageitem.h"
 #include "scribusapi.h"
 #include "styles/paragraphstyle.h"
 
@@ -110,7 +111,7 @@
 	bool m_reverse {false};
 	bool m_rtl {false};
 	bool m_textEditMode {false};
-	double m_colGap {0.0};
+	QVector<TextColumnGeometry> m_columnGeometry;
 	double m_cor {0.0};
 	double m_distLeft {0.0};
 	double m_distRight {0.0};
@@ -125,7 +126,6 @@
 	double m_offset {0.0};
 	double m_rightMargin {0.0};
 	double m_scaling {0.0};
-	int m_cols {0};
 	int m_currCol {1};
 	int m_currTab {0};
 	int m_mouseX {0};
Index: scribus/ui/hruler.cpp
===================================================================
--- scribus/ui/hruler.cpp	(revision 27829)
+++ scribus/ui/hruler.cpp	(working copy)
@@ -22,6 +22,7 @@
  ***************************************************************************/
 
 #include <cmath>
+#include <utility>
 
 #include <QCursor>
 #include <QMouseEvent>
@@ -100,11 +101,12 @@
 double Hruler::localToTextPos(int x) const
 {
 	double canvasPos = (x + m_view->contentsX()) / m_scaling;
-	if (m_rtl && m_currItem)
+	if (m_currItem && !m_columnGeometry.isEmpty() && m_currCol > 0)
 	{
-		double colWidth = (textWidth() - m_colGap * (m_cols - 1)) / m_cols;
-		double colEnd = textBase() + (colWidth + m_colGap) * (m_currCol - 1.0) + colWidth;
-		return colEnd - canvasPos;
+		const TextColumnGeometry& column = m_columnGeometry.at(qBound(0, m_currCol - 1, static_cast<int>(m_columnGeometry.size()) - 1));
+		if (m_rtl)
+			return textBase() + column.left + column.width - canvasPos;
+		return canvasPos - textBase() - column.left;
 	}
 	return canvasPos - textBase();
 }
@@ -139,13 +141,12 @@
 	if (pos + 1 < (mx + grabRadius) && pos + 1 > (mx - grabRadius))
 		result = rc_rightFrameDist;
 	
-	double colWidth = (textWidth() - m_colGap * (m_cols - 1)) / m_cols;
-	
 	m_currCol = 0;
-	for (int currCol = 0; currCol < m_cols; ++currCol)
+	for (int currCol = 0; currCol < m_columnGeometry.size(); ++currCol)
 	{
-		pos = textPosToLocal((colWidth + m_colGap) * currCol);
-		fpo = QRect(pos, topline, static_cast<int>(colWidth * m_scaling), rulerheight);
+		const TextColumnGeometry& column = m_columnGeometry.at(currCol);
+		pos = textPosToLocal(column.left);
+		fpo = QRect(pos, topline, static_cast<int>(column.width * m_scaling), rulerheight);
 		if (fpo.contains(mp))
 		{
 			m_currCol = currCol + 1;
@@ -153,12 +154,11 @@
 		}
 	}
 	if (m_currCol == 0)
-	{
-		m_currCol = 1;
 		return result;
-	}
 	
-	double colOffset = (colWidth + m_colGap) * (m_currCol - 1.0);
+	const TextColumnGeometry& column = m_columnGeometry.at(m_currCol - 1);
+	double colOffset = column.left;
+	double colWidth = column.width;
 	double colEnd    = colOffset + colWidth;
 
 	// Map a logical value (distance from logical start) to canvas text position
@@ -179,7 +179,7 @@
 		return rc_leftMargin;
 
 	// Right Margin handle — rightMargin is always from the logical end side
-	pos = textPosToLocal(colOffset + m_rightMargin);
+	pos = textPosToLocal(logicalToCanvas(colWidth - m_rightMargin));
 	fpo = QRect(m_rtl ? (pos - 1) : (pos - grabRadius), midline, grabRadius, rulerheight / 2);
 	if (fpo.contains(mp))
 		return rc_rightMargin;
@@ -254,7 +254,6 @@
 		{
 			bool mustApplyStyle = false;
 			ParagraphStyle paraStyle;
-			double colWidth = (textWidth() - m_colGap * (m_cols - 1)) / m_cols;
 			switch (m_rulerCode)
 			{
 				case rc_leftFrameDist:
@@ -277,7 +276,7 @@
 					emit DocChanged(false);
 					break;
 				case rc_rightMargin:
-					paraStyle.setRightMargin(colWidth - m_rightMargin);
+					paraStyle.setRightMargin(m_rightMargin);
 					mustApplyStyle = true;
 					emit DocChanged(false);
 					break;
@@ -344,7 +343,8 @@
 	if (m_textEditMode)
 	{
 		int colStart, colEnd;
-		double colWidth = (textWidth() - m_colGap * (m_cols - 1)) / m_cols;
+		const TextColumnGeometry& column = m_columnGeometry.at(qBound(0, m_currCol - 1, static_cast<int>(m_columnGeometry.size()) - 1));
+		double colWidth = column.width;
 		double oldInd;
 		if (m_rulerCode == rc_leftFrameDist || m_rulerCode == rc_rightFrameDist)
 		{
@@ -353,15 +353,19 @@
 		}
 		else
 		{
-			colStart = textPosToLocal((colWidth + m_colGap) * (m_currCol - 1.0));
-			colEnd   = textPosToLocal((colWidth + m_colGap) * (m_currCol - 1.0) + colWidth);
+			colStart = textPosToLocal(column.left);
+			colEnd   = textPosToLocal(column.left + column.width);
 		}
 		QPointF mousePos = m->position();
 		if (m_mousePressed && (mousePos.y() < height()) && (mousePos.y() > 0) && (mousePos.x() > colStart - m_doc->guidesPrefs().grabRadius) && (mousePos.x() < colEnd + m_doc->guidesPrefs().grabRadius))
 		{
 			QApplication::changeOverrideCursor(QCursor(Qt::SizeHorCursor));
-			double toplimit = textWidth() + m_distRight - (m_colGap * (m_cols - 1.0)) - 1.0;
-			double toplimit2 = textWidth() + m_distLeft - (m_colGap * (m_cols - 1.0)) - 1.0;
+			double totalColumnWidth = 0.0;
+			for (const TextColumnGeometry& value : std::as_const(m_columnGeometry))
+				totalColumnWidth += value.width;
+			const double totalGap = qMax(0.0, textWidth() - totalColumnWidth);
+			double toplimit = textWidth() + m_distRight - totalGap - 1.0;
+			double toplimit2 = textWidth() + m_distLeft - totalGap - 1.0;
 			// In RTL dragging right moves away from the logical start (right edge),
 			// so negate the delta for all logical-position markers.
 			double dx = (m_mouseX - mousePos.x()) / m_scaling;
@@ -370,11 +374,11 @@
 			// canvas-relative position (distance from textBase) for MarkerMoved.
 			// In LTR: canvasRel = logicalValue (distance from left edge)
 			// In RTL:  canvasRel = colEnd - textBase() - logicalValue
-			double colOffset = (colWidth + m_colGap) * (m_currCol - 1.0);
+			double colOffset = column.left;
 			auto toCanvasRel = [&](double logicalValue) -> double {
 				if (m_rtl)
 					return colOffset + colWidth - logicalValue;
-				return logicalValue;
+				return colOffset + logicalValue;
 			};
 			switch (m_rulerCode)
 			{
@@ -417,12 +421,12 @@
 					repaint();
 					break;
 				case rc_rightMargin:
-					m_rightMargin -= logicalDx;
+					m_rightMargin += logicalDx;
 					if (m_rightMargin < 0)
 						m_rightMargin = 0;
 					if (m_rightMargin > colWidth - 1)
 						m_rightMargin  = colWidth - 1;
-					emit MarkerMoved(textBase(), toCanvasRel(m_rightMargin));
+					emit MarkerMoved(textBase(), toCanvasRel(colWidth - m_rightMargin));
 					repaint();
 					break;
 				case rc_tab:
@@ -499,7 +503,10 @@
 			return;
 
 		const ParagraphStyle& style(m_doc->appMode == modeEdit ? tItem->currentStyle() : tItem->itemText.defaultStyle());
-		TabManager *dia = new TabManager(this, m_doc->unitIndex(), style.tabValues(), tItem->columnWidth());
+		double minimumColumnWidth = 0.0;
+		for (const TextColumnGeometry& column : tItem->columnGeometry())
+			minimumColumnWidth = minimumColumnWidth == 0.0 ? column.width : qMin(minimumColumnWidth, column.width);
+		TabManager *dia = new TabManager(this, m_doc->unitIndex(), style.tabValues(), minimumColumnWidth);
 		dia->setRtl(m_rtl);
 		if (dia->exec())
 		{
@@ -551,10 +558,10 @@
 			p.translate( textPosToLocal(m_distLeft) - textPosToLocal(m_itemEndPos - m_itemPos - m_distRight), 0);
 			p.translate(-textPosToLocal(0), 0);
 		}
-		for (int currCol = 0; currCol < m_cols; ++currCol)
+		for (const TextColumnGeometry& column : std::as_const(m_columnGeometry))
 		{
-			double colWidth = (textWidth() - m_colGap * (m_cols - 1.0)) / m_cols;
-			double pos = (colWidth + m_colGap) * currCol;
+			double colWidth = column.width;
+			double pos = column.left;
 			double endPos = pos + colWidth;
 
 			p.fillRect(QRect(QPoint(textPosToLocal(pos), 0), QPoint(textPosToLocal(endPos), bottomline)), backgroundColor);
@@ -587,7 +594,7 @@
 				p.drawPolygon(cr2);
 
 				// Right Margin (logical end = left side), triangle opens right
-				xPos = textPosToLocal(endPos - m_rightMargin);
+				xPos = textPosToLocal(pos + m_rightMargin);
 				QPolygon cr3;
 				cr3.setPoints(3, xPos, tabline, xPos + 8, tabline, xPos, bottomline);
 				p.drawPolygon(cr3);
@@ -661,7 +668,7 @@
 				p.drawPolygon(cr2);
 
 				// Right Margin
-				xPos = textPosToLocal(pos + m_rightMargin);
+				xPos = textPosToLocal(endPos - m_rightMargin);
 				QPolygon cr3;
 				cr3.setPoints(3, xPos - 8, tabline, xPos, tabline, xPos, bottomline);
 				p.drawPolygon(cr3);
@@ -972,18 +979,23 @@
 		m_lineCorr = item->lineWidth() / 2.0;
 	else
 		m_lineCorr = 0;
-	m_colGap = item->m_columnGap;
-	m_cols = item->m_columns;
 	m_distLeft  = item->textToFrameDistLeft();
 	m_distRight = item->textToFrameDistRight();
+	m_columnGeometry.clear();
+	if (const PageItem_TextFrame* textFrame = item->asTextFrame())
+	{
+		m_columnGeometry = textFrame->columnGeometry();
+		const double contentLeft = m_distLeft + m_lineCorr;
+		for (TextColumnGeometry& column : m_columnGeometry)
+			column.left -= contentLeft;
+	}
+	if (m_columnGeometry.isEmpty())
+		m_columnGeometry.append(TextColumnGeometry { 0.0, textWidth() });
 
 	const ParagraphStyle& currentStyle = item->currentStyle();
 	m_firstIndent = currentStyle.firstIndent();
 	m_leftMargin = currentStyle.leftMargin();
-	double columnWidth = (item->width() - (item->columnGap() * (item->columns() - 1.0))
-						  - item->textToFrameDistLeft() - item->textToFrameDistLeft()
-						  - 2 * m_lineCorr) / item->columns();
-	m_rightMargin = columnWidth - currentStyle.rightMargin();
+	m_rightMargin = currentStyle.rightMargin();
 	m_reverse = item->imageFlippedH();
 	setRtl(item->currentStyle().direction() == ParagraphStyle::RTL);
 	m_textEditMode = true;
Index: resources/iconsets/1_7_0/1_7_0.xml
===================================================================
--- resources/iconsets/1_7_0/1_7_0.xml	(revision 27829)
+++ resources/iconsets/1_7_0/1_7_0.xml	(working copy)
@@ -545,6 +545,7 @@
 		<icon id="paragraph-align-top" file="16/paragraph-align-top.svg" />
 		<icon id="paragraph-align-vertical-center" file="16/paragraph-align-vertical-center.svg" />
 		<icon id="paragraph-columns" file="16/paragraph-columns.svg" />
+		<icon id="paragraph-column-gap" file="16/paragraph-column-gap.svg" />
 		<icon id="paragraph-effects-style" file="16/paragraph-effects-style.svg" />
 		<icon id="paragraph-indent-firstline" file="16/paragraph-firstline.svg" />
 		<icon id="paragraph-indent-left" file="16/paragraph-indent-left.svg" />	
Index: resources/iconsets/1_7_0/16/paragraph-column-gap.svg
===================================================================
--- resources/iconsets/1_7_0/16/paragraph-column-gap.svg	(nonexistent)
+++ resources/iconsets/1_7_0/16/paragraph-column-gap.svg	(working copy)
@@ -0,0 +1,4 @@
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><style>@import '../colors.css';</style>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M1 2h5v12H1V2Zm1 1v10h3V3H2Zm8-1h5v12h-5V2Zm1 1v10h3V3h-3Z"/>
+<path d="M7 7h2v2H7V7Z" class="error"/>
+</svg>

buttar

2026-09-14 08:06

reporter   ~0054470

A little preview of the proposed UI. More in the clip shared in the IRC group.
custom-columns-PROPOSED.png (34,397 bytes)   
custom-columns-PROPOSED.png (34,397 bytes)   

Issue History

Date Modified Username Field Change
2004-01-14 04:25 estebandido New Issue
2004-01-14 11:38 cbradney Note Added: 0000216
2004-01-14 11:38 cbradney Priority normal => low
2004-01-14 11:38 cbradney Product Version => 1.3
2004-01-14 11:38 cbradney Additional Information Updated
2005-02-19 13:33 plinnell Product Version 1.3 => 1.6 milestone
2005-02-19 16:35 louisdesjardins Note Added: 0003746
2005-06-15 14:12 ringerc Relationship added related to 0002098
2005-06-16 03:37 ringerc Note Added: 0005051
2005-06-16 03:37 ringerc Status new => acknowledged
2005-06-16 12:31 louisdesjardins Note Added: 0005053
2005-06-16 13:08 ringerc Note Added: 0005054
2015-09-17 20:08 Kunda Category Story Editor / Text Frames => Story Ed/Txt Frames
2015-09-17 20:12 Kunda Category Story Ed/Txt Frames => Story Editor / Text Frames
2016-02-13 23:12 Kunda Relationship added related to 0000212
2025-04-27 19:16 cbradney Category Story Editor / Text Frames => Text Frames / Story Editor
2026-09-14 05:39 buttar Note Added: 0054465
2026-09-14 05:39 buttar File Added: cc1-custom-text-column-geometry-v1.02.patch
2026-09-14 05:39 buttar File Added: cc2-custom-text-column-persistence-v1.01.patch
2026-09-14 05:39 buttar File Added: cc3-custom-text-column-editor-v1.04.patch
2026-09-14 05:41 buttar Note Added: 0054466
2026-09-14 05:44 buttar Note Added: 0054467
2026-09-14 08:01 buttar Note Added: 0054469
2026-09-14 08:01 buttar File Added: cc3-custom-text-column-editor-v1.06.patch
2026-09-14 08:02 buttar Tag Attached: #please_test
2026-09-14 08:06 buttar Note Added: 0054470
2026-09-14 08:06 buttar File Added: custom-columns-PROPOSED.png