diff --git a/build.gradle.kts b/build.gradle.kts index 2c7e09f..3565a16 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -69,7 +69,7 @@ dependencies { implementation("com.google.code.gson:gson:2.8.9") implementation("com.squareup.okhttp3:okhttp:4.9.3") implementation("com.squareup.okio:okio:3.4.0") - testImplementation("junit:junit:4.12") + testImplementation("junit:junit:4.13.1") } tasks { diff --git a/gradle.properties b/gradle.properties index 03eb271..c4b98fd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,5 @@ -# Gradle Releases -> https://github.com/gradle/gradle/releases -gradleVersion=8.6 -# Java language level used to compile sources and to generate the files for - Java 11 is required since 2020.3 -javaVersion=17 +# Gradle JVM settings +org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m kotlin.code.style=official # Opt-out flag for bundling Kotlin standard library. # See https://plugins.jetbrains.com/docs/intellij/kotlin.html#kotlin-standard-library for details. @@ -15,13 +13,13 @@ platformType=AI #TODO: set to 2025.1.1.2 platformVersion=2024.3.1.7 pluginGroup=com.github.grishberg.android -pluginName=android-layout-inspector-plugin +pluginName=YALI # See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html # for insight into build numbers and IntelliJ Platform versions. #TODO: set to 252.* sinceBuild=243.22562.145 -pluginVersion=25.08.04.0 +pluginVersion=25.08.05.0 # Use the latest of Android plugin versionAdd commentMore actions # see https://github.com/JetBrains/intellij-platform-gradle-plugin/releases androidPluginVersion=243.22562.218 -studioCompilePath=/Applications/Android Studio Preview.app/Contents +#localASVersion=/Applications/Android Studio.app/Contents diff --git a/src/main/kotlin/com/github/grishberg/android/layoutinspector/domain/LayoutResultOutput.kt b/src/main/kotlin/com/github/grishberg/android/layoutinspector/domain/LayoutResultOutput.kt index 5f11f42..f06d9c5 100644 --- a/src/main/kotlin/com/github/grishberg/android/layoutinspector/domain/LayoutResultOutput.kt +++ b/src/main/kotlin/com/github/grishberg/android/layoutinspector/domain/LayoutResultOutput.kt @@ -3,10 +3,11 @@ package com.github.grishberg.android.layoutinspector.domain import com.android.layoutinspector.model.LayoutFileData interface LayoutResultOutput { + /** * Show hierarchy and screenshot */ - fun showResult(resultOutput: LayoutFileData, label: String? = null) + fun showResult(resultOutput: LayoutFileData, label: String? = null, isRefresh: Boolean = false) fun showError(error: String) diff --git a/src/main/kotlin/com/github/grishberg/android/layoutinspector/domain/Logic.kt b/src/main/kotlin/com/github/grishberg/android/layoutinspector/domain/Logic.kt index b7692e2..2e66e9e 100644 --- a/src/main/kotlin/com/github/grishberg/android/layoutinspector/domain/Logic.kt +++ b/src/main/kotlin/com/github/grishberg/android/layoutinspector/domain/Logic.kt @@ -9,14 +9,14 @@ import com.github.grishberg.android.layoutinspector.process.LayoutFileSystem import com.github.grishberg.android.layoutinspector.process.LayoutInspectorCaptureTask import com.github.grishberg.android.layoutinspector.process.RecordingConfig import com.github.grishberg.android.layoutinspector.process.providers.ScreenSizeProvider +import java.io.IOException +import java.text.SimpleDateFormat +import java.util.Date import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.launch -import java.io.IOException -import java.text.SimpleDateFormat -import java.util.Date private const val TAG = "Logic" @@ -33,6 +33,7 @@ class Logic( private val coroutineScope: CoroutineScope, private val dispatchers: CoroutinesDispatchers ) { + private val screenSizeProvider = ScreenSizeProvider(dispatchers) private var recordingJob: Job? = null private var isOpenedLayout = false @@ -88,7 +89,6 @@ class Logic( val dpPerPixels = (density / 160.0) logger.d("$TAG: dp per pixels = $dpPerPixels") - val config = RecordingConfig( recordOptions.client, window, @@ -114,7 +114,7 @@ class Logic( } val windowList: List = windows.await() if (windowList.any { it.displayName == config.clientWindow.displayName }) { - captureLayouts(config) + captureLayouts(config, isRefresh = true) } else { recodLayoutFromNewDevice() } @@ -123,7 +123,7 @@ class Logic( } private suspend fun captureLayouts( - config: RecordingConfig, + config: RecordingConfig, isRefresh: Boolean = false ) { val task = LayoutInspectorCaptureTask(layoutFileSystem, coroutineScope, logger, dispatchers) @@ -134,13 +134,12 @@ class Logic( if (liResult.data == null || liResult.root == null) { output.showError(liResult.error) } else { - onSuccessCaptured(config, liResult) + onSuccessCaptured(config, liResult, isRefresh) } } private fun onSuccessCaptured( - config: RecordingConfig, - liResult: LayoutInspectorResult, + config: RecordingConfig, liResult: LayoutInspectorResult, isRefresh: Boolean = false ) { val dpPerPixels = config.dpPerPixels val fileNamePrefix = config.recordOptions.fileNamePrefix @@ -158,7 +157,7 @@ class Logic( metaRepository.serialize() logger.d("$TAG: Received result") - output.showResult(LayoutFileData.fromLayoutInspectorResult(liResult), config.recordOptions.label) + output.showResult(LayoutFileData.fromLayoutInspectorResult(liResult), config.recordOptions.label, isRefresh) isOpenedLayout = true } diff --git a/src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/LayoutsPreviewConfiguration.kt b/src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/LayoutsPreviewConfiguration.kt new file mode 100644 index 0000000..9c18ef2 --- /dev/null +++ b/src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/LayoutsPreviewConfiguration.kt @@ -0,0 +1,6 @@ +package com.github.grishberg.android.layoutinspector.ui + +import java.awt.Dimension +import java.awt.geom.AffineTransform + +data class LayoutsPreviewConfiguration(val size: Dimension, val transform: AffineTransform) \ No newline at end of file diff --git a/src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/Main.kt b/src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/Main.kt index 92f9100..05b7656 100644 --- a/src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/Main.kt +++ b/src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/Main.kt @@ -408,6 +408,10 @@ class Main( copyScreenShot.addActionListener { copyScreenshotToClipboard() } toolsMenu.add(copyScreenShot) + val syncLayoutsScaleMenuItem = JMenuItem("Sync scale from other layouts panel") + syncLayoutsScaleMenuItem.addActionListener { copyLayoutsConfiguration() } + toolsMenu.add(syncLayoutsScaleMenuItem) + return toolsMenu } @@ -494,7 +498,7 @@ class Main( logic.openFile() } - override fun showResult(resultOutput: LayoutFileData, label: String?) { + override fun showResult(resultOutput: LayoutFileData, label: String?, isRefresh: Boolean) { val trimmedLabel = label?.trim() if (!trimmedLabel.isNullOrEmpty()) { this.title = trimmedLabel @@ -502,7 +506,7 @@ class Main( this.title = TITLE } - layoutPanel.showLayoutResult(resultOutput) + layoutPanel.showLayoutResult(resultOutput, isRefresh) treePanel.showLayoutResult(resultOutput) findDialog.updateRootNode(resultOutput.node) splitPane1.invalidate() @@ -671,6 +675,17 @@ class Main( layoutPanel.repaint() } + fun copyLayoutsConfiguration() { + windowsManager.getLayoutsPreviewConfiguration(this)?.let { + layoutPanel.setLayoutsConfigurationIfNeeded(it) + layoutPanel.repaint() + } + } + + fun getLayoutsPreviewConfiguration(): LayoutsPreviewConfiguration { + return layoutPanel.getLayoutsPreviewConfiguration() + } + private companion object { private const val TITLE = "Yet Another Android Layout Inspector." } diff --git a/src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/WindowsManager.kt b/src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/WindowsManager.kt index 8f6ba69..d9919b0 100644 --- a/src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/WindowsManager.kt +++ b/src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/WindowsManager.kt @@ -30,6 +30,16 @@ class WindowsManager( windows.remove(window) } + fun getLayoutsPreviewConfiguration(currentWindow: Main): LayoutsPreviewConfiguration? { + if (windows.size != 2) { + logger.d("getLayoutsPreviewConfiguration: there is ${windows.size} windows") + return null + } + + val referenceWindow = windows.first { it != currentWindow } + return referenceWindow.getLayoutsPreviewConfiguration() + } + fun startScreenshotTest(comparableWindow: Main): Boolean { if (windows.size != 2) { logger.d("startScreenshotTest: there is ${windows.size} windows") @@ -62,4 +72,4 @@ class WindowsManager( ) return true } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/info/flat/FlatPropertiesWithFilterPanel.kt b/src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/info/flat/FlatPropertiesWithFilterPanel.kt index 957ec20..eb17ae9 100644 --- a/src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/info/flat/FlatPropertiesWithFilterPanel.kt +++ b/src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/info/flat/FlatPropertiesWithFilterPanel.kt @@ -140,6 +140,7 @@ class FlatPropertiesWithFilterPanel( } result[entry.key] = rows } + createViewPropertiesData(result, node) } else { val rows = mutableListOf() rows.add( @@ -178,11 +179,19 @@ class FlatPropertiesWithFilterPanel( private fun createSummary(result: MutableMap>, node: AbstractViewNode) { val widthProperty: ViewProperty? val heightProperty: ViewProperty? + val viewFlags: ViewProperty? + val privateFlags: ViewProperty? + if (node is ViewNode) { widthProperty = node.getProperty("measurement:mMeasuredWidth") ?: node.getProperty("measuredWidth") heightProperty = node.getProperty("measurement:mMeasuredHeight") ?: node.getProperty("measuredHeight") + viewFlags = node.getProperty("mViewFlags") + privateFlags = node.getProperty("mPrivateFlags") } else { + viewFlags = null + privateFlags = null + widthProperty = ViewProperty( "width", "width", @@ -261,10 +270,259 @@ class FlatPropertiesWithFilterPanel( ) ) } + // add flags decoding + val mViewFlags = hexStringToInt(viewFlags?.value) + val mPrivateFlags = hexStringToInt(privateFlags?.value) + + if (mViewFlags != null && mPrivateFlags != null) { + val stateProperty = ViewProperty( + "view state", + "state", + category = null, + value = decodeFlagsToState(mPrivateFlags, mViewFlags), + isSizeProperty = false, + ) + + rows.add( + RowInfoImpl( + property = stateProperty, + sizeInDp = false, + roundDp = false, + dpPerPixels = 0.0, + alterName = "state", + isSummary = true, + ) + ) + + val isLayoutRequestedProperty = ViewProperty( + "isLayoutRequested", + "isLayoutRequested", + category = null, + value = decodeIsLayoutRequested(mPrivateFlags), + isSizeProperty = false, + ) + + rows.add( + RowInfoImpl( + property = isLayoutRequestedProperty, + sizeInDp = false, + roundDp = false, + dpPerPixels = 0.0, + alterName = "isLayoutRequested", + isSummary = true, + ) + ) + + } result["Summary"] = rows } + private fun createViewPropertiesData(result: MutableMap>, node: ViewNode) { + val rows = mutableListOf() + val viewFlags = node.getProperty("mViewFlags") + val privateFlags = node.getProperty("mPrivateFlags") + val privateFlags3 = node.getProperty("mPrivateFlags3") + + // add flags decoding + val mViewFlags = hexStringToInt(viewFlags?.value) + val mPrivateFlags = hexStringToInt(privateFlags?.value) + val mPrivateFlags3 = hexStringToInt(privateFlags3?.value) + + if (mViewFlags != null && mPrivateFlags != null) { + val visibilityValue = when (mViewFlags and VISIBILITY_MASK) { + VISIBLE -> "visible" + INVISIBLE -> "invisible" + GONE -> "gone" + else -> "" + } + rows.add(createRowInfo(fullName = "Visibility", value = visibilityValue)) + + rows.add( + createRowInfo( + fullName = "Focusable", + value = ((mViewFlags and FOCUSABLE) == FOCUSABLE) + ) + ) + rows.add( + createRowInfo( + fullName = "Enabled mask", + value = ((mViewFlags and ENABLED_MASK) == ENABLED) + ) + ) + rows.add( + createRowInfo( + fullName = "Draw mask", + value = ((mViewFlags and DRAW_MASK) == WILL_NOT_DRAW) + ) + ) + rows.add( + createRowInfo( + fullName = "Scrollbars hor", + value = ((mViewFlags and SCROLLBARS_HORIZONTAL) != 0) + ) + ) + + rows.add( + createRowInfo( + fullName = "Scrollbars vert", + value = ((mViewFlags and SCROLLBARS_VERTICAL) != 0) + ) + ) + + rows.add( + createRowInfo( + fullName = "Clickable", + value = ((mViewFlags and CLICKABLE) != 0) + ) + ) + rows.add( + createRowInfo( + fullName = "Long clickable", + value = ((mViewFlags and LONG_CLICKABLE) != 0) + ) + ) + + rows.add( + createRowInfo( + fullName = "Context clickable", + value = ((mViewFlags and CONTEXT_CLICKABLE) != 0) + ) + ) + + rows.add( + createRowInfo( + fullName = "Is root namspace", + value = ((mPrivateFlags and PFLAG_IS_ROOT_NAMESPACE) != 0) + ) + ) + + rows.add( + createRowInfo( + fullName = "Focused", + value = ((mPrivateFlags and PFLAG_FOCUSED) != 0) + ) + ) + + + rows.add( + createRowInfo( + fullName = "Selected", + value = ((mPrivateFlags and PFLAG_SELECTED) != 0) + ) + ) + + rows.add( + createRowInfo( + fullName = "Prepressed", + value = if ((mPrivateFlags and PFLAG_PREPRESSED) != 0) + "prepressed" + else if ((mPrivateFlags and PFLAG_PRESSED) != 0) "pressed" else "" + ) + ) + + rows.add( + createRowInfo( + fullName = "Hovered", + value = ((mPrivateFlags and PFLAG_HOVERED) != 0) + ) + ) + rows.add( + createRowInfo( + fullName = "Activated", + value = ((mPrivateFlags and PFLAG_ACTIVATED) != 0) + ) + ) + + rows.add( + createRowInfo( + fullName = "Invalidated", + value = ((mPrivateFlags and PFLAG_INVALIDATED) != 0) + ) + ) + + rows.add( + createRowInfo( + fullName = "Dirty", + value = ((mPrivateFlags and PFLAG_DIRTY_MASK) != 0) + ) + ) + + rows.add( + createRowInfo( + fullName = "isLayoutRequested", + value = (mPrivateFlags and PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT + ) + ) + + mPrivateFlags3?.let { + val isLaidOut = (mPrivateFlags3 and PFLAG3_IS_LAID_OUT) == PFLAG3_IS_LAID_OUT + + rows.add( + createRowInfo( + fullName = "isLaidOut", + value = isLaidOut + ) + ) + + rows.add( + createRowInfo( + fullName = "isLayoutValid", + value = ((mPrivateFlags and PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT) && isLaidOut + ) + ) + } + } + + result["Flags"] = rows + } + + private fun createRowInfo( + fullName: String, + value: Boolean, + name: String? = null, + category: String? = null, + isSizeProperty: Boolean = false, + isSummary: Boolean = false + ): RowInfoImpl = createRowInfo( + fullName = fullName, value = value.toString(), name = name, + category = category, + isSizeProperty = isSizeProperty, + isSummary = isSummary + ) + + private fun createRowInfo( + fullName: String, + value: String, + name: String? = null, + category: String? = null, + isSizeProperty: Boolean = false, + isSummary: Boolean = false + ): RowInfoImpl { + val property = ViewProperty( + fullName, + name ?: fullName, + category = category, + value = value, + isSizeProperty = isSizeProperty, + ) + + return RowInfoImpl( + property = property, + sizeInDp = false, + roundDp = false, + dpPerPixels = 0.0, + alterName = fullName, + isSummary = isSummary, + ) + } + + private fun hexStringToInt(hex: String?): Int? { + if (hex == null) return null + val cleanedHex = hex.removePrefix("0x").removePrefix("0X") + return cleanedHex.toIntOrNull(16) + } + override fun setSizeDpMode(enabled: Boolean) { val shouldInvalidate = sizeInDp != enabled sizeInDp = enabled @@ -316,4 +574,73 @@ class FlatPropertiesWithFilterPanel( clipboard.setContents(stringSelection, null) } } + + private fun decodeIsLayoutRequested(mPrivateFlags: Int): String { + val value = (mPrivateFlags and PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT + return if (value) "true" else "false" + } + + private fun decodeFlagsToState(mPrivateFlags: Int, mViewFlags: Int): String { + val out = StringBuilder(256) + when (mViewFlags and VISIBILITY_MASK) { + VISIBLE -> out.append('V') + INVISIBLE -> out.append('I') + GONE -> out.append('G') + else -> out.append('.') + } + out.append(if ((mViewFlags and FOCUSABLE) == FOCUSABLE) 'F' else '.') + out.append(if ((mViewFlags and ENABLED_MASK) == ENABLED) 'E' else '.') + out.append(if ((mViewFlags and DRAW_MASK) == WILL_NOT_DRAW) '.' else 'D') + out.append(if ((mViewFlags and SCROLLBARS_HORIZONTAL) != 0) 'H' else '.') + out.append(if ((mViewFlags and SCROLLBARS_VERTICAL) != 0) 'V' else '.') + out.append(if ((mViewFlags and CLICKABLE) != 0) 'C' else '.') + out.append(if ((mViewFlags and LONG_CLICKABLE) != 0) 'L' else '.') + out.append(if ((mViewFlags and CONTEXT_CLICKABLE) != 0) 'X' else '.') + out.append(' ') + out.append(if ((mPrivateFlags and PFLAG_IS_ROOT_NAMESPACE) != 0) 'R' else '.') + out.append(if ((mPrivateFlags and PFLAG_FOCUSED) != 0) 'F' else '.') + out.append(if ((mPrivateFlags and PFLAG_SELECTED) != 0) 'S' else '.') + if ((mPrivateFlags and PFLAG_PREPRESSED) != 0) { + out.append('p') + } else { + out.append(if ((mPrivateFlags and PFLAG_PRESSED) != 0) 'P' else '.') + } + out.append(if ((mPrivateFlags and PFLAG_HOVERED) != 0) 'H' else '.') + out.append(if ((mPrivateFlags and PFLAG_ACTIVATED) != 0) 'A' else '.') + out.append(if ((mPrivateFlags and PFLAG_INVALIDATED) != 0) 'I' else '.') + out.append(if ((mPrivateFlags and PFLAG_DIRTY_MASK) != 0) 'D' else '.') + return out.toString() + } + + private companion object { + const val PFLAG3_IS_LAID_OUT: Int = 0x4 + const val PFLAG_FORCE_LAYOUT: Int = 0x00001000 + private const val PFLAG_PRESSED = 0x00004000 + const val WILL_NOT_DRAW: Int = 0x00000080 + const val VISIBLE: Int = 0x00000000 + const val INVISIBLE: Int = 0x00000004 + const val GONE: Int = 0x00000008 + const val VISIBILITY_MASK: Int = 0x0000000C + const val FOCUSABLE: Int = 0x00000001 + + const val ENABLED: Int = 0x00000000 + + const val ENABLED_MASK: Int = 0x00000020 + const val DRAW_MASK: Int = 0x00000080 + const val SCROLLBARS_HORIZONTAL: Int = 0x00000100 + const val SCROLLBARS_VERTICAL: Int = 0x00000200 + const val CLICKABLE: Int = 0x00004000 + const val LONG_CLICKABLE: Int = 0x00200000 + const val CONTEXT_CLICKABLE: Int = 0x00800000 + + const val PFLAG_IS_ROOT_NAMESPACE: Int = 0x00000008 + const val PFLAG_FOCUSED: Int = 0x00000002 + const val PFLAG_SELECTED: Int = 0x00000004 + private const val PFLAG_PREPRESSED = 0x02000000 + private const val PFLAG_HOVERED = 0x10000000 + const val PFLAG_ACTIVATED: Int = 0x40000000 + const val PFLAG_INVALIDATED: Int = -0x80000000 + const val PFLAG_DIRTY_MASK: Int = 0x00200000 + + } } diff --git a/src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/layout/LayoutPanel.kt b/src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/layout/LayoutPanel.kt index 499da5e..bc63a29 100644 --- a/src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/layout/LayoutPanel.kt +++ b/src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/layout/LayoutPanel.kt @@ -5,6 +5,7 @@ import com.android.layoutinspector.model.LayoutFileData import com.github.grishberg.android.layoutinspector.domain.AbstractViewNode import com.github.grishberg.android.layoutinspector.domain.MetaRepository import com.github.grishberg.android.layoutinspector.settings.SettingsFacade +import com.github.grishberg.android.layoutinspector.ui.LayoutsPreviewConfiguration import com.github.grishberg.android.layoutinspector.ui.common.SimpleComponentListener import com.github.grishberg.android.layoutinspector.ui.screenshottest.ScreenshotPainter import java.awt.Dimension @@ -12,6 +13,7 @@ import java.awt.Graphics import java.awt.Graphics2D import java.awt.Point import java.awt.event.ComponentEvent +import java.awt.geom.AffineTransform import java.awt.geom.NoninvertibleTransformException import java.awt.geom.Point2D import java.awt.geom.Rectangle2D @@ -97,9 +99,14 @@ class LayoutPanel( }) } - fun showLayoutResult(layoutData: LayoutFileData) { + fun showLayoutResult(layoutData: LayoutFileData, isRefresh: Boolean = false) { logic.showLayoutResult(layoutData) - fitZoom() + + // Only preserve camera position for refresh operations, reset zoom for new captures + if (!isRefresh) { + fitZoom() + } + repaint() invalidate() } @@ -183,4 +190,21 @@ class LayoutPanel( fun copyScreenshotToClipboard() { clipboardManager.copyToClipboard(screenshot) } + + fun getLayoutsPreviewConfiguration(): LayoutsPreviewConfiguration { + val transform = AffineTransform(zoomAndPanListener.getCoordTransform()) + this.transformedPoint + return LayoutsPreviewConfiguration( + size = logic.imageSize, transform = transform + ) + } + + fun setLayoutsConfigurationIfNeeded(cfg: LayoutsPreviewConfiguration) { + if (cfg.size != logic.imageSize) { + return + } + + zoomAndPanListener.setCoordinatesTransformDirect(cfg.transform) + repaint() + } } diff --git a/src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/layout/ZoomAndPanListener.kt b/src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/layout/ZoomAndPanListener.kt index ed0aa8e..58fdf34 100644 --- a/src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/layout/ZoomAndPanListener.kt +++ b/src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/layout/ZoomAndPanListener.kt @@ -44,6 +44,14 @@ class ZoomAndPanListener( } } + /** + * Sets the coordinate transform directly without additional offset corrections. + * Use this when copying transforms between LayoutPanels to preserve exact camera position. + */ + fun setCoordinatesTransformDirect(coordinatedTransform: AffineTransform) { + this.coordTransform = AffineTransform(coordinatedTransform) + } + override fun mouseClicked(e: MouseEvent) = Unit override fun mousePressed(e: MouseEvent) { val point = e.point