From 91c5aab3511c2a5d0ceba7047c965c4d797e2dd2 Mon Sep 17 00:00:00 2001 From: Alon Albert Date: Thu, 9 May 2024 06:44:59 -0700 Subject: [PATCH] Sync Lines Between Panels When Possible Not all users would want this so it's controlled by a menu item. --- .../kotlin/explorer/KotlinExplorer.kt | 39 +++++++++------ .../kotlin/explorer/SourceTextArea.kt | 49 +++++++++++++++++++ .../dev/romainguy/kotlin/explorer/State.kt | 2 + .../explorer/{SwingPanel.kt => Swing.kt} | 9 ++++ .../kotlin/explorer/code/CodeTextArea.kt | 41 +++++++++++----- .../resources/themes/kotlin_explorer.xml | 2 +- 6 files changed, 115 insertions(+), 27 deletions(-) create mode 100644 src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/SourceTextArea.kt rename src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/{SwingPanel.kt => Swing.kt} (90%) diff --git a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/KotlinExplorer.kt b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/KotlinExplorer.kt index cb51cb10..d97323fb 100644 --- a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/KotlinExplorer.kt +++ b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/KotlinExplorer.kt @@ -37,6 +37,7 @@ import androidx.compose.ui.input.key.Key.Companion.L import androidx.compose.ui.input.key.Key.Companion.O import androidx.compose.ui.input.key.Key.Companion.P import androidx.compose.ui.input.key.Key.Companion.R +import androidx.compose.ui.input.key.Key.Companion.S import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.style.TextAlign.Companion.Center import androidx.compose.ui.unit.DpSize @@ -131,17 +132,22 @@ private class UiState(val explorerState: ExplorerState, window: ComposeWindow) { } } - val sourceTextArea = sourceTextArea(focusTracker, explorerState).apply { requestFocusInWindow() } - val byteCodeTextArea = byteCodeTextArea(explorerState, focusTracker) - val dexTextArea = dexTextArea(explorerState, focusTracker) + val sourceTextArea: SourceTextArea = sourceTextArea(focusTracker, explorerState).apply { requestFocusInWindow() } + val byteCodeTextArea = byteCodeTextArea(explorerState, focusTracker, sourceTextArea) + val dexTextArea = dexTextArea(explorerState, focusTracker, sourceTextArea) val oatTextArea = oatTextArea(explorerState, focusTracker) + val codeTextAreas = listOf(byteCodeTextArea, dexTextArea, oatTextArea) val findDialog = FindDialog(window, searchListener).apply { searchContext.searchWrap = true } var showSettings by DialogState(!explorerState.toolPaths.isValid) val updatePresentationMode: (Boolean) -> Unit = { - listOf(dexTextArea, oatTextArea).forEach { area -> area.presentationMode = it } + listOf(byteCodeTextArea, dexTextArea, oatTextArea).forEach { area -> area.presentationMode = it } + } + val updateSyncLinesEnabled: (Boolean) -> Unit = { + listOf(byteCodeTextArea, dexTextArea).forEach { area -> area.isSyncLinesEnabled = it } + sourceTextArea.isSyncLinesEnabled = it } val updateShowLineNumbers: (Boolean) -> Unit = { @@ -169,6 +175,8 @@ private fun FrameWindowScope.KotlinExplorer( ) { val uiState = remember { UiState(explorerState, window) } + uiState.sourceTextArea.addCodeTextAreas(uiState.byteCodeTextArea, uiState.dexTextArea) + val sourcePanel: @Composable () -> Unit = { SourcePanel(uiState.sourceTextArea, explorerState, uiState.showSettings) } val byteCodePanel: @Composable () -> Unit = @@ -193,6 +201,7 @@ private fun FrameWindowScope.KotlinExplorer( { panels = explorerState.getPanels(sourcePanel, byteCodePanel, dexPanel, oatPanel) }, uiState.updateShowLineNumbers, uiState.updatePresentationMode, + uiState.updateSyncLinesEnabled, ) if (uiState.showSettings) { @@ -322,19 +331,19 @@ private fun Title(text: String) { ) } -private fun sourceTextArea(focusTracker: FocusListener, explorerState: ExplorerState): RSyntaxTextArea { - return RSyntaxTextArea().apply { +private fun sourceTextArea(focusTracker: FocusListener, explorerState: ExplorerState): SourceTextArea { + return SourceTextArea(explorerState.syncLines).apply { configureSyntaxTextArea(SyntaxConstants.SYNTAX_STYLE_KOTLIN, focusTracker) SwingUtilities.invokeLater { requestFocusInWindow() } document.addDocumentListener(DocumentChangeListener { explorerState.sourceCode = text }) } } -private fun byteCodeTextArea(state: ExplorerState, focusTracker: FocusListener) = - codeTextArea(state, focusTracker) +private fun byteCodeTextArea(state: ExplorerState, focusTracker: FocusListener, sourceTextArea: SourceTextArea) = + codeTextArea(state, focusTracker, sourceTextArea = sourceTextArea) -private fun dexTextArea(state: ExplorerState, focusTracker: FocusListener) = - codeTextArea(state, focusTracker) +private fun dexTextArea(state: ExplorerState, focusTracker: FocusListener, sourceTextArea: SourceTextArea) = + codeTextArea(state, focusTracker, sourceTextArea = sourceTextArea) private fun oatTextArea(state: ExplorerState, focusTracker: FocusListener) = codeTextArea(state, focusTracker, hasLineNumbers = false) @@ -342,10 +351,11 @@ private fun oatTextArea(state: ExplorerState, focusTracker: FocusListener) = private fun codeTextArea( state: ExplorerState, focusTracker: FocusListener, - hasLineNumbers: Boolean = true + hasLineNumbers: Boolean = true, + sourceTextArea: SourceTextArea? = null, ): CodeTextArea { val codeStyle = CodeStyle(state.indent, state.showLineNumbers && hasLineNumbers, state.lineNumberWidth) - return CodeTextArea(state.presentationMode, codeStyle).apply { + return CodeTextArea(state.presentationMode, codeStyle, state.syncLines, sourceTextArea).apply { configureSyntaxTextArea(SyntaxConstants.SYNTAX_STYLE_NONE, focusTracker) } } @@ -365,6 +375,7 @@ private fun FrameWindowScope.MainMenu( onPanelsUpdated: () -> Unit, onShowLineNumberChanged: (Boolean) -> Unit, onPresentationModeChanged: (Boolean) -> Unit, + onSyncLinesChanged: (Boolean) -> Unit, ) { val scope = rememberCoroutineScope() @@ -409,6 +420,7 @@ private fun FrameWindowScope.MainMenu( MenuCheckboxItem("Show Line Numbers", CtrlShift(L), explorerState::showLineNumbers) { onShowLineNumberChanged(it) } + MenuCheckboxItem("Sync lines", Ctrl(S), explorerState::syncLines, onSyncLinesChanged) Separator() MenuCheckboxItem("Show Logs", Ctrl(L), explorerState::showLogs) Separator() @@ -442,7 +454,6 @@ private fun RSyntaxTextArea.configureSyntaxTextArea(syntaxStyle: String, focusTr tabsEmulated = true tabSize = 4 applyTheme(this) - currentLineHighlightColor = java.awt.Color.decode("#F5F8FF") addFocusListener(focusTracker) } @@ -526,4 +537,4 @@ private fun ExplorerState.setWindowState(windowState: WindowState) { private fun CodeStyle.withSettings(indent: Int, lineNumberWidth: Int) = copy(indent = indent, lineNumberWidth = lineNumberWidth) -private fun CodeStyle.withShowLineNumbers(value: Boolean) = copy(showLineNumbers = value) \ No newline at end of file +private fun CodeStyle.withShowLineNumbers(value: Boolean) = copy(showLineNumbers = value) diff --git a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/SourceTextArea.kt b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/SourceTextArea.kt new file mode 100644 index 00000000..91559e51 --- /dev/null +++ b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/SourceTextArea.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.romainguy.kotlin.explorer + +import dev.romainguy.kotlin.explorer.code.CodeTextArea +import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent + +class SourceTextArea(var isSyncLinesEnabled: Boolean) : RSyntaxTextArea() { + private val codeTextAreas = mutableListOf() + + init { + addMouseListener(object : MouseAdapter() { + override fun mouseClicked(event: MouseEvent) { + if (isSyncLinesEnabled) { + codeTextAreas.forEach { + it.gotoSourceLine(getLineOfOffset(viewToModel2D(event.point))) + } + } + } + }) + } + + fun addCodeTextAreas(vararg codeTextAreas: CodeTextArea) { + this.codeTextAreas.addAll(codeTextAreas) + } + + fun gotoLine(src: CodeTextArea, line: Int) { + caretPosition = getLineStartOffset(line.coerceIn(0 until lineCount)) + centerCaretInView() + // Sync other `CodeTextArea` to same line as the `src` sent + codeTextAreas.filter { it !== src }.forEach { it.gotoSourceLine(line) } + } +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/State.kt b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/State.kt index ec59de3e..99a572d5 100644 --- a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/State.kt +++ b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/State.kt @@ -33,6 +33,7 @@ private const val ShowLineNumbers = "SHOW_LINE_NUMBERS" private const val ShowByteCode = "SHOW_BYTE_CODE" private const val ShowDex = "SHOW_DEX" private const val ShowOat = "SHOW_OAT" +private const val SyncLines = "SYNC_LINES" private const val Indent = "Indent" private const val LineNumberWidth = "LINE_NUMBER_WIDTH" private const val WindowPosX = "WINDOW_X" @@ -57,6 +58,7 @@ class ExplorerState { var showDex by BooleanState(ShowDex, true) var showOat by BooleanState(ShowOat, true) var showLogs by mutableStateOf(false) + var syncLines by BooleanState(SyncLines, true) var lineNumberWidth by IntState(LineNumberWidth, 4) var indent by IntState(Indent, 4) var sourceCode: String = readSourceCode(toolPaths) diff --git a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/SwingPanel.kt b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Swing.kt similarity index 90% rename from src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/SwingPanel.kt rename to src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Swing.kt index dd27bba0..c484bdbe 100644 --- a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/SwingPanel.kt +++ b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Swing.kt @@ -31,7 +31,10 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toComposeImageBitmap import java.awt.Component import java.awt.GraphicsEnvironment +import java.awt.Point import java.awt.image.BufferedImage +import javax.swing.JTextArea +import javax.swing.JViewport /** * A [SwingPanel] that supports a Dialog rendered over it @@ -69,6 +72,12 @@ fun DialogSupportingSwingPanel( } } +fun JTextArea.centerCaretInView() { + val viewport = parent as? JViewport ?: return + val linePos = modelToView2D(caretPosition).bounds.centerY.toInt() + viewport.viewPosition = Point(0, maxOf(0, linePos - viewport.height / 2)) +} + private fun Component.getScreenShot(): BufferedImage? { if (width == 0 || height == 0) { return null diff --git a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/code/CodeTextArea.kt b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/code/CodeTextArea.kt index bc8feecf..631e7163 100644 --- a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/code/CodeTextArea.kt +++ b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/code/CodeTextArea.kt @@ -16,23 +16,27 @@ package dev.romainguy.kotlin.explorer.code +import dev.romainguy.kotlin.explorer.SourceTextArea +import dev.romainguy.kotlin.explorer.centerCaretInView import dev.romainguy.kotlin.explorer.code.CodeContent.* import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea import java.awt.BasicStroke import java.awt.Graphics import java.awt.Graphics2D import java.awt.RenderingHints +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent import java.awt.geom.GeneralPath import javax.swing.event.CaretEvent -import javax.swing.event.CaretListener -open class CodeTextArea( +class CodeTextArea( presentationMode: Boolean = false, codeStyle: CodeStyle, + var isSyncLinesEnabled: Boolean, + private val sourceTextArea: SourceTextArea?, ) : RSyntaxTextArea() { private var code: Code? = null private var jumpOffsets: JumpOffsets? = null - private var content: CodeContent = Empty var presentationMode = presentationMode @@ -52,9 +56,31 @@ open class CodeTextArea( init { addCaretListener(::caretUpdate) + + if (sourceTextArea != null) { + addMouseListener(object : MouseAdapter() { + override fun mouseClicked(event: MouseEvent) { + println(size) + val codeLine = getLineOfOffset(viewToModel2D(event.point)) + val line = code?.getSourceLine(codeLine) ?: return + sourceTextArea.gotoLine(this@CodeTextArea, line - 1) + } + }) + } } + fun setContent(value: CodeContent) { + content = value + updateContent() + } + + fun gotoSourceLine(sourceLine: Int) { + val line = code?.getCodeLine(sourceLine + 1) ?: return + caretPosition = getLineStartOffset(line.coerceIn(0 until lineCount)) + centerCaretInView() + } + private fun updatePreservingCaretLine() { val line = getLineOfOffset(caretPosition) val oldText = text @@ -65,11 +91,6 @@ open class CodeTextArea( } } - fun setContent(value: CodeContent) { - content = value - updateContent() - } - private fun updateContent() { val position = caretPosition code = null @@ -86,10 +107,6 @@ open class CodeTextArea( caretPosition = minOf(position, document.length) } - final override fun addCaretListener(listener: CaretListener?) { - super.addCaretListener(listener) - } - override fun paintComponent(g: Graphics?) { super.paintComponent(g) jumpOffsets?.let { jump -> diff --git a/src/jvmMain/resources/themes/kotlin_explorer.xml b/src/jvmMain/resources/themes/kotlin_explorer.xml index 91a48512..cf683578 100644 --- a/src/jvmMain/resources/themes/kotlin_explorer.xml +++ b/src/jvmMain/resources/themes/kotlin_explorer.xml @@ -6,7 +6,7 @@ - +