├── .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 | 
7 |
8 | 
9 |
10 | ## Download
11 | 
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 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/src/main/resources/META-INF/pluginIcon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/main/resources/icons/caliper.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------