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:
parent
654a81a136
commit
f2ed6a0dab
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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" />
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue