Skip to content

Commit 0b555f2

Browse files
committed
Add Replace option
1 parent fb96558 commit 0b555f2

3 files changed

Lines changed: 252 additions & 91 deletions

File tree

ComposeTextEditorFind/src/commonMain/kotlin/com/darkrockstudios/texteditor/find/FindBar.kt

Lines changed: 175 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.darkrockstudios.texteditor.find
22

3+
import androidx.compose.animation.AnimatedVisibility
34
import androidx.compose.foundation.layout.*
45
import androidx.compose.foundation.text.KeyboardActions
56
import androidx.compose.foundation.text.KeyboardOptions
@@ -16,7 +17,7 @@ import androidx.compose.ui.text.input.ImeAction
1617
import androidx.compose.ui.unit.dp
1718

1819
/**
19-
* A find bar UI component that provides search functionality.
20+
* A find bar UI component that provides search and replace functionality.
2021
*
2122
* @param state The FindState managing the search
2223
* @param onClose Called when the user closes the find bar
@@ -33,6 +34,8 @@ fun FindBar(
3334
requestFocus: Boolean = true
3435
) {
3536
var searchText by remember { mutableStateOf(state.query) }
37+
var replaceText by remember { mutableStateOf("") }
38+
var showReplace by remember { mutableStateOf(false) }
3639
val focusRequester = remember { FocusRequester() }
3740

3841
// Request focus when shown
@@ -47,109 +50,191 @@ fun FindBar(
4750
tonalElevation = 2.dp,
4851
shadowElevation = 2.dp
4952
) {
50-
Row(
53+
Column(
5154
modifier = Modifier
5255
.fillMaxWidth()
53-
.padding(horizontal = 8.dp, vertical = 4.dp),
54-
verticalAlignment = Alignment.CenterVertically,
55-
horizontalArrangement = Arrangement.spacedBy(8.dp)
56+
.padding(horizontal = 8.dp, vertical = 4.dp)
5657
) {
57-
// Search input
58-
OutlinedTextField(
59-
value = searchText,
60-
onValueChange = { newValue ->
61-
searchText = newValue
62-
state.search(newValue)
63-
},
64-
modifier = Modifier
65-
.weight(1f)
66-
.focusRequester(focusRequester)
67-
.onPreviewKeyEvent { event ->
68-
if (event.type == KeyEventType.KeyDown) {
69-
when {
70-
event.key == Key.Enter && event.isShiftPressed -> {
71-
state.findPrevious()
72-
true
73-
}
58+
// First row: Find
59+
Row(
60+
modifier = Modifier.fillMaxWidth(),
61+
verticalAlignment = Alignment.CenterVertically,
62+
horizontalArrangement = Arrangement.spacedBy(8.dp)
63+
) {
64+
// Search input
65+
OutlinedTextField(
66+
value = searchText,
67+
onValueChange = { newValue ->
68+
searchText = newValue
69+
state.search(newValue)
70+
},
71+
modifier = Modifier
72+
.weight(1f)
73+
.focusRequester(focusRequester)
74+
.onPreviewKeyEvent { event ->
75+
if (event.type == KeyEventType.KeyDown) {
76+
when {
77+
event.key == Key.Enter && event.isShiftPressed -> {
78+
state.findPrevious()
79+
true
80+
}
7481

75-
event.key == Key.Enter -> {
76-
state.findNext()
77-
true
78-
}
82+
event.key == Key.Enter -> {
83+
state.findNext()
84+
true
85+
}
7986

80-
event.key == Key.Escape -> {
81-
onClose()
82-
true
83-
}
87+
event.key == Key.Escape -> {
88+
onClose()
89+
true
90+
}
8491

85-
else -> false
92+
else -> false
93+
}
94+
} else false
95+
},
96+
placeholder = { Text(strings.placeholder) },
97+
singleLine = true,
98+
trailingIcon = {
99+
if (searchText.isNotEmpty()) {
100+
IconButton(
101+
onClick = {
102+
searchText = ""
103+
state.clearSearch()
104+
},
105+
modifier = Modifier.size(20.dp)
106+
) {
107+
Icon(
108+
imageVector = Icons.Default.Clear,
109+
contentDescription = strings.clearSearch,
110+
modifier = Modifier.size(16.dp)
111+
)
86112
}
87-
} else false
88-
},
89-
placeholder = { Text(strings.placeholder) },
90-
singleLine = true,
91-
trailingIcon = {
92-
if (searchText.isNotEmpty()) {
93-
IconButton(
94-
onClick = {
95-
searchText = ""
96-
state.clearSearch()
97-
},
98-
modifier = Modifier.size(20.dp)
99-
) {
100-
Icon(
101-
imageVector = Icons.Default.Clear,
102-
contentDescription = strings.clearSearch,
103-
modifier = Modifier.size(16.dp)
104-
)
105113
}
106-
}
107-
},
108-
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
109-
keyboardActions = KeyboardActions(
110-
onSearch = { state.findNext() }
111-
)
112-
)
113-
114-
// Match count
115-
if (state.query.isNotEmpty()) {
116-
Text(
117-
text = if (state.matchCount > 0) {
118-
strings.matchCount(state.currentMatchIndex + 1, state.matchCount)
119-
} else {
120-
strings.noMatches
121114
},
122-
style = MaterialTheme.typography.bodySmall,
123-
color = if (state.matchCount == 0) {
124-
MaterialTheme.colorScheme.error
125-
} else {
126-
MaterialTheme.colorScheme.onSurface
127-
}
115+
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
116+
keyboardActions = KeyboardActions(
117+
onSearch = { state.findNext() }
118+
)
128119
)
129-
}
130120

131-
// Previous button
132-
TextButton(
133-
onClick = { state.findPrevious() },
134-
enabled = state.matchCount > 0
135-
) {
136-
Text(strings.previousMatch)
137-
}
121+
// Match count
122+
if (state.query.isNotEmpty()) {
123+
Text(
124+
text = if (state.matchCount > 0) {
125+
strings.matchCount(state.currentMatchIndex + 1, state.matchCount)
126+
} else {
127+
strings.noMatches
128+
},
129+
style = MaterialTheme.typography.bodySmall,
130+
color = if (state.matchCount == 0) {
131+
MaterialTheme.colorScheme.error
132+
} else {
133+
MaterialTheme.colorScheme.onSurface
134+
}
135+
)
136+
}
138137

139-
// Next button
140-
TextButton(
141-
onClick = { state.findNext() },
142-
enabled = state.matchCount > 0
143-
) {
144-
Text(strings.nextMatch)
138+
// Previous button
139+
TextButton(
140+
onClick = { state.findPrevious() },
141+
enabled = state.matchCount > 0
142+
) {
143+
Text(strings.previousMatch)
144+
}
145+
146+
// Next button
147+
TextButton(
148+
onClick = { state.findNext() },
149+
enabled = state.matchCount > 0
150+
) {
151+
Text(strings.nextMatch)
152+
}
153+
154+
// Replace toggle button
155+
TextButton(onClick = { showReplace = !showReplace }) {
156+
Text(if (showReplace) strings.hideReplace else strings.showReplace)
157+
}
158+
159+
// Close button
160+
TextButton(onClick = {
161+
state.clearSearch()
162+
onClose()
163+
}) {
164+
Text(strings.close)
165+
}
145166
}
146167

147-
// Close button
148-
TextButton(onClick = {
149-
state.clearSearch()
150-
onClose()
151-
}) {
152-
Text(strings.close)
168+
// Second row: Replace (animated visibility)
169+
AnimatedVisibility(visible = showReplace) {
170+
Row(
171+
modifier = Modifier
172+
.fillMaxWidth()
173+
.padding(top = 4.dp),
174+
verticalAlignment = Alignment.CenterVertically,
175+
horizontalArrangement = Arrangement.spacedBy(8.dp)
176+
) {
177+
// Replace input
178+
OutlinedTextField(
179+
value = replaceText,
180+
onValueChange = { replaceText = it },
181+
modifier = Modifier
182+
.weight(1f)
183+
.onPreviewKeyEvent { event ->
184+
if (event.type == KeyEventType.KeyDown) {
185+
when {
186+
event.key == Key.Enter -> {
187+
state.replaceCurrent(replaceText)
188+
true
189+
}
190+
191+
event.key == Key.Escape -> {
192+
onClose()
193+
true
194+
}
195+
196+
else -> false
197+
}
198+
} else false
199+
},
200+
placeholder = { Text(strings.replacePlaceholder) },
201+
singleLine = true,
202+
trailingIcon = {
203+
if (replaceText.isNotEmpty()) {
204+
IconButton(
205+
onClick = { replaceText = "" },
206+
modifier = Modifier.size(20.dp)
207+
) {
208+
Icon(
209+
imageVector = Icons.Default.Clear,
210+
contentDescription = strings.clearSearch,
211+
modifier = Modifier.size(16.dp)
212+
)
213+
}
214+
}
215+
},
216+
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
217+
keyboardActions = KeyboardActions(
218+
onDone = { state.replaceCurrent(replaceText) }
219+
)
220+
)
221+
222+
// Replace button
223+
TextButton(
224+
onClick = { state.replaceCurrent(replaceText) },
225+
enabled = state.matchCount > 0
226+
) {
227+
Text(strings.replace)
228+
}
229+
230+
// Replace All button
231+
TextButton(
232+
onClick = { state.replaceAll(replaceText) },
233+
enabled = state.matchCount > 0
234+
) {
235+
Text(strings.replaceAll)
236+
}
237+
}
153238
}
154239
}
155240
}

ComposeTextEditorFind/src/commonMain/kotlin/com/darkrockstudios/texteditor/find/FindBarStrings.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ data class FindBarStrings(
1717
* Example: "3 of 15"
1818
*/
1919
val matchCount: (current: Int, total: Int) -> String,
20+
val replacePlaceholder: String,
21+
val replace: String,
22+
val replaceAll: String,
23+
val showReplace: String,
24+
val hideReplace: String,
2025
) {
2126
companion object {
2227
/**
@@ -29,7 +34,12 @@ data class FindBarStrings(
2934
previousMatch = "Prev",
3035
nextMatch = "Next",
3136
close = "Close",
32-
matchCount = { current, total -> "$current of $total" }
37+
matchCount = { current, total -> "$current of $total" },
38+
replacePlaceholder = "Replace with...",
39+
replace = "Replace",
40+
replaceAll = "All",
41+
showReplace = "Replace",
42+
hideReplace = "Hide",
3343
)
3444
}
3545
}

0 commit comments

Comments
 (0)