From 15a0d642ffe02373a9142c70aa131cfbbf695f65 Mon Sep 17 00:00:00 2001 From: hstyi Date: Mon, 19 May 2025 18:31:51 +0800 Subject: [PATCH] feat: support block selection (#594) --- .../app/termora/terminal/SelectionModel.kt | 10 ++ .../termora/terminal/SelectionModelImpl.kt | 95 ++++++++++++++----- .../TerminalPanelMouseSelectionAdapter.kt | 9 ++ 3 files changed, 89 insertions(+), 25 deletions(-) diff --git a/src/main/kotlin/app/termora/terminal/SelectionModel.kt b/src/main/kotlin/app/termora/terminal/SelectionModel.kt index 0abe76f..a0b9987 100644 --- a/src/main/kotlin/app/termora/terminal/SelectionModel.kt +++ b/src/main/kotlin/app/termora/terminal/SelectionModel.kt @@ -21,6 +21,16 @@ interface SelectionModel { */ fun setSelection(startPosition: Position, endPosition: Position) + /** + * 设置块选中模式 + */ + fun setBlockSelection(block: Boolean) + + /** + * 是否是块选中模式 + */ + fun isBlockSelection(): Boolean + /** * 获取开始选中的位置 */ diff --git a/src/main/kotlin/app/termora/terminal/SelectionModelImpl.kt b/src/main/kotlin/app/termora/terminal/SelectionModelImpl.kt index a99f79f..26243bf 100644 --- a/src/main/kotlin/app/termora/terminal/SelectionModelImpl.kt +++ b/src/main/kotlin/app/termora/terminal/SelectionModelImpl.kt @@ -7,6 +7,7 @@ import kotlin.math.min open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel { private var startPosition = Position.unknown private var endPosition = Position.unknown + private var block = false private val document = terminal.getDocument() internal companion object { @@ -67,29 +68,60 @@ open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel { return sb.toString() } - val iterator = getChars(getSelectionStartPosition(), getSelectionEndPosition()) - while (iterator.hasNext()) { - val line = iterator.next() - val chars = line.chars() - if (chars.isEmpty() || chars.first().first.isNull) { - continue - } + val start = getSelectionStartPosition() + val end = getSelectionEndPosition() - for (e in chars) { - if (e.first.isSoftHyphen) { - continue - } else if (e.first.isNull) { - break + if (isBlockSelection()) { + val left = min(start.x, end.x) + val right = max(start.x, end.x) + val top = min(start.y, end.y) + val bottom = max(start.y, end.y) + + for (lineNum in top..bottom) { + val line = document.getLine(lineNum) + val chars = line.chars() + + // 块选中要处理超出边界 + val from = (left - 1).coerceAtLeast(0) + val to = right.coerceAtMost(chars.size) + + if (from < to) { + val selected = chars.subList(from, to) + .filter { !it.first.isNull && !it.first.isSoftHyphen } + .joinToString("") { it.first.toString() } + sb.append(selected) + } + + if (lineNum != bottom) { + sb.appendLine() } - sb.append(e.first) } - if (line.wrapped) { - continue - } + } else { + val iterator = getChars(start, end) + while (iterator.hasNext()) { + val line = iterator.next() + val chars = line.chars() + if (chars.isEmpty() || chars.first().first.isNull) { + continue + } - if (iterator.hasNext()) { - sb.appendLine() + for (e in chars) { + if (e.first.isSoftHyphen) { + continue + } else if (e.first.isNull) { + break + } + sb.append(e.first) + } + + if (line.wrapped) { + continue + } + + if (iterator.hasNext()) { + sb.appendLine() + } } } @@ -171,6 +203,12 @@ open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel { fireSelectionChanged() } + override fun setBlockSelection(block: Boolean) { + this.block = block + } + + override fun isBlockSelection() = block + override fun getSelectionStartPosition(): Position { return startPosition } @@ -202,13 +240,20 @@ open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel { } override fun hasSelection(x: Int, y: Int): Boolean { - return hasSelection() && isPointInsideArea( - startPosition, - endPosition, - x, - y, - terminal.getTerminalModel().getCols() - ) + + if (hasSelection().not()) return false + + // 如果是块选中 + if (isBlockSelection()) { + val left = min(startPosition.x, endPosition.x) + val right = max(startPosition.x, endPosition.x) + val top = min(startPosition.y, endPosition.y) + val bottom = max(startPosition.y, endPosition.y) + + return x in left..right && y in top..bottom + } + + return isPointInsideArea(startPosition, endPosition, x, y, terminal.getTerminalModel().getCols()) } diff --git a/src/main/kotlin/app/termora/terminal/panel/TerminalPanelMouseSelectionAdapter.kt b/src/main/kotlin/app/termora/terminal/panel/TerminalPanelMouseSelectionAdapter.kt index 694255f..369d18e 100644 --- a/src/main/kotlin/app/termora/terminal/panel/TerminalPanelMouseSelectionAdapter.kt +++ b/src/main/kotlin/app/termora/terminal/panel/TerminalPanelMouseSelectionAdapter.kt @@ -134,6 +134,8 @@ class TerminalPanelMouseSelectionAdapter(private val terminalPanel: TerminalPane // 如果不判断的话可能会导致移动了一点点就就进入选择状态了 val diff = terminalPanel.getAverageCharWidth() / 5.0 if (abs(mousePressedPoint.y - e.y) >= diff || abs(mousePressedPoint.x - e.x) >= diff) { + // 设置选中模式 + terminal.getSelectionModel().setBlockSelection(isOnlyAltDown(e)) beginSelect( Position(x = mousePressedPoint.x, y = mousePressedPoint.y), ) @@ -141,6 +143,13 @@ class TerminalPanelMouseSelectionAdapter(private val terminalPanel: TerminalPane } } + private fun isOnlyAltDown(e: MouseEvent): Boolean { + return e.isAltDown && + e.isMetaDown.not() && + e.isControlDown.not() && + e.isShiftDown.not() && + e.isAltGraphDown.not() + } private fun beginSelect(position: Position) {