├── .gitignore ├── LICENSE.txt ├── README.MD ├── assets ├── find.png ├── measurements.png ├── preview.png └── start.png ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main ├── kotlin │ └── com │ │ ├── android │ │ └── layoutinspector │ │ │ ├── GerHierarchyException.kt │ │ │ ├── LayoutInspectorBridge.kt │ │ │ ├── LayoutInspectorCaptureOptions.kt │ │ │ ├── LayoutInspectorResult.kt │ │ │ ├── LayoutParserException.kt │ │ │ ├── common │ │ │ ├── AdbFacade.kt │ │ │ ├── AppLogger.kt │ │ │ └── PluginLogger.kt │ │ │ ├── model │ │ │ ├── ClientWindow.kt │ │ │ ├── DisplayInfo.kt │ │ │ ├── LayoutFileData.kt │ │ │ ├── ModernClientViewInspector.kt │ │ │ ├── TreePathUtils.kt │ │ │ ├── ViewNode.kt │ │ │ └── ViewProperty.kt │ │ │ └── parser │ │ │ ├── DisplayInfoFactory.kt │ │ │ ├── LayoutFileDataParser.kt │ │ │ ├── ViewNodeParser.kt │ │ │ ├── ViewNodeV2Decoder.kt │ │ │ ├── ViewNodeV2Parser.kt │ │ │ └── ViewPropertyParser.kt │ │ └── github │ │ └── grishberg │ │ ├── android │ │ ├── layoutinspector │ │ │ ├── common │ │ │ │ ├── CoroutinesDispatchers.kt │ │ │ │ ├── CoroutinesDispatchersImpl.kt │ │ │ │ └── MainScope.kt │ │ │ ├── domain │ │ │ │ ├── AbstractViewNode.kt │ │ │ │ ├── ClientWindowsInput.kt │ │ │ │ ├── DialogsInput.kt │ │ │ │ ├── DumpViewNode.kt │ │ │ │ ├── LayoutParserInput.kt │ │ │ │ ├── LayoutRecordOptions.kt │ │ │ │ ├── LayoutRecordOptionsInput.kt │ │ │ │ ├── LayoutResultOutput.kt │ │ │ │ ├── Logic.kt │ │ │ │ ├── MetaRepository.kt │ │ │ │ ├── RecordInfoInput.kt │ │ │ │ └── WindowsListInput.kt │ │ │ ├── process │ │ │ │ ├── HierarchyDump.kt │ │ │ │ ├── HierarchyDumpParser.kt │ │ │ │ ├── LayoutFileSystem.kt │ │ │ │ ├── LayoutInspectorCaptureTask.kt │ │ │ │ ├── LayoutParserImpl.kt │ │ │ │ ├── RecordingConfig.kt │ │ │ │ ├── TreeMerger.kt │ │ │ │ └── providers │ │ │ │ │ ├── ClientWindowsProvider.kt │ │ │ │ │ ├── DeviceProvider.kt │ │ │ │ │ └── ScreenSizeProvider.kt │ │ │ ├── settings │ │ │ │ ├── JsonSettings.kt │ │ │ │ ├── Settings.kt │ │ │ │ └── SettingsFacade.kt │ │ │ └── ui │ │ │ │ ├── ButtonsBuilder.kt │ │ │ │ ├── KeyBinder.kt │ │ │ │ ├── Main.kt │ │ │ │ ├── WindowsManager.kt │ │ │ │ ├── common │ │ │ │ ├── ColorUtils.kt │ │ │ │ ├── JNumberField.kt │ │ │ │ ├── LabeledGridBuilder.kt │ │ │ │ ├── MenuAcceleratorHelper.kt │ │ │ │ ├── SimpleComponentListener.kt │ │ │ │ ├── SimpleMouseListener.kt │ │ │ │ └── SimpleMouseMotionListener.kt │ │ │ │ ├── dialogs │ │ │ │ ├── ClientWrapper.kt │ │ │ │ ├── CloseByEscapeDialog.kt │ │ │ │ ├── DevicesCompoBoxModel.kt │ │ │ │ ├── FindDialog.kt │ │ │ │ ├── LoadingDialog.kt │ │ │ │ ├── NewLayoutDialog.kt │ │ │ │ ├── SupportBalloon.kt │ │ │ │ ├── WindowsDialog.kt │ │ │ │ └── bookmarks │ │ │ │ │ ├── BookmarkInfo.kt │ │ │ │ │ ├── Bookmarks.kt │ │ │ │ │ ├── BookmarksDialog.kt │ │ │ │ │ └── NewBookmarkDialog.kt │ │ │ │ ├── info │ │ │ │ ├── PropertiesPanel.kt │ │ │ │ ├── RowInfoImpl.kt │ │ │ │ └── flat │ │ │ │ │ ├── BoardTableCellRenderer.kt │ │ │ │ │ ├── FlatGroupTableModel.kt │ │ │ │ │ ├── FlatPropertiesWithFilterPanel.kt │ │ │ │ │ ├── Row.kt │ │ │ │ │ ├── TableValue.kt │ │ │ │ │ └── filter │ │ │ │ │ ├── FilterView.kt │ │ │ │ │ ├── PropertiesTableFilter.kt │ │ │ │ │ └── SimpleFilterTextView.kt │ │ │ │ ├── layout │ │ │ │ ├── DistanceBetweenTwoShape.kt │ │ │ │ ├── ImageHelper.kt │ │ │ │ ├── ImageTransferable.kt │ │ │ │ ├── LayoutLogic.kt │ │ │ │ ├── LayoutModel.kt │ │ │ │ ├── LayoutPanel.kt │ │ │ │ ├── LayoutsEnabledState.kt │ │ │ │ ├── ScreenshotClipboardManager.kt │ │ │ │ └── ZoomAndPanListener.kt │ │ │ │ ├── screenshottest │ │ │ │ ├── ScreenshotPainter.kt │ │ │ │ ├── ScreenshotTestDialog.kt │ │ │ │ ├── ScreenshotTestLogic.kt │ │ │ │ └── ScreenshotTestView.kt │ │ │ │ ├── theme │ │ │ │ ├── MaterialDarkColors.kt │ │ │ │ ├── MaterialLiteColors.kt │ │ │ │ ├── ThemeColors.kt │ │ │ │ ├── ThemeProxy.kt │ │ │ │ └── Themes.kt │ │ │ │ └── tree │ │ │ │ ├── EmptyTreeIcon.kt │ │ │ │ ├── IconsStore.kt │ │ │ │ ├── ItemViewRenderer.kt │ │ │ │ ├── NodeViewTreeCellRenderer.kt │ │ │ │ ├── TextForegroundColor.kt │ │ │ │ ├── TreeItem.kt │ │ │ │ ├── TreePanel.kt │ │ │ │ ├── TreeViewNodeMenu.kt │ │ │ │ └── ViewNodeTreeModel.kt │ │ └── li │ │ │ ├── PluginState.kt │ │ │ ├── ShowLayoutInspectorAction.kt │ │ │ ├── StorageService.kt │ │ │ └── ui │ │ │ └── NotificationHelperImpl.kt │ │ └── androidstudio │ │ └── plugins │ │ ├── AdbWrapper.kt │ │ ├── AsAction.kt │ │ ├── ConnectedDeviceInfoProvider.kt │ │ └── NotificationHelper.kt └── resources │ ├── META-INF │ ├── plugin.xml │ └── pluginIcon.svg │ └── icons │ ├── caliper.svg │ ├── dark │ ├── appbar.png │ ├── cardView.png │ ├── constraint_layout.png │ ├── contentCardView.png │ ├── coordinator_layout.png │ ├── fab.png │ ├── fitscreen.png │ ├── fitscreen.svg │ ├── frame_layout.png │ ├── help.png │ ├── help.svg │ ├── imageView.png │ ├── linear_layout.png │ ├── nestedScrollView.png │ ├── node_type_compose.svg │ ├── recyclerView.png │ ├── relativeLayout.png │ ├── resetzoom.png │ ├── resetzoom.svg │ ├── text.png │ ├── toolbar.png │ ├── view.png │ ├── viewPager.png │ ├── viewSwitcher.png │ └── viewstub.png │ ├── light │ ├── appbar.png │ ├── cardView.png │ ├── constraint_layout.png │ ├── contentCardView.png │ ├── coordinator_layout.png │ ├── fab.png │ ├── fitscreen.svg │ ├── frame_layout.png │ ├── help.svg │ ├── imageView.png │ ├── linear_layout.png │ ├── nestedScrollView.png │ ├── node_type_compose.svg │ ├── recyclerView.png │ ├── relativeLayout.png │ ├── resetzoom.svg │ ├── text.png │ ├── toolbar.png │ ├── view.png │ ├── viewPager.png │ ├── viewSwitcher.png │ └── viewstub.png │ └── loading.gif └── test ├── java └── com │ └── github │ └── grishberg │ └── android │ └── layoutinspector │ └── process │ ├── HierarchyDumpParserTest.kt │ └── TreeMergerTest.kt └── resources └── dump.xml /.gitignore: -------------------------------------------------------------------------------- 1 | /layouts 2 | # Created by .ignore support plugin (hsz.mobi) 3 | .gradle 4 | *.iml 5 | build/ 6 | out 7 | meta 8 | layouts 9 | .idea 10 | ### Java template 11 | # Compiled class file 12 | *.class 13 | 14 | # Log file 15 | *.log 16 | 17 | # BlueJ files 18 | *.ctxt 19 | 20 | # Mobile Tools for Java (J2ME) 21 | .mtj.tmp/ 22 | 23 | # Package Files # 24 | *.war 25 | *.ear 26 | *.zip 27 | *.tar.gz 28 | *.rar 29 | 30 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 31 | hs_err_pid* 32 | 33 | .DS_Store 34 | 35 | /.idea 36 | /.intellijPlatform 37 | /.kotlin 38 | log.txt 39 | android-layout-inspector-settings.json 40 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Yet another android layout inspector 2 | 3 | More stable Android Layout inspector than Android Studio Layout Inspector. 4 | Allows you to switch between displaying dimensions in **PX** and **DP** (only for new layout captures, not for opened files) 5 | 6 | ![preview](assets/preview.png) 7 | 8 | ![Searching mode](assets/find.png) 9 | 10 | ## Download 11 | ![GitHub All Releases](https://img.shields.io/github/downloads/Grigory-Rylov/android-layout-inspector/total?color=%234caf50&style=for-the-badge) 12 | 13 | Find [YALI](https://plugins.jetbrains.com/plugin/15227-yali) in AS plugins repository. 14 | 15 | OR 16 | 17 | 1) [Download latest release](https://github.com/Grigory-Rylov/android-layout-inspector/releases) 18 | 2) Preferences -> Plugins -> Install plugin from Disk... 19 | 3) Select android-layout-inspector-plugin-{VERSION}.jar 20 | 21 | ## Getting started 22 | 23 | Menu Tools -> Launch YALI 24 | 25 | ## Why Yet another android layout inspector? 26 | Because AS layout inspector sometimes cannot download layouts for some reasons. 27 | Also you can switch to *DP* dimension mode. 28 | 29 | ## Measure distance between two element 30 | 1) Select first element by **Mouse click** - it will be select by red square 31 | 2) Select second element by **Mouse click + Ctrl(Cmd)** - it will be select by yellow square. 32 | 33 | In the status bar will shown distance between first and second 34 | 35 | 36 | 37 | ## Measure distance with ruler 38 | 1) Press **Shift** and move mouse to see where will be first point of ruler (black rectangle) 39 | 2) **Mouse click + Shift** to start ruler mode 40 | 3) **Mouse drag + Shift** to change ruler size 41 | You will see ruler size in current units in status bar 42 | 43 | ## Hotkeys 44 | ### Files 45 | **Ctrl + o** - Open file dialog 46 | 47 | **Ctrl + n** - Record new layout 48 | 49 | ### Layout tree 50 | **Ctrl + c** - Copy node name 51 | 52 | **Ctrl + Shift + c** - Copy node ID 53 | 54 | **Mouse click + Ctrl** - Select element to measure distance from selected to current. 55 | 56 | **Mouse drag + Shift** - Measure ruler. 57 | 58 | **Mouse right click** - Show distance from selected view to current point. 59 | ### Properties table 60 | **Ctrl + c** - Copy property value 61 | 62 | ### Find 63 | **Ctrl + f** - Open find dialog (type text and press **Enter**) 64 | 65 | ### Layouts 66 | **z** - reset zoom to 100% 67 | **f** - fit zoom to layout panel width 68 | 69 | # Support me if you like YALI =) 70 | **ETH ERC20 tokens** : `0x25Ca16AD3c4e9BD1e6e5FDD77eDB019386B68591` 71 | 72 | **BNB BEP20 tokens** : `0x25Ca16AD3c4e9BD1e6e5FDD77eDB019386B68591` 73 | 74 | **USDT TRC20** : `TSo3X6K54nYq3S64wML4M4xFgTNiENkHwC` 75 | 76 | **BTC** : `bc1qmm5lp389scuk2hghgyzdztddwgjnxqa2awrrue` 77 | 78 | https://www.tinkoff.ru/cf/4KNjR2SMOAj 79 | 80 | # License 81 | 82 | Yet another android layout inspector is released under the [Apache License, Version 2.0](LICENSE.txt). 83 | -------------------------------------------------------------------------------- /assets/find.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/assets/find.png -------------------------------------------------------------------------------- /assets/measurements.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/assets/measurements.png -------------------------------------------------------------------------------- /assets/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/assets/preview.png -------------------------------------------------------------------------------- /assets/start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/assets/start.png -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 2 | 3 | plugins { 4 | // Java support 5 | id("java") 6 | // Kotlin support 7 | id("org.jetbrains.kotlin.jvm") version "2.1.0" 8 | // Gradle IntelliJ Platform Plugin 9 | id("org.jetbrains.intellij.platform") version "2.6.0" 10 | // Gradle Changelog Plugin 11 | id("org.jetbrains.changelog") version "1.3.1" 12 | // Protobuf support 13 | id("com.google.protobuf") version "0.9.4" 14 | } 15 | 16 | fun properties(key: String) = project.findProperty(key).toString() 17 | 18 | group = properties("pluginGroup") 19 | version = properties("pluginVersion") 20 | 21 | // Configure Java compatibility 22 | java { 23 | toolchain { 24 | languageVersion.set(JavaLanguageVersion.of(17)) 25 | } 26 | sourceCompatibility = JavaVersion.VERSION_17 27 | targetCompatibility = JavaVersion.VERSION_17 28 | } 29 | 30 | kotlin { 31 | jvmToolchain { 32 | languageVersion.set(JavaLanguageVersion.of(17)) 33 | } 34 | } 35 | 36 | // Configure project's dependencies 37 | repositories { 38 | mavenCentral() 39 | intellijPlatform { defaultRepositories() } 40 | } 41 | 42 | dependencies { 43 | intellijPlatform { 44 | androidStudio(properties("platformVersion")) 45 | bundledPlugin("org.jetbrains.android") 46 | } 47 | implementation(platform("io.projectreactor:reactor-bom:2024.0.0")) 48 | implementation("io.projectreactor.netty:reactor-netty-http:1.1.13") 49 | implementation("io.projectreactor.netty:reactor-netty-core:1.1.13") 50 | implementation("io.rsocket:rsocket-core:1.1.3") 51 | implementation("io.rsocket:rsocket-transport-netty:1.1.3") 52 | implementation("io.rsocket.broker:rsocket-broker-frames:0.3.0") 53 | implementation("org.jooq:joor-java-8:0.9.7") 54 | implementation("com.google.code.gson:gson:2.8.9") 55 | implementation("com.squareup.okhttp3:okhttp:4.9.3") 56 | implementation("com.squareup.okio:okio:3.4.0") 57 | testImplementation("junit:junit:4.12") 58 | } 59 | 60 | protobuf { 61 | protoc { 62 | artifact = "com.google.protobuf:protoc:3.25.1" 63 | } 64 | generateProtoTasks { 65 | all().forEach { task -> 66 | task.builtins { 67 | named("java") { 68 | option("lite") 69 | } 70 | } 71 | } 72 | } 73 | } 74 | 75 | sourceSets { 76 | main { 77 | proto { 78 | srcDir("proto") 79 | } 80 | } 81 | } 82 | 83 | tasks { 84 | patchPluginXml { 85 | version = properties("pluginVersion") 86 | sinceBuild = properties("pluginSinceBuild") 87 | untilBuild = properties("pluginUntilBuild") 88 | changeNotes = """ 89 | Support Android Studio Ladybug.
90 | Improved tree renderer.
91 | Fixed uiautomator dump.
92 | """ 93 | } 94 | 95 | runIde { 96 | jvmArgs = listOf( 97 | "-Dide.mac.message.dialogs.as.sheets=false", 98 | "-Djb.privacy.policy.text=", 99 | "-Djb.consents.confirmation.enabled=false" 100 | ) 101 | } 102 | 103 | withType().configureEach { 104 | compilerOptions { 105 | jvmTarget.set(JvmTarget.JVM_17) 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Gradle Releases -> https://github.com/gradle/gradle/releases 2 | gradleVersion=8.6 3 | # Java language level used to compile sources and to generate the files for - Java 11 is required since 2020.3 4 | javaVersion=17 5 | kotlin.code.style=official 6 | # Opt-out flag for bundling Kotlin standard library. 7 | # See https://plugins.jetbrains.com/docs/intellij/kotlin.html#kotlin-standard-library for details. 8 | # suppress inspection "UnusedProperty" 9 | kotlin.stdlib.default.dependency=false 10 | # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html 11 | # Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22 12 | platformPlugins=android 13 | # IntelliJ Platform Properties -> https://github.com/JetBrains/gradle-intellij-plugin#intellij-platform-properties 14 | platformType=AI 15 | platformVersion=2025.1.1.2 16 | pluginGroup=com.github.grishberg.android 17 | pluginName=android-layout-inspector-plugin 18 | # See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html 19 | # for insight into build numbers and IntelliJ Platform versions. 20 | pluginSinceBuild=233 21 | pluginUntilBuild=252.* 22 | pluginVersion=25.05.29.0 23 | studioCompilePath=/Applications/Android Studio Preview.app/Contents 24 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } 4 | gradlePluginPortal() 5 | mavenCentral() 6 | } 7 | } 8 | 9 | rootProject.name = 'android-layout-inspector-plugin' -------------------------------------------------------------------------------- /src/main/kotlin/com/android/layoutinspector/GerHierarchyException.kt: -------------------------------------------------------------------------------- 1 | package com.android.layoutinspector 2 | 3 | class GerHierarchyException: LayoutParserException() 4 | -------------------------------------------------------------------------------- /src/main/kotlin/com/android/layoutinspector/LayoutInspectorBridge.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.android.layoutinspector 17 | 18 | import com.android.layoutinspector.common.AppLogger 19 | import com.android.layoutinspector.model.ClientWindow 20 | import com.android.layoutinspector.model.ViewNode 21 | import com.android.layoutinspector.parser.ViewNodeParser 22 | import java.io.ByteArrayInputStream 23 | import java.io.ByteArrayOutputStream 24 | import java.io.IOException 25 | import java.io.ObjectOutputStream 26 | import java.util.concurrent.TimeUnit 27 | import javax.imageio.ImageIO 28 | 29 | private const val TAG = "LayoutInspectorBridge" 30 | 31 | object LayoutInspectorBridge { 32 | 33 | @JvmStatic val V2_MIN_API = 23 34 | 35 | @JvmStatic 36 | suspend fun captureView( 37 | logger: AppLogger, window: ClientWindow, options: LayoutInspectorCaptureOptions, timeoutInSeconds: Long 38 | ): LayoutInspectorResult { 39 | val hierarchy = window.loadWindowData(options, timeoutInSeconds, TimeUnit.SECONDS) 40 | ?: return LayoutInspectorResult.createErrorResult( 41 | "There was a timeout error capturing the layout data from the device.\n" + "The device may be too slow, the captured view may be too complex, or the view may contain animations.\n\n" + "Please retry with a simplified view and ensure the device is responsive." 42 | ) 43 | val root: ViewNode? 44 | try { 45 | logger.d("$TAG parse hierarchy") 46 | root = ViewNodeParser.parse(hierarchy, options.version) 47 | logger.d("$TAG parse hierarchy done. root is $root") 48 | 49 | } catch (e: StringIndexOutOfBoundsException) { 50 | return LayoutInspectorResult.createErrorResult("Unexpected error: $e") 51 | } catch (e: IOException) { 52 | return LayoutInspectorResult.createErrorResult("Unexpected error: $e") 53 | } 54 | if (root == null) { 55 | return LayoutInspectorResult.createErrorResult( 56 | "Unable to parse view hierarchy" 57 | ) 58 | } 59 | // Get the preview of the root node 60 | logger.d("$TAG Get the preview of the root node") 61 | val preview = window.loadViewImage( 62 | root, timeoutInSeconds, TimeUnit.SECONDS 63 | ) ?: return LayoutInspectorResult.createErrorResult("Unable to obtain preview image") 64 | logger.d("$TAG preview downloaded") 65 | val bytes = ByteArrayOutputStream(4096) 66 | var output: ObjectOutputStream? = null 67 | try { 68 | output = ObjectOutputStream(bytes) 69 | output.writeUTF(options.toString()) 70 | output.writeInt(hierarchy.size) 71 | output.write(hierarchy) 72 | output.writeInt(preview.size) 73 | output.write(preview) 74 | } catch (e: IOException) { 75 | return LayoutInspectorResult.createErrorResult( 76 | "Unexpected error while saving hierarchy snapshot: $e" 77 | ) 78 | } finally { 79 | try { 80 | output?.close() 81 | } catch (e: IOException) { 82 | return LayoutInspectorResult.createErrorResult( 83 | "Unexpected error while closing hierarchy snapshot: $e" 84 | ) 85 | } 86 | } 87 | return LayoutInspectorResult( 88 | root = root, 89 | data = bytes.toByteArray(), 90 | previewImage = ImageIO.read(ByteArrayInputStream(preview)), 91 | options = options, 92 | error = "" 93 | ) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/kotlin/com/android/layoutinspector/LayoutInspectorCaptureOptions.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.android.layoutinspector 17 | import com.google.gson.JsonObject 18 | import com.google.gson.JsonParser 19 | enum class ProtocolVersion(val value: String) { 20 | Version1("1"), 21 | Version2("2") 22 | } 23 | class LayoutInspectorCaptureOptions { 24 | var version = ProtocolVersion.Version1 25 | var title = "" 26 | override fun toString(): String { 27 | return serialize() 28 | } 29 | private fun serialize(): String { 30 | val obj = JsonObject() 31 | obj.addProperty(VERSION, version.value) 32 | obj.addProperty(TITLE, title) 33 | return obj.toString() 34 | } 35 | fun parse(json: String) { 36 | val obj = JsonParser().parse(json).asJsonObject 37 | version = ProtocolVersion.valueOf("Version${obj.get(VERSION).asString}") 38 | title = obj.get(TITLE).asString 39 | } 40 | companion object { 41 | private val VERSION = "version" 42 | private val TITLE = "title" 43 | } 44 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/android/layoutinspector/LayoutInspectorResult.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.android.layoutinspector 17 | 18 | import com.github.grishberg.android.layoutinspector.domain.AbstractViewNode 19 | import java.awt.image.BufferedImage 20 | 21 | /** 22 | * Represents result of a capture 23 | * Success: data is not null, and error is the empty string 24 | * Error: data is null, and error a non empty error message 25 | */ 26 | class LayoutInspectorResult( 27 | val root: AbstractViewNode?, 28 | val previewImage: BufferedImage?, 29 | val data: ByteArray?, 30 | val options: LayoutInspectorCaptureOptions?, 31 | val error: String, 32 | ) { 33 | 34 | companion object { 35 | 36 | fun createErrorResult(error: String) = LayoutInspectorResult( 37 | root = null, previewImage = null, data = null, options = null, error = error 38 | ) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/kotlin/com/android/layoutinspector/LayoutParserException.kt: -------------------------------------------------------------------------------- 1 | package com.android.layoutinspector 2 | 3 | open class LayoutParserException : Exception { 4 | constructor() : super() 5 | 6 | constructor(message: String) : super(message) 7 | 8 | constructor(e: Throwable) : super(e) 9 | 10 | constructor(message: String, t: Throwable) : super(message) 11 | } 12 | -------------------------------------------------------------------------------- /src/main/kotlin/com/android/layoutinspector/common/AdbFacade.kt: -------------------------------------------------------------------------------- 1 | package com.android.layoutinspector.common 2 | 3 | import com.android.ddmlib.IDevice 4 | 5 | interface AdbFacade { 6 | fun isConnected(): Boolean 7 | fun getDevices(): List 8 | fun hasInitialDeviceList(): Boolean 9 | fun connect() 10 | fun connect(remoterAddress: String) 11 | fun stop() 12 | } 13 | -------------------------------------------------------------------------------- /src/main/kotlin/com/android/layoutinspector/common/AppLogger.kt: -------------------------------------------------------------------------------- 1 | package com.android.layoutinspector.common 2 | 3 | interface AppLogger { 4 | fun d(msg: String) 5 | fun e(msg: String) 6 | fun e(msg: String, t: Throwable) 7 | fun w(msg: String) 8 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/android/layoutinspector/common/PluginLogger.kt: -------------------------------------------------------------------------------- 1 | package com.android.layoutinspector.common 2 | 3 | import com.intellij.openapi.diagnostic.Logger 4 | 5 | class PluginLogger : AppLogger { 6 | private val log: Logger = Logger.getInstance("YALI") 7 | override fun d(msg: String) { 8 | log.debug(msg) 9 | } 10 | 11 | override fun e(msg: String) { 12 | log.error(msg) 13 | } 14 | 15 | override fun e(msg: String, t: Throwable) { 16 | log.error(msg, t) 17 | } 18 | 19 | override fun w(msg: String) { 20 | log.warn(msg) 21 | } 22 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/android/layoutinspector/model/DisplayInfo.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.android.layoutinspector.model 17 | 18 | /** 19 | * Contains information used to draw selection boxes over each [ViewNode] in layout inspector's 20 | * preview box. Create using [com.android.layoutinspector.parser.DisplayInfoFactory] 21 | */ 22 | data class DisplayInfo( 23 | val willNotDraw: Boolean, 24 | val isVisible: Boolean, 25 | val left: Int, 26 | val top: Int, 27 | val width: Int, 28 | val height: Int, 29 | val scrollX: Int, 30 | val scrollY: Int, 31 | val clipChildren: Boolean, 32 | val translateX: Float, 33 | val translateY: Float, 34 | val scaleX: Float, 35 | val scaleY: Float, 36 | val contentDesc: String? 37 | ) { 38 | fun getCopyAtOrigin(): DisplayInfo { 39 | return this.copy(left = 0, top = 0) 40 | } 41 | 42 | companion object { 43 | fun createEmpty(): DisplayInfo { 44 | return DisplayInfo( 45 | false, false, 0, 0, 0, 0, 0, 46 | 0, false, 0f, 0f, 0f, 0f, null 47 | ) 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/android/layoutinspector/model/LayoutFileData.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.android.layoutinspector.model 17 | 18 | import com.android.layoutinspector.LayoutInspectorCaptureOptions 19 | import com.android.layoutinspector.LayoutInspectorResult 20 | import com.github.grishberg.android.layoutinspector.domain.AbstractViewNode 21 | import java.awt.image.BufferedImage 22 | 23 | /** 24 | * Data model for a parsed .li file. Create using methods in [com.android.layoutinspector.parser.LayoutFileDataParser] 25 | */ 26 | data class LayoutFileData( 27 | val bufferedImage: BufferedImage?, 28 | val node: AbstractViewNode?, 29 | val options: LayoutInspectorCaptureOptions 30 | ) { 31 | companion object { 32 | fun fromLayoutInspectorResult(result: LayoutInspectorResult): LayoutFileData { 33 | return LayoutFileData(result.previewImage, result.root, result.options!!) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/kotlin/com/android/layoutinspector/model/ModernClientViewInspector.kt: -------------------------------------------------------------------------------- 1 | package com.android.layoutinspector.model 2 | 3 | import com.android.ddmlib.Client 4 | import com.android.ddmlib.DebugViewDumpHandler 5 | import com.android.layoutinspector.common.AppLogger 6 | import kotlinx.coroutines.flow.MutableStateFlow 7 | import kotlinx.coroutines.flow.filterNotNull 8 | import kotlinx.coroutines.flow.first 9 | import java.nio.ByteBuffer 10 | import java.util.concurrent.TimeUnit 11 | 12 | class ModernClientViewInspector : ClientWindow.ClientViewInspector { 13 | override suspend fun dumpViewHierarchy( 14 | logger: AppLogger, 15 | client: Client, 16 | clientWindowTitle: String, 17 | skipChildren: Boolean, 18 | includeProperties: Boolean, 19 | useV2: Boolean, 20 | timeout: Long, 21 | timeUnit: TimeUnit 22 | ): ByteArray? { 23 | val handler = DumpViewHierarchyDebugViewDumpHandler() 24 | client.dumpViewHierarchy(clientWindowTitle, skipChildren, includeProperties, useV2, handler) 25 | return handler.value.filterNotNull().first() 26 | } 27 | 28 | private inner class DumpViewHierarchyDebugViewDumpHandler : DebugViewDumpHandler() { 29 | val value = MutableStateFlow(null) 30 | override fun handleViewDebugResult(data: ByteBuffer?) { 31 | value.value = data?.array() 32 | } 33 | } 34 | 35 | override suspend fun captureView( 36 | logger: AppLogger, 37 | client: Client, 38 | clientWindowTitle: String, 39 | node: ViewNode, 40 | timeout: Long, 41 | timeUnit: TimeUnit 42 | ): ByteArray? { 43 | val handler = CaptureViewDebugViewDumpHandler() 44 | client.captureView(clientWindowTitle, node.toString(), handler) 45 | return handler.value.filterNotNull().first() 46 | } 47 | 48 | private inner class CaptureViewDebugViewDumpHandler : DebugViewDumpHandler() { 49 | val value = MutableStateFlow(null) 50 | 51 | override fun handleViewDebugResult(data: ByteBuffer?) { 52 | value.value = data?.array() 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/android/layoutinspector/model/TreePathUtils.kt: -------------------------------------------------------------------------------- 1 | package com.android.layoutinspector.model 2 | 3 | import com.github.grishberg.android.layoutinspector.domain.AbstractViewNode 4 | import com.google.common.collect.Lists 5 | import javax.swing.tree.TreePath 6 | 7 | object TreePathUtils { 8 | 9 | /** Finds the path from node to the root. */ 10 | @JvmStatic 11 | fun getPath(node: AbstractViewNode): TreePath { 12 | return getPathImpl(node, null) 13 | } 14 | 15 | /** Finds the path from node to the parent. */ 16 | @JvmStatic 17 | fun getPathFromParent(node: AbstractViewNode, root: AbstractViewNode): TreePath { 18 | return getPathImpl(node, root) 19 | } 20 | 21 | private fun getPathImpl(node: AbstractViewNode, root: AbstractViewNode?): TreePath { 22 | var node: AbstractViewNode? = node 23 | val nodes = Lists.newArrayList() 24 | do { 25 | nodes.add(0, node) 26 | node = node?.parent as AbstractViewNode? 27 | } while (node != null && node !== root) 28 | if (root != null && node === root) { 29 | nodes.add(0, root) 30 | } 31 | return TreePath(nodes.toTypedArray()) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/kotlin/com/android/layoutinspector/model/ViewProperty.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.android.layoutinspector.model 17 | 18 | import com.google.common.collect.ComparisonChain 19 | import com.google.common.collect.Ordering 20 | 21 | /** 22 | * Represents a property of a [com.android.layoutinspector.model.ViewNode]. 23 | */ 24 | data class ViewProperty( 25 | val fullName: String, 26 | val name: String, 27 | val category: String?, 28 | val value: String, 29 | val isSizeProperty: Boolean = false, 30 | val intValue: Int = 0 31 | ) : Comparable { 32 | 33 | override fun toString(): String { 34 | return "$fullName=$value" 35 | } 36 | 37 | override fun compareTo(other: ViewProperty): Int { 38 | return ComparisonChain.start() 39 | .compare(category, other.category, CATEGORY_COMPARATOR) 40 | .compare(name, other.name) 41 | .result() 42 | } 43 | 44 | override fun equals(other: Any?): Boolean { 45 | if (other !is ViewProperty) { 46 | return false 47 | } 48 | val other = other as ViewProperty? 49 | return !(category != other!!.category || name != other.name) 50 | } 51 | 52 | override fun hashCode(): Int { 53 | var result = fullName.hashCode() 54 | result = 31 * result + name.hashCode() 55 | result = 31 * result + (category?.hashCode() ?: 0) 56 | result = 31 * result + (value?.hashCode() ?: 0) 57 | return result 58 | } 59 | 60 | companion object { 61 | private val CATEGORY_COMPARATOR = 62 | Ordering.natural>().nullsFirst() 63 | } 64 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/android/layoutinspector/parser/DisplayInfoFactory.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.android.layoutinspector.parser 17 | import com.android.layoutinspector.model.DisplayInfo 18 | import com.android.layoutinspector.model.ViewProperty 19 | object DisplayInfoFactory { 20 | fun createDisplayInfoFromNamedProperties(namedProperties: Map): DisplayInfo { 21 | val left = getInt(getProperty(namedProperties, "mLeft", "layout:mLeft", "left"), 0) 22 | val top = getInt(getProperty(namedProperties, "mTop", "layout:mTop", "top"), 0) 23 | val width = getInt(getProperty(namedProperties, "getWidth()", "layout:getWidth()", "width"), 10) 24 | val height = getInt(getProperty(namedProperties, "getHeight()", "layout:getHeight()", "height"), 10) 25 | val scrollX = getInt(getProperty(namedProperties, "mScrollX", "scrolling:mScrollX", "scrollX"), 0) 26 | val scrollY = getInt(getProperty(namedProperties, "mScrollY", "scrolling:mScrollY", "scrollY"), 0) 27 | val willNotDraw = 28 | getBoolean(getProperty(namedProperties, "willNotDraw()", "drawing:willNotDraw()", "willNotDraw"), false) 29 | val clipChildren = getBoolean( 30 | getProperty(namedProperties, "getClipChildren()", "drawing:getClipChildren()", "clipChildren"), true 31 | ) 32 | val translateX = 33 | getFloat(getProperty(namedProperties, "getTranslationX", "drawing:getTranslationX()", "translationX"), 0f) 34 | val translateY = 35 | getFloat(getProperty(namedProperties, "getTranslationY", "drawing:getTranslationY()", "translationY"), 0f) 36 | val scaleX = getFloat(getProperty(namedProperties, "getScaleX()", "drawing:getScaleX()", "scaleX"), 1f) 37 | val scaleY = getFloat(getProperty(namedProperties, "getScaleY()", "drawing:getScaleY()", "scaleY"), 1f) 38 | var descProp = getProperty(namedProperties, "accessibility:getContentDescription()", "contentDescription") 39 | var contentDescription: String? = if (descProp != null && descProp.value != "null") 40 | descProp.value 41 | else 42 | null 43 | if (contentDescription == null) { 44 | descProp = getProperty(namedProperties, "text:mText") 45 | contentDescription = if (descProp != null && descProp.value != "null") 46 | descProp.value 47 | else 48 | null 49 | } 50 | val visibility = getProperty(namedProperties, "getVisibility()", "misc:getVisibility()", "visibility") 51 | val isVisible = (visibility == null 52 | || "0" == visibility.value 53 | || "VISIBLE" == visibility.value) 54 | return DisplayInfo( 55 | willNotDraw, 56 | isVisible, 57 | left, 58 | top, 59 | width, 60 | height, 61 | scrollX, 62 | scrollY, 63 | clipChildren, 64 | translateX, 65 | translateY, 66 | scaleX, 67 | scaleY, 68 | contentDescription 69 | ) 70 | } 71 | private fun getProperty(namedProperties: Map, name: String, vararg altNames: String): ViewProperty? { 72 | var property: ViewProperty? = namedProperties[name] 73 | var i = 0 74 | while (property == null && i < altNames.size) { 75 | property = namedProperties[altNames[i]] 76 | i++ 77 | } 78 | return property 79 | } 80 | 81 | private fun getBoolean(p: ViewProperty?, defaultValue: Boolean): Boolean { 82 | if (p != null) { 83 | return try { 84 | java.lang.Boolean.parseBoolean(p.value) 85 | } catch (e: NumberFormatException) { 86 | defaultValue 87 | } 88 | } 89 | return defaultValue 90 | } 91 | private fun getInt(p: ViewProperty?, defaultValue: Int): Int { 92 | if (p != null) { 93 | return try { 94 | Integer.parseInt(p.value) 95 | } catch (e: NumberFormatException) { 96 | defaultValue 97 | } 98 | } 99 | return defaultValue 100 | } 101 | private fun getFloat(p: ViewProperty?, defaultValue: Float): Float { 102 | if (p != null) { 103 | return try { 104 | java.lang.Float.parseFloat(p.value) 105 | } catch (e: NumberFormatException) { 106 | defaultValue 107 | } 108 | } 109 | return defaultValue 110 | } 111 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/android/layoutinspector/parser/LayoutFileDataParser.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.android.layoutinspector.parser 17 | 18 | import com.android.layoutinspector.LayoutInspectorCaptureOptions 19 | import com.android.layoutinspector.model.LayoutFileData 20 | import com.android.layoutinspector.model.ViewNode 21 | import java.awt.image.BufferedImage 22 | import java.io.ByteArrayInputStream 23 | import java.io.File 24 | import java.io.IOException 25 | import java.io.ObjectInputStream 26 | import java.nio.file.Files 27 | import javax.imageio.ImageIO 28 | 29 | object LayoutFileDataParser { 30 | 31 | /** 32 | * List of [ViewProperty] to be skipped since the framework won't correctly report their data. 33 | * See ag/64673340 34 | */ 35 | @JvmStatic val SKIPPED_PROPERTIES = listOf("bg_", "fg_") 36 | 37 | @Throws(IOException::class) 38 | @JvmStatic 39 | fun parseFromFile(file: File): LayoutFileData { 40 | return parseFromBytes(Files.readAllBytes(file.toPath())) 41 | } 42 | 43 | @Throws(IOException::class) 44 | @JvmStatic 45 | fun parseFromBytes( 46 | bytes: ByteArray, skippedProperties: Collection = SKIPPED_PROPERTIES 47 | ): LayoutFileData { 48 | val bufferedImage: BufferedImage? 49 | var node: ViewNode? = null 50 | val options = LayoutInspectorCaptureOptions() 51 | var previewBytes = ByteArray(0) 52 | ObjectInputStream(ByteArrayInputStream(bytes)).use { input -> 53 | // Parse options 54 | options.parse(input.readUTF()) 55 | // Parse view node 56 | val nodeBytes = ByteArray(input.readInt()) 57 | input.readFully(nodeBytes) 58 | node = ViewNodeParser.parse(nodeBytes, options.version, skippedProperties) 59 | if (node == null) { 60 | throw IOException("Error parsing view node") 61 | } 62 | // Preview image 63 | previewBytes = ByteArray(input.readInt()) 64 | input.readFully(previewBytes) 65 | } 66 | bufferedImage = ImageIO.read(ByteArrayInputStream(previewBytes)) 67 | return LayoutFileData(bufferedImage, node, options) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/kotlin/com/android/layoutinspector/parser/ViewNodeV2Decoder.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.android.layoutinspector.parser 17 | import java.nio.ByteBuffer 18 | import java.nio.charset.Charset 19 | import java.util.* 20 | /** 21 | * Decodes view hierarchy v2 protocol created by ViewHierarchyEncoder in Android framework. 22 | */ 23 | internal class ViewNodeV2Decoder(private val mBuf: ByteBuffer) { 24 | fun hasRemaining(): Boolean { 25 | return mBuf.hasRemaining() 26 | } 27 | fun readObject(): Any { 28 | val sig = mBuf.get() 29 | return when (sig) { 30 | SIG_BOOLEAN -> if (mBuf.get().toInt() == 0) java.lang.Boolean.FALSE else java.lang.Boolean.TRUE 31 | SIG_BYTE -> mBuf.get() 32 | SIG_SHORT -> mBuf.short 33 | SIG_INT -> mBuf.int 34 | SIG_LONG -> mBuf.long 35 | SIG_FLOAT -> mBuf.float 36 | SIG_DOUBLE -> mBuf.double 37 | SIG_STRING -> readString() 38 | SIG_MAP -> readMap() 39 | else -> throw DecoderException( 40 | sig, 41 | mBuf.position() - 1 42 | ) 43 | } 44 | } 45 | private fun readString(): String { 46 | val len = mBuf.short.toInt() 47 | val b = ByteArray(len) 48 | mBuf.get(b, 0, len) 49 | return String(b, Charset.forName("utf-8")) 50 | } 51 | private fun readMap(): Map { 52 | val m = HashMap() 53 | while (true) { 54 | val o = readObject() 55 | if (o !is Short) { 56 | throw DecoderException("Expected short key, got " + o.javaClass) 57 | } 58 | if (o == SIG_END_MAP) { 59 | break 60 | } 61 | m[o] = readObject() 62 | } 63 | return m 64 | } 65 | class DecoderException : RuntimeException { 66 | constructor( 67 | seen: Byte, 68 | pos: Int 69 | ) : super(String.format("Unexpected byte %c seen at position %d", seen.toChar(), pos)) 70 | constructor(msg: String) : super(msg) 71 | } 72 | companion object { 73 | // Prefixes for simple primitives. These match the JNI definitions. 74 | const val SIG_BOOLEAN: Byte = 'Z'.toByte() 75 | const val SIG_BYTE: Byte = 'B'.toByte() 76 | const val SIG_SHORT: Byte = 'S'.toByte() 77 | const val SIG_INT: Byte = 'I'.toByte() 78 | const val SIG_LONG: Byte = 'J'.toByte() 79 | const val SIG_FLOAT: Byte = 'F'.toByte() 80 | const val SIG_DOUBLE: Byte = 'D'.toByte() 81 | // Prefixes for some commonly used objects 82 | const val SIG_STRING: Byte = 'R'.toByte() 83 | const val SIG_MAP: Byte = 'M'.toByte() // a map with an short key 84 | const val SIG_END_MAP: Short = 0 85 | } 86 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/android/layoutinspector/parser/ViewPropertyParser.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.android.layoutinspector.parser 17 | 18 | import com.android.layoutinspector.model.ViewProperty 19 | 20 | object ViewPropertyParser { 21 | private val availableProprtyFullNames = listOf( 22 | "layout:mBottom", "layout:mLeft", "layout:mRight", "layout:mTop", 23 | "layout:getHeight()", "layout:getWidth()", "layout:getBaseline()", "layout:layout_bottomMargin", 24 | "layout:layout_endMargin", "layout:layout_leftMargin", "layout:layout_rightMargin", "layout:layout_startMargin", 25 | "layout:layout_topMargin", "layout:layout_height", "layout:layout_width", "layout:getWidth()", 26 | "measurement:mMeasuredHeight", "measurement:mMeasuredWidth", "measurement:mMinHeight", "measurement:mMinWidth", 27 | "measurement:getMeasuredHeightAndState()", "measurement:getMeasuredWidthAndState()", 28 | "drawing:getPivotX()", "drawing:getPivotY()", "drawing:getTranslationX()", "drawing:getTranslationY()", 29 | "drawing:getTranslationZ()", "drawing:getX()", "drawing:getY()", "drawing:getZ()", 30 | // protocol v2 31 | "layout:left", "layout:right", 32 | "layout:bottom", "layout:top", 33 | "layout:width", "layout:height", 34 | "measurement:measuredWidth", "measurement:minWidth", "measurement:measuredHeight", "measurement:minHeight", 35 | "drawing:translationX", "drawing:translationY", "drawing:translationZ", 36 | "drawing:pivotX", "drawing:pivotY" 37 | ) 38 | 39 | fun parse(propertyFullName: String, value: String): ViewProperty { 40 | val colonIndex = propertyFullName.indexOf(':') 41 | var category: String? 42 | var name: String? 43 | if (colonIndex != -1) { 44 | category = propertyFullName.substring(0, colonIndex) 45 | name = propertyFullName.substring(colonIndex + 1) 46 | } else { 47 | category = null 48 | name = propertyFullName 49 | } 50 | var isSizeProperty = isSizeProperty(category, propertyFullName, value) 51 | var intValue = 0 52 | 53 | try { 54 | intValue = Integer.valueOf(value) 55 | } catch (e: NumberFormatException) { 56 | isSizeProperty = false 57 | } 58 | return ViewProperty(propertyFullName, name, category, value, isSizeProperty, intValue) 59 | } 60 | 61 | private fun isSizeProperty(category: String?, propertyFullName: String, value: String): Boolean { 62 | if (value == "null" || value == "-1" || value == "0" || value == "-2147483648") { 63 | return false 64 | } 65 | if (category == "padding") { 66 | return true 67 | } 68 | return availableProprtyFullNames.contains(propertyFullName) 69 | } 70 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/common/CoroutinesDispatchers.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.common 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | 5 | interface CoroutinesDispatchers { 6 | val worker: CoroutineDispatcher 7 | val ui: CoroutineDispatcher 8 | } 9 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/common/CoroutinesDispatchersImpl.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.common 2 | 3 | import com.intellij.openapi.application.EDT 4 | import kotlinx.coroutines.CoroutineDispatcher 5 | import kotlinx.coroutines.Dispatchers 6 | 7 | class CoroutinesDispatchersImpl : CoroutinesDispatchers { 8 | override val worker: CoroutineDispatcher = Dispatchers.IO 9 | override val ui: CoroutineDispatcher = Dispatchers.EDT as CoroutineDispatcher 10 | } 11 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/common/MainScope.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.common 2 | 3 | import com.intellij.openapi.application.EDT 4 | import kotlinx.coroutines.CoroutineScope 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.SupervisorJob 7 | import kotlinx.coroutines.cancelChildren 8 | import kotlin.coroutines.CoroutineContext 9 | 10 | class MainScope : CoroutineScope { 11 | private val job = SupervisorJob() 12 | override val coroutineContext: CoroutineContext 13 | get() = job + Dispatchers.EDT 14 | 15 | fun destroy() = coroutineContext.cancelChildren() 16 | } 17 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/domain/AbstractViewNode.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.domain 2 | 3 | import javax.swing.tree.TreeNode 4 | 5 | interface AbstractViewNode : TreeNode { 6 | 7 | val children: List 8 | val id: String? 9 | val name: String 10 | val locationOnScreenX: Int 11 | val locationOnScreenY: Int 12 | val width: Int 13 | val height: Int 14 | val isVisible: Boolean 15 | val hash: String 16 | val typeAsString: String 17 | val text: String? 18 | 19 | fun cloneWithNewParent(newParent: AbstractViewNode): AbstractViewNode 20 | } 21 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/domain/ClientWindowsInput.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.domain 2 | 3 | import com.android.layoutinspector.model.ClientWindow 4 | 5 | /** 6 | * Returns client windows from device. 7 | */ 8 | interface ClientWindowsInput { 9 | suspend fun getClientWindows(options: LayoutRecordOptions): List 10 | } 11 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/domain/DialogsInput.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.domain 2 | 3 | import java.io.File 4 | 5 | interface DialogsInput { 6 | fun showOpenFileDialogAndReturnResult(): File? 7 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/domain/DumpViewNode.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.domain 2 | 3 | import java.util.Collections 4 | import java.util.Enumeration 5 | import javax.swing.tree.TreeNode 6 | 7 | data class DumpViewNode( 8 | val parent: AbstractViewNode?, 9 | val pkg: String, 10 | override val name: String, 11 | override val id: String?, 12 | val globalLeft: Int, 13 | val globalTop: Int, 14 | val globalRight: Int, 15 | val globalBottom: Int, 16 | override val text: String?, 17 | ) : AbstractViewNode { 18 | 19 | override val locationOnScreenX: Int = globalLeft 20 | override val locationOnScreenY: Int = globalTop 21 | 22 | override val children = mutableListOf() 23 | override val width: Int = globalRight - globalLeft 24 | override val height: Int = globalBottom - globalTop 25 | override val isVisible: Boolean = true 26 | override val hash: String = "" 27 | 28 | override val typeAsString: String 29 | 30 | init { 31 | val lastDotPost = name.lastIndexOf(".") 32 | typeAsString = if (lastDotPost >= 0) { 33 | name.substring(lastDotPost + 1) 34 | } else { 35 | name 36 | } 37 | } 38 | 39 | fun addChild(child: AbstractViewNode) { 40 | children.add(child) 41 | } 42 | 43 | override fun getChildAt(childIndex: Int): TreeNode = children[childIndex] 44 | 45 | override fun getChildCount(): Int = children.size 46 | 47 | override fun getParent(): TreeNode? = parent 48 | 49 | override fun getIndex(node: TreeNode?): Int = children.indexOf(node) 50 | 51 | override fun getAllowsChildren(): Boolean = true 52 | 53 | override fun isLeaf(): Boolean = childCount == 0 54 | 55 | override fun children(): Enumeration = Collections.enumeration(children) 56 | 57 | override fun cloneWithNewParent(newParent: AbstractViewNode): AbstractViewNode { 58 | val cloned = DumpViewNode(newParent, pkg, name, id, globalLeft, globalTop, globalRight, globalBottom, text) 59 | 60 | for(child in children) { 61 | cloned.addChild(child.cloneWithNewParent(cloned)) 62 | } 63 | 64 | return cloned 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/domain/LayoutParserInput.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.domain 2 | 3 | import com.android.layoutinspector.model.LayoutFileData 4 | import java.io.File 5 | 6 | interface LayoutParserInput { 7 | fun parseFromBytes(bytes: ByteArray): LayoutFileData 8 | 9 | fun parseFromFile(file: File): LayoutFileData 10 | } 11 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/domain/LayoutRecordOptions.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.domain 2 | 3 | import com.android.ddmlib.Client 4 | import com.android.ddmlib.IDevice 5 | 6 | data class LayoutRecordOptions( 7 | var device: IDevice, 8 | val client: Client, 9 | val timeoutInSeconds: Int, 10 | val fileNamePrefix: String, 11 | val v2Enabled: Boolean, 12 | val dumpViewModeEnabled: Boolean, 13 | val label: String? = null, 14 | ) 15 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/domain/LayoutRecordOptionsInput.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.domain 2 | 3 | interface LayoutRecordOptionsInput { 4 | suspend fun getLayoutOptions(): LayoutRecordOptions? 5 | } 6 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/domain/LayoutResultOutput.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.domain 2 | 3 | import com.android.layoutinspector.model.LayoutFileData 4 | 5 | interface LayoutResultOutput { 6 | /** 7 | * Show hierarchy and screenshot 8 | */ 9 | fun showResult(resultOutput: LayoutFileData, label: String? = null) 10 | 11 | fun showError(error: String) 12 | 13 | fun showLoading() 14 | 15 | fun hideLoading() 16 | } 17 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/domain/RecordInfoInput.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.domain 2 | 3 | interface RecordInfoInput { 4 | suspend fun getLayoutRecordOptions(): LayoutRecordOptions? 5 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/domain/WindowsListInput.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.domain 2 | 3 | import com.android.layoutinspector.model.ClientWindow 4 | 5 | /** 6 | * Allows to select window from list. 7 | */ 8 | interface WindowsListInput { 9 | suspend fun getSelectedWindow(windows: List): ClientWindow 10 | } 11 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/process/HierarchyDump.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.process 2 | 3 | import com.android.ddmlib.CollectingOutputReceiver 4 | import com.android.ddmlib.IDevice 5 | import com.android.ddmlib.IShellOutputReceiver 6 | import com.android.layoutinspector.common.AppLogger 7 | import java.io.BufferedReader 8 | import java.io.File 9 | import java.util.concurrent.TimeUnit 10 | import kotlinx.coroutines.Dispatchers 11 | import kotlinx.coroutines.withContext 12 | import kotlinx.coroutines.runBlocking 13 | import com.github.grishberg.android.layoutinspector.common.CoroutinesDispatchers 14 | 15 | private const val TAG = "HierarchyDump" 16 | 17 | class HierarchyDump( 18 | private val device: IDevice, 19 | private val layoutFileSystem: LayoutFileSystem, 20 | private val logger: AppLogger, 21 | private val dispatchers: CoroutinesDispatchers, 22 | ) { 23 | 24 | private val PATTERN = "UI\\shierchary\\sdumped\\sto:\\s([^ ]+.xml)".toRegex() 25 | 26 | 27 | suspend fun getHierarchyDump(): String? { 28 | return withContext(dispatchers.worker) { 29 | val receiver = CollectingOutputReceiver() 30 | device.executeShellCommand("uiautomator dump", receiver) 31 | receiver.awaitCompletion(60L, TimeUnit.SECONDS) 32 | logger.d("$TAG: getViewDumps() receiver.output: ${receiver.output}") 33 | val output = receiver.output ?: return@withContext null 34 | val matchResult = PATTERN.find(output)?: return@withContext null 35 | 36 | logger.d("$TAG: getViewDumps() output: $output") 37 | val localFile = uploadDumpFile(matchResult.groupValues[1]) 38 | 39 | logger.d("$TAG: getViewDumps() start reading: ${localFile.name}") 40 | return@withContext readStringFile(localFile) 41 | } 42 | } 43 | 44 | 45 | private suspend fun uploadDumpFile(fileName: String): File { 46 | if (!layoutFileSystem.dumpsDir.exists()) { 47 | layoutFileSystem.dumpsDir.mkdirs() 48 | } 49 | val localFile = File(layoutFileSystem.dumpsDir, "window_dump.xml") 50 | withContext(dispatchers.worker) { 51 | device.pullFile(fileName, localFile.absolutePath) 52 | } 53 | return localFile 54 | } 55 | 56 | private fun readStringFile(file: File): String { 57 | val bufferedReader: BufferedReader = file.bufferedReader() 58 | return bufferedReader.use { it.readText() } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/process/HierarchyDumpParser.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.process 2 | 3 | import com.github.grishberg.android.layoutinspector.domain.DumpViewNode 4 | import java.io.ByteArrayInputStream 5 | import java.io.InputStream 6 | import java.nio.charset.StandardCharsets 7 | import javax.xml.parsers.SAXParser 8 | import javax.xml.parsers.SAXParserFactory 9 | import org.xml.sax.Attributes 10 | import org.xml.sax.helpers.DefaultHandler 11 | 12 | class HierarchyDumpParser { 13 | 14 | private val BOUNDS_PATTERN = "\\[(\\d+),(\\d+)\\]\\[(\\d+),(\\d+)\\]".toRegex() 15 | 16 | fun parseDump(viewDump: String): DumpViewNode? { 17 | val factory = SAXParserFactory.newInstance() 18 | val saxParser: SAXParser = factory.newSAXParser() 19 | val handler = ViewDumpHandler() 20 | 21 | val stream: InputStream = ByteArrayInputStream(viewDump.toByteArray(StandardCharsets.UTF_8)) 22 | saxParser.parse(stream, handler) 23 | return handler.rootNode 24 | } 25 | 26 | private inner class ViewDumpHandler : DefaultHandler() { 27 | 28 | var state: State? = null 29 | 30 | var rootNode: DumpViewNode? = null 31 | 32 | override fun startElement(uri: String, localName: String?, qName: String, attributes: Attributes) { 33 | state = when (qName) { 34 | "hierarchy" -> { 35 | HierarchyState() 36 | } 37 | 38 | "node" -> { 39 | val nodeState = NodeState(rootNode) 40 | nodeState.processAttributes(attributes) 41 | rootNode = nodeState.createNode() 42 | nodeState 43 | 44 | } 45 | 46 | else -> throw IllegalStateException() 47 | } 48 | } 49 | 50 | override fun endElement(uri: String, localName: String, qName: String) { 51 | state?.endElement(uri, localName, qName) 52 | 53 | if (qName == "node") { 54 | rootNode?.parent?.let { 55 | if (it !is DumpViewNode) { 56 | throw IllegalStateException() 57 | } 58 | rootNode = it 59 | } 60 | } 61 | } 62 | 63 | override fun characters(ch: CharArray, start: Int, length: Int) { 64 | state?.characters(ch, start, length) 65 | } 66 | } 67 | 68 | private interface State { 69 | 70 | fun processAttributes(attributes: Attributes) 71 | 72 | fun characters(ch: CharArray, start: Int, length: Int) 73 | 74 | fun endElement(uri: String, localName: String, qName: String) 75 | } 76 | 77 | private inner class HierarchyState : State { 78 | 79 | override fun processAttributes(attributes: Attributes) = Unit 80 | 81 | override fun characters(ch: CharArray, start: Int, length: Int) = Unit 82 | override fun endElement(uri: String, localName: String, qName: String) = Unit 83 | } 84 | 85 | private inner class NodeState( 86 | private val parentNode: DumpViewNode? 87 | ) : State { 88 | 89 | private lateinit var newNode: DumpViewNode 90 | 91 | override fun processAttributes(attributes: Attributes) { 92 | val pkg = attributes.getValue("package") 93 | val className = attributes.getValue("class") 94 | val id = attributes.getValue("resource-id") 95 | val globalBounds = attributes.getValue("bounds") 96 | val rectBounds = parseBounds(globalBounds) 97 | newNode = DumpViewNode( 98 | parent = parentNode, 99 | pkg = pkg, 100 | name = className, 101 | id = parseId(id), 102 | rectBounds.left, 103 | rectBounds.top, 104 | rectBounds.right, 105 | rectBounds.bottom, 106 | attributes.getValue("text") 107 | ) 108 | parentNode?.addChild(newNode) 109 | } 110 | 111 | private fun parseId(id: String?): String? { 112 | if (id == null) { 113 | return null 114 | } 115 | 116 | val pos = id.lastIndexOf(":id/") 117 | if (pos < 0) { 118 | return id 119 | } 120 | return id.substring(pos + 1) 121 | } 122 | 123 | private fun parseBounds(bounds: String): Rect { 124 | val resultMatch = BOUNDS_PATTERN.find(bounds) ?: return Rect(0, 0, 0, 0) 125 | return Rect( 126 | resultMatch.groupValues[1].toInt(), 127 | resultMatch.groupValues[2].toInt(), 128 | resultMatch.groupValues[3].toInt(), 129 | resultMatch.groupValues[4].toInt() 130 | ) 131 | } 132 | 133 | override fun characters(ch: CharArray, start: Int, length: Int) = Unit 134 | 135 | override fun endElement(uri: String, localName: String, qName: String) = Unit 136 | 137 | fun createNode(): DumpViewNode { 138 | return newNode 139 | } 140 | } 141 | 142 | private data class Rect(val left: Int, val top: Int, val right: Int, val bottom: Int) 143 | } 144 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/process/LayoutFileSystem.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.process 2 | 3 | import com.android.layoutinspector.common.AppLogger 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.GlobalScope 6 | import kotlinx.coroutines.launch 7 | import java.io.BufferedOutputStream 8 | import java.io.File 9 | import java.io.FileOutputStream 10 | 11 | private const val TAG = "FileSystem" 12 | const val LAYOUTS_DIR = "layouts" 13 | const val DUMPS_DIR = "dumps" 14 | 15 | class LayoutFileSystem( 16 | private val logger: AppLogger, 17 | baseDir: File 18 | ) { 19 | val layoutDir = File(baseDir, LAYOUTS_DIR) 20 | 21 | val dumpsDir = File(baseDir, DUMPS_DIR) 22 | 23 | init { 24 | if (!layoutDir.exists()) { 25 | layoutDir.mkdirs() 26 | } 27 | } 28 | 29 | fun saveLayoutToFile(fileName: String, data: ByteArray) { 30 | GlobalScope.launch(Dispatchers.IO) { 31 | saveToFile(fileName, data) 32 | } 33 | } 34 | 35 | private fun saveToFile(fileName: String, data: ByteArray) { 36 | val dir = layoutDir 37 | if (!dir.exists()) { 38 | dir.mkdirs() 39 | } 40 | 41 | var bs: BufferedOutputStream? = null 42 | val file = File(dir, fileName) 43 | 44 | try { 45 | val fs = FileOutputStream(file) 46 | bs = BufferedOutputStream(fs) 47 | bs.write(data) 48 | bs.close() 49 | bs = null 50 | } catch (e: java.lang.Exception) { 51 | logger.e("$TAG: save trace file failed", e) 52 | e.printStackTrace() 53 | } 54 | 55 | if (bs != null) try { 56 | bs.close() 57 | } catch (e: Exception) { 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/process/LayoutInspectorCaptureTask.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.process 2 | 3 | import com.android.layoutinspector.LayoutInspectorBridge 4 | import com.android.layoutinspector.LayoutInspectorBridge.V2_MIN_API 5 | import com.android.layoutinspector.LayoutInspectorCaptureOptions 6 | import com.android.layoutinspector.LayoutInspectorResult 7 | import com.android.layoutinspector.ProtocolVersion 8 | import com.android.layoutinspector.common.AppLogger 9 | import com.github.grishberg.android.layoutinspector.domain.DumpViewNode 10 | import com.github.grishberg.android.layoutinspector.common.CoroutinesDispatchers 11 | import kotlinx.coroutines.CoroutineScope 12 | import kotlinx.coroutines.async 13 | 14 | private const val TAG = "LayoutInspectorCaptureTask" 15 | 16 | class LayoutInspectorCaptureTask( 17 | private val layoutFileSystem: LayoutFileSystem, 18 | private val scope: CoroutineScope, 19 | private val logger: AppLogger, 20 | private val dispatchers: CoroutinesDispatchers, 21 | ) { 22 | 23 | suspend fun capture(recordingConfig: RecordingConfig): LayoutInspectorResult { 24 | 25 | val layoutResultAsync = scope.async(dispatchers.worker) { 26 | logger.d("$TAG: start capture view timeout = ${recordingConfig.timeoutInSeconds}") 27 | 28 | try { 29 | val version: ProtocolVersion = determineProtocolVersion( 30 | recordingConfig.client.device.version.apiLevel, recordingConfig.v2Enabled 31 | ) 32 | val options = LayoutInspectorCaptureOptions() 33 | options.version = version 34 | 35 | return@async LayoutInspectorBridge.captureView( 36 | logger, recordingConfig.clientWindow, options, recordingConfig.timeoutInSeconds.toLong() 37 | ) 38 | } catch (e: Exception) { 39 | logger.e(TAG, e) 40 | throw e 41 | } 42 | } 43 | val layoutAsyncResult = layoutResultAsync.await() 44 | 45 | val dumpNodeAsyncResult: DumpViewNode? = if (recordingConfig.recordOptions.dumpViewModeEnabled) { 46 | val viewDumpsAsync = scope.async(dispatchers.worker) { 47 | return@async getViewDumps(recordingConfig) 48 | } 49 | viewDumpsAsync.await() 50 | } else { 51 | null 52 | } 53 | 54 | val layoutRootNode = layoutAsyncResult.root 55 | logger.d("$TAG: prepare capture : dumpViewModeEnabled = ${recordingConfig.recordOptions.dumpViewModeEnabled}, dumpResult != null : ${dumpNodeAsyncResult != null}, layoutRootNode != null: ${layoutRootNode != null}") 56 | val result = 57 | if (recordingConfig.recordOptions.dumpViewModeEnabled && dumpNodeAsyncResult != null && layoutRootNode != null) { 58 | logger.w("$TAG: found dump and layouts, try to merge") 59 | 60 | val treeMerger = TreeMerger(logger) 61 | 62 | LayoutInspectorResult( 63 | treeMerger.mergeNodes(layoutRootNode, dumpNodeAsyncResult), 64 | layoutAsyncResult.previewImage, 65 | layoutAsyncResult.data, 66 | layoutAsyncResult.options, 67 | layoutAsyncResult.error 68 | ) 69 | } else { 70 | layoutAsyncResult 71 | } 72 | 73 | logger.d("$TAG: capturing is done, error: ${result.error}") 74 | return result 75 | } 76 | 77 | private suspend fun getViewDumps(recordingConfig: RecordingConfig): DumpViewNode? { 78 | logger.d("$TAG: getViewDumps()") 79 | val dumper = HierarchyDump(recordingConfig.client.device, layoutFileSystem, logger, dispatchers) 80 | val hierarchyDump = dumper.getHierarchyDump() 81 | logger.d("$TAG: getViewDumps() : hierarchyDump received ${hierarchyDump != null}") 82 | val dumpString = hierarchyDump ?: return null 83 | 84 | val dumpParser = HierarchyDumpParser() 85 | return dumpParser.parseDump(dumpString) 86 | } 87 | 88 | private fun determineProtocolVersion(apiVersion: Int, v2Enabled: Boolean): ProtocolVersion { 89 | return if (apiVersion >= V2_MIN_API && v2Enabled) ProtocolVersion.Version2 else ProtocolVersion.Version1 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/process/LayoutParserImpl.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.process 2 | 3 | import com.android.layoutinspector.parser.LayoutFileDataParser 4 | import com.github.grishberg.android.layoutinspector.domain.LayoutParserInput 5 | import java.io.File 6 | 7 | class LayoutParserImpl : LayoutParserInput { 8 | override fun parseFromBytes(bytes: ByteArray) = LayoutFileDataParser.parseFromBytes(bytes) 9 | 10 | override fun parseFromFile(file: File) = LayoutFileDataParser.parseFromFile(file) 11 | } 12 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/process/RecordingConfig.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.process 2 | 3 | import com.android.ddmlib.Client 4 | import com.android.layoutinspector.model.ClientWindow 5 | import com.github.grishberg.android.layoutinspector.domain.LayoutRecordOptions 6 | 7 | data class RecordingConfig( 8 | val client: Client, 9 | val clientWindow: ClientWindow, 10 | val timeoutInSeconds: Int, 11 | val v2Enabled: Boolean, 12 | val dpPerPixels: Double, 13 | val recordOptions: LayoutRecordOptions, 14 | ) 15 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/process/TreeMerger.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.process 2 | 3 | import com.android.layoutinspector.common.AppLogger 4 | import com.android.layoutinspector.model.ViewNode 5 | import com.github.grishberg.android.layoutinspector.domain.AbstractViewNode 6 | import com.github.grishberg.android.layoutinspector.domain.DumpViewNode 7 | 8 | private const val COMPOSE_FULL_NAME = "androidx.compose.ui.platform.ComposeView" 9 | 10 | class TreeMerger(private val logger: AppLogger) { 11 | 12 | fun mergeNodes(layoutRootNode: AbstractViewNode, dumpViewNode: DumpViewNode): AbstractViewNode { 13 | logger.d("YALI TreeMerger: try to merge") 14 | val layoutPath = mutableListOf() 15 | extractAllComposeNodes(layoutRootNode, layoutPath) 16 | 17 | val dumpPath = mutableListOf() 18 | extractAllComposeNodes(dumpViewNode, dumpPath) 19 | 20 | if (layoutPath.isEmpty() || dumpPath.isEmpty()) { 21 | logger.w("YALI TreeMerger: ComposeView is not found") 22 | return layoutRootNode 23 | } 24 | 25 | for (path in layoutPath) { 26 | val suitableNodePath = dumpPath.firstOrNull() { it == path } ?: continue 27 | val layoutComposeNode = path.targetNode 28 | if (layoutComposeNode is ViewNode) { 29 | logger.w("YALI TreeMerger: ComposeView is found and replaced size=${suitableNodePath.targetNode.width} x ${suitableNodePath.targetNode.height}") 30 | layoutComposeNode.replaceChildren(suitableNodePath.targetNode.children) 31 | } 32 | } 33 | 34 | return layoutRootNode 35 | } 36 | 37 | private fun extractAllComposeNodes(root: AbstractViewNode, paths: MutableList) { 38 | if (root.name == COMPOSE_FULL_NAME) { 39 | paths.add(NodePath(root)) 40 | return 41 | } 42 | for (child in root.children) { 43 | extractAllComposeNodes(child, paths) 44 | } 45 | } 46 | 47 | private class NodePath( 48 | val targetNode: AbstractViewNode, 49 | ) { 50 | 51 | private val path: List 52 | 53 | init { 54 | path = cratePathToRoot(targetNode) 55 | } 56 | 57 | override fun equals(other: Any?): Boolean { 58 | if (other !is NodePath) { 59 | return false 60 | } 61 | if (targetNode.name != other.targetNode.name) { 62 | return false 63 | } 64 | if (!isIdSame(targetNode.id, other.targetNode.id)) { 65 | return false 66 | } 67 | if (targetNode.locationOnScreenX != other.targetNode.locationOnScreenX || targetNode.locationOnScreenY != other.targetNode.locationOnScreenY /*|| targetNode.width != other.targetNode.width || targetNode.height != other.targetNode.height*/) { 68 | return false 69 | } 70 | if (other.path.size != path.size) { 71 | return false 72 | } 73 | 74 | for (i in path.indices) { 75 | val thisId = path[i].id 76 | val otherId = other.path[i].id 77 | if (!isIdSame(thisId, otherId)) { 78 | return false 79 | } 80 | } 81 | 82 | return true 83 | } 84 | 85 | private fun isIdSame(thisId: String?, otherId: String?): Boolean { 86 | if ((thisId == null || thisId == "" || thisId == "NO_ID") && (otherId == null || otherId == "" || otherId == "NO_ID")) { 87 | return true 88 | } 89 | return thisId == otherId 90 | } 91 | 92 | override fun hashCode(): Int { 93 | return targetNode.name.hashCode() + path.hashCode() 94 | } 95 | 96 | private fun cratePathToRoot(targetNode: AbstractViewNode): List { 97 | val result = mutableListOf() 98 | if (targetNode.parent == null) { 99 | return emptyList() 100 | } 101 | 102 | var parent = targetNode.parent as AbstractViewNode? 103 | while (parent != null) { 104 | result.add(parent) 105 | parent = parent.parent as AbstractViewNode? 106 | } 107 | return result 108 | 109 | } 110 | 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/process/providers/ClientWindowsProvider.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.process.providers 2 | 3 | import com.android.layoutinspector.common.AppLogger 4 | import com.android.layoutinspector.model.ClientWindow 5 | import com.github.grishberg.android.layoutinspector.domain.ClientWindowsInput 6 | import com.github.grishberg.android.layoutinspector.domain.LayoutRecordOptions 7 | import java.util.concurrent.TimeUnit 8 | 9 | class ClientWindowsProvider( 10 | private val logger: AppLogger 11 | ) : ClientWindowsInput { 12 | override suspend fun getClientWindows(options: LayoutRecordOptions): List { 13 | return ClientWindow.getAllV2( 14 | logger, 15 | options.client, 16 | options.timeoutInSeconds.toLong(), 17 | TimeUnit.SECONDS 18 | ) ?: emptyList() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/process/providers/DeviceProvider.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.process.providers 2 | 3 | import com.android.ddmlib.IDevice 4 | 5 | interface DeviceProvider { 6 | fun stop() 7 | fun reconnect() 8 | suspend fun requestDevices(): List 9 | val deviceChangedActions: MutableSet 10 | val isReconnectionAllowed: Boolean 11 | 12 | interface DeviceChangedAction { 13 | fun deviceConnected(device: IDevice) 14 | fun deviceDisconnected(device: IDevice) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/process/providers/ScreenSizeProvider.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.process.providers 2 | 3 | import com.android.ddmlib.* 4 | import java.awt.Dimension 5 | import java.io.IOException 6 | import java.util.concurrent.TimeUnit 7 | import java.util.regex.Pattern 8 | import kotlinx.coroutines.withContext 9 | import com.github.grishberg.android.layoutinspector.common.CoroutinesDispatchers 10 | 11 | private const val SHELL_COMMAND_FOR_SCREEN_SIZE = "dumpsys window" 12 | 13 | class ScreenSizeProvider(private val dispatchers: CoroutinesDispatchers) { 14 | suspend fun getScreenSize(device: IDevice): Dimension { 15 | val screenSize: String = 16 | executeShellCommandAndReturnOutput(device, SHELL_COMMAND_FOR_SCREEN_SIZE) 17 | val size = parseScreenSize(screenSize) 18 | return Dimension(size[0], size[1]) 19 | } 20 | 21 | private fun parseScreenSize(dumpsisWindow: String): IntArray { 22 | var width = 0 23 | var height = 0 24 | val pattern = "mSystem=\\(\\d*,\\d*\\)-\\((\\d*),(\\d*)\\)" 25 | // Create a Pattern object 26 | val r = Pattern.compile(pattern) 27 | // Now create matcher object. 28 | val m = r.matcher(dumpsisWindow) 29 | if (m.find()) { 30 | width = m.group(1).toInt() 31 | height = m.group(2).toInt() 32 | } 33 | return intArrayOf(width, height) 34 | } 35 | 36 | private suspend fun executeShellCommandAndReturnOutput(device: IDevice, command: String): String { 37 | return withContext(dispatchers.worker) { 38 | val receiver = CollectingOutputReceiver() 39 | device.executeShellCommand(command, receiver, 60, TimeUnit.SECONDS) 40 | receiver.output 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/settings/JsonSettings.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.settings 2 | 3 | import com.android.layoutinspector.common.AppLogger 4 | import com.google.gson.GsonBuilder 5 | import com.google.gson.stream.JsonReader 6 | import java.io.* 7 | 8 | private const val TAG = "JsonSettings" 9 | 10 | 11 | class JsonSettings ( 12 | private val log: AppLogger, 13 | private val baseDir: File 14 | ) : Settings { 15 | private val gson = GsonBuilder().enableComplexMapKeySerialization().setPrettyPrinting().create() 16 | 17 | private val settingsMap = mutableMapOf() 18 | private val settingsFile = File(baseDir,"android-layout-inspector-settings.json") 19 | 20 | init { 21 | try { 22 | val fileReader = FileReader(settingsFile) 23 | val reader = JsonReader(fileReader) 24 | val map: Map? = gson.fromJson(reader, MutableMap::class.java) 25 | if (map != null) { 26 | settingsMap.putAll(map) 27 | } 28 | } catch (e: FileNotFoundException) { 29 | log.d("$TAG: there is no settings file.") 30 | } catch (e: Exception) { 31 | log.e("$TAG: read settings error", e) 32 | } 33 | 34 | } 35 | 36 | override fun getBoolValueOrDefault(name: String, default: Boolean): Boolean { 37 | val boolVal = settingsMap[name] 38 | if (boolVal is Boolean) { 39 | return boolVal 40 | } 41 | return default 42 | } 43 | 44 | override fun getIntValueOrDefault(name: String, default: Int): Int { 45 | val value = settingsMap[name] 46 | if (value is Double) { 47 | return value.toInt() 48 | } 49 | if (value is Int) { 50 | return value 51 | } 52 | return default 53 | } 54 | 55 | @Suppress("UNCHECKED_CAST") 56 | override fun getStringList(name: String): List { 57 | val value = settingsMap[name] 58 | if (value is List<*>) { 59 | return value as List 60 | } 61 | return listOf() 62 | } 63 | 64 | override fun setBoolValue(name: String, value: Boolean) { 65 | settingsMap[name] = value 66 | } 67 | 68 | override fun setIntValue(name: String, value: Int) { 69 | settingsMap[name] = value 70 | } 71 | 72 | override fun setStringValue(name: String, value: String) { 73 | settingsMap[name] = value 74 | } 75 | 76 | override fun getStringValueOrDefault(name: String, default: String): String { 77 | val value = settingsMap[name] 78 | if (value is String) { 79 | return value 80 | } 81 | return default 82 | } 83 | 84 | override fun getStringValue(name: String): String? { 85 | val value = settingsMap[name] 86 | if (value is String) { 87 | return value 88 | } 89 | return null 90 | } 91 | 92 | override fun setStringList(name: String, value: List) { 93 | settingsMap[name] = value 94 | } 95 | 96 | override fun save() { 97 | settingsFile.createNewFile() 98 | var outputStream: FileOutputStream? = null 99 | try { 100 | outputStream = FileOutputStream(settingsFile) 101 | val bufferedWriter: BufferedWriter 102 | bufferedWriter = BufferedWriter(OutputStreamWriter(outputStream, "UTF-8")) 103 | gson.toJson(settingsMap, bufferedWriter) 104 | bufferedWriter.close() 105 | log.d("$TAG settings are saved") 106 | } catch (e: FileNotFoundException) { 107 | log.e("$TAG: save settings error", e) 108 | } catch (e: IOException) { 109 | log.e("$TAG: save settings error", e) 110 | } finally { 111 | if (outputStream != null) { 112 | try { 113 | outputStream.flush() 114 | outputStream.close() 115 | } catch (e: IOException) { 116 | } 117 | } 118 | } 119 | } 120 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/settings/Settings.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.settings 2 | 3 | interface Settings { 4 | fun getBoolValueOrDefault(name: String, default: Boolean = false): Boolean 5 | fun getIntValueOrDefault(name: String, default: Int): Int 6 | fun getStringValueOrDefault(name: String, default: String): String 7 | fun getStringValue(name: String): String? 8 | fun getStringList(name: String): List 9 | fun setBoolValue(name: String, value: Boolean) 10 | fun setIntValue(name: String, value: Int) 11 | fun setStringList(name: String, value: List) 12 | fun setStringValue(name: String, value: String) 13 | fun save() 14 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/settings/SettingsFacade.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.settings 2 | 3 | interface SettingsFacade { 4 | var captureLayoutTimeout: Long 5 | 6 | var clientWindowsTimeout: Long 7 | 8 | var allowedSelectHiddenView: Boolean 9 | 10 | var lastLayoutDialogPath: String 11 | 12 | var fileNamePrefix: String 13 | 14 | var ignoreLastClickedView: Boolean 15 | 16 | var roundDimensions: Boolean 17 | 18 | var lastProcessName: String 19 | 20 | var lastWindowName: String 21 | 22 | var lastFilter: String 23 | 24 | var showSerifsInTheMiddleOfSelected: Boolean 25 | 26 | var showSerifsInTheMiddleAll: Boolean 27 | 28 | var isSecondProtocolVersionEnabled: Boolean 29 | 30 | var isDumpViewModeEnabled: Boolean 31 | 32 | fun shouldShowSizeInDp(): Boolean 33 | 34 | fun showSizeInDp(state: Boolean) 35 | 36 | fun shouldStopAdbAfterJob(): Boolean 37 | 38 | fun setStopAdbAfterJob(selected: Boolean) 39 | 40 | var lastVersion: String 41 | } 42 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/ButtonsBuilder.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui 2 | 3 | import com.github.grishberg.android.layoutinspector.domain.Logic 4 | import com.github.grishberg.android.layoutinspector.ui.layout.LayoutPanel 5 | import com.github.grishberg.android.layoutinspector.ui.theme.Themes 6 | import com.github.grishberg.android.layoutinspector.ui.tree.IconsStore 7 | import java.awt.Dimension 8 | import java.awt.event.ActionEvent 9 | import java.awt.event.ActionListener 10 | import javax.swing.JButton 11 | import javax.swing.JToolBar 12 | 13 | private const val BUTTON_SIZE = 22 14 | 15 | class ButtonsBuilder( 16 | private val layoutPanel: LayoutPanel, 17 | private val main: Main, 18 | private val themes: Themes, 19 | private val logic: Logic, 20 | ) : ActionListener { 21 | private val iconStore = IconsStore(BUTTON_SIZE - 8) 22 | 23 | fun addToolbarButtons(toolBar: JToolBar) { 24 | val resetZoomButton = makeToolbarButton( 25 | "1:1", "resetzoom", 26 | Actions.RESET_ZOOM, 27 | "Reset zoom (z)" 28 | ) 29 | toolBar.add(resetZoomButton) 30 | 31 | val fitScreenButton = makeToolbarButton( 32 | "{-}", "fitscreen", 33 | Actions.FIT_TO_SCREEN, 34 | "Fits to screen (f)" 35 | ) 36 | toolBar.add(fitScreenButton) 37 | 38 | val refreshLayoutButton = makeToolbarButton( 39 | "(@)", "refresh", 40 | Actions.REFRESH, 41 | "Refresh layout (Ctrl/Cmd + R)" 42 | ) 43 | toolBar.add(refreshLayoutButton) 44 | 45 | 46 | val helpButton = makeToolbarButton( 47 | "?", "help", 48 | Actions.HELP, 49 | "Go to home page" 50 | ) 51 | toolBar.add(helpButton) 52 | } 53 | 54 | private fun makeToolbarButton( 55 | altText: String, 56 | iconName: String, 57 | actionCommand: Actions, 58 | toolTipText: String 59 | ): JButton? { 60 | val imageLocation = if (themes.isDark) { 61 | "/icons/dark/$iconName.svg" 62 | } else { 63 | "/icons/light/$iconName.svg" 64 | } 65 | val button = JButton(altText) 66 | button.actionCommand = actionCommand.name 67 | button.toolTipText = toolTipText 68 | button.addActionListener(this) 69 | button.preferredSize = Dimension(48, BUTTON_SIZE) 70 | button.maximumSize = Dimension(48, BUTTON_SIZE) 71 | return button 72 | } 73 | 74 | override fun actionPerformed(e: ActionEvent) { 75 | if (e.actionCommand == Actions.RESET_ZOOM.name) { 76 | layoutPanel.resetZoom() 77 | return 78 | } 79 | 80 | if (e.actionCommand == Actions.FIT_TO_SCREEN.name) { 81 | layoutPanel.fitZoom() 82 | return 83 | } 84 | 85 | if (e.actionCommand == Actions.HELP.name) { 86 | main.goToHelp() 87 | return 88 | } 89 | 90 | if (e.actionCommand == Actions.REFRESH.name) { 91 | logic.refreshLayout() 92 | return 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/KeyBinder.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui 2 | 3 | import com.github.grishberg.android.layoutinspector.domain.Logic 4 | import com.github.grishberg.android.layoutinspector.ui.layout.LayoutPanel 5 | import java.awt.KeyboardFocusManager 6 | import java.awt.Toolkit 7 | import java.awt.event.ActionEvent 8 | import java.awt.event.KeyEvent 9 | import javax.swing.AbstractAction 10 | import javax.swing.JComponent 11 | import javax.swing.JTextField 12 | import javax.swing.KeyStroke 13 | 14 | class KeyBinder( 15 | keyBinderComponent: JComponent, 16 | private val layoutPanel: LayoutPanel, 17 | private val logic: Logic, 18 | private val main: Main 19 | ) { 20 | val condition = JComponent.WHEN_IN_FOCUSED_WINDOW 21 | val inputMap = keyBinderComponent.getInputMap(condition) 22 | val actionMap = keyBinderComponent.actionMap 23 | private val keyboardFocusManager = KeyboardFocusManager.getCurrentKeyboardFocusManager() 24 | 25 | init { 26 | //addKeyMapWithCtrl(KeyEvent.VK_C, CopySelectedFullClassNameAction()) 27 | //addKeyMap(KeyEvent.VK_ESCAPE, RemoveSelectionAction()) 28 | addKeyMapWithCtrl(KeyEvent.VK_O, OpenFileDialogAction(false)) 29 | addKeyMapWithCtrl(KeyEvent.VK_R, RefreshLayoutAction()) 30 | addKeyMapWithCtrlShift(KeyEvent.VK_O, OpenFileDialogAction(true)) 31 | 32 | addKeyMapWithCtrl(KeyEvent.VK_N, NewTraceAction()) 33 | addKeyMap(KeyEvent.VK_Z, ResetZoomAction()) 34 | addKeyMap(KeyEvent.VK_F, FitZoomAction()) 35 | addKeyMap(KeyEvent.VK_L, ToggleLayoutAction()) 36 | addKeyMapWithCtrl(KeyEvent.VK_F, GoToFindAction()) 37 | } 38 | 39 | private fun addKeyMapWithCtrl(keyCode: Int, action: AbstractAction) { 40 | addKeyMap(keyCode, Toolkit.getDefaultToolkit().menuShortcutKeyMask, action) 41 | } 42 | 43 | private fun addKeyMapWithCtrlShift(keyCode: Int, action: AbstractAction) { 44 | addKeyMap(keyCode, Toolkit.getDefaultToolkit().menuShortcutKeyMask + ActionEvent.SHIFT_MASK, action) 45 | } 46 | 47 | private fun addKeyMapWithCtrlAlt(keyCode: Int, action: AbstractAction) { 48 | addKeyMap(keyCode, Toolkit.getDefaultToolkit().menuShortcutKeyMask + ActionEvent.ALT_MASK, action) 49 | } 50 | 51 | private fun addKeyMap(keyCode: Int, action: AbstractAction) { 52 | val keyStroke: KeyStroke = KeyStroke.getKeyStroke(keyCode, 0) 53 | inputMap.put(keyStroke, keyStroke.toString()) 54 | actionMap.put(keyStroke.toString(), action) 55 | } 56 | 57 | 58 | private fun addKeyMap(keyCode: Int, modifiers: Int, action: AbstractAction) { 59 | val keyStroke: KeyStroke = KeyStroke.getKeyStroke(keyCode, modifiers) 60 | inputMap.put(keyStroke, keyStroke.toString()) 61 | actionMap.put(keyStroke.toString(), action) 62 | } 63 | 64 | private fun shouldSkip(e: ActionEvent): Boolean { 65 | val focused = keyboardFocusManager.focusOwner 66 | if (focused is JTextField) { 67 | return true 68 | } 69 | return false 70 | } 71 | 72 | private inner class OpenFileDialogAction(private val inNewWindow: Boolean) : AbstractAction() { 73 | override fun actionPerformed(e: ActionEvent) { 74 | if (shouldSkip(e)) return 75 | main.openExistingFile(inNewWindow) 76 | } 77 | } 78 | 79 | private inner class NewTraceAction : AbstractAction() { 80 | override fun actionPerformed(e: ActionEvent) { 81 | if (shouldSkip(e)) return 82 | logic.startRecording() 83 | } 84 | } 85 | private inner class RefreshLayoutAction : AbstractAction() { 86 | override fun actionPerformed(e: ActionEvent) { 87 | if (shouldSkip(e)) return 88 | logic.refreshLayout() 89 | } 90 | } 91 | 92 | private inner class FitZoomAction : AbstractAction() { 93 | override fun actionPerformed(e: ActionEvent) { 94 | if (shouldSkip(e)) return 95 | layoutPanel.fitZoom() 96 | } 97 | } 98 | 99 | private inner class ToggleLayoutAction : AbstractAction() { 100 | override fun actionPerformed(e: ActionEvent) { 101 | if (shouldSkip(e)) return 102 | main.toggleShowingLayouts() 103 | } 104 | } 105 | 106 | private inner class ResetZoomAction : AbstractAction() { 107 | override fun actionPerformed(e: ActionEvent) { 108 | if (shouldSkip(e)) return 109 | layoutPanel.resetZoom() 110 | } 111 | } 112 | 113 | 114 | private inner class GoToFindAction : AbstractAction() { 115 | override fun actionPerformed(e: ActionEvent) { 116 | if (shouldSkip(e)) return 117 | main.showFindDialog() 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/WindowsManager.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui 2 | 3 | import com.android.layoutinspector.common.AdbFacade 4 | import com.android.layoutinspector.common.AppLogger 5 | import com.github.grishberg.android.layoutinspector.process.providers.DeviceProvider 6 | import com.github.grishberg.android.layoutinspector.settings.SettingsFacade 7 | import java.io.File 8 | 9 | /** 10 | * Manages all Main instances. 11 | */ 12 | class WindowsManager( 13 | private val logger: AppLogger, 14 | ) { 15 | private val windows = mutableListOf
() 16 | 17 | fun createWindow( 18 | mode: OpenWindowMode, 19 | settingsFacade: SettingsFacade, 20 | deviceProvider: DeviceProvider, 21 | adb: AdbFacade, 22 | baseDir: File 23 | ): Main { 24 | val main = Main(this, mode, settingsFacade, logger, deviceProvider, adb, baseDir) 25 | windows.add(main) 26 | return main 27 | } 28 | 29 | fun onDestroyed(window: Main) { 30 | windows.remove(window) 31 | } 32 | 33 | fun startScreenshotTest(comparableWindow: Main): Boolean { 34 | if (windows.size != 2) { 35 | logger.d("startScreenshotTest: there is ${windows.size} windows") 36 | return false 37 | } 38 | 39 | val referenceWindow = windows.first { it != comparableWindow } 40 | 41 | val otherWindowScreenshot = comparableWindow.screenshot() 42 | if (otherWindowScreenshot == null) { 43 | logger.d("startScreenshotTest: other window has no layout") 44 | return false 45 | } 46 | 47 | val referenceScreenshot = referenceWindow.screenshot() 48 | if (referenceScreenshot == null) { 49 | logger.d("startScreenshotTest: reference window has no layout") 50 | return false 51 | } 52 | 53 | if (otherWindowScreenshot.width != referenceScreenshot.width || 54 | otherWindowScreenshot.height != referenceScreenshot.height 55 | ) { 56 | logger.d("startScreenshotTest: images size not equals") 57 | return false 58 | } 59 | comparableWindow.screenshotTest( 60 | referenceScreenshot, 61 | otherWindowScreenshot, 62 | ) 63 | return true 64 | } 65 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/common/ColorUtils.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.common 2 | 3 | import java.awt.Color 4 | 5 | fun hexToColor(colorInHex: String?): Color? { 6 | if (colorInHex == null) return null 7 | val colorInt = Integer.parseInt(colorInHex, 16) 8 | return Color(colorInt) 9 | } 10 | 11 | fun colorToHex(color: Color?): String? { 12 | if (color == null) { 13 | return null 14 | } 15 | return "%x%x%x".format(color.red, color.green, color.blue) 16 | } 17 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/common/JNumberField.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.common 2 | 3 | import java.awt.Toolkit 4 | import java.awt.event.KeyEvent 5 | import javax.swing.JTextField 6 | 7 | class JNumberField(columns: Int) : JTextField(columns) { 8 | var value: Int 9 | get() { 10 | try { 11 | return Integer.valueOf(text) 12 | } catch (e: NumberFormatException) { 13 | e.printStackTrace() 14 | return 0 15 | } 16 | } 17 | set(value) { 18 | text = value.toString() 19 | } 20 | 21 | override fun processKeyEvent(ev: KeyEvent) { 22 | val c = ev.getKeyChar() 23 | val keyCode = ev.keyCode 24 | if (!(Character.isDigit(c) || keyCode == KeyEvent.VK_BACK_SPACE || keyCode == KeyEvent.VK_ENTER || 25 | keyCode == KeyEvent.VK_DELETE || keyCode == KeyEvent.VK_LEFT || 26 | keyCode == KeyEvent.VK_RIGHT || keyCode == KeyEvent.VK_META || 27 | keyCode == KeyEvent.VK_SHIFT || keyCode == KeyEvent.VK_CONTROL || 28 | ((keyCode == KeyEvent.VK_C || keyCode == KeyEvent.VK_V || 29 | keyCode == KeyEvent.VK_A || keyCode == KeyEvent.VK_X) && 30 | (ev.modifiers == Toolkit.getDefaultToolkit().menuShortcutKeyMask))) 31 | ) { 32 | ev.consume() 33 | } 34 | super.processKeyEvent(ev) 35 | return 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/common/LabeledGridBuilder.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.common 2 | 3 | import java.awt.GridBagConstraints 4 | import java.awt.GridBagLayout 5 | import javax.swing.JComponent 6 | import javax.swing.JLabel 7 | import javax.swing.JPanel 8 | import javax.swing.border.EmptyBorder 9 | 10 | class LabeledGridBuilder { 11 | val content = JPanel() 12 | 13 | private val labelConstraints = GridBagConstraints() 14 | private val fieldConstraints = GridBagConstraints() 15 | 16 | init { 17 | content.border = EmptyBorder(8, 8, 8, 8) 18 | content.layout = GridBagLayout() 19 | 20 | labelConstraints.weightx = 0.0 21 | labelConstraints.gridwidth = 1 22 | labelConstraints.gridy = 0 23 | labelConstraints.gridx = 0 24 | 25 | fieldConstraints.gridwidth = 3 26 | fieldConstraints.fill = GridBagConstraints.HORIZONTAL 27 | fieldConstraints.gridy = 0 28 | } 29 | 30 | fun addLabeledComponent(labelText: String, component: JComponent) { 31 | addLabeledComponent(JLabel(labelText), component) 32 | } 33 | 34 | fun addLabeledComponent(label: JLabel, component: JComponent) { 35 | fieldConstraints.gridwidth = 1 36 | content.add(label, labelConstraints) 37 | 38 | content.add(component, fieldConstraints) 39 | 40 | labelConstraints.gridy++ 41 | fieldConstraints.gridy++ 42 | } 43 | 44 | fun addSingleComponent(component: JComponent) { 45 | fieldConstraints.gridwidth = 4 46 | content.add(component, fieldConstraints) 47 | fieldConstraints.gridy++ 48 | labelConstraints.gridy++ 49 | } 50 | 51 | fun addMainAndSlaveComponent(mainComponent: JComponent, slaveComponent: JComponent) { 52 | fieldConstraints.fill = GridBagConstraints.HORIZONTAL 53 | fieldConstraints.gridwidth = 1 54 | fieldConstraints.gridx = 0 55 | fieldConstraints.weightx = 3/4.0 56 | content.add(mainComponent, fieldConstraints) 57 | 58 | fieldConstraints.gridwidth = 1 59 | fieldConstraints.fill = GridBagConstraints.PAGE_END 60 | fieldConstraints.gridx = 1 61 | fieldConstraints.weightx = 1/4.0 62 | content.add(slaveComponent, fieldConstraints) 63 | 64 | fieldConstraints.fill = GridBagConstraints.HORIZONTAL 65 | fieldConstraints.gridx = 0 66 | fieldConstraints.gridy++ 67 | labelConstraints.gridy++ 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/common/MenuAcceleratorHelper.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.common 2 | 3 | import java.awt.Toolkit 4 | import java.awt.event.ActionEvent 5 | import javax.swing.KeyStroke 6 | 7 | fun createAccelerator(keyChar: Char): KeyStroke { 8 | return KeyStroke.getKeyStroke(keyChar) 9 | } 10 | 11 | fun createControlAccelerator(keyChar: Char): KeyStroke { 12 | return KeyStroke.getKeyStroke(keyChar, Toolkit.getDefaultToolkit().menuShortcutKeyMask) 13 | } 14 | 15 | fun createControlAccelerator(keyChar: Int): KeyStroke { 16 | return KeyStroke.getKeyStroke(keyChar, Toolkit.getDefaultToolkit().menuShortcutKeyMask) 17 | } 18 | 19 | fun createControlShiftAccelerator(keyChar: Char): KeyStroke { 20 | return KeyStroke.getKeyStroke(keyChar, Toolkit.getDefaultToolkit().menuShortcutKeyMask + ActionEvent.SHIFT_MASK) 21 | } 22 | 23 | fun createControlAltAccelerator(keyChar: Char): KeyStroke { 24 | return KeyStroke.getKeyStroke(keyChar, Toolkit.getDefaultToolkit().menuShortcutKeyMask + ActionEvent.ALT_MASK) 25 | } 26 | 27 | fun createShiftAccelerator(keyChar: Char): KeyStroke { 28 | return KeyStroke.getKeyStroke(keyChar, ActionEvent.SHIFT_MASK) 29 | } 30 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/common/SimpleComponentListener.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.common 2 | 3 | import java.awt.event.ComponentEvent 4 | import java.awt.event.ComponentListener 5 | 6 | open class SimpleComponentListener : ComponentListener { 7 | override fun componentResized(e: ComponentEvent) = Unit 8 | override fun componentShown(e: ComponentEvent) = Unit 9 | override fun componentMoved(e: ComponentEvent) = Unit 10 | override fun componentHidden(e: ComponentEvent) = Unit 11 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/common/SimpleMouseListener.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.common 2 | 3 | import java.awt.event.MouseEvent 4 | import java.awt.event.MouseListener 5 | 6 | abstract class SimpleMouseListener : MouseListener { 7 | override fun mouseReleased(e: MouseEvent) = Unit 8 | 9 | override fun mouseEntered(e: MouseEvent) = Unit 10 | 11 | override fun mouseExited(e: MouseEvent) = Unit 12 | 13 | override fun mousePressed(e: MouseEvent) = Unit 14 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/common/SimpleMouseMotionListener.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.common 2 | 3 | import java.awt.event.MouseEvent 4 | import java.awt.event.MouseMotionListener 5 | 6 | abstract class SimpleMouseMotionListener : MouseMotionListener { 7 | override fun mouseDragged(e: MouseEvent) = Unit 8 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/dialogs/ClientWrapper.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.dialogs 2 | 3 | import com.android.ddmlib.Client 4 | 5 | data class ClientWrapper(val client: Client) { 6 | override fun toString(): String { 7 | val pkgName = client.clientData.clientDescription 8 | 9 | return pkgName ?: "${client.clientData.pid}" 10 | } 11 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/dialogs/CloseByEscapeDialog.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.dialogs 2 | 3 | import java.awt.Frame 4 | import java.awt.event.ActionEvent 5 | import java.awt.event.WindowAdapter 6 | import java.awt.event.WindowEvent 7 | import javax.swing.* 8 | 9 | /** 10 | * Dialog closed by ESCAPE key. 11 | */ 12 | open class CloseByEscapeDialog( 13 | owner: Frame, title: String, modal: Boolean = false 14 | ) : JDialog(owner, title, modal) { 15 | init { 16 | addWindowListener(object : WindowAdapter() { 17 | override fun windowClosing(we: WindowEvent) { 18 | onDialogClosed() 19 | isVisible = false 20 | } 21 | }) 22 | } 23 | 24 | override fun createRootPane(): JRootPane { 25 | val rootPane = JRootPane() 26 | val stroke = KeyStroke.getKeyStroke("ESCAPE") 27 | val actionListener: Action = object : AbstractAction() { 28 | override fun actionPerformed(actionEvent: ActionEvent?) { 29 | onDialogClosed() 30 | isVisible = false 31 | } 32 | } 33 | val inputMap: InputMap = rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) 34 | inputMap.put(stroke, "ESCAPE") 35 | rootPane.actionMap.put("ESCAPE", actionListener) 36 | return rootPane 37 | } 38 | 39 | open fun onDialogClosed() = Unit 40 | } 41 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/dialogs/DevicesCompoBoxModel.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.dialogs 2 | 3 | import com.android.ddmlib.IDevice 4 | import javax.swing.MutableComboBoxModel 5 | import javax.swing.event.ListDataListener 6 | 7 | interface DeviceWrapper { 8 | val device: IDevice 9 | } 10 | 11 | class RealDeviceWrapper(override val device: IDevice) : DeviceWrapper { 12 | override fun toString(): String { 13 | return device.toString() 14 | } 15 | 16 | override fun equals(other: Any?): Boolean { 17 | if (other is RealDeviceWrapper) { 18 | return device.serialNumber == other.device.serialNumber 19 | } 20 | return false 21 | } 22 | } 23 | 24 | class DevicesCompoBoxModel : MutableComboBoxModel { 25 | private val devices = mutableListOf() 26 | private var selectedItem: DeviceWrapper? = null 27 | private val listeners = mutableListOf() 28 | 29 | override fun setSelectedItem(anItem: Any?) { 30 | if (anItem == null) { 31 | selectedItem = null 32 | return 33 | } 34 | if (anItem is DeviceWrapper) { 35 | selectedItem = anItem 36 | return 37 | } 38 | throw IllegalStateException("Trying to add $anItem") 39 | } 40 | 41 | override fun getSelectedItem(): Any? = selectedItem 42 | 43 | override fun getSize(): Int = devices.size 44 | 45 | override fun addElement(item: DeviceWrapper) { 46 | devices.add(item) 47 | } 48 | 49 | override fun addListDataListener(l: ListDataListener) { 50 | listeners.add(l) 51 | } 52 | 53 | override fun removeListDataListener(l: ListDataListener) { 54 | listeners.remove(l) 55 | } 56 | 57 | override fun getElementAt(index: Int): DeviceWrapper = devices[index] 58 | 59 | 60 | override fun removeElementAt(index: Int) { 61 | devices.removeAt(index) 62 | } 63 | 64 | override fun insertElementAt(item: DeviceWrapper, index: Int) { 65 | devices.add(index, item) 66 | } 67 | 68 | override fun removeElement(obj: Any) { 69 | devices.remove(obj) 70 | } 71 | 72 | fun contains(item: IDevice): Boolean { 73 | for (d in devices) { 74 | if (d.device.serialNumber == item.serialNumber) { 75 | return true 76 | } 77 | } 78 | return false 79 | } 80 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/dialogs/FindDialog.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.dialogs 2 | 3 | import com.android.layoutinspector.model.ViewNode 4 | import com.github.grishberg.android.layoutinspector.domain.AbstractViewNode 5 | import java.awt.FlowLayout 6 | import java.util.Locale 7 | import javax.swing.* 8 | 9 | private const val TOP_OFFSET = 16 10 | private const val FIND_LABEL_DEFAULT_TEXT = "results not found" 11 | 12 | class FindDialog( 13 | private val owner: JFrame 14 | ) : CloseByEscapeDialog(owner, "Find") { 15 | 16 | private val findField: JTextField 17 | private val prevButton: JButton 18 | private val nextButton: JButton 19 | private val resultLabel: JLabel 20 | private val checkboxOnlyInId: JCheckBox 21 | private var onlyId = false 22 | private var onlyName = false 23 | private val results = mutableListOf() 24 | private var currentIndex = 0 25 | private var searchText = "" 26 | 27 | private var rootNode: AbstractViewNode? = null 28 | var foundAction: OnFoundAction? = null 29 | 30 | init { 31 | val content = JPanel() 32 | content.layout = FlowLayout() 33 | findField = JTextField(20) 34 | findField.toolTipText = "Input text and press " 35 | findField.addActionListener { 36 | find() 37 | } 38 | 39 | checkboxOnlyInId = JCheckBox("Only ID") 40 | checkboxOnlyInId.addItemListener { e -> 41 | onlyId = e.stateChange == 1 42 | } 43 | 44 | prevButton = JButton("<") 45 | prevButton.addActionListener { 46 | prev() 47 | } 48 | nextButton = JButton(">") 49 | nextButton.addActionListener { 50 | next() 51 | } 52 | resultLabel = JLabel(FIND_LABEL_DEFAULT_TEXT) 53 | 54 | content.add(findField) 55 | content.add(checkboxOnlyInId) 56 | content.add(prevButton) 57 | content.add(nextButton) 58 | content.add(resultLabel) 59 | 60 | contentPane = content 61 | pack() 62 | } 63 | 64 | override fun onDialogClosed() { 65 | foundAction?.onFoundDialogClosed() 66 | } 67 | 68 | private fun find() { 69 | val text = findField.text 70 | if (text.isEmpty()) { 71 | return 72 | } 73 | searchText = text.lowercase(Locale.getDefault()) 74 | results.clear() 75 | resultLabel.text = FIND_LABEL_DEFAULT_TEXT 76 | currentIndex = 0 77 | 78 | rootNode?.let { 79 | processChildren(it) 80 | } 81 | 82 | if (results.size > 0) { 83 | foundAction?.onFound(results) 84 | } 85 | updateLabelAndNavigateToTree() 86 | } 87 | 88 | private fun prev() { 89 | if (results.isEmpty()) { 90 | return 91 | } 92 | currentIndex-- 93 | if (currentIndex < 0) { 94 | currentIndex = results.size - 1 95 | } 96 | updateLabelAndNavigateToTree() 97 | } 98 | 99 | private fun next() { 100 | if (results.isEmpty()) { 101 | return 102 | } 103 | currentIndex++ 104 | if (currentIndex > results.size - 1) { 105 | currentIndex = 0 106 | } 107 | updateLabelAndNavigateToTree() 108 | } 109 | 110 | private fun updateLabelAndNavigateToTree() { 111 | if (results.size > 0) { 112 | resultLabel.text = "found $currentIndex / ${results.size}" 113 | foundAction?.onSelectedFoundItem(results[currentIndex]) 114 | } 115 | } 116 | 117 | private fun processChildren(node: AbstractViewNode) { 118 | if (isSuitable(node)) { 119 | results.add(node) 120 | } 121 | 122 | val count = node.childCount 123 | for (i in 0 until count) { 124 | processChildren(node.getChildAt(i) as AbstractViewNode) 125 | } 126 | } 127 | 128 | private fun isSuitable(node: AbstractViewNode): Boolean { 129 | if (onlyId) { 130 | if (node.id != null) { 131 | return node.id!!.contains(searchText, true) 132 | } 133 | } else if (onlyName) { 134 | return node.name.contains(searchText, true) 135 | } 136 | 137 | if (node.name.contains(searchText, true)) { 138 | return true 139 | } 140 | if (node.id != null) { 141 | return node.id!!.contains(searchText, true) 142 | } 143 | return false 144 | } 145 | 146 | fun updateRootNode(root: AbstractViewNode?) { 147 | rootNode = root 148 | resultLabel.text = FIND_LABEL_DEFAULT_TEXT 149 | results.clear() 150 | currentIndex = 0 151 | } 152 | 153 | fun showDialog() { 154 | val x = (owner.width) / 2 - (width / 2) 155 | val y = TOP_OFFSET 156 | setLocation(x, y) 157 | isVisible = true 158 | } 159 | 160 | interface OnFoundAction { 161 | fun onFound(foundItems: List) 162 | fun onSelectedFoundItem(node: AbstractViewNode) 163 | fun onFoundDialogClosed() 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/dialogs/LoadingDialog.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.dialogs 2 | 3 | import java.awt.BorderLayout 4 | import java.awt.Frame 5 | import java.awt.event.WindowAdapter 6 | import java.awt.event.WindowEvent 7 | import javax.swing.BorderFactory 8 | import javax.swing.ImageIcon 9 | import javax.swing.JDialog 10 | import javax.swing.JLabel 11 | import javax.swing.JPanel 12 | import javax.swing.WindowConstants 13 | 14 | interface LoadingDialogClosedEventListener { 15 | fun onLoadingDialogClosed() 16 | } 17 | 18 | class LoadingDialog( 19 | owner: Frame, 20 | eventListener: LoadingDialogClosedEventListener 21 | ) : JDialog(owner, false) { 22 | 23 | init { 24 | val panel = JPanel() 25 | panel.layout = BorderLayout(4, 4) 26 | panel.border = BorderFactory.createEmptyBorder(32, 32, 32, 32) 27 | 28 | val cldr = this.javaClass.classLoader 29 | val imageURL = cldr.getResource("icons/loading.gif") 30 | val imageIcon = ImageIcon(imageURL) 31 | val iconLabel = JLabel() 32 | iconLabel.setIcon(imageIcon) 33 | imageIcon.imageObserver = iconLabel 34 | 35 | val label = JLabel("Loading...") 36 | label.setHorizontalAlignment(JLabel.CENTER); 37 | iconLabel.setHorizontalAlignment(JLabel.CENTER); 38 | panel.add(iconLabel, BorderLayout.CENTER) 39 | panel.add(label, BorderLayout.PAGE_START) 40 | setContentPane(panel) 41 | defaultCloseOperation = WindowConstants.DO_NOTHING_ON_CLOSE 42 | pack() 43 | 44 | addWindowListener(object : WindowAdapter() { 45 | override fun windowClosed(e: WindowEvent) = Unit 46 | 47 | override fun windowClosing(e: WindowEvent) { 48 | eventListener.onLoadingDialogClosed() 49 | } 50 | }) 51 | } 52 | 53 | 54 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/dialogs/SupportBalloon.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.dialogs 2 | 3 | class SupportBalloon { 4 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/dialogs/WindowsDialog.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.dialogs 2 | 3 | import com.android.layoutinspector.common.AppLogger 4 | import com.android.layoutinspector.model.ClientWindow 5 | import com.github.grishberg.android.layoutinspector.domain.WindowsListInput 6 | import com.github.grishberg.android.layoutinspector.settings.SettingsFacade 7 | import com.intellij.ui.components.JBList 8 | import com.intellij.ui.components.JBScrollPane 9 | import java.awt.BorderLayout 10 | import java.awt.Dimension 11 | import java.awt.Frame 12 | import java.awt.event.ActionEvent 13 | import java.awt.event.MouseAdapter 14 | import java.awt.event.MouseEvent 15 | import javax.swing.AbstractAction 16 | import javax.swing.DefaultListModel 17 | import javax.swing.JButton 18 | import javax.swing.JDialog 19 | import javax.swing.JLabel 20 | import javax.swing.JList 21 | import javax.swing.JPanel 22 | import javax.swing.KeyStroke 23 | import javax.swing.ListSelectionModel 24 | 25 | private const val TITLE = "Select window" 26 | private const val TAG = "WindowsDialog" 27 | 28 | class WindowsDialog( 29 | private val owner: Frame, 30 | private val settings: SettingsFacade, 31 | private val logger: AppLogger 32 | ) : CloseByEscapeDialog(owner, TITLE, true), WindowsListInput { 33 | private val clientWindowList: JList 34 | 35 | private val clientWindowListModel = DefaultListModel() 36 | private val startButton = JButton("Start") 37 | 38 | init { 39 | clientWindowList = JBList(clientWindowListModel) 40 | clientWindowList.selectionMode = ListSelectionModel.SINGLE_SELECTION 41 | val listScroll = JBScrollPane(clientWindowList) 42 | clientWindowList.addMouseListener(object : MouseAdapter() { 43 | override fun mouseClicked(evt: MouseEvent) { 44 | if (evt.clickCount == 2) { // Double-click detected 45 | isVisible = false 46 | } 47 | } 48 | }) 49 | 50 | clientWindowList.getInputMap(JBList.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke("ENTER"), "start") 51 | clientWindowList.actionMap.put("start", object : AbstractAction() { 52 | override fun actionPerformed(e: ActionEvent) { 53 | if (clientWindowList.selectedIndex >= 0) { 54 | isVisible = false 55 | } 56 | } 57 | }) 58 | 59 | clientWindowList.addListSelectionListener { 60 | if (clientWindowList.selectedIndex >= 0) { 61 | startButton.isEnabled = true 62 | } 63 | } 64 | 65 | startButton.addActionListener { 66 | isVisible = false 67 | } 68 | 69 | listScroll.preferredSize = Dimension(640, 400) 70 | val content = JPanel() 71 | content.layout = BorderLayout() 72 | content.add(JLabel("Windows:"), BorderLayout.NORTH) 73 | content.add(listScroll, BorderLayout.CENTER) 74 | content.add(startButton, BorderLayout.SOUTH) 75 | startButton.isEnabled = false 76 | contentPane = content 77 | pack() 78 | } 79 | 80 | override suspend fun getSelectedWindow(windows: List): ClientWindow { 81 | logger.d("$TAG show dialog for client $windows") 82 | 83 | clientWindowListModel.clear() 84 | 85 | var selectedIndex = -1 86 | for (index in windows.indices) { 87 | val window = windows[index] 88 | clientWindowListModel.addElement(window) 89 | if (window.displayName == settings.lastWindowName) { 90 | selectedIndex = index 91 | } 92 | logger.d("$TAG found window $window") 93 | } 94 | setLocationRelativeTo(owner) 95 | startButton.isEnabled = false 96 | if (selectedIndex >= 0) { 97 | clientWindowList.selectedIndex = selectedIndex 98 | } 99 | isVisible = true 100 | 101 | logger.d("$TAG dialog is closed") 102 | val selectedWindow = clientWindowListModel[clientWindowList.selectedIndex] 103 | settings.lastWindowName = selectedWindow?.displayName ?: "" 104 | return selectedWindow 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/dialogs/bookmarks/BookmarkInfo.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.dialogs.bookmarks 2 | 3 | import com.github.grishberg.android.layoutinspector.domain.AbstractViewNode 4 | import java.awt.Color 5 | 6 | data class BookmarkInfo( 7 | val node: AbstractViewNode, 8 | var description: String?, 9 | var color: Color? 10 | ) 11 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/dialogs/bookmarks/Bookmarks.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.dialogs.bookmarks 2 | 3 | import com.github.grishberg.android.layoutinspector.domain.AbstractViewNode 4 | import java.awt.Color 5 | 6 | typealias BookmarksChangedListener = () -> Unit 7 | 8 | class Bookmarks { 9 | private val _items = mutableListOf() 10 | val listeners = mutableListOf() 11 | 12 | val items: List 13 | get() = _items 14 | 15 | fun add(newBookmark: BookmarkInfo) { 16 | _items.add(newBookmark) 17 | listeners.forEach { 18 | it.invoke() 19 | } 20 | } 21 | 22 | fun remove(item: BookmarkInfo) { 23 | _items.remove(item) 24 | listeners.forEach { 25 | it.invoke() 26 | } 27 | } 28 | 29 | fun edit(item: BookmarkInfo, newValue: BookmarkInfo) { 30 | item.color = newValue.color 31 | item.description = newValue.description 32 | listeners.forEach { 33 | it.invoke() 34 | } 35 | } 36 | 37 | fun find(text: String): BookmarkInfo? { 38 | return null 39 | } 40 | 41 | fun getForegroundForItem(value: AbstractViewNode, defaultTextForeground: Color): Color { 42 | var color = defaultTextForeground 43 | for (bookmarkInfo in _items) { 44 | if (bookmarkInfo.node != value) { 45 | continue 46 | } 47 | bookmarkInfo.color?.let { 48 | color = it 49 | } 50 | break 51 | } 52 | return color 53 | } 54 | 55 | fun getBookmarkInfoForNode(selectedViewNode: AbstractViewNode): BookmarkInfo? { 56 | for (bookmarkInfo in _items) { 57 | if (bookmarkInfo.node == selectedViewNode) { 58 | return bookmarkInfo 59 | } 60 | } 61 | return null 62 | } 63 | 64 | fun set(newItems: List) { 65 | _items.clear() 66 | _items.addAll(newItems) 67 | 68 | listeners.forEach { 69 | it.invoke() 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/dialogs/bookmarks/BookmarksDialog.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.dialogs.bookmarks 2 | 3 | import com.github.grishberg.android.layoutinspector.ui.dialogs.CloseByEscapeDialog 4 | import javax.swing.JFrame 5 | import javax.swing.JTable 6 | 7 | class BookmarksDialog( 8 | private val owner: JFrame 9 | ) : CloseByEscapeDialog(owner, "Bookmarks") { 10 | private val table: JTable 11 | 12 | init { 13 | table = JTable() 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/dialogs/bookmarks/NewBookmarkDialog.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.dialogs.bookmarks 2 | 3 | import com.github.grishberg.android.layoutinspector.domain.AbstractViewNode 4 | import com.github.grishberg.android.layoutinspector.ui.dialogs.CloseByEscapeDialog 5 | import java.awt.BorderLayout 6 | import java.awt.Color 7 | import java.awt.event.ActionEvent 8 | import java.awt.event.ComponentAdapter 9 | import java.awt.event.ComponentEvent 10 | import javax.swing.* 11 | import javax.swing.border.EmptyBorder 12 | 13 | class NewBookmarkDialog( 14 | private val owner: JFrame, 15 | private val selectedViewNode: AbstractViewNode 16 | ) : CloseByEscapeDialog(owner, "New bookmark", true) { 17 | var result: BookmarkInfo? = null 18 | private set 19 | 20 | private var colorChooser: JColorChooser 21 | private var bookmarkName: JTextField 22 | private var selectedColor = Color(201, 137, 255, 117) 23 | 24 | 25 | init { 26 | val content = JPanel() 27 | content.border = EmptyBorder(4, 4, 4, 4) 28 | content.layout = BorderLayout() 29 | 30 | bookmarkName = JTextField(10) 31 | bookmarkName.addActionListener { 32 | closeAfterSuccess() 33 | } 34 | content.add(bookmarkName, BorderLayout.PAGE_START) 35 | colorChooser = JColorChooser(selectedColor) 36 | colorChooser.selectionModel.addChangeListener { selectedColor = colorChooser.color } 37 | content.add(colorChooser, BorderLayout.CENTER) 38 | 39 | val okButton = JButton("OK") 40 | getRootPane().defaultButton = okButton 41 | content.add(okButton, BorderLayout.PAGE_END) 42 | okButton.addActionListener(object : AbstractAction() { 43 | override fun actionPerformed(e: ActionEvent) { 44 | closeAfterSuccess() 45 | } 46 | }) 47 | contentPane = content 48 | defaultCloseOperation = DO_NOTHING_ON_CLOSE 49 | 50 | addComponentListener(object : ComponentAdapter() { 51 | override fun componentShown(ce: ComponentEvent) { 52 | bookmarkName.requestFocusInWindow() 53 | } 54 | }) 55 | defaultCloseOperation = DO_NOTHING_ON_CLOSE 56 | pack() 57 | 58 | } 59 | 60 | private fun closeAfterSuccess() { 61 | result = BookmarkInfo(selectedViewNode, bookmarkName.text, selectedColor) 62 | clearAndHide() 63 | } 64 | 65 | private fun clearAndHide() { 66 | bookmarkName.isEnabled = true 67 | bookmarkName.text = null 68 | isVisible = false 69 | } 70 | 71 | fun showDialog() { 72 | result = null 73 | setLocationRelativeTo(owner) 74 | isVisible = true 75 | } 76 | 77 | fun showEditDialog(bookmarkInfo: BookmarkInfo) { 78 | bookmarkInfo.description?.let { 79 | bookmarkName.text = bookmarkInfo.description 80 | } 81 | 82 | bookmarkInfo.color?.let { 83 | colorChooser.color = it 84 | } 85 | setLocationRelativeTo(owner) 86 | isVisible = true 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/info/PropertiesPanel.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.info 2 | 3 | import com.github.grishberg.android.layoutinspector.domain.AbstractViewNode 4 | import javax.swing.JComponent 5 | 6 | interface PropertiesPanel { 7 | 8 | fun getComponent(): JComponent 9 | 10 | fun showProperties(node: AbstractViewNode) 11 | 12 | fun setSizeDpMode(enabled: Boolean) 13 | 14 | fun roundDp(enabled: Boolean) 15 | } 16 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/info/RowInfoImpl.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.info 2 | 3 | import com.android.layoutinspector.model.ViewProperty 4 | import java.math.RoundingMode 5 | import java.text.DecimalFormat 6 | 7 | /** 8 | * Table row value model. 9 | */ 10 | data class RowInfoImpl( 11 | private val property: ViewProperty, 12 | private val sizeInDp: Boolean, 13 | private val roundDp: Boolean, 14 | private val dpPerPixels: Double, 15 | private val alterName: String? = null, 16 | val isSummary: Boolean = false, 17 | ) { 18 | 19 | fun name() = alterName ?: property.name 20 | 21 | fun value(): String { 22 | 23 | if (sizeInDp && property.isSizeProperty && dpPerPixels > 1) { 24 | return roundOffDecimal(property.intValue.toDouble() / dpPerPixels) + " dp" 25 | } 26 | return property.value 27 | } 28 | 29 | override fun toString(): String { 30 | return property.name 31 | } 32 | 33 | private fun roundOffDecimal(number: Double): String { 34 | val df = DecimalFormat(if (roundDp) "#" else "#.##") 35 | df.roundingMode = RoundingMode.CEILING 36 | return df.format(number) 37 | } 38 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/info/flat/BoardTableCellRenderer.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.info.flat 2 | 3 | import com.github.grishberg.android.layoutinspector.ui.theme.ThemeColors 4 | import java.awt.Component 5 | import java.awt.Insets 6 | import javax.swing.JTable 7 | import javax.swing.border.CompoundBorder 8 | import javax.swing.border.EmptyBorder 9 | import javax.swing.table.DefaultTableCellRenderer 10 | 11 | 12 | private const val BORDER_SIZE = 4 13 | 14 | class BoardTableCellRenderer( 15 | private val themeColors: ThemeColors 16 | ) : DefaultTableCellRenderer() { 17 | 18 | override fun getTableCellRendererComponent( 19 | table: JTable, value: Any, 20 | isSelected: Boolean, hasFocus: Boolean, row: Int, col: Int 21 | ): Component { 22 | val c = super.getTableCellRendererComponent( 23 | table, value, 24 | isSelected, hasFocus, row, col 25 | ) 26 | 27 | val viewRow = table.convertRowIndexToModel(row) 28 | if (viewRow < 0) { 29 | return c 30 | } 31 | val valueAt = table.model.getValueAt(viewRow, col) 32 | 33 | when (valueAt) { 34 | is TableValue.Empty, 35 | is TableValue.Header -> { 36 | border = CompoundBorder( 37 | EmptyBorder(Insets(BORDER_SIZE, BORDER_SIZE, BORDER_SIZE, BORDER_SIZE)), 38 | border 39 | ) 40 | c.foreground = themeColors.groupForeground 41 | c.background = themeColors.groupBackground 42 | } 43 | is TableValue.PropertyValue, 44 | is TableValue.PropertyName -> { 45 | if (isSelected) { 46 | return c 47 | } 48 | c.foreground = table.foreground 49 | c.background = themeColors.propertiesPanelBackground 50 | } 51 | } 52 | 53 | return c 54 | } 55 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/info/flat/FlatGroupTableModel.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.info.flat 2 | 3 | import com.github.grishberg.android.layoutinspector.ui.info.RowInfoImpl 4 | import javax.swing.table.DefaultTableModel 5 | 6 | private const val NAME_COLUMN = 0 7 | 8 | class FlatGroupTableModel( 9 | data: Map> 10 | ) : DefaultTableModel() { 11 | private val list = mutableListOf() 12 | 13 | init { 14 | prepareData(data) 15 | columnCount = 2 16 | } 17 | 18 | private fun prepareData(data: Map>) { 19 | for (e in data.entries) { 20 | list.add(Row.HeaderRow(e.key)) 21 | 22 | for (item in e.value) { 23 | list.add(Row.ValueRow(item)) 24 | } 25 | } 26 | 27 | rowCount = list.size 28 | } 29 | 30 | fun updateData(data: Map>) { 31 | list.clear() 32 | prepareData(data) 33 | fireTableDataChanged() 34 | } 35 | 36 | override fun getColumnName(column: Int): String { 37 | return when (column) { 38 | 0 -> "Property name" 39 | else -> "Value" 40 | } 41 | } 42 | 43 | override fun getColumnClass(columnIndex: Int): Class<*>? { 44 | return String::class.java 45 | } 46 | 47 | override fun isCellEditable(row: Int, column: Int): Boolean = false 48 | 49 | override fun getValueAt(row: Int, column: Int): Any { 50 | val rowItem = list[row] 51 | when (rowItem) { 52 | 53 | is Row.ValueRow -> { 54 | if (column == NAME_COLUMN) { 55 | return TableValue.PropertyName(rowItem.property, rowItem.property.isSummary) 56 | } 57 | return TableValue.PropertyValue(rowItem.property) 58 | } 59 | is Row.HeaderRow -> { 60 | if (column == NAME_COLUMN) { 61 | return TableValue.Header(rowItem.name) 62 | } 63 | return TableValue.Empty 64 | } 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/info/flat/Row.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.info.flat 2 | 3 | import com.github.grishberg.android.layoutinspector.ui.info.RowInfoImpl 4 | 5 | sealed class Row { 6 | class ValueRow(val property: RowInfoImpl) : Row() 7 | 8 | class HeaderRow(val name: String) : Row() 9 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/info/flat/TableValue.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.info.flat 2 | 3 | import com.github.grishberg.android.layoutinspector.ui.info.RowInfoImpl 4 | 5 | sealed class TableValue { 6 | class PropertyValue(val property: RowInfoImpl) : TableValue() { 7 | override fun toString(): String { 8 | return property.value() 9 | } 10 | } 11 | 12 | class PropertyName(val property: RowInfoImpl, val isSummary: Boolean = false) : TableValue() { 13 | override fun toString(): String { 14 | return property.name() 15 | } 16 | } 17 | 18 | class Header(val name: String) : TableValue() { 19 | override fun toString(): String { 20 | return name 21 | } 22 | } 23 | 24 | object Empty : TableValue() { 25 | override fun toString(): String = "" 26 | } 27 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/info/flat/filter/FilterView.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.info.flat.filter 2 | 3 | import java.awt.Component 4 | 5 | interface FilterView { 6 | val component: Component 7 | 8 | val filterText: String 9 | 10 | fun setOnTextChangedListener(listener: (String) -> Unit) 11 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/info/flat/filter/PropertiesTableFilter.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.info.flat.filter 2 | 3 | import com.github.grishberg.android.layoutinspector.ui.info.flat.FlatGroupTableModel 4 | import com.github.grishberg.android.layoutinspector.ui.info.flat.TableValue 5 | import java.util.regex.Matcher 6 | import java.util.regex.Pattern 7 | import javax.swing.RowFilter 8 | 9 | 10 | class PropertiesTableFilter( 11 | text: String 12 | ) : RowFilter() { 13 | 14 | 15 | private val regex: Pattern = Pattern.compile(text, Pattern.CASE_INSENSITIVE) 16 | private val matcher: Matcher = regex.matcher(""); 17 | 18 | override fun include(entry: Entry): Boolean { 19 | val item = entry.getValue(PROPERTY_NAME_COLUMN) as TableValue 20 | return when (item) { 21 | is TableValue.Header -> { 22 | return true 23 | } 24 | is TableValue.PropertyName -> { 25 | if (item.isSummary) { 26 | return true 27 | } 28 | matcher.reset(item.toString()) 29 | return matcher.find() 30 | } 31 | is TableValue.PropertyValue -> { 32 | return true 33 | } 34 | else -> true 35 | } 36 | } 37 | 38 | private companion object { 39 | private const val PROPERTY_NAME_COLUMN = 0 40 | } 41 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/info/flat/filter/SimpleFilterTextView.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.info.flat.filter 2 | 3 | import com.github.grishberg.android.layoutinspector.settings.SettingsFacade 4 | import java.awt.BorderLayout 5 | import javax.swing.JButton 6 | import javax.swing.JLabel 7 | import javax.swing.JPanel 8 | import javax.swing.JTextField 9 | import javax.swing.event.DocumentEvent 10 | import javax.swing.event.DocumentListener 11 | 12 | class SimpleFilterTextView( 13 | private val settings: SettingsFacade 14 | ) : FilterView { 15 | private var listener: (String) -> Unit = {} 16 | 17 | private val filterTextField = JTextField(settings.lastFilter) 18 | 19 | override val component = JPanel(BorderLayout()) 20 | override val filterText: String 21 | get() = filterTextField.text 22 | 23 | init { 24 | val panel = component 25 | val label = JLabel("Filter") 26 | panel.add(label, BorderLayout.WEST) 27 | 28 | panel.add(filterTextField, BorderLayout.CENTER) 29 | 30 | val clearButton = JButton("x") 31 | clearButton.toolTipText = "clear text" 32 | panel.add(clearButton, BorderLayout.EAST) 33 | 34 | filterTextField.document.addDocumentListener(object : DocumentListener { 35 | override fun changedUpdate(e: DocumentEvent) { 36 | onTextChanged() 37 | } 38 | 39 | override fun removeUpdate(e: DocumentEvent) { 40 | onTextChanged() 41 | } 42 | 43 | override fun insertUpdate(e: DocumentEvent) { 44 | onTextChanged() 45 | } 46 | 47 | private fun onTextChanged() { 48 | settings.lastFilter = filterTextField.text 49 | listener.invoke(filterTextField.text) 50 | } 51 | }) 52 | 53 | clearButton.addActionListener { 54 | filterTextField.text = "" 55 | } 56 | } 57 | 58 | 59 | override fun setOnTextChangedListener(listener: (String) -> Unit) { 60 | this.listener = listener 61 | } 62 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/layout/ImageHelper.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.layout 2 | 3 | import java.awt.Graphics2D 4 | import java.awt.GraphicsEnvironment 5 | import java.awt.Transparency 6 | import java.awt.image.BufferedImage 7 | 8 | 9 | class ImageHelper { 10 | private val GFX_CONFIG = GraphicsEnvironment.getLocalGraphicsEnvironment().defaultScreenDevice.defaultConfiguration 11 | 12 | fun copyImage(source: BufferedImage?, shouldCopyTransparency: Boolean = true): BufferedImage? { 13 | if (source == null) { 14 | return null 15 | } 16 | val newImage = GFX_CONFIG.createCompatibleImage( 17 | source.width, 18 | source.height, 19 | if (shouldCopyTransparency) source.transparency else Transparency.OPAQUE 20 | ) 21 | // get the graphics context of the new image to draw the old image on 22 | val g2d = newImage.graphics as Graphics2D 23 | 24 | 25 | g2d.drawImage(source, 0, 0, null) 26 | g2d.dispose() 27 | return newImage 28 | } 29 | 30 | fun copyForClipboard(source: BufferedImage): BufferedImage { 31 | val original: BufferedImage = source 32 | val newImage = BufferedImage( 33 | original.getWidth(null), original.getHeight(null), BufferedImage.TYPE_INT_RGB 34 | ) 35 | val g = newImage.createGraphics() 36 | g.drawImage(original, 0, 0, null) 37 | g.dispose() 38 | return newImage 39 | } 40 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/layout/ImageTransferable.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.layout 2 | 3 | import java.awt.datatransfer.DataFlavor 4 | import java.awt.datatransfer.Transferable 5 | import java.awt.datatransfer.UnsupportedFlavorException 6 | import java.awt.image.BufferedImage 7 | import java.io.IOException 8 | 9 | class ImageTransferable(val image: BufferedImage) : Transferable { 10 | override fun getTransferDataFlavors(): Array { 11 | return arrayOf(DataFlavor.imageFlavor) 12 | } 13 | 14 | override fun isDataFlavorSupported(flavor: DataFlavor): Boolean { 15 | return DataFlavor.imageFlavor.equals(flavor) 16 | } 17 | 18 | @kotlin.Throws(UnsupportedFlavorException::class, IOException::class) 19 | override fun getTransferData(flavor: DataFlavor): Any { 20 | if (isDataFlavorSupported(flavor)) { 21 | return image 22 | } 23 | throw UnsupportedFlavorException(flavor) 24 | } 25 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/layout/LayoutModel.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.layout 2 | 3 | import com.github.grishberg.android.layoutinspector.domain.AbstractViewNode 4 | import java.awt.Shape 5 | 6 | data class LayoutModel( 7 | val rect: Shape, 8 | val node: AbstractViewNode, 9 | val children: List) 10 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/layout/LayoutsEnabledState.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.layout 2 | 3 | class LayoutsEnabledState { 4 | var isEnabled = true 5 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/layout/ScreenshotClipboardManager.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.layout 2 | 3 | import com.android.layoutinspector.common.AppLogger 4 | import java.awt.Toolkit 5 | import java.awt.image.BufferedImage 6 | 7 | class ScreenshotClipboardManager( 8 | private val imageHelper: ImageHelper, 9 | private val logger: AppLogger, 10 | ) { 11 | fun copyToClipboard(image: BufferedImage?) { 12 | if (image == null) { 13 | return 14 | } 15 | try { 16 | val newImage = imageHelper.copyForClipboard(image) 17 | val t = ImageTransferable(newImage) 18 | Toolkit.getDefaultToolkit().systemClipboard 19 | .setContents(t, null) 20 | } catch (e: Exception) { 21 | logger.e("Can't copy image to clipboard", e) 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/screenshottest/ScreenshotPainter.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.screenshottest 2 | 3 | interface ScreenshotPainter { 4 | fun paintDifferencePixel(x: Int, y: Int) 5 | 6 | fun invalidate() 7 | 8 | fun clearDifferences() 9 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/screenshottest/ScreenshotTestDialog.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.screenshottest 2 | 3 | import com.github.grishberg.android.layoutinspector.ui.dialogs.CloseByEscapeDialog 4 | import java.awt.BorderLayout 5 | import java.awt.event.WindowAdapter 6 | import java.awt.event.WindowEvent 7 | import java.awt.image.BufferedImage 8 | import javax.swing.BorderFactory 9 | import javax.swing.JFrame 10 | import javax.swing.JLabel 11 | import javax.swing.JPanel 12 | import javax.swing.WindowConstants 13 | 14 | private const val TOP_OFFSET = 16 15 | 16 | class ScreenshotTestDialog( 17 | private val owner: JFrame, 18 | private val screenshotPainter: ScreenshotPainter, 19 | ) : CloseByEscapeDialog(owner, "Screenshot test", false), ScreenshotTestView { 20 | 21 | private val logic = ScreenshotTestLogic(this) 22 | val label = JLabel("Comparing...") 23 | val panel = JPanel() 24 | 25 | init { 26 | panel.layout = BorderLayout(4, 4) 27 | panel.border = BorderFactory.createEmptyBorder(32, 32, 32, 32) 28 | 29 | label.horizontalAlignment = JLabel.CENTER; 30 | panel.add(label, BorderLayout.CENTER) 31 | setContentPane(panel) 32 | defaultCloseOperation = WindowConstants.DO_NOTHING_ON_CLOSE 33 | pack() 34 | 35 | addWindowListener(object : WindowAdapter() { 36 | override fun windowClosed(e: WindowEvent) = Unit 37 | 38 | override fun windowClosing(e: WindowEvent) { 39 | screenshotPainter.clearDifferences() 40 | } 41 | }) 42 | } 43 | 44 | fun showDialog( 45 | reference: BufferedImage, 46 | comparable: BufferedImage, 47 | ) { 48 | val x = (owner.width) / 2 - (width / 2) 49 | val y = TOP_OFFSET 50 | setLocation(x, y) 51 | logic.compare(reference, comparable, screenshotPainter) 52 | isVisible = true 53 | } 54 | 55 | override fun onDialogClosed() { 56 | super.onDialogClosed() 57 | screenshotPainter.clearDifferences() 58 | } 59 | 60 | override fun showNoDifferences() { 61 | label.text = "Screenshots are equals" 62 | pack() 63 | } 64 | 65 | override fun showHasDifferences(differencesCount: Int) { 66 | val pixelText = if (differencesCount > 1) "pixels" else "pixel" 67 | label.text = "There are some differences: $differencesCount $pixelText" 68 | pack() 69 | } 70 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/screenshottest/ScreenshotTestLogic.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.screenshottest 2 | 3 | import java.awt.image.BufferedImage 4 | 5 | class ScreenshotTestLogic( 6 | private val view: ScreenshotTestView, 7 | ) { 8 | fun compare( 9 | reference: BufferedImage, 10 | comparable: BufferedImage, 11 | screenshotPainter: ScreenshotPainter 12 | ) { 13 | var differencesCount = 0 14 | for (x in 0 until reference.width) { 15 | for (y in 0 until comparable.height) { 16 | if (reference.getRGB(x, y) != comparable.getRGB(x, y)) { 17 | differencesCount++ 18 | screenshotPainter.paintDifferencePixel(x, y) 19 | } 20 | } 21 | } 22 | 23 | screenshotPainter.invalidate() 24 | 25 | if (differencesCount == 0){ 26 | view.showNoDifferences() 27 | } else { 28 | view.showHasDifferences(differencesCount) 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/screenshottest/ScreenshotTestView.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.screenshottest 2 | 3 | interface ScreenshotTestView { 4 | fun showNoDifferences() 5 | 6 | fun showHasDifferences(differencesCount: Int) 7 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/theme/MaterialDarkColors.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.theme 2 | 3 | import com.github.grishberg.android.layoutinspector.ui.tree.IconsStore 4 | import java.awt.Color 5 | import javax.swing.UIManager 6 | 7 | class MaterialDarkColors : ThemeColors { 8 | private val primaryColor = Color(0xBB86CC) 9 | private val secondaryColor = Color(0x000000) 10 | private val errorColor = Color(0xCF6679) 11 | private val foregroundColor = Color(0xffffff) 12 | private val foregroundMediumColor = Color(255, 255, 255, 153) 13 | 14 | override val treeBackground = UIManager.getColor("Tree.background") 15 | override val selectionBackground = Color(249, 192, 98, 0) 16 | override val hoverBackground = Color(foregroundColor.red, foregroundColor.green, foregroundColor.blue, 30) 17 | 18 | 19 | override val foreground1 = foregroundColor 20 | override val selectionForeground1 = foregroundColor 21 | override val hiddenForeground1 = Color(157, 157, 157) 22 | override val hoveredForeground1 = UIManager.getColor("Button.mouseHoverColor") ?: foregroundColor 23 | override val selectionHiddenForeground1 = Color(selectionForeground1.red, selectionForeground1.green, selectionForeground1.blue, 220) 24 | 25 | override val foreground2 = foregroundMediumColor 26 | override val selectionForeground2 = Color(selectionForeground1.red, selectionForeground1.green, selectionForeground1.blue, 220) 27 | override val hiddenForeground2 = Color(130, 130, 130) 28 | override val hoveredForeground2 = UIManager.getColor("Button.mouseHoverColor") ?: foreground2 29 | override val selectionHiddenForeground2 = secondaryColor 30 | 31 | override val foundTextColor = errorColor 32 | override val selectedFoundTextColor = primaryColor 33 | 34 | override val groupForeground = Color(187, 187, 187) 35 | override val groupBackground = Color(86, 86, 86) 36 | override val propertiesPanelHovered = Color(70, 70, 70) 37 | override val propertiesPanelBackground= Color(44, 44, 44) 38 | 39 | private val iconsStore = IconsStore() 40 | override val textIcon = iconsStore.createImageIcon("/icons/dark/text.png") 41 | override val fabIcon = iconsStore.createImageIcon("/icons/dark/fab.png") 42 | override val appBarIcon = iconsStore.createImageIcon("/icons/dark/appbar.png") 43 | override val coordinatorLayoutIcon = iconsStore.createImageIcon("/icons/dark/coordinator_layout.png") 44 | override val constraintLayoutIcon = iconsStore.createImageIcon("/icons/dark/constraint_layout.png") 45 | override val frameLayoutIcon = iconsStore.createImageIcon("/icons/dark/frame_layout.png") 46 | override val linearLayoutIcon = iconsStore.createImageIcon("/icons/dark/linear_layout.png") 47 | override val cardViewIcon = iconsStore.createImageIcon("/icons/dark/cardView.png") 48 | override val viewStubIcon = iconsStore.createImageIcon("/icons/dark/viewstub.png") 49 | override val toolbarIcon = iconsStore.createImageIcon("/icons/dark/toolbar.png") 50 | override val listViewIcon = iconsStore.createImageIcon("/icons/dark/recyclerView.png") 51 | override val relativeLayoutIcon = iconsStore.createImageIcon("/icons/dark/relativeLayout.png") 52 | override val imageViewIcon = iconsStore.createImageIcon("/icons/dark/imageView.png") 53 | override val nestedScrollViewIcon = iconsStore.createImageIcon("/icons/dark/nestedScrollView.png") 54 | override val viewSwitcherIcon = iconsStore.createImageIcon("/icons/dark/viewSwitcher.png") 55 | override val viewPagerIcon = iconsStore.createImageIcon("/icons/dark/viewPager.png") 56 | override val viewIcon = iconsStore.createImageIcon("/icons/dark/view.png") 57 | override val composeIcon = iconsStore.createImageIcon("/icons/dark/node_type_compose.svg") 58 | } 59 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/theme/MaterialLiteColors.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.theme 2 | 3 | import com.github.grishberg.android.layoutinspector.ui.tree.IconsStore 4 | import java.awt.Color 5 | import javax.swing.UIManager 6 | 7 | class MaterialLiteColors : ThemeColors { 8 | private val primaryColor = Color(0x6200EE) 9 | 10 | private val foregroundColor = Color(0x000000) 11 | private val foregroundMediumColor = Color(0, 0, 0, 153) 12 | 13 | override val treeBackground = UIManager.getColor("Tree.background") 14 | override val selectionBackground = Color(195, 219, 247,0) 15 | 16 | override val hoverBackground = Color( 17 | foregroundColor.red, 18 | foregroundColor.green, 19 | foregroundColor.blue, 20 | 20 21 | ) 22 | 23 | override val foreground1 = foregroundColor 24 | override val selectionForeground1 = Color(255, 255, 255) 25 | override val hiddenForeground1 = UIManager.getColor("Label.disabledForeground") 26 | override val hoveredForeground1 = foregroundColor 27 | override val selectionHiddenForeground1 = selectionForeground1 28 | 29 | override val foreground2 = foregroundMediumColor 30 | override val selectionForeground2 = foregroundMediumColor 31 | override val hiddenForeground2 = UIManager.getColor("Label.disabledForeground") 32 | override val hoveredForeground2 = selectionForeground2 33 | override val selectionHiddenForeground2 = hiddenForeground2 34 | 35 | override val foundTextColor = Color(204, 42, 49) 36 | override val selectedFoundTextColor = primaryColor 37 | 38 | override val groupForeground = Color.BLACK 39 | override val groupBackground = Color(242, 242, 242) 40 | override val propertiesPanelHovered = treeBackground 41 | override val propertiesPanelBackground = treeBackground 42 | 43 | private val iconsStore = IconsStore() 44 | 45 | override val textIcon = iconsStore.createImageIcon("/icons/light/text.png") 46 | override val fabIcon = iconsStore.createImageIcon("/icons/light/fab.png") 47 | override val appBarIcon = iconsStore.createImageIcon("/icons/light/appbar.png") 48 | override val coordinatorLayoutIcon = iconsStore.createImageIcon("/icons/light/coordinator_layout.png") 49 | override val constraintLayoutIcon = iconsStore.createImageIcon("/icons/light/constraint_layout.png") 50 | override val frameLayoutIcon = iconsStore.createImageIcon("/icons/light/frame_layout.png") 51 | override val linearLayoutIcon = iconsStore.createImageIcon("/icons/light/linear_layout.png") 52 | override val cardViewIcon = iconsStore.createImageIcon("/icons/light/cardView.png") 53 | override val viewStubIcon = iconsStore.createImageIcon("/icons/light/viewstub.png") 54 | override val toolbarIcon = iconsStore.createImageIcon("/icons/light/toolbar.png") 55 | override val listViewIcon = iconsStore.createImageIcon("/icons/light/recyclerView.png") 56 | override val relativeLayoutIcon = iconsStore.createImageIcon("/icons/light/relativeLayout.png") 57 | override val imageViewIcon = iconsStore.createImageIcon("/icons/light/imageView.png") 58 | override val nestedScrollViewIcon = iconsStore.createImageIcon("/icons/light/nestedScrollView.png") 59 | override val viewSwitcherIcon = iconsStore.createImageIcon("/icons/light/viewSwitcher.png") 60 | override val viewPagerIcon = iconsStore.createImageIcon("/icons/light/viewPager.png") 61 | override val viewIcon = iconsStore.createImageIcon("/icons/light/view.png") 62 | override val composeIcon = iconsStore.createImageIcon("/icons/light/node_type_compose.svg") 63 | } 64 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/theme/ThemeColors.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.theme 2 | 3 | import java.awt.Color 4 | import javax.swing.ImageIcon 5 | 6 | interface ThemeColors { 7 | val treeBackground: Color 8 | val selectionBackground: Color 9 | val hoverBackground: Color 10 | 11 | val foreground1: Color 12 | val selectionForeground1: Color 13 | 14 | val foreground2: Color 15 | val selectionForeground2: Color 16 | 17 | val hiddenForeground1: Color 18 | val hiddenForeground2: Color 19 | 20 | val hoveredForeground1: Color 21 | val hoveredForeground2: Color 22 | 23 | val groupForeground: Color 24 | val groupBackground: Color 25 | 26 | val propertiesPanelHovered: Color 27 | val propertiesPanelBackground: Color 28 | 29 | val selectionHiddenForeground1: Color 30 | val selectionHiddenForeground2: Color 31 | val foundTextColor: Color 32 | val selectedFoundTextColor: Color 33 | 34 | val textIcon: ImageIcon 35 | val fabIcon: ImageIcon 36 | val appBarIcon: ImageIcon 37 | val coordinatorLayoutIcon: ImageIcon 38 | val constraintLayoutIcon: ImageIcon 39 | val frameLayoutIcon: ImageIcon 40 | val linearLayoutIcon: ImageIcon 41 | val cardViewIcon: ImageIcon 42 | val viewStubIcon: ImageIcon 43 | val toolbarIcon: ImageIcon 44 | val listViewIcon: ImageIcon 45 | val relativeLayoutIcon: ImageIcon 46 | val imageViewIcon: ImageIcon 47 | val nestedScrollViewIcon: ImageIcon 48 | val viewSwitcherIcon: ImageIcon 49 | val viewPagerIcon: ImageIcon 50 | val viewIcon: ImageIcon 51 | val composeIcon: ImageIcon 52 | 53 | fun addColorChangedAction(action: () -> Unit) = Unit 54 | fun removeColorChangedAction(action: () -> Unit) = Unit 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/theme/ThemeProxy.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.theme 2 | 3 | import java.awt.Color 4 | import javax.swing.ImageIcon 5 | 6 | class ThemeProxy : ThemeColors { 7 | var currentTheme: ThemeColors = MaterialLiteColors() 8 | set(value) { 9 | field = value 10 | actions.forEach { 11 | it.invoke() 12 | } 13 | } 14 | 15 | override val hoverBackground: Color 16 | get() = currentTheme.hoverBackground 17 | 18 | override val treeBackground: Color 19 | get() = currentTheme.treeBackground 20 | 21 | override val selectionHiddenForeground1: Color 22 | get() = currentTheme.selectionHiddenForeground1 23 | 24 | override val selectionHiddenForeground2: Color 25 | get() = currentTheme.selectionHiddenForeground2 26 | 27 | override val foundTextColor: Color 28 | get() = currentTheme.foundTextColor 29 | 30 | override val selectedFoundTextColor: Color 31 | get() = currentTheme.selectedFoundTextColor 32 | 33 | override val foreground2: Color 34 | get() = currentTheme.foreground2 35 | 36 | override val selectionForeground2: Color 37 | get() = currentTheme.selectionForeground2 38 | 39 | override val selectionBackground: Color 40 | get() = currentTheme.selectionBackground 41 | 42 | override val foreground1: Color 43 | get() = currentTheme.foreground1 44 | 45 | override val selectionForeground1: Color 46 | get() = currentTheme.selectionForeground1 47 | 48 | override val hiddenForeground1: Color 49 | get() = currentTheme.hiddenForeground1 50 | 51 | override val hiddenForeground2: Color 52 | get() = currentTheme.hiddenForeground2 53 | 54 | override val hoveredForeground1: Color 55 | get() = currentTheme.hoveredForeground1 56 | 57 | override val hoveredForeground2: Color 58 | get() = currentTheme.hoveredForeground2 59 | 60 | 61 | override val textIcon: ImageIcon 62 | get() = currentTheme.textIcon 63 | 64 | override val fabIcon: ImageIcon 65 | get() = currentTheme.fabIcon 66 | 67 | override val appBarIcon: ImageIcon 68 | get() = currentTheme.appBarIcon 69 | 70 | override val coordinatorLayoutIcon: ImageIcon 71 | get() = currentTheme.coordinatorLayoutIcon 72 | 73 | override val constraintLayoutIcon: ImageIcon 74 | get() = currentTheme.constraintLayoutIcon 75 | 76 | override val frameLayoutIcon: ImageIcon 77 | get() = currentTheme.frameLayoutIcon 78 | 79 | override val linearLayoutIcon: ImageIcon 80 | get() = currentTheme.linearLayoutIcon 81 | 82 | override val cardViewIcon: ImageIcon 83 | get() = currentTheme.cardViewIcon 84 | 85 | override val viewStubIcon: ImageIcon 86 | get() = currentTheme.viewStubIcon 87 | 88 | override val toolbarIcon: ImageIcon 89 | get() = currentTheme.toolbarIcon 90 | 91 | override val listViewIcon: ImageIcon 92 | get() = currentTheme.listViewIcon 93 | 94 | override val relativeLayoutIcon: ImageIcon 95 | get() = currentTheme.relativeLayoutIcon 96 | 97 | override val imageViewIcon: ImageIcon 98 | get() = currentTheme.imageViewIcon 99 | 100 | override val nestedScrollViewIcon: ImageIcon 101 | get() = currentTheme.nestedScrollViewIcon 102 | 103 | override val viewSwitcherIcon: ImageIcon 104 | get() = currentTheme.viewSwitcherIcon 105 | 106 | override val viewPagerIcon: ImageIcon 107 | get() = currentTheme.viewPagerIcon 108 | 109 | override val viewIcon: ImageIcon 110 | get() = currentTheme.nestedScrollViewIcon 111 | 112 | override val composeIcon: ImageIcon 113 | get() = currentTheme.composeIcon 114 | 115 | override val groupForeground: Color 116 | get() = currentTheme.groupForeground 117 | 118 | override val groupBackground: Color 119 | get() = currentTheme.groupBackground 120 | 121 | override val propertiesPanelHovered: Color 122 | get() = currentTheme.propertiesPanelHovered 123 | 124 | override val propertiesPanelBackground: Color 125 | get() = currentTheme.propertiesPanelBackground 126 | 127 | private val actions = mutableListOf<() -> Unit>() 128 | 129 | override fun addColorChangedAction(action: () -> Unit) { 130 | actions.add(action) 131 | } 132 | 133 | override fun removeColorChangedAction(action: () -> Unit) { 134 | actions.remove(action) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/theme/Themes.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.theme 2 | 3 | import com.android.layoutinspector.common.AppLogger 4 | import com.intellij.ui.JBColor 5 | 6 | import java.awt.Frame 7 | import javax.swing.SwingUtilities 8 | import javax.swing.UnsupportedLookAndFeelException 9 | 10 | class Themes( 11 | private val owner: Frame, private val themeProxy: ThemeProxy, logger: AppLogger 12 | ) { 13 | 14 | val isDark: Boolean 15 | get() = !JBColor.isBright() 16 | 17 | init { 18 | try { 19 | if (isDark) { 20 | setDarkTheme() 21 | } else { 22 | setLiteTheme() 23 | } 24 | } catch (e: UnsupportedLookAndFeelException) { 25 | logger.e("Error while switching theme", e) 26 | } 27 | } 28 | 29 | private fun setDarkTheme(set: Boolean = false) { 30 | if (set) { 31 | themeProxy.currentTheme = MaterialDarkColors() 32 | return 33 | } 34 | SwingUtilities.updateComponentTreeUI(owner) 35 | themeProxy.currentTheme = MaterialDarkColors() 36 | } 37 | 38 | private fun setLiteTheme(set: Boolean = false) { 39 | 40 | if (set) { 41 | themeProxy.currentTheme = MaterialLiteColors() 42 | return 43 | } 44 | SwingUtilities.updateComponentTreeUI(owner) 45 | themeProxy.currentTheme = MaterialLiteColors() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/tree/EmptyTreeIcon.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.tree 2 | 3 | import java.awt.Component 4 | import java.awt.Graphics 5 | import javax.swing.Icon 6 | 7 | class EmptyTreeIcon : Icon { 8 | override fun paintIcon(c: Component, g: Graphics?, x: Int, y: Int) = Unit 9 | override fun getIconHeight() = SIZE 10 | override fun getIconWidth() = SIZE 11 | 12 | companion object { 13 | const val SIZE = 0 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/tree/IconsStore.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.tree 2 | 3 | import com.intellij.openapi.util.IconLoader 4 | import java.awt.Graphics2D 5 | import java.awt.RenderingHints 6 | import java.awt.image.BufferedImage 7 | import javax.swing.Icon 8 | import javax.swing.ImageIcon 9 | 10 | 11 | private const val ICON_SIZE = 18 12 | 13 | class IconsStore(private val iconSize: Int = ICON_SIZE) { 14 | fun createImageIcon(path: String, altText: String = ""): ImageIcon { 15 | val icon = IconLoader.getIcon(path, this.javaClass) 16 | 17 | return resizeIcon(icon, iconSize, iconSize) 18 | } 19 | 20 | private fun resizeIcon(icon: Icon, width: Int, height: Int): ImageIcon { 21 | val bufferedImage = BufferedImage( 22 | icon.iconWidth, icon.iconHeight, 23 | BufferedImage.TYPE_4BYTE_ABGR 24 | ) 25 | val bufImageG: Graphics2D = bufferedImage.createGraphics() 26 | icon.paintIcon(null, bufImageG, 0, 0) 27 | bufImageG.dispose() 28 | 29 | val resizedImg = BufferedImage( 30 | width, height, 31 | BufferedImage.TYPE_4BYTE_ABGR 32 | ) 33 | 34 | val g2: Graphics2D = resizedImg.createGraphics() 35 | g2.setRenderingHint( 36 | RenderingHints.KEY_INTERPOLATION, 37 | RenderingHints.VALUE_INTERPOLATION_BILINEAR 38 | ) 39 | g2.drawImage(bufferedImage, 0, 0, width, height, null) 40 | g2.dispose() 41 | 42 | return ImageIcon(resizedImg) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/tree/TextForegroundColor.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.tree 2 | 3 | import java.awt.Color 4 | 5 | class TextForegroundColor( 6 | private val default: Color, 7 | private val selection: Color, 8 | private val hidden: Color, 9 | private val selectedHidden: Color, 10 | private val hovered: Color, 11 | private val selectedHovered: Color, 12 | private val highlighted: Color, 13 | private val selectedHighlighted: Color 14 | ) { 15 | fun textForeground(isSelected: Boolean, isHovered: Boolean, isHighlighted: Boolean, isVisible: Boolean): Color { 16 | /* 17 | if (isHovered) { 18 | if (isSelected) { 19 | return selectedHovered 20 | } 21 | return hovered 22 | } 23 | */ 24 | if (isHighlighted) { 25 | if (isSelected) { 26 | return selectedHighlighted 27 | } 28 | return highlighted 29 | } 30 | 31 | if (!isVisible) { 32 | if (isSelected) { 33 | return selectedHidden 34 | } 35 | return hidden 36 | } 37 | if (isSelected) { 38 | return selection 39 | } 40 | 41 | return default 42 | } 43 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/tree/TreeItem.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.tree 2 | 3 | import com.intellij.ui.JBColor 4 | import java.awt.Color 5 | import javax.swing.ImageIcon 6 | 7 | interface TreeItem { 8 | fun setForeground(textColor1: Color, textColor2: Color) 9 | fun setIcon(newIcon: ImageIcon) 10 | fun setBackgroundSelectionColor(color: Color) 11 | 12 | fun prepareTreeItem( 13 | type: String, description: String = "", 14 | sel: Boolean, 15 | expanded: Boolean, 16 | leaf: Boolean, 17 | hovered: Boolean, 18 | hasFocus: Boolean = false 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/tree/TreeViewNodeMenu.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.tree 2 | 3 | import com.github.grishberg.android.layoutinspector.domain.AbstractViewNode 4 | import com.github.grishberg.android.layoutinspector.domain.MetaRepository 5 | import com.github.grishberg.android.layoutinspector.ui.common.createControlAccelerator 6 | import com.github.grishberg.android.layoutinspector.ui.common.createControlAltAccelerator 7 | import com.github.grishberg.android.layoutinspector.ui.dialogs.bookmarks.Bookmarks 8 | import com.github.grishberg.android.layoutinspector.ui.dialogs.bookmarks.NewBookmarkDialog 9 | import javax.swing.JFrame 10 | import javax.swing.JMenuItem 11 | import javax.swing.JPopupMenu 12 | 13 | typealias CalculateDistanceDelegate = () -> Unit 14 | 15 | class TreeViewNodeMenu( 16 | val owner: JFrame, 17 | val treePanel: TreePanel, 18 | val selectedViewNode: AbstractViewNode, 19 | val meta: MetaRepository, 20 | val bookmarks: Bookmarks, 21 | val calculateDistanceDelegate: CalculateDistanceDelegate? 22 | ) : JPopupMenu() { 23 | private val addToBookmark = JMenuItem("Add to bookmarks") 24 | private val editBookmark = JMenuItem("Edit bookmark") 25 | private val deleteBookmark = JMenuItem("Delete bookmark") 26 | private val calculateDistance = JMenuItem("Calculate distance") 27 | private val hideView = JMenuItem("Hide from layout") 28 | private val removeFromHidden = JMenuItem("Show on layout") 29 | private val copyId = JMenuItem("Copy id").apply { 30 | accelerator = createControlAltAccelerator('C') 31 | } 32 | private val copyShortClassName = JMenuItem("Copy short class name").apply { 33 | accelerator = createControlAccelerator('C') 34 | } 35 | 36 | init { 37 | addToBookmark.addActionListener { 38 | val bookmarksDialog = NewBookmarkDialog(owner, selectedViewNode) 39 | bookmarksDialog.showDialog() 40 | 41 | bookmarksDialog.result?.let { 42 | bookmarks.add(it) 43 | } 44 | } 45 | 46 | val existingBookmarkInfo = bookmarks.getBookmarkInfoForNode(selectedViewNode) 47 | if (existingBookmarkInfo == null) { 48 | add(addToBookmark) 49 | } else { 50 | add(editBookmark) 51 | 52 | editBookmark.addActionListener { 53 | val bookmarksDialog = NewBookmarkDialog(owner, selectedViewNode) 54 | bookmarksDialog.showEditDialog(existingBookmarkInfo) 55 | 56 | bookmarksDialog.result?.let { 57 | bookmarks.edit(existingBookmarkInfo, it) 58 | } 59 | } 60 | 61 | add(deleteBookmark) 62 | deleteBookmark.addActionListener { 63 | bookmarks.remove(existingBookmarkInfo) 64 | } 65 | } 66 | 67 | if (meta.shouldHideInLayout(selectedViewNode)) { 68 | add(removeFromHidden) 69 | removeFromHidden.addActionListener { 70 | meta.removeFromHiddenViews(selectedViewNode) 71 | 72 | } 73 | } else { 74 | add(hideView) 75 | hideView.addActionListener { 76 | meta.addToHiddenViews(selectedViewNode) 77 | } 78 | } 79 | 80 | calculateDistanceDelegate?.let { delegate -> 81 | add(calculateDistance) 82 | calculateDistance.addActionListener { 83 | delegate.invoke() 84 | } 85 | } 86 | 87 | copyId.addActionListener { 88 | treePanel.copyIdToClipboard() 89 | } 90 | copyShortClassName.addActionListener { 91 | treePanel.copyShortNameToClipboard() 92 | } 93 | add(copyId) 94 | add(copyShortClassName) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/layoutinspector/ui/tree/ViewNodeTreeModel.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.ui.tree 2 | 3 | import com.github.grishberg.android.layoutinspector.domain.AbstractViewNode 4 | import javax.swing.event.TreeModelListener 5 | import javax.swing.tree.TreeModel 6 | import javax.swing.tree.TreePath 7 | 8 | class ViewNodeTreeModel(private val viewNode: AbstractViewNode) : TreeModel { 9 | override fun getRoot() = viewNode 10 | 11 | override fun isLeaf(node: Any?): Boolean { 12 | if (node is AbstractViewNode) { 13 | return node.isLeaf 14 | } 15 | return false 16 | } 17 | 18 | override fun getChildCount(parent: Any?): Int { 19 | if (parent is AbstractViewNode) { 20 | return parent.childCount 21 | } 22 | return 0 23 | } 24 | 25 | override fun removeTreeModelListener(l: TreeModelListener?) { 26 | } 27 | 28 | override fun valueForPathChanged(path: TreePath?, newValue: Any?) { 29 | } 30 | 31 | override fun getIndexOfChild(parent: Any?, child: Any?): Int { 32 | if (parent is AbstractViewNode && child is AbstractViewNode) { 33 | return parent.getIndex(child) 34 | } 35 | return -1 36 | } 37 | 38 | override fun getChild(parent: Any?, index: Int): Any { 39 | if (parent is AbstractViewNode) { 40 | return parent.getChildAt(index) 41 | } 42 | throw IllegalStateException("No child at $index from $parent") 43 | } 44 | 45 | override fun addTreeModelListener(l: TreeModelListener?) { 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/li/PluginState.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.li 2 | 3 | import com.github.grishberg.android.layoutinspector.settings.SettingsFacade 4 | 5 | class PluginState : SettingsFacade { 6 | var shouldShowSizeInDp = false 7 | 8 | override var captureLayoutTimeout: Long = 60 9 | 10 | override var clientWindowsTimeout: Long = 60 11 | 12 | override var allowedSelectHiddenView: Boolean = false 13 | 14 | override var lastLayoutDialogPath: String = "" 15 | 16 | override var fileNamePrefix: String = "" 17 | 18 | override var ignoreLastClickedView: Boolean = true 19 | 20 | override var roundDimensions: Boolean = true 21 | 22 | override var lastProcessName: String = "" 23 | 24 | override var lastWindowName: String = "" 25 | 26 | override var lastFilter: String = "" 27 | 28 | override var showSerifsInTheMiddleOfSelected: Boolean = true 29 | 30 | override var showSerifsInTheMiddleAll: Boolean = false 31 | 32 | override var isSecondProtocolVersionEnabled: Boolean = false 33 | 34 | override var lastVersion: String = "" 35 | 36 | override var isDumpViewModeEnabled: Boolean = false 37 | 38 | override fun shouldShowSizeInDp(): Boolean = shouldShowSizeInDp 39 | 40 | override fun showSizeInDp(state: Boolean) { 41 | shouldShowSizeInDp = state 42 | } 43 | 44 | override fun shouldStopAdbAfterJob(): Boolean = false 45 | 46 | override fun setStopAdbAfterJob(selected: Boolean) = Unit 47 | } 48 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/li/ShowLayoutInspectorAction.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.li 2 | 3 | import com.android.ddmlib.IDevice 4 | import com.android.layoutinspector.common.AdbFacade 5 | import com.android.layoutinspector.common.PluginLogger 6 | import com.github.grishberg.android.layoutinspector.process.providers.DeviceProvider 7 | import com.github.grishberg.android.layoutinspector.settings.SettingsFacade 8 | import com.github.grishberg.android.layoutinspector.ui.OpenWindowMode 9 | import com.github.grishberg.android.layoutinspector.ui.WindowsManager 10 | import com.github.grishberg.android.layoutinspector.ui.tree.EmptyTreeIcon 11 | import com.github.grishberg.android.li.ui.NotificationHelperImpl 12 | import com.github.grishberg.androidstudio.plugins.* 13 | import com.intellij.ide.plugins.PluginManagerCore 14 | import com.intellij.openapi.actionSystem.AnActionEvent 15 | import com.intellij.openapi.extensions.PluginId 16 | import com.intellij.openapi.project.Project 17 | import java.io.File 18 | import javax.swing.UIManager 19 | 20 | private const val PLUGIN_DIR = "captures/YALI" 21 | 22 | class ShowLayoutInspectorAction : AsAction() { 23 | private val windowsManager by lazy { WindowsManager(PluginLogger()) } 24 | 25 | override fun actionPerformed(e: AnActionEvent, project: Project) { 26 | val settings = StorageService.getInstance().state ?: PluginState() 27 | val adbProvider = object : AdbProvider { 28 | override fun getAdb(): AdbWrapper { 29 | return AdbWrapperImpl(project) 30 | } 31 | } 32 | val notificationHelper = NotificationHelperImpl(project) 33 | val provider = ConnectedDeviceInfoProvider(adbProvider, notificationHelper) 34 | 35 | createUi() 36 | 37 | val main = windowsManager.createWindow( 38 | OpenWindowMode.DEFAULT, 39 | settings, 40 | DeviceProviderImpl(provider), 41 | AdbFacadeImpl(provider, adbProvider.getAdb()), 42 | prepareBaseDir(project) 43 | ) 44 | main.initUi() 45 | 46 | showSupportBannerIfNeeded(settings, notificationHelper) 47 | } 48 | 49 | private fun showSupportBannerIfNeeded(settings: SettingsFacade, notificationHelper: NotificationHelper) { 50 | val plugin = 51 | PluginManagerCore.getPlugin(PluginId.getId("com.github.grishberg.android.android-layout-inspector-plugin")) 52 | ?: return 53 | if (plugin.version != settings.lastVersion) { 54 | settings.lastVersion = plugin.version 55 | notificationHelper.supportInfo( 56 | "Support me if you like YALI =)", 57 | "BNB,ETH tokens : 0x25Ca16AD3c4e9BD1e6e5FDD77eDB019386B68591\n\n" + 58 | "USDT TRC20 : TSo3X6K54nYq3S64wML4M4xFgTNiENkHwC\n\n" + 59 | "BTC : bc1qmm5lp389scuk2hghgyzdztddwgjnxqa2awrrue\n\n" + 60 | "https://www.tinkoff.ru/cf/4KNjR2SMOAj" 61 | ) 62 | } 63 | } 64 | 65 | private fun prepareBaseDir(project: Project): File { 66 | val baseDir = if (project.basePath != null) 67 | File(project.basePath, PLUGIN_DIR) 68 | else 69 | File(PLUGIN_DIR) 70 | 71 | if (!baseDir.exists()) { 72 | baseDir.mkdirs() 73 | } 74 | 75 | return baseDir 76 | } 77 | 78 | private fun createUi() { 79 | val emptyIcon = EmptyTreeIcon() 80 | UIManager.put("Tree.leafIcon", emptyIcon) 81 | } 82 | 83 | private class DeviceProviderImpl( 84 | private val provider: ConnectedDeviceInfoProvider 85 | ) : DeviceProvider { 86 | override val isReconnectionAllowed: Boolean 87 | get() = false 88 | 89 | override val deviceChangedActions: MutableSet 90 | get() = mutableSetOf() 91 | 92 | override fun reconnect() = Unit 93 | 94 | override suspend fun requestDevices(): List { 95 | val devicesInfo = provider.provideDeviceInfo() ?: return emptyList() 96 | return devicesInfo.devices 97 | } 98 | 99 | override fun stop() = Unit 100 | } 101 | 102 | private class AdbFacadeImpl( 103 | private val provider: ConnectedDeviceInfoProvider, 104 | private val adb: AdbWrapper, 105 | ) : AdbFacade { 106 | override fun connect() = Unit 107 | 108 | override fun connect(remoterAddress: String) = Unit 109 | 110 | override fun getDevices(): List { 111 | val devicesInfo = provider.provideDeviceInfo() ?: return emptyList() 112 | return devicesInfo.devices 113 | } 114 | 115 | override fun hasInitialDeviceList(): Boolean = true 116 | 117 | override fun isConnected(): Boolean = adb.isReady() 118 | 119 | override fun stop() = Unit 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/li/StorageService.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.li 2 | 3 | import com.intellij.openapi.application.ApplicationManager 4 | import com.intellij.openapi.components.PersistentStateComponent 5 | import com.intellij.openapi.components.State 6 | import com.intellij.openapi.components.Storage 7 | 8 | @State( 9 | name = "PluginSettingsState", 10 | storages = [Storage("yali-settings.xml")] 11 | ) 12 | class StorageService : PersistentStateComponent { 13 | private var storage = PluginState() 14 | 15 | override fun getState(): PluginState = storage 16 | 17 | override fun loadState(newStorage: PluginState) { 18 | storage = newStorage 19 | } 20 | 21 | companion object { 22 | @JvmStatic 23 | fun getInstance(): PersistentStateComponent { 24 | return ApplicationManager.getApplication().getService(StorageService::class.java) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/android/li/ui/NotificationHelperImpl.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.li.ui 2 | 3 | import com.github.grishberg.androidstudio.plugins.NotificationHelper 4 | import com.intellij.notification.NotificationDisplayType 5 | import com.intellij.notification.NotificationGroup 6 | import com.intellij.notification.NotificationGroupManager 7 | import com.intellij.notification.NotificationType 8 | import com.intellij.openapi.project.Project 9 | 10 | class NotificationHelperImpl(private val project: Project) : NotificationHelper { 11 | 12 | override fun info(message: String) { 13 | NotificationGroupManager.getInstance() 14 | .getNotificationGroup("Notification Group") 15 | .createNotification(escapeString(message), NotificationType.INFORMATION) 16 | .notify(project) 17 | } 18 | 19 | override fun info(title: String, message: String) { 20 | NotificationGroupManager.getInstance() 21 | .getNotificationGroup("Notification Group") 22 | .createNotification(title, escapeString(message), NotificationType.INFORMATION) 23 | .notify(project) 24 | } 25 | 26 | override fun supportInfo(title: String, message: String) { 27 | NotificationGroupManager.getInstance() 28 | .getNotificationGroup("Support YALI Notification") 29 | .createNotification(title, escapeString(message), NotificationType.INFORMATION) 30 | .notify(project) 31 | } 32 | 33 | override fun error(message: String) { 34 | NotificationGroupManager.getInstance() 35 | .getNotificationGroup("Error Notification Group") 36 | .createNotification(escapeString(message), NotificationType.ERROR) 37 | .notify(project) 38 | } 39 | 40 | private fun escapeString(string: String) = string.replace("\n".toRegex(), "\n
") 41 | } 42 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/androidstudio/plugins/AdbWrapper.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.androidstudio.plugins 2 | 3 | import com.android.ddmlib.IDevice 4 | import com.intellij.openapi.project.Project 5 | import org.jetbrains.android.sdk.AndroidSdkUtils 6 | 7 | interface AdbWrapper { 8 | fun isReady(): Boolean 9 | fun connectedDevices(): List 10 | } 11 | 12 | class AdbWrapperImpl(project: Project) : AdbWrapper { 13 | val androidBridge = AndroidSdkUtils.getDebugBridge(project) 14 | 15 | override fun isReady(): Boolean { 16 | if (androidBridge == null) { 17 | return false 18 | } 19 | 20 | return androidBridge.isConnected && androidBridge.hasInitialDeviceList() 21 | } 22 | 23 | override fun connectedDevices(): List { 24 | return androidBridge?.devices?.asList() ?: emptyList() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/androidstudio/plugins/AsAction.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.androidstudio.plugins 2 | 3 | import com.intellij.openapi.actionSystem.AnAction 4 | import com.intellij.openapi.actionSystem.AnActionEvent 5 | import com.intellij.openapi.actionSystem.PlatformDataKeys 6 | import com.intellij.openapi.project.Project 7 | 8 | abstract class AsAction : AnAction() { 9 | override fun actionPerformed(e: AnActionEvent) { 10 | actionPerformed(e, e.getData(PlatformDataKeys.PROJECT)!!) 11 | } 12 | 13 | abstract fun actionPerformed(e: AnActionEvent, project: Project) 14 | } 15 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/androidstudio/plugins/ConnectedDeviceInfoProvider.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.androidstudio.plugins 2 | 3 | import com.android.ddmlib.IDevice 4 | 5 | data class ConnectedDeviceInfo( 6 | val devices: List 7 | ) 8 | 9 | interface AdbProvider { 10 | fun getAdb(): AdbWrapper 11 | } 12 | 13 | class ConnectedDeviceInfoProvider( 14 | private val adbProvider: AdbProvider, 15 | private val notificationHelper: NotificationHelper 16 | ) { 17 | fun provideDeviceInfo(): ConnectedDeviceInfo? { 18 | val adb = adbProvider.getAdb() 19 | 20 | if (!adb.isReady()) { 21 | notificationHelper.error("No platform configured") 22 | return null 23 | } 24 | 25 | val devices = adb.connectedDevices() 26 | if (devices.isEmpty()) { 27 | notificationHelper.error("No devices found") 28 | } 29 | return ConnectedDeviceInfo(devices) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/grishberg/androidstudio/plugins/NotificationHelper.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.androidstudio.plugins 2 | 3 | interface NotificationHelper { 4 | fun info(message: String) 5 | fun info(title: String, message: String) 6 | fun supportInfo(title: String, message: String) 7 | 8 | fun error(message: String) 9 | } 10 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | com.github.grishberg.android.android-layout-inspector-plugin 3 | YALI 4 | Grigory Rylov 5 | 6 | 8 | This is analog of Android Studio Layout Manager but with ability to switch size to DP
9 | Also you can mark some elements in tree with bookmarks
10 |
11 | There are two basic ways to open YALI: 12 |
    13 |
  • Through the Tools->Launch YALI menu
  • 14 |
  • By searching for "Launch YALI" in "Find Actions" (osx: cmd+shift+a, windows/linux: ctrl+shift+a)
  • 15 |
16 | 17 |
18 | ]]>
19 | 20 | 21 | 22 | 23 | 24 | 26 | org.jetbrains.android 27 | com.intellij.modules.androidstudio 28 | com.intellij.modules.platform 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 41 | 42 | 46 | 47 | 48 | 49 | 50 |
51 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/pluginIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/main/resources/icons/caliper.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /src/main/resources/icons/dark/appbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/dark/appbar.png -------------------------------------------------------------------------------- /src/main/resources/icons/dark/cardView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/dark/cardView.png -------------------------------------------------------------------------------- /src/main/resources/icons/dark/constraint_layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/dark/constraint_layout.png -------------------------------------------------------------------------------- /src/main/resources/icons/dark/contentCardView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/dark/contentCardView.png -------------------------------------------------------------------------------- /src/main/resources/icons/dark/coordinator_layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/dark/coordinator_layout.png -------------------------------------------------------------------------------- /src/main/resources/icons/dark/fab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/dark/fab.png -------------------------------------------------------------------------------- /src/main/resources/icons/dark/fitscreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/dark/fitscreen.png -------------------------------------------------------------------------------- /src/main/resources/icons/dark/fitscreen.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 8 | 11 | 13 | 16 | 18 | 20 | 21 | -------------------------------------------------------------------------------- /src/main/resources/icons/dark/frame_layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/dark/frame_layout.png -------------------------------------------------------------------------------- /src/main/resources/icons/dark/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/dark/help.png -------------------------------------------------------------------------------- /src/main/resources/icons/dark/help.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/main/resources/icons/dark/imageView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/dark/imageView.png -------------------------------------------------------------------------------- /src/main/resources/icons/dark/linear_layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/dark/linear_layout.png -------------------------------------------------------------------------------- /src/main/resources/icons/dark/nestedScrollView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/dark/nestedScrollView.png -------------------------------------------------------------------------------- /src/main/resources/icons/dark/recyclerView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/dark/recyclerView.png -------------------------------------------------------------------------------- /src/main/resources/icons/dark/relativeLayout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/dark/relativeLayout.png -------------------------------------------------------------------------------- /src/main/resources/icons/dark/resetzoom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/dark/resetzoom.png -------------------------------------------------------------------------------- /src/main/resources/icons/dark/resetzoom.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/resources/icons/dark/text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/dark/text.png -------------------------------------------------------------------------------- /src/main/resources/icons/dark/toolbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/dark/toolbar.png -------------------------------------------------------------------------------- /src/main/resources/icons/dark/view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/dark/view.png -------------------------------------------------------------------------------- /src/main/resources/icons/dark/viewPager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/dark/viewPager.png -------------------------------------------------------------------------------- /src/main/resources/icons/dark/viewSwitcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/dark/viewSwitcher.png -------------------------------------------------------------------------------- /src/main/resources/icons/dark/viewstub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/dark/viewstub.png -------------------------------------------------------------------------------- /src/main/resources/icons/light/appbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/light/appbar.png -------------------------------------------------------------------------------- /src/main/resources/icons/light/cardView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/light/cardView.png -------------------------------------------------------------------------------- /src/main/resources/icons/light/constraint_layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/light/constraint_layout.png -------------------------------------------------------------------------------- /src/main/resources/icons/light/contentCardView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/light/contentCardView.png -------------------------------------------------------------------------------- /src/main/resources/icons/light/coordinator_layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/light/coordinator_layout.png -------------------------------------------------------------------------------- /src/main/resources/icons/light/fab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/light/fab.png -------------------------------------------------------------------------------- /src/main/resources/icons/light/fitscreen.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/main/resources/icons/light/frame_layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/light/frame_layout.png -------------------------------------------------------------------------------- /src/main/resources/icons/light/help.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/main/resources/icons/light/imageView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/light/imageView.png -------------------------------------------------------------------------------- /src/main/resources/icons/light/linear_layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/light/linear_layout.png -------------------------------------------------------------------------------- /src/main/resources/icons/light/nestedScrollView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/light/nestedScrollView.png -------------------------------------------------------------------------------- /src/main/resources/icons/light/recyclerView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/light/recyclerView.png -------------------------------------------------------------------------------- /src/main/resources/icons/light/relativeLayout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/light/relativeLayout.png -------------------------------------------------------------------------------- /src/main/resources/icons/light/resetzoom.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/resources/icons/light/text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/light/text.png -------------------------------------------------------------------------------- /src/main/resources/icons/light/toolbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/light/toolbar.png -------------------------------------------------------------------------------- /src/main/resources/icons/light/view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/light/view.png -------------------------------------------------------------------------------- /src/main/resources/icons/light/viewPager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/light/viewPager.png -------------------------------------------------------------------------------- /src/main/resources/icons/light/viewSwitcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/light/viewSwitcher.png -------------------------------------------------------------------------------- /src/main/resources/icons/light/viewstub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/light/viewstub.png -------------------------------------------------------------------------------- /src/main/resources/icons/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grigory-Rylov/android-layout-inspector/27355093cc189d728a40b10ac809edcf14763442/src/main/resources/icons/loading.gif -------------------------------------------------------------------------------- /src/test/java/com/github/grishberg/android/layoutinspector/process/HierarchyDumpParserTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.process 2 | 3 | import java.io.BufferedReader 4 | import java.io.File 5 | import junit.framework.TestCase.assertEquals 6 | import junit.framework.TestCase.assertNotNull 7 | import org.junit.Test 8 | 9 | class HierarchyDumpParserTest { 10 | 11 | 12 | private val underTest = HierarchyDumpParser() 13 | 14 | @Test 15 | fun test() { 16 | val result = underTest.parseDump(readFile(getDumpFile())) 17 | 18 | assertNotNull(result) 19 | 20 | assertEquals(0, result!!.locationOnScreenX) 21 | assertEquals(0, result.locationOnScreenY) 22 | assertEquals(1080, result.width) 23 | assertEquals(1920, result.height) 24 | 25 | val firstChild = result.children.first() 26 | assertNotNull(firstChild) 27 | 28 | val content = firstChild.children.first() 29 | assertNotNull(content) 30 | 31 | assertEquals("android:id/content", content.id) 32 | assertEquals(0, content.locationOnScreenX) 33 | assertEquals(1080, content.width) 34 | assertEquals(63, content.locationOnScreenY) 35 | assertEquals(1794, content.height) 36 | 37 | val frameLayout = content.children.first() 38 | assertNotNull(frameLayout) 39 | assertEquals("android.widget.FrameLayout", frameLayout.name) 40 | } 41 | 42 | private fun readFile(file: File): String { 43 | val bufferedReader: BufferedReader = file.bufferedReader() 44 | return bufferedReader.use { it.readText() } 45 | } 46 | 47 | private fun getDumpFile(): File { 48 | var classLoader = javaClass.classLoader 49 | val filePath = classLoader.getResource("dump.xml")?.file ?: throw IllegalStateException() 50 | return File(filePath) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/test/java/com/github/grishberg/android/layoutinspector/process/TreeMergerTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.grishberg.android.layoutinspector.process 2 | 3 | import com.android.layoutinspector.common.AppLogger 4 | import com.android.layoutinspector.model.ViewNode 5 | import com.github.grishberg.android.layoutinspector.domain.DumpViewNode 6 | import junit.framework.TestCase.assertEquals 7 | import junit.framework.TestCase.assertTrue 8 | import org.junit.Test 9 | 10 | private const val COMPOSE_FULL_NAME = "androidx.compose.ui.platform.ComposeView" 11 | private const val VIEW_ROOT_FULL_NAME = "com.android.internal.policy.DecorView" 12 | private const val LL_FULL_NAME = "android.widget.LinearLayout" 13 | private const val FL_FULL_NAME = "android.widget.FrameLayout" 14 | private const val TARGET_CHILD_NAME = "TargetChild1" 15 | 16 | class TreeMergerTest { 17 | 18 | private val underTest = TreeMerger(LoggerStub()) 19 | 20 | @Test 21 | fun mergeSeveralCompose() { 22 | val viewNodes = createViewNodes() 23 | val dumpNodes = createDumpNodes() 24 | 25 | val result = underTest.mergeNodes(viewNodes, dumpNodes) 26 | 27 | val root = result 28 | val llNode = root.children.first() 29 | 30 | assertEquals(LL_FULL_NAME, llNode.name) 31 | 32 | val flNode = llNode.children.first() 33 | assertEquals(FL_FULL_NAME, flNode.name) 34 | assertEquals("id/content", flNode.id) 35 | 36 | val flNode2 = flNode.children.first() 37 | assertEquals(FL_FULL_NAME, flNode2.name) 38 | 39 | val controlsView = flNode2.children[1] 40 | assertEquals("id/controls", controlsView.id) 41 | 42 | assertTrue(controlsView.children.isNotEmpty()) 43 | val targetChild = controlsView.children.first() 44 | assertEquals(TARGET_CHILD_NAME, targetChild.name) 45 | } 46 | 47 | @Test 48 | fun `merge when dump has no current ComposeView`() { 49 | val viewNodes = createViewNodes() 50 | val dumpNodes = createDumpNodesWithWrongPath() 51 | 52 | val result = underTest.mergeNodes(viewNodes, dumpNodes) 53 | 54 | val root = result 55 | val llNode = root.children.first() 56 | 57 | assertEquals(LL_FULL_NAME, llNode.name) 58 | 59 | val flNode = llNode.children.first() 60 | assertEquals(FL_FULL_NAME, flNode.name) 61 | assertEquals("id/content", flNode.id) 62 | 63 | val flNode2 = flNode.children.first() 64 | assertEquals(FL_FULL_NAME, flNode2.name) 65 | 66 | val controlsView = flNode2.children[1] 67 | assertEquals("id/controls", controlsView.id) 68 | 69 | assertTrue(controlsView.children.isEmpty()) 70 | } 71 | 72 | private fun createViewNodes(): ViewNode { 73 | val root = ViewNode(null, VIEW_ROOT_FULL_NAME, "hash_root") 74 | 75 | val llNode = addChild(root, LL_FULL_NAME) 76 | 77 | val flNode = addChild(llNode, FL_FULL_NAME, "id/content") 78 | 79 | val flNode2 = addChild(flNode, FL_FULL_NAME) 80 | 81 | val contentView = addChild(flNode2, "com.github.grishberg.painting.pixels.painting.ZoomableView", "id/content") 82 | 83 | val controlsView = addChild(flNode2, COMPOSE_FULL_NAME, "id/controls") 84 | 85 | return root 86 | } 87 | 88 | private fun addChild(root: ViewNode, className: String, id: String? = "NO_ID"): ViewNode { 89 | val child = ViewNode(root, className, "1234") 90 | child.id = id 91 | root.children.add(child) 92 | return child 93 | } 94 | 95 | private fun createDumpNodes(): DumpViewNode { 96 | val root = DumpViewNode(null, "", FL_FULL_NAME, null, 0, 0, 0, 0, null) 97 | 98 | val llNode = addChild(root, "android.widget.LinearLayout") 99 | 100 | val flNode = addChild(llNode, FL_FULL_NAME, "id/content") 101 | 102 | val flNode2 = addChild(flNode, FL_FULL_NAME) 103 | 104 | val controlsView = addChild(flNode2, COMPOSE_FULL_NAME, "id/controls") 105 | 106 | val contentView = addChild(flNode2, "android.view.View", "id/content") 107 | 108 | addChild(controlsView, TARGET_CHILD_NAME) 109 | 110 | return root 111 | } 112 | 113 | private fun createDumpNodesWithWrongPath(): DumpViewNode { 114 | val root = DumpViewNode(null, "", FL_FULL_NAME, null, 0, 0, 0, 0, null) 115 | 116 | val llNode = addChild(root, LL_FULL_NAME) 117 | 118 | val flNode = addChild(llNode, FL_FULL_NAME, "id/screen") 119 | 120 | val flNode2 = addChild(flNode, FL_FULL_NAME) 121 | 122 | val controlsView = addChild(flNode2, COMPOSE_FULL_NAME, "id/controls") 123 | 124 | val contentView = addChild(flNode2, "android.view.View", "id/content") 125 | 126 | addChild(controlsView, TARGET_CHILD_NAME) 127 | 128 | return root 129 | } 130 | 131 | private fun addChild(root: DumpViewNode, className: String, id: String? = null): DumpViewNode { 132 | val child = DumpViewNode(root, "pkg", className, id, 0, 0, 0, 0, null) 133 | root.addChild(child) 134 | return child 135 | } 136 | 137 | private class LoggerStub : AppLogger { 138 | 139 | override fun d(msg: String) = Unit 140 | 141 | override fun e(msg: String) = Unit 142 | 143 | override fun e(msg: String, t: Throwable) = Unit 144 | 145 | override fun w(msg: String) = Unit 146 | } 147 | 148 | } 149 | --------------------------------------------------------------------------------