From eb154eda5fb7e8827fcdb6efdf145d1e7ff462dc Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 17 Jul 2026 09:12:12 +0400 Subject: [PATCH] Move by rows on Up/Down in editor tables. --- .../SourceFiles/iv/editor/iv_editor_state.cpp | 108 ++++++++++++++++++ .../SourceFiles/iv/editor/iv_editor_state.h | 4 + .../iv/editor/iv_editor_widget.cpp | 42 ++++++- 3 files changed, 153 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/iv/editor/iv_editor_state.cpp b/Telegram/SourceFiles/iv/editor/iv_editor_state.cpp index 16c0040fa0..013958f682 100644 --- a/Telegram/SourceFiles/iv/editor/iv_editor_state.cpp +++ b/Telegram/SourceFiles/iv/editor/iv_editor_state.cpp @@ -9219,6 +9219,114 @@ std::optional State::firstTableCellOrdinalFromActiveTitle() const { return std::nullopt; } +std::optional State::adjacentRowTableCellOrdinal(bool down) const { + const auto descriptor = textNode(_activeTextOrdinal); + if (!descriptor) { + return std::nullopt; + } + const auto leaf = descriptor->leaf; + if (leaf.kind != LeafKind::TableCellText) { + return std::nullopt; + } + const auto owner = block(leaf.block); + if (!owner || owner->kind != BlockKind::Table) { + return std::nullopt; + } + const auto grid = BuildTableGrid(*owner, tableRenderLimits()); + const auto active = [&]() -> const TableGridCellReference* { + for (const auto &candidate : grid.cells) { + if (candidate.rowIndex == leaf.tableRowIndex + && candidate.cellIndex == leaf.tableCellIndex) { + return &candidate; + } + } + return nullptr; + }(); + if (!active) { + return std::nullopt; + } + const auto column = active->columnFrom; + const auto step = down ? 1 : -1; + for (auto targetRow = down ? active->rowTill : (active->rowFrom - 1); + targetRow >= 0 && targetRow < grid.rowCount; + targetRow += step) { + auto best = (const TableGridCellReference*)nullptr; + auto bestDistance = std::numeric_limits::max(); + for (const auto &candidate : grid.cells) { + if (candidate.rowFrom > targetRow + || candidate.rowTill <= targetRow) { + continue; + } + const auto distance = (column < candidate.columnFrom) + ? (candidate.columnFrom - column) + : (column >= candidate.columnTill) + ? (column - candidate.columnTill + 1) + : 0; + if (distance < bestDistance) { + bestDistance = distance; + best = &candidate; + } + } + if (best) { + const auto ordinal = textNodeOrdinal({ + .kind = LeafKind::TableCellText, + .block = leaf.block, + .tableRowIndex = best->rowIndex, + .tableCellIndex = best->cellIndex, + }); + return (ordinal >= 0) + ? std::make_optional(ordinal) + : std::nullopt; + } + } + return std::nullopt; +} + +std::optional State::tableTitleOrdinalFromActiveCell() const { + const auto descriptor = textNode(_activeTextOrdinal); + if (!descriptor) { + return std::nullopt; + } + const auto leaf = descriptor->leaf; + if (leaf.kind != LeafKind::TableCellText) { + return std::nullopt; + } + const auto owner = block(leaf.block); + if (!owner || owner->kind != BlockKind::Table) { + return std::nullopt; + } + const auto ordinal = textNodeOrdinal({ + .kind = LeafKind::BlockText, + .block = leaf.block, + }); + return (ordinal >= 0) ? std::make_optional(ordinal) : std::nullopt; +} + +std::optional State::ordinalAfterActiveTable() const { + const auto descriptor = textNode(_activeTextOrdinal); + if (!descriptor) { + return std::nullopt; + } + const auto leaf = descriptor->leaf; + if (leaf.kind != LeafKind::TableCellText) { + return std::nullopt; + } + const auto owner = block(leaf.block); + if (!owner || owner->kind != BlockKind::Table) { + return std::nullopt; + } + for (auto i = _activeTextOrdinal + 1, count = textNodeCount(); + i != count; + ++i) { + const auto &candidate = _textNodes[i].leaf; + if (candidate.block != leaf.block + || candidate.kind != LeafKind::TableCellText) { + return i; + } + } + return std::nullopt; +} + void State::collectBoundarySteps( const std::vector &blocks, const BlockContainerPath &container, diff --git a/Telegram/SourceFiles/iv/editor/iv_editor_state.h b/Telegram/SourceFiles/iv/editor/iv_editor_state.h index b18e7082b3..5a00614d2e 100644 --- a/Telegram/SourceFiles/iv/editor/iv_editor_state.h +++ b/Telegram/SourceFiles/iv/editor/iv_editor_state.h @@ -272,6 +272,10 @@ public: [[nodiscard]] std::optional previousEditableOrdinal() const; [[nodiscard]] std::optional nextEditableOrdinal() const; [[nodiscard]] std::optional firstTableCellOrdinalFromActiveTitle() const; + [[nodiscard]] std::optional adjacentRowTableCellOrdinal( + bool down) const; + [[nodiscard]] std::optional tableTitleOrdinalFromActiveCell() const; + [[nodiscard]] std::optional ordinalAfterActiveTable() const; [[nodiscard]] BoundaryTarget activeBoundaryTarget(bool forward) const; [[nodiscard]] std::vector boundarySteps( bool forward) const; diff --git a/Telegram/SourceFiles/iv/editor/iv_editor_widget.cpp b/Telegram/SourceFiles/iv/editor/iv_editor_widget.cpp index 8e4ff5c9d7..aae815c3a1 100644 --- a/Telegram/SourceFiles/iv/editor/iv_editor_widget.cpp +++ b/Telegram/SourceFiles/iv/editor/iv_editor_widget.cpp @@ -9506,7 +9506,47 @@ bool Widget::handleFieldKey(QKeyEvent *e) { QTextCursor::MoveAnchor); } if (!handled) { - if (const auto articlePoint = activeFieldCursorArticlePoint()) { + const auto articlePoint = activeFieldCursorArticlePoint(); + const auto activeLeaf = _state->activeLeafPath(); + const auto inTableCell = activeLeaf + && (activeLeaf->kind == StateLeafKind::TableCellText); + const auto activateTableNavigationOrdinal = [&](int ordinal) { + if (articlePoint) { + if (const auto target = adjacentRowTarget( + ordinal, + *articlePoint, + down)) { + activateVerticalTarget(*target); + return; + } + } + const auto activated = commitAndActivateTextOrdinal( + ordinal, + 0, + 0, + ActivateReveal::Reveal); + if (activated && !down) { + setActiveFieldCursorOffset(_state->activeTextLength()); + } + handled = true; + }; + if (inTableCell) { + if (const auto ordinal + = _state->adjacentRowTableCellOrdinal(down)) { + activateTableNavigationOrdinal(*ordinal); + } else if (!down) { + if (const auto ordinal + = _state->tableTitleOrdinalFromActiveCell()) { + activateTableNavigationOrdinal(*ordinal); + } + } else if (const auto ordinal + = _state->ordinalAfterActiveTable()) { + activateTableNavigationOrdinal(*ordinal); + } else { + activateTrailingParagraph(); + handled = true; + } + } else if (articlePoint) { if (const auto ordinal = adjacentTextEditableOrdinal(down)) { if (const auto target = adjacentRowTarget( *ordinal,