Skip to content

Commit 9cc1e9a

Browse files
committed
search bar in resources
1 parent 899081d commit 9cc1e9a

8 files changed

Lines changed: 146 additions & 40 deletions

File tree

composeApp/src/commonMain/composeResources/values/strings.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,6 @@
115115
<string name="lookup_popup_current_class">This is the current class.</string>
116116
<string name="lookup_popup_runtime">Likely an Android or Java runtime class.</string>
117117

118+
<string name="resources_search_placeholder">Filter by id or name</string>
119+
118120
</resources>

composeApp/src/commonMain/kotlin/me/lkl/dalvikus/tree/Search.kt renamed to composeApp/src/commonMain/kotlin/me/lkl/dalvikus/tree/TreeSearch.kt

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,8 @@ import kotlinx.coroutines.flow.flow
1717
import me.lkl.dalvikus.icons.FamilyHistory
1818
import me.lkl.dalvikus.icons.ThreadUnread
1919
import me.lkl.dalvikus.tree.dex.DexEntryClassNode
20-
21-
data class SearchOptions(
22-
val caseSensitive: Boolean = false,
23-
val useRegex: Boolean = false
24-
)
20+
import me.lkl.dalvikus.util.SearchOptions
21+
import me.lkl.dalvikus.util.createSearchMatcher
2522

2623
enum class TreeSearchResultType {
2724
TREE_NODE,
@@ -53,22 +50,7 @@ fun searchTreeBFS(
5350
maxResults: Int = 500
5451
): Flow<TreeSearchResult> = flow {
5552
// TODO search for resources by resource name, not by literal
56-
val matcher: (String) -> Boolean = if (options.useRegex) {
57-
// Compile regex with case option
58-
val regex = try {
59-
Regex(
60-
query,
61-
if (options.caseSensitive) setOf() else setOf(RegexOption.IGNORE_CASE)
62-
)
63-
} catch (_: Exception) {
64-
return@flow // Invalid regex: skip search
65-
}
66-
{ input -> regex.containsMatchIn(input) }
67-
} else {
68-
{ input ->
69-
input.contains(query, ignoreCase = !options.caseSensitive)
70-
}
71-
}
53+
val matcher: (String) -> Boolean = createSearchMatcher(query, options) ?: return@flow
7254

7355
val queue = ArrayDeque<Node>()
7456
queue.add(root)
@@ -94,7 +76,6 @@ fun searchTreeBFS(
9476
}
9577
}
9678

97-
// Search method references
9879
classDef.methods.forEach { method ->
9980
method.implementation?.instructions?.forEach { instruction ->
10081
if (instruction is ReferenceInstruction) {
@@ -115,9 +96,10 @@ fun searchTreeBFS(
11596
}
11697
} else if (instruction is WideLiteralInstruction) {
11798
val value = instruction.wideLiteral.toString()
118-
val hexValue = "0x${instruction.wideLiteral.toString(16).lowercase()}"
99+
val sign = if (value.startsWith("-")) "-" else ""
100+
val hexValue = "${sign}0x${instruction.wideLiteral.toString(16).removePrefix(sign)}"
119101
if (matcher(value) || matcher(hexValue)) {
120-
emit(TreeSearchResult(current, "$value ($hexValue)", TreeSearchResultType.LITERAL))
102+
emit(TreeSearchResult(current, "$value (dec) / $hexValue (hex)", TreeSearchResultType.LITERAL))
121103
resultsFound++
122104
}
123105
}
@@ -132,6 +114,7 @@ fun searchTreeBFS(
132114
}
133115
}
134116

117+
135118
fun ClassDef.getStringPool(): List<String> {
136119
val stringPool = mutableSetOf<String>()
137120

composeApp/src/commonMain/kotlin/me/lkl/dalvikus/ui/editor/suggestions/Popups.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ fun HexPopup(
130130

131131
when (cleanedHex.length) {
132132
8 -> {
133-
if(cleanedHex.startsWith("7e") || cleanedHex.startsWith("7f")) {
133+
if(!isNegative && (cleanedHex.startsWith("7e") || cleanedHex.startsWith("7f"))) {
134134
"$cleanedHex (resource ID) = ${viewModel.tryResolveResIdText(unsignedValue)} (resolved) "
135135
} else {
136136
val floatValue = Float.fromBits(signedValue.toInt())

composeApp/src/commonMain/kotlin/me/lkl/dalvikus/ui/resources/ResourcesView.kt

Lines changed: 90 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,15 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
88
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
99
import androidx.compose.foundation.lazy.rememberLazyListState
1010
import androidx.compose.foundation.rememberScrollbarAdapter
11+
import androidx.compose.foundation.shape.RoundedCornerShape
12+
import androidx.compose.foundation.text.input.TextFieldState
13+
import androidx.compose.foundation.text.input.rememberTextFieldState
14+
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
1115
import androidx.compose.foundation.text.selection.SelectionContainer
1216
import androidx.compose.material.icons.Icons
17+
import androidx.compose.material.icons.filled.Clear
1318
import androidx.compose.material.icons.filled.QuestionMark
19+
import androidx.compose.material.icons.filled.Search
1420
import androidx.compose.material.icons.outlined.*
1521
import androidx.compose.material3.*
1622
import androidx.compose.runtime.*
@@ -19,28 +25,91 @@ import androidx.compose.ui.Modifier
1925
import androidx.compose.ui.graphics.Color
2026
import androidx.compose.ui.text.style.TextOverflow
2127
import androidx.compose.ui.unit.dp
28+
import dalvikus.composeapp.generated.resources.Res
29+
import dalvikus.composeapp.generated.resources.all_elements
30+
import dalvikus.composeapp.generated.resources.resources_search_placeholder
31+
import io.github.composegears.valkyrie.MatchCase
32+
import io.github.composegears.valkyrie.RegularExpression
2233
import me.lkl.dalvikus.tree.archive.ApkNode
2334
import me.lkl.dalvikus.ui.uiTreeRoot
2435
import me.lkl.dalvikus.util.CollapseCard
36+
import me.lkl.dalvikus.util.SearchOptions
37+
import me.lkl.dalvikus.util.createSearchMatcher
38+
import me.lkl.dalvikus.util.to0xHex
39+
import org.jetbrains.compose.resources.stringResource
2540

41+
@OptIn(ExperimentalMaterial3Api::class)
2642
@Composable
2743
fun ResourcesView() {
2844
val scope = rememberCoroutineScope()
45+
val searchBarState = rememberSearchBarState()
46+
val searchFieldState = rememberTextFieldState()
47+
var searchOptions by remember { mutableStateOf(SearchOptions()) }
48+
49+
val searchField =
50+
@Composable {
51+
SearchBarDefaults.InputField(
52+
searchBarState = searchBarState,
53+
textFieldState = searchFieldState,
54+
onSearch = {},
55+
placeholder = {
56+
Text(
57+
stringResource(Res.string.resources_search_placeholder),
58+
maxLines = 1,
59+
overflow = TextOverflow.Ellipsis
60+
)
61+
},
62+
leadingIcon = {
63+
Icon(Icons.Default.Search, contentDescription = null)
64+
},
65+
trailingIcon = {
66+
Row {
67+
if (searchFieldState.text.isNotEmpty()) {
68+
IconToggleButton(
69+
checked = searchOptions.useRegex,
70+
onCheckedChange = { searchOptions = searchOptions.copy(useRegex = it) }) {
71+
Icon(Icons.Filled.RegularExpression, contentDescription = "Regular expression")
72+
}
73+
IconToggleButton(
74+
checked = searchOptions.caseSensitive,
75+
onCheckedChange = { searchOptions = searchOptions.copy(caseSensitive = it) }) {
76+
Icon(Icons.Filled.MatchCase, contentDescription = "Case sensitive")
77+
}
78+
IconButton(onClick = {
79+
searchFieldState.setTextAndPlaceCursorAtEnd("")
80+
}) {
81+
Icon(Icons.Default.Clear, contentDescription = "Clear")
82+
}
83+
}
84+
}
85+
}
86+
)
87+
}
2988

3089
Scaffold(
31-
containerColor = Color.Transparent
90+
containerColor = Color.Transparent,
91+
topBar = {
92+
TopSearchBar(
93+
state = searchBarState,
94+
inputField = searchField,
95+
shape = RoundedCornerShape(16.dp),
96+
tonalElevation = 4.dp,
97+
shadowElevation = 0.dp,
98+
windowInsets = SearchBarDefaults.windowInsets,
99+
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
100+
scrollBehavior = null
101+
)
102+
}
32103
) { innerPadding ->
33104
Column(
34105
modifier = Modifier.fillMaxSize().padding(innerPadding)
35106
) {
36-
ApkResourceCards()
107+
ApkResourceCards(searchFieldState, searchOptions)
37108
}
38109
}
39110
}
40111

41112
val resourceTypesIcons = mapOf(
42-
"all" to Icons.Outlined.Android,
43-
44113
"anim" to Icons.Outlined.PlayArrow,
45114
"animator" to Icons.Outlined.Movie,
46115
"attr" to Icons.Outlined.Tune,
@@ -63,22 +132,31 @@ val resourceTypesIcons = mapOf(
63132
)
64133

65134
@Composable
66-
private fun ApkResourceCards() {
67-
// TODO add search bar for resource ids in base 16, base 10, and resource name.
135+
private fun ApkResourceCards(searchFieldState: TextFieldState, searchOptions: SearchOptions) {
68136
val treeRootChildren by uiTreeRoot.childrenFlow.collectAsState()
69137
val apks = treeRootChildren.filterIsInstance<ApkNode>()
70138

71139
val gridState = rememberLazyGridState()
72140
var selectedResType by remember { mutableStateOf("all") }
73141

142+
val searchMatcher by remember(searchFieldState.text, searchOptions) {
143+
derivedStateOf { createSearchMatcher(searchFieldState.text.toString().replace(" ", ""), searchOptions) }
144+
}
145+
74146
Column(modifier = Modifier.fillMaxSize()) {
75-
// Filter chips row
76147
FlowRow(
77148
modifier = Modifier
78149
.fillMaxWidth()
79150
.padding(8.dp),
80151
horizontalArrangement = Arrangement.spacedBy(8.dp)
81152
) {
153+
FilterChip(
154+
// TODO remove elevation = null when https://youtrack.jetbrains.com/issue/CMP-2868 is fixed.
155+
elevation = null,
156+
selected = selectedResType == "all",
157+
onClick = { selectedResType = "all" },
158+
label = { Text(stringResource(Res.string.all_elements)) }
159+
)
82160
resourceTypesIcons.forEach { (type, icon) ->
83161
FilterChip(
84162
// TODO remove elevation = null when https://youtrack.jetbrains.com/issue/CMP-2868 is fixed.
@@ -118,6 +196,8 @@ private fun ApkResourceCards() {
118196
if (resSpecList == null) return@CollapseCard
119197
val resourceSpecs = resSpecList.filter { resourceSpec ->
120198
resourceSpec != null && (selectedResType == "all" || resourceSpec.type.name == selectedResType)
199+
&& (searchMatcher == null || searchMatcher!!(resourceSpec.name)
200+
|| searchMatcher!!(resourceSpec.id.toLong().toString()) || searchMatcher!!(resourceSpec.id.toLong().to0xHex()))
121201
}
122202

123203
val innerListState = rememberLazyListState()
@@ -134,9 +214,9 @@ private fun ApkResourceCards() {
134214
items(resourceSpecs.size) { index ->
135215
val resourceSpec = resourceSpecs[index]
136216
val resId = resourceSpec!!.id
137-
val resIdPkg = String.format("%02X", resId.packageId)
138-
val resIdTypeId = String.format("%02X", resId.type)
139-
val resIdEntryNumber = String.format("%04X", resId.entry)
217+
val resIdPkg = String.format("%02x", resId.packageId)
218+
val resIdTypeId = String.format("%02x", resId.type)
219+
val resIdEntryNumber = String.format("%04x", resId.entry)
140220

141221
ListItem(
142222
headlineContent = {

composeApp/src/commonMain/kotlin/me/lkl/dalvikus/ui/tabs/WelcomeView.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ fun WelcomeView() {
3939
Box(
4040
modifier = Modifier
4141
.fillMaxSize()
42-
.background(MaterialTheme.colorScheme.surfaceContainer)
42+
.background(MaterialTheme.colorScheme.surfaceContainerLow)
4343
.padding(24.dp),
4444
contentAlignment = Alignment.Center
4545
) {
@@ -79,7 +79,7 @@ fun WelcomeView() {
7979
modifier = Modifier.padding(horizontal = 8.dp)
8080
)
8181

82-
Row(
82+
FlowRow(
8383
horizontalArrangement = Arrangement.spacedBy(12.dp),
8484
modifier = Modifier.padding(top = 8.dp)
8585
) {
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package me.lkl.dalvikus.util
2+
3+
import org.stringtemplate.v4.ST
4+
5+
fun String.is0xHex(): Boolean {
6+
return this.startsWith("0x") && this.drop(2).all { it.isDigit() || (it in 'a'..'f') || (it in 'A'..'F') }
7+
}
8+
9+
fun Long.to0xHex(): String {
10+
val sign = if (this < 0) "-" else ""
11+
return "${sign}0x${this.toString(16).lowercase()}"
12+
}
13+
14+
fun String.base10To0xOrNull(): String? {
15+
return this.toLongOrNull()?.to0xHex()
16+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package me.lkl.dalvikus.util
2+
3+
data class SearchOptions(
4+
val caseSensitive: Boolean = false,
5+
val useRegex: Boolean = false
6+
)
7+
8+
fun createSearchMatcher(query: String, options: SearchOptions): ((String) -> Boolean)? {
9+
return if (options.useRegex) {
10+
try {
11+
val regex = if (options.caseSensitive) {
12+
Regex(query)
13+
} else {
14+
Regex(query, RegexOption.IGNORE_CASE)
15+
}
16+
{ input -> regex.containsMatchIn(input) }
17+
} catch (e: Exception) {
18+
null // Invalid regex
19+
}
20+
} else {
21+
{ input -> input.contains(query, ignoreCase = !options.caseSensitive) }
22+
}
23+
}

composeApp/src/jvmMain/kotlin/me/lkl/dalvikus/lexer/JavaLexerHighlight.jvm.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import androidx.compose.ui.text.font.FontStyle
1111
import androidx.compose.ui.text.font.FontWeight
1212
import androidx.compose.ui.text.withStyle
1313
import me.lkl.dalvikus.ui.editor.highlight.CodeHighlightColors
14+
import me.lkl.dalvikus.util.base10To0xOrNull
15+
import me.lkl.dalvikus.util.is0xHex
1416

1517
actual fun highlightJavaCode(code: String, colors: CodeHighlightColors): AnnotatedString {
1618
val lexer = Java20Lexer(CharStreams.fromString(code))
@@ -29,11 +31,11 @@ actual fun highlightJavaCode(code: String, colors: CodeHighlightColors): Annotat
2931
val end = token.stopIndex + 1
3032

3133
if (token.type == Java20Lexer.IntegerLiteral) {
32-
if (token.text.startsWith("0x") || token.text.startsWith("-0x")) {
34+
if (token.text.is0xHex()) {
3335
addStringAnnotation("hex", token.text, start, end)
3436
} else {
35-
token.text.toLongOrNull()?.let {
36-
addStringAnnotation("hex", "0x${it.toString(16).uppercase()}", start, end)
37+
token.text.base10To0xOrNull()?.let {
38+
addStringAnnotation("hex", it, start, end)
3739
}
3840
}
3941
}

0 commit comments

Comments
 (0)