Skip to content

Commit 70f4589

Browse files
committed
Improve UX of filter dialog by disabling buttons that have no effect
and providing empty state message
1 parent 7662c05 commit 70f4589

File tree

3 files changed

+157
-124
lines changed

3 files changed

+157
-124
lines changed

fints/src/main/java/org/totschnig/fints/Composables.kt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package org.totschnig.fints
22

33
import android.graphics.Bitmap
4-
import android.graphics.BitmapFactory
5-
import android.graphics.Canvas
64
import android.graphics.Color
75
import android.os.Parcelable
86
import androidx.annotation.StringRes
@@ -47,7 +45,6 @@ import androidx.compose.ui.Modifier
4745
import androidx.compose.ui.focus.FocusRequester
4846
import androidx.compose.ui.focus.focusRequester
4947
import androidx.compose.ui.graphics.asImageBitmap
50-
import androidx.compose.ui.platform.LocalContext
5148
import androidx.compose.ui.res.painterResource
5249
import androidx.compose.ui.res.stringResource
5350
import androidx.compose.ui.semantics.Role

myExpenses/src/main/java/org/totschnig/myexpenses/compose/FilterDialog.kt

Lines changed: 156 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import androidx.compose.foundation.layout.Row
1111
import androidx.compose.foundation.layout.defaultMinSize
1212
import androidx.compose.foundation.layout.fillMaxSize
1313
import androidx.compose.foundation.layout.fillMaxWidth
14+
import androidx.compose.foundation.layout.height
1415
import androidx.compose.foundation.layout.padding
16+
import androidx.compose.foundation.layout.wrapContentHeight
1517
import androidx.compose.foundation.rememberScrollState
1618
import androidx.compose.foundation.selection.selectable
1719
import androidx.compose.foundation.selection.selectableGroup
@@ -72,6 +74,7 @@ import androidx.compose.ui.semantics.contentDescription
7274
import androidx.compose.ui.semantics.customActions
7375
import androidx.compose.ui.semantics.invisibleToUser
7476
import androidx.compose.ui.semantics.semantics
77+
import androidx.compose.ui.text.style.TextAlign
7578
import androidx.compose.ui.tooling.preview.Preview
7679
import androidx.compose.ui.unit.dp
7780
import androidx.compose.ui.window.Dialog
@@ -263,21 +266,26 @@ fun FilterDialog(
263266
properties = DialogProperties(usePlatformDefaultWidth = isLarge),
264267
onDismissRequest = onDismiss
265268
) {
266-
Surface(modifier = Modifier.testTag(TEST_TAG_DIALOG)
269+
Surface(modifier = Modifier
270+
.testTag(TEST_TAG_DIALOG)
267271
.conditional(
268272
isLarge,
269273
ifTrue = { defaultMinSize(minHeight = 400.dp) },
270274
ifFalse = { fillMaxSize() }
271-
) ) {
272-
Column {
275+
)) {
276+
Column(modifier = Modifier
277+
.conditional(isLarge && criteriaSet.value.isEmpty()) {
278+
height(400.dp)
279+
}
280+
) {
273281
Box(
274282
modifier = Modifier.fillMaxWidth(),
275283
) {
276284
ActionButton(
277-
stringResource(android.R.string.cancel),
278-
Icons.Filled.Clear,
279-
Modifier.align(Alignment.CenterStart),
280-
onDismiss
285+
hintText = stringResource(android.R.string.cancel),
286+
icon = Icons.Filled.Clear,
287+
modifier = Modifier.align(Alignment.CenterStart),
288+
onclick = onDismiss
281289
)
282290
Text(
283291
text = stringResource(R.string.menu_search),
@@ -286,15 +294,16 @@ fun FilterDialog(
286294
)
287295
Row(modifier = Modifier.align(Alignment.CenterEnd)) {
288296
ActionButton(
289-
stringResource(R.string.clear_all_filters),
290-
Icons.Filled.ClearAll
291-
297+
hintText = stringResource(R.string.clear_all_filters),
298+
icon = Icons.Filled.ClearAll,
299+
enabled = criteriaSet.value.isNotEmpty()
292300
) {
293301
criteriaSet.value = emptySet()
294302
}
295303
ActionButton(
296-
stringResource(R.string.apply),
297-
Icons.Filled.Done
304+
hintText = stringResource(R.string.apply),
305+
icon = Icons.Filled.Done,
306+
enabled = isDirty
298307
) {
299308
onConfirmRequest(
300309
when (criteriaSet.value.size) {
@@ -356,127 +365,142 @@ fun FilterDialog(
356365
}
357366
}
358367
}
359-
Row(
360-
Modifier
361-
.minimumInteractiveComponentSize()
362-
.padding(horizontal = 16.dp)
363-
.selectableGroup(),
364-
horizontalArrangement = Arrangement.spacedBy(4.dp),
365-
verticalAlignment = Alignment.CenterVertically
366-
) {
367368

368-
val options = listOf(R.string.matchAll, R.string.matchAny)
369-
options.forEachIndexed { index, labelRes ->
370-
Row(
371-
Modifier
372-
.weight(1f)
373-
.selectable(
374-
selected = index == selectedComplex,
375-
onClick = { setSelectedComplex(index) },
376-
role = Role.RadioButton
377-
), verticalAlignment = Alignment.CenterVertically
378-
) {
379-
RadioButton(
380-
onClick = null,
381-
selected = index == selectedComplex
382-
)
383-
Text(
384-
text = stringResource(labelRes),
385-
modifier = Modifier.padding(start = 8.dp)
386-
)
369+
if (criteriaSet.value.isEmpty()) {
370+
Text(
371+
text= stringResource(R.string.filter_dialog_empty),
372+
modifier = Modifier
373+
.padding(horizontal = 24.dp)
374+
.fillMaxWidth()
375+
.weight(1f)
376+
.wrapContentHeight(),
377+
textAlign = TextAlign.Center
378+
)
379+
} else {
380+
Row(
381+
Modifier
382+
.minimumInteractiveComponentSize()
383+
.padding(horizontal = 16.dp)
384+
.selectableGroup(),
385+
horizontalArrangement = Arrangement.spacedBy(4.dp),
386+
verticalAlignment = Alignment.CenterVertically
387+
) {
388+
389+
val options = listOf(R.string.matchAll, R.string.matchAny)
390+
options.forEachIndexed { index, labelRes ->
391+
Row(
392+
Modifier
393+
.weight(1f)
394+
.selectable(
395+
selected = index == selectedComplex,
396+
onClick = { setSelectedComplex(index) },
397+
role = Role.RadioButton
398+
), verticalAlignment = Alignment.CenterVertically
399+
) {
400+
RadioButton(
401+
onClick = null,
402+
selected = index == selectedComplex
403+
)
404+
Text(
405+
text = stringResource(labelRes),
406+
modifier = Modifier.padding(start = 8.dp)
407+
)
408+
}
387409
}
388410
}
389-
}
390-
391-
Column(Modifier
392-
.verticalScroll(rememberScrollState())
393-
.semantics {
394-
collectionInfo = CollectionInfo(criteriaSet.value.size, 1)
395-
}
396-
) {
397411

398-
criteriaSet.value.forEachIndexed { index, criterion ->
399-
val negate = { criteriaSet.value = criteriaSet.value.negate(index) }
400-
val delete = { criteriaSet.value -= criterion }
401-
val edit = {
402-
currentEdit.value = criterion
403-
handleEdit(criterion)
412+
Column(Modifier
413+
.verticalScroll(rememberScrollState())
414+
.semantics {
415+
collectionInfo = CollectionInfo(criteriaSet.value.size, 1)
404416
}
405-
val title = stringResource(criterion.displayTitle)
406-
val symbol = criterion.displaySymbol.first
407-
val prettyPrint = ((criterion as? NotCriterion)?.criterion
408-
?: criterion).prettyPrint(LocalContext.current)
409-
val contentDescription = criterion.contentDescription(LocalContext.current)
410-
val labelDelete = stringResource(R.string.menu_delete)
411-
val labelEdit = stringResource(R.string.menu_edit)
412-
Row(
413-
modifier = Modifier
414-
.padding(horizontal = 16.dp)
415-
.clearAndSetSemantics {
416-
this.contentDescription = contentDescription
417-
collectionItemInfo = CollectionItemInfo(index, 1, 1, 1)
418-
customActions = listOf(
419-
CustomAccessibilityAction(
420-
label = "Negate",
421-
action = {
422-
negate()
423-
true
424-
}
425-
),
426-
CustomAccessibilityAction(
427-
label = labelDelete,
428-
action = {
429-
delete()
430-
true
431-
}
432-
),
433-
CustomAccessibilityAction(
434-
label = labelEdit,
435-
action = {
436-
edit
437-
true
438-
}
439-
)
440-
)
441-
},
442-
verticalAlignment = Alignment.CenterVertically
443-
) {
444-
Icon(
445-
imageVector = criterion.displayIcon,
446-
contentDescription = title
447-
)
448-
IconButton(
449-
modifier = Modifier.semantics {
450-
invisibleToUser()
451-
},
452-
onClick = negate
453-
) {
454-
CharIcon(symbol)
417+
) {
418+
419+
criteriaSet.value.forEachIndexed { index, criterion ->
420+
val negate = { criteriaSet.value = criteriaSet.value.negate(index) }
421+
val delete = { criteriaSet.value -= criterion }
422+
val edit = {
423+
currentEdit.value = criterion
424+
handleEdit(criterion)
455425
}
456-
Text(
457-
modifier = Modifier.weight(1f),
458-
text = prettyPrint
459-
)
460-
IconButton(
461-
onClick = delete
426+
val title = stringResource(criterion.displayTitle)
427+
val symbol = criterion.displaySymbol.first
428+
val prettyPrint = ((criterion as? NotCriterion)?.criterion
429+
?: criterion).prettyPrint(LocalContext.current)
430+
val contentDescription =
431+
criterion.contentDescription(LocalContext.current)
432+
val labelDelete = stringResource(R.string.menu_delete)
433+
val labelEdit = stringResource(R.string.menu_edit)
434+
Row(
435+
modifier = Modifier
436+
.padding(horizontal = 16.dp)
437+
.clearAndSetSemantics {
438+
this.contentDescription = contentDescription
439+
collectionItemInfo = CollectionItemInfo(index, 1, 1, 1)
440+
customActions = listOf(
441+
CustomAccessibilityAction(
442+
label = "Negate",
443+
action = {
444+
negate()
445+
true
446+
}
447+
),
448+
CustomAccessibilityAction(
449+
label = labelDelete,
450+
action = {
451+
delete()
452+
true
453+
}
454+
),
455+
CustomAccessibilityAction(
456+
label = labelEdit,
457+
action = {
458+
edit
459+
true
460+
}
461+
)
462+
)
463+
},
464+
verticalAlignment = Alignment.CenterVertically
462465
) {
463466
Icon(
464-
Icons.Filled.Delete,
465-
contentDescription = labelDelete
467+
imageVector = criterion.displayIcon,
468+
contentDescription = title
466469
)
467-
}
468-
IconButton(
469-
onClick = edit
470-
) {
471-
Icon(
472-
Icons.Filled.Edit,
473-
contentDescription = labelEdit
470+
IconButton(
471+
modifier = Modifier.semantics {
472+
invisibleToUser()
473+
},
474+
onClick = negate
475+
) {
476+
CharIcon(symbol)
477+
}
478+
Text(
479+
modifier = Modifier.weight(1f),
480+
text = prettyPrint
474481
)
482+
IconButton(
483+
onClick = delete
484+
) {
485+
Icon(
486+
Icons.Filled.Delete,
487+
contentDescription = labelDelete
488+
)
489+
}
490+
IconButton(
491+
onClick = edit
492+
) {
493+
Icon(
494+
Icons.Filled.Edit,
495+
contentDescription = labelEdit
496+
)
497+
}
475498
}
476499
}
477500
}
478501
}
479502
}
503+
480504
showCommentFilterPrompt?.let {
481505
var search by rememberSaveable { mutableStateOf(it.searchString) }
482506

@@ -498,7 +522,8 @@ fun FilterDialog(
498522
val focusRequester = remember { FocusRequester() }
499523
val keyboardController = LocalSoftwareKeyboardController.current
500524
OutlinedTextField(
501-
modifier = Modifier.focusRequester(focusRequester)
525+
modifier = Modifier
526+
.focusRequester(focusRequester)
502527
.onFocusChanged {
503528
if (it.isFocused) {
504529
keyboardController?.show()
@@ -545,6 +570,7 @@ fun ActionButton(
545570
hintText: String,
546571
icon: ImageVector,
547572
modifier: Modifier = Modifier,
573+
enabled: Boolean = true,
548574
onclick: () -> Unit,
549575
) {
550576
TooltipBox(
@@ -553,7 +579,7 @@ fun ActionButton(
553579
state = rememberTooltipState(),
554580
modifier = modifier
555581
) {
556-
IconButton(onClick = onclick) {
582+
IconButton(onClick = onclick, enabled = enabled) {
557583
Icon(
558584
imageVector = icon,
559585
contentDescription = hintText
@@ -568,7 +594,16 @@ fun Set<Criterion>.negate(atIndex: Int) = mapIndexed { index, criterion ->
568594
} else criterion
569595
}.toSet()
570596

571-
@Preview(device = "id:pixel")
597+
@Preview
598+
@Composable
599+
fun FilterDialogEmpty() {
600+
FilterDialog(
601+
account = null,
602+
sumInfo = SumInfo.EMPTY
603+
)
604+
}
605+
606+
@Preview
572607
@Composable
573608
fun FilterDialogPreview() {
574609
FilterDialog(

myExpenses/src/main/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1045,4 +1045,5 @@
10451045
<string name="budget_import_cannot_be_imported">The following budgets refer to accounts that are not synchronized:</string>
10461046
<string name="budget_import_none_found">No budgets found.</string>
10471047
<string name="webui_download_no_data">After printing, exporting or creating backups in the app, files will be available from here for download.</string>
1048+
<string name="filter_dialog_empty">No criteria added yet. Tap the buttons above to define your query and refine your results.</string>
10481049
</resources>

0 commit comments

Comments
 (0)