11package com.darkrockstudios.texteditor.find
22
3+ import androidx.compose.animation.AnimatedVisibility
34import androidx.compose.foundation.layout.*
45import androidx.compose.foundation.text.KeyboardActions
56import androidx.compose.foundation.text.KeyboardOptions
@@ -16,7 +17,7 @@ import androidx.compose.ui.text.input.ImeAction
1617import 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 }
0 commit comments