fix: Ensure items in accessibility dialogs are clickable (#1112)

The copy button meant that some dialogs did not return the item click.

Fix this by having the adapter listen for clicks and forward them on.
Pre-emptively move the adapter to core.ui, as it's going to be useful
for the other accessiblity delegates.

Fixes #1108
This commit is contained in:
Nik Clayton 2024-11-19 15:04:22 +01:00 committed by GitHub
parent 654a81a136
commit f2ed6a0dab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 107 additions and 67 deletions

View File

@ -1,21 +1,13 @@
package app.pachli.util package app.pachli.util
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.Spannable import android.text.Spannable
import android.text.style.URLSpan import android.text.style.URLSpan
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityManager import android.view.accessibility.AccessibilityManager
import android.widget.ArrayAdapter
import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat.getSystemService
import androidx.core.view.AccessibilityDelegateCompat import androidx.core.view.AccessibilityDelegateCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat
@ -26,7 +18,7 @@ import app.pachli.adapter.FilterableStatusViewHolder
import app.pachli.adapter.StatusBaseViewHolder import app.pachli.adapter.StatusBaseViewHolder
import app.pachli.core.activity.openLink import app.pachli.core.activity.openLink
import app.pachli.core.network.model.Status.Companion.MAX_MEDIA_ATTACHMENTS import app.pachli.core.network.model.Status.Companion.MAX_MEDIA_ATTACHMENTS
import app.pachli.databinding.SimpleListItem1CopyButtonBinding import app.pachli.core.ui.ArrayAdapterWithCopyButton
import app.pachli.interfaces.StatusActionListener import app.pachli.interfaces.StatusActionListener
import app.pachli.viewdata.IStatusViewData import app.pachli.viewdata.IStatusViewData
import app.pachli.viewdata.NotificationViewData import app.pachli.viewdata.NotificationViewData
@ -222,8 +214,9 @@ class ListStatusAccessibilityDelegate<T : IStatusViewData>(
ArrayAdapterWithCopyButton( ArrayAdapterWithCopyButton(
host.context, host.context,
textLinks, textLinks,
), ) { position -> host.context.openLink(links[position].link) },
) { _, which -> host.context.openLink(links[which].link) } null,
)
.show() .show()
.let { forceFocus(it.listView) } .let { forceFocus(it.listView) }
} }
@ -241,10 +234,11 @@ class ListStatusAccessibilityDelegate<T : IStatusViewData>(
ArrayAdapterWithCopyButton( ArrayAdapterWithCopyButton(
host.context, host.context,
stringMentions, stringMentions,
), ) { position ->
) { _, which -> statusActionListener.onViewAccount(mentions[position].id)
statusActionListener.onViewAccount(mentions[which].id) },
} null,
)
.show() .show()
.let { forceFocus(it.listView) } .let { forceFocus(it.listView) }
} }
@ -258,10 +252,11 @@ class ListStatusAccessibilityDelegate<T : IStatusViewData>(
ArrayAdapterWithCopyButton( ArrayAdapterWithCopyButton(
host.context, host.context,
tags, tags,
), ) { position ->
) { _, which -> statusActionListener.onViewTag(tags[position].toString())
statusActionListener.onViewTag(tags[which].toString()) },
} null,
)
.show() .show()
.let { forceFocus(it.listView) } .let { forceFocus(it.listView) }
} }
@ -413,40 +408,3 @@ class ListStatusAccessibilityDelegate<T : IStatusViewData>(
private data class LinkSpanInfo(val text: String, val link: String) private data class LinkSpanInfo(val text: String, val link: String)
} }
/**
* An [ArrayAdapter] that shows a "copy" button next to each item. When clicked
* the text of the item is copied to the clipboard and a toast is shown (if
* appropriate).
*/
private class ArrayAdapterWithCopyButton<T : CharSequence>(
context: Context,
items: List<T>,
) : ArrayAdapter<T>(context, R.layout.simple_list_item_1_copy_button, items) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val binding = if (convertView == null) {
SimpleListItem1CopyButtonBinding.inflate(LayoutInflater.from(context), parent, false)
} else {
SimpleListItem1CopyButtonBinding.bind(convertView)
}
getItem(position)?.let { text ->
binding.text1.text = text
binding.copy.setOnClickListener {
val clipboard = getSystemService(context, ClipboardManager::class.java) as ClipboardManager
val clip = ClipData.newPlainText("", text)
clipboard.setPrimaryClip(clip)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
Toast.makeText(
context,
context.getString(R.string.item_copied),
Toast.LENGTH_SHORT,
).show()
}
}
}
return binding.root
}
}

View File

@ -782,8 +782,6 @@
<string name="pref_title_confirm_status_language">Revisar idioma de la publicación antes de publicar</string> <string name="pref_title_confirm_status_language">Revisar idioma de la publicación antes de publicar</string>
<string name="compose_warn_language_dialog_accept_and_dont_ask_fmt">Publicar como está (%1$s) y no volver a preguntar</string> <string name="compose_warn_language_dialog_accept_and_dont_ask_fmt">Publicar como está (%1$s) y no volver a preguntar</string>
<string name="upload_failed_msg_fmt">Se intentará subir nuevamente cuando envíes la publicación. Si vuelve a fallar, la publicación se guardará en tus borradores.\n\nEl error fue: %1$s</string> <string name="upload_failed_msg_fmt">Se intentará subir nuevamente cuando envíes la publicación. Si vuelve a fallar, la publicación se guardará en tus borradores.\n\nEl error fue: %1$s</string>
<string name="item_copied">Texto copiado</string>
<string name="action_copy_item">Copiar ítem</string>
<plurals name="notification_severed_relationships_summary_followers_fmt"> <plurals name="notification_severed_relationships_summary_followers_fmt">
<item quantity="one">%1$s seguidor eliminado</item> <item quantity="one">%1$s seguidor eliminado</item>
<item quantity="many">%1$s seguidores eliminados</item> <item quantity="many">%1$s seguidores eliminados</item>
@ -798,4 +796,4 @@
<string name="main_viewmodel_error_set_active_account">El inicio de sesión falló con el siguiente error:\n\n%1$s</string> <string name="main_viewmodel_error_set_active_account">El inicio de sesión falló con el siguiente error:\n\n%1$s</string>
<string name="main_viewmodel_error_refresh_account">La actualización de la cuenta falló con el siguiente error:\n\n%1$s\n\nPuedes continuar, pero tus listas y filtros pueden estar incompletos.</string> <string name="main_viewmodel_error_refresh_account">La actualización de la cuenta falló con el siguiente error:\n\n%1$s\n\nPuedes continuar, pero tus listas y filtros pueden estar incompletos.</string>
<string name="action_relogin">Volver a iniciar sesión</string> <string name="action_relogin">Volver a iniciar sesión</string>
</resources> </resources>

View File

@ -765,6 +765,4 @@
<string name="conversation_0_recipients">Ei muita osanottajia</string> <string name="conversation_0_recipients">Ei muita osanottajia</string>
<string name="compose_warn_language_dialog_accept_and_dont_ask_fmt">Julkaise muutoksetta (%1$s) äläkä kysy uudelleen</string> <string name="compose_warn_language_dialog_accept_and_dont_ask_fmt">Julkaise muutoksetta (%1$s) äläkä kysy uudelleen</string>
<string name="pref_title_confirm_status_language">Tarkasta julkaisun kieli ennen julkaisemista</string> <string name="pref_title_confirm_status_language">Tarkasta julkaisun kieli ennen julkaisemista</string>
<string name="action_copy_item">Kopioi kohde</string>
<string name="item_copied">Teksti kopioitu</string>
</resources> </resources>

View File

@ -527,8 +527,6 @@
<string name="search_operator_where_all">Gach post ▾</string> <string name="search_operator_where_all">Gach post ▾</string>
<string name="search_operator_where_library">Do leabharlann</string> <string name="search_operator_where_library">Do leabharlann</string>
<string name="search_operator_where_dialog_all_hint">Poist i do leabharlann agus i do phoist phoiblí</string> <string name="search_operator_where_dialog_all_hint">Poist i do leabharlann agus i do phoist phoiblí</string>
<string name="action_copy_item">Cóipeáil mír</string>
<string name="item_copied">Cóipeáladh téacs</string>
<string name="upload_failed_msg_fmt">Déanfar an t-uaslódáil a aisghabháil nuair a sheolann tú an post. Má theipeann air arís sábhálfar an post i do dhréachtaí.\n\nEarráid a bhí ann: %1$s</string> <string name="upload_failed_msg_fmt">Déanfar an t-uaslódáil a aisghabháil nuair a sheolann tú an post. Má theipeann air arís sábhálfar an post i do dhréachtaí.\n\nEarráid a bhí ann: %1$s</string>
<string name="upload_failed_modify_attachment">Mionathraigh iatán</string> <string name="upload_failed_modify_attachment">Mionathraigh iatán</string>
<string name="duration_30_days">laethanta 30</string> <string name="duration_30_days">laethanta 30</string>

View File

@ -850,8 +850,6 @@
<string name="search_operator_where_dialog_public">Public posts</string> <string name="search_operator_where_dialog_public">Public posts</string>
<string name="search_operator_where_dialog_public_hint">Public, searchable posts known by server</string> <string name="search_operator_where_dialog_public_hint">Public, searchable posts known by server</string>
<string name="action_copy_item">Copy item</string>
<string name="item_copied">Text copied</string>
<string name="upload_failed_msg_fmt">The upload will be retried when you send the post. If it fails again the post will be saved in your drafts.\n\nThe error was: %1$s</string> <string name="upload_failed_msg_fmt">The upload will be retried when you send the post. If it fails again the post will be saved in your drafts.\n\nThe error was: %1$s</string>
<string name="upload_failed_modify_attachment">Modify attachment</string> <string name="upload_failed_modify_attachment">Modify attachment</string>

View File

@ -0,0 +1,82 @@
/*
* Copyright 2024 Pachli Association
*
* This file is a part of Pachli.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Pachli; if not,
* see <http://www.gnu.org/licenses>.
*/
package app.pachli.core.ui
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Build
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.Toast
import androidx.core.content.ContextCompat
import app.pachli.core.ui.databinding.SimpleListItem1CopyButtonBinding
/**
* An [ArrayAdapter] that shows a "copy" button next to each item.
*
* If the "copy" button is clicked the text of the item is copied to the clipboard
* and a toast is shown (if appropriate).
*
* If the item is clicked then [listener] is called with the position of the
* clicked item.
*/
class ArrayAdapterWithCopyButton<T : CharSequence>(
context: Context,
items: List<T>,
private val listener: OnClickListener,
) : ArrayAdapter<T>(context, R.layout.simple_list_item_1_copy_button, android.R.id.text1, items) {
fun interface OnClickListener {
/** @param position Index of the item the user clicked. */
fun onClick(position: Int)
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val binding = if (convertView == null) {
SimpleListItem1CopyButtonBinding.inflate(LayoutInflater.from(context), parent, false).apply {
text1.setOnClickListener { listener.onClick(position) }
copy.setOnClickListener {
getItem(position)?.let { text ->
val clipboard = ContextCompat.getSystemService(
context,
ClipboardManager::class.java,
) as ClipboardManager
val clip = ClipData.newPlainText("", text)
clipboard.setPrimaryClip(clip)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
Toast.makeText(
context,
context.getString(R.string.item_copied),
Toast.LENGTH_SHORT,
).show()
}
}
}
}
} else {
SimpleListItem1CopyButtonBinding.bind(convertView)
}
getItem(position)?.let { binding.text1.text = it }
return binding.root
}
}

View File

@ -31,10 +31,10 @@
android:id="@android:id/text1" android:id="@android:id/text1"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_weight="1" android:layout_weight="1"
android:gravity="center_vertical" android:gravity="center_vertical"
android:textAppearance="?android:attr/textAppearanceListItemSmall" android:textAppearance="?android:attr/textAppearanceListItemSmall"
android:focusable="true"
tools:ignore="SelectableText" tools:ignore="SelectableText"
tools:text="#TestHashTag" /> tools:text="#TestHashTag" />

View File

@ -14,4 +14,6 @@
<string name="action_hashtags">Etiquetas</string> <string name="action_hashtags">Etiquetas</string>
<string name="title_links_dialog">Enlaces</string> <string name="title_links_dialog">Enlaces</string>
<string name="title_hashtags_dialog">Etiquetas</string> <string name="title_hashtags_dialog">Etiquetas</string>
<string name="item_copied">Texto copiado</string>
<string name="action_copy_item">Copiar ítem</string>
</resources> </resources>

View File

@ -14,4 +14,6 @@
<string name="action_hashtags">Aihetunnisteet</string> <string name="action_hashtags">Aihetunnisteet</string>
<string name="title_links_dialog">Linkit</string> <string name="title_links_dialog">Linkit</string>
<string name="title_hashtags_dialog">Aihetunnisteet</string> <string name="title_hashtags_dialog">Aihetunnisteet</string>
<string name="item_copied">Teksti kopioitu</string>
<string name="action_copy_item">Kopioi kohde</string>
</resources> </resources>

View File

@ -14,4 +14,6 @@
<string name="title_hashtags_dialog">Haischlibeanna</string> <string name="title_hashtags_dialog">Haischlibeanna</string>
<string name="action_refresh">Athnuaigh</string> <string name="action_refresh">Athnuaigh</string>
<string name="url_domain_notifier">" (🔗 %s)"</string> <string name="url_domain_notifier">" (🔗 %s)"</string>
</resources> <string name="item_copied">Cóipeáladh téacs</string>
<string name="action_copy_item">Cóipeáil mír</string>
</resources>

View File

@ -14,4 +14,6 @@
<string name="action_hashtags">Hashtags</string> <string name="action_hashtags">Hashtags</string>
<string name="title_links_dialog">Links</string> <string name="title_links_dialog">Links</string>
<string name="title_hashtags_dialog">Hashtags</string> <string name="title_hashtags_dialog">Hashtags</string>
<string name="item_copied">Text copied</string>
<string name="action_copy_item">Copy item</string>
</resources> </resources>