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