From 278d9e9a91d7e53f325485bb09f6fd1d986d3677 Mon Sep 17 00:00:00 2001 From: SanjaySargam Date: Fri, 16 Aug 2024 11:43:43 +0530 Subject: [PATCH] feat: keyboard shortcuts helper --- .../main/java/com/ichi2/anki/AnkiActivity.kt | 26 ++++++ .../main/java/com/ichi2/anki/CardBrowser.kt | 33 +++++++ .../java/com/ichi2/anki/CardTemplateEditor.kt | 16 ++++ .../main/java/com/ichi2/anki/DeckPicker.kt | 22 +++++ .../main/java/com/ichi2/anki/NoteEditor.kt | 11 +++ .../src/main/java/com/ichi2/compat/Compat.kt | 13 +++ .../java/com/ichi2/compat/CompatHelper.kt | 6 ++ .../main/java/com/ichi2/compat/CompatV23.kt | 12 +++ .../main/java/com/ichi2/compat/CompatV24.kt | 89 +++++++++++++++++++ AnkiDroid/src/main/res/values/02-strings.xml | 19 ++++ .../src/main/res/values/06-statistics.xml | 1 + .../src/main/res/values/07-cardbrowser.xml | 11 +++ .../src/main/res/values/10-preferences.xml | 3 + 13 files changed, 262 insertions(+) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiActivity.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiActivity.kt index 8213f74e496c..31295b740a97 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiActivity.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiActivity.kt @@ -13,6 +13,9 @@ import android.media.AudioManager import android.net.Uri import android.os.Build import android.os.Bundle +import android.view.KeyEvent +import android.view.KeyboardShortcutGroup +import android.view.Menu import android.view.MenuItem import android.view.View import android.view.ViewGroup @@ -54,6 +57,8 @@ import com.ichi2.anki.preferences.sharedPrefs import com.ichi2.anki.snackbar.showSnackbar import com.ichi2.anki.workarounds.AppLoadedFromBackupWorkaround.showedActivityFailedScreen import com.ichi2.async.CollectionLoader +import com.ichi2.compat.CompatHelper +import com.ichi2.compat.CompatHelper.Companion.showKeyboardShortcutsDialog import com.ichi2.compat.customtabs.CustomTabActivityHelper import com.ichi2.compat.customtabs.CustomTabsFallback import com.ichi2.compat.customtabs.CustomTabsHelper @@ -102,6 +107,27 @@ open class AnkiActivity : AppCompatActivity, SimpleMessageDialogListener { } } + override fun onProvideKeyboardShortcuts( + data: MutableList, + menu: Menu?, + deviceId: Int + ) { + // Include all shortcuts here so that the keyboard shortcut dialog can be opened from anywhere in the app using the Alt+K shortcut, + // ensuring users have quick access to all available shortcuts regardless of the current screen. + val shortcutGroups = CompatHelper.compat.getAllShortcuts(this) + data.addAll(shortcutGroups) + super.onProvideKeyboardShortcuts(data, menu, deviceId) + } + + override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_K && event.isAltPressed) { + // Alt+K: Show keyboard shortcuts dialog + showKeyboardShortcutsDialog() + return true + } + return super.onKeyUp(keyCode, event) + } + override fun onStart() { super.onStart() customTabActivityHelper.bindCustomTabsService(this) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt index 9f17bdd957ff..dec288f3ea75 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt @@ -116,6 +116,7 @@ import com.ichi2.anki.widgets.DeckDropDownAdapter.SubtitleListener import com.ichi2.annotations.NeedsTest import com.ichi2.async.renderBrowserQA import com.ichi2.compat.CompatHelper.Companion.registerReceiverCompat +import com.ichi2.compat.CompatV24.Shortcut import com.ichi2.libanki.Card import com.ichi2.libanki.CardId import com.ichi2.libanki.ChangeManager @@ -2387,6 +2388,38 @@ open class CardBrowser : // Values related to persistent state data private const val ALL_DECKS_ID = 0L + val shortcuts = listOf( + Shortcut("Ctrl+Shift+A", R.string.edit_tags_dialog), + Shortcut("Ctrl+A", R.string.card_browser_select_all), + Shortcut("Ctrl+Shift+E", R.string.export_cards), + Shortcut("Ctrl+E", R.string.menu_add_note), + Shortcut("E", R.string.cardeditor_title_edit_card), + Shortcut("Ctrl+D", R.string.card_browser_change_deck), + Shortcut("Ctrl+K", R.string.toggle_mark), + Shortcut("Ctrl+Alt+R", R.string.reschedule_cards), + Shortcut("DEL", R.string.delete_card_title), + Shortcut("Ctrl+Alt+N", R.string.reset_card_dialog_title), + Shortcut("Ctrl+Alt+T", R.string.toggle_cards_notes), + Shortcut("T", R.string.card_browser_search_by_tag), + Shortcut("Ctrl+Shift+S", R.string.reposition_cards), + Shortcut("Ctrl+Alt+S", R.string.card_browser_list_my_searches), + Shortcut("Ctrl+S", R.string.card_browser_list_my_searches_save), + Shortcut("Alt+S", R.string.card_browser_show_suspended), + Shortcut("Ctrl+Shift+J", R.string.toggle_bury_cards), + Shortcut("Ctrl+J", R.string.toggle_suspended_cards), + Shortcut("Ctrl+Shift+I", R.string.display_card_info), + Shortcut("Ctrl+O", R.string.show_order_dialog), + Shortcut("Ctrl+M", R.string.card_browser_show_marked), + Shortcut("ESCAPE", R.string.card_browser_select_none), + Shortcut("Ctrl+NUMPAD_1", R.string.gesture_flag_red), + Shortcut("Ctrl+NUMPAD_2", R.string.gesture_flag_orange), + Shortcut("Ctrl+NUMPAD_3", R.string.gesture_flag_green), + Shortcut("Ctrl+NUMPAD_4", R.string.gesture_flag_blue), + Shortcut("Ctrl+NUMPAD_5", R.string.gesture_flag_pink), + Shortcut("Ctrl+NUMPAD_6", R.string.gesture_flag_turquoise), + Shortcut("Ctrl+NUMPAD_7", R.string.gesture_flag_purple) + ) + fun clearLastDeckId() = SharedPreferencesLastDeckIdRepository.clearLastDeckId() @VisibleForTesting diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateEditor.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateEditor.kt index ad4e362f1c40..1922eeb8d19f 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateEditor.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateEditor.kt @@ -73,6 +73,7 @@ import com.ichi2.anki.utils.ext.isImageOcclusion import com.ichi2.anki.utils.postDelayed import com.ichi2.annotations.NeedsTest import com.ichi2.compat.CompatHelper.Companion.getSerializableCompat +import com.ichi2.compat.CompatV24.Shortcut import com.ichi2.libanki.Collection import com.ichi2.libanki.Note import com.ichi2.libanki.NoteId @@ -1244,5 +1245,20 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener { @Suppress("unused") private const val REQUEST_CARD_BROWSER_APPEARANCE = 1 + + val shortcuts = listOf( + Shortcut("Ctrl+P", R.string.perform_preview), + Shortcut("Ctrl+NUMPAD_1", R.string.edit_front), + Shortcut("Ctrl+NUMPAD_2", R.string.edit_back), + Shortcut("Ctrl+NUMPAD_3", R.string.edit_styling), + Shortcut("Ctrl+S", R.string.save), + Shortcut("Ctrl+I", R.string.card_template_editor_insert_field), + Shortcut("Ctrl+A", R.string.add_card), + Shortcut("Ctrl+R", R.string.rename_card), + Shortcut("Ctrl+B", R.string.card_template_browser_appearance_title), + Shortcut("Ctrl+D", R.string.delete_card), + Shortcut("Ctrl+O", R.string.card_template_editor_deck_override), + Shortcut("Ctrl+M", R.string.copy_markdown) + ) } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt index b85f14e35aed..9538466e7e76 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt @@ -166,6 +166,7 @@ import com.ichi2.async.sendNotificationForAsyncOperation import com.ichi2.compat.CompatHelper.Companion.getSerializableCompat import com.ichi2.compat.CompatHelper.Companion.registerReceiverCompat import com.ichi2.compat.CompatHelper.Companion.sdkVersion +import com.ichi2.compat.CompatV24.Shortcut import com.ichi2.libanki.ChangeManager import com.ichi2.libanki.Consts import com.ichi2.libanki.DeckId @@ -2636,6 +2637,27 @@ open class DeckPicker : private const val AUTOMATIC_SYNC_MINIMAL_INTERVAL_IN_MINUTES: Long = 10 private const val SWIPE_TO_SYNC_TRIGGER_DISTANCE = 400 + val shortcuts = listOf( + Shortcut("A", R.string.menu_add_note), + Shortcut("Ctrl+B", R.string.backup_restore), + Shortcut("B", R.string.card_browser_context_menu), + Shortcut("Y", R.string.pref_cat_sync), + Shortcut("SLASH", R.string.deck_conf_cram_search), + Shortcut("S", R.string.study_deck), + Shortcut("T", R.string.open_statistics), + Shortcut("C", R.string.check_db), + Shortcut("D", R.string.new_deck), + Shortcut("F", R.string.new_dynamic_deck), + Shortcut("Shift+DEL", R.string.delete_deck_without_confirmation), + Shortcut("DEL", R.string.delete_deck_title), + Shortcut("R", R.string.rename_deck), + Shortcut("P", R.string.open_settings), + Shortcut("M", R.string.check_media), + Shortcut("Ctrl+E", R.string.export_collection), + Shortcut("Ctrl+Shift+I", R.string.menu_import), + Shortcut("Ctrl+Shift+N", R.string.model_browser_label) + ) + // Animation utility methods used by renderPage() method fun fadeIn(view: View?, duration: Int, translation: Float = 0f, startAction: Runnable? = Runnable { view!!.visibility = View.VISIBLE }): ViewPropertyAnimator { view!!.alpha = 0f diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditor.kt b/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditor.kt index 4666a5466998..13d4fd1afffb 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditor.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditor.kt @@ -134,6 +134,7 @@ import com.ichi2.annotations.NeedsTest import com.ichi2.compat.CompatHelper import com.ichi2.compat.CompatHelper.Companion.getSerializableCompat import com.ichi2.compat.CompatHelper.Companion.registerReceiverCompat +import com.ichi2.compat.CompatV24.Shortcut import com.ichi2.libanki.Card import com.ichi2.libanki.Collection import com.ichi2.libanki.Consts @@ -2671,6 +2672,16 @@ class NoteEditor : AnkiFragment(R.layout.note_editor), DeckSelectionListener, Su private const val PREF_NOTE_EDITOR_FONT_SIZE = "note_editor_font_size" private const val PREF_NOTE_EDITOR_CUSTOM_BUTTONS = "note_editor_custom_buttons" + val shortcuts = listOf( + Shortcut("Ctrl+ENTER", R.string.save), + Shortcut("Ctrl+D", R.string.deck_selection_dialog), + Shortcut("Ctrl+L", R.string.card_template_editor_group), + Shortcut("Ctrl+N", R.string.note_selection_spinner), + Shortcut("Ctrl+Shift+T", R.string.tags_dialog), + Shortcut("Ctrl+Shift+C", R.string.multimedia_editor_popup_cloze), + Shortcut("Ctrl+P", R.string.perform_preview) + ) + private fun shouldReplaceNewlines(): Boolean { return AnkiDroidApp.instance.sharedPrefs() .getBoolean(PREF_NOTE_EDITOR_NEWLINE_REPLACE, true) diff --git a/AnkiDroid/src/main/java/com/ichi2/compat/Compat.kt b/AnkiDroid/src/main/java/com/ichi2/compat/Compat.kt index f0c0b783b3fa..4339aa819102 100644 --- a/AnkiDroid/src/main/java/com/ichi2/compat/Compat.kt +++ b/AnkiDroid/src/main/java/com/ichi2/compat/Compat.kt @@ -29,6 +29,7 @@ import android.graphics.Bitmap.CompressFormat import android.media.MediaRecorder import android.net.Uri import android.os.Bundle +import android.view.KeyboardShortcutGroup import android.view.View import androidx.annotation.CheckResult import androidx.core.view.OnReceiveContentListener @@ -217,6 +218,18 @@ interface Compat { onReceiveContentListener: OnReceiveContentListener ) + /** + * @see CompatHelper.showKeyboardShortcutsDialog + */ + fun showKeyboardShortcutsDialog( + activity: Activity + ) + + /** + * Get all keyboard shortcuts + */ + fun getAllShortcuts(activity: Activity): List + /** * Converts a locale to a 'two letter' code (ISO-639-1 + ISO 3166-1 alpha-2) * Locale("spa", "MEX", "001") => Locale("es", "MX", "001") diff --git a/AnkiDroid/src/main/java/com/ichi2/compat/CompatHelper.kt b/AnkiDroid/src/main/java/com/ichi2/compat/CompatHelper.kt index 2d09a4067590..8c1f00803c06 100644 --- a/AnkiDroid/src/main/java/com/ichi2/compat/CompatHelper.kt +++ b/AnkiDroid/src/main/java/com/ichi2/compat/CompatHelper.kt @@ -30,6 +30,7 @@ import android.view.KeyCharacterMap.deviceHasKey import android.view.KeyEvent.KEYCODE_PAGE_DOWN import android.view.KeyEvent.KEYCODE_PAGE_UP import androidx.core.content.ContextCompat +import com.ichi2.anki.AnkiActivity import com.ichi2.compat.CompatHelper.Companion.compat import java.io.Serializable @@ -193,5 +194,10 @@ class CompatHelper private constructor() { @ContextCompat.RegisterReceiverFlags flags: Int ) = ContextCompat.registerReceiver(this, receiver, filter, flags) + + /** + * Shows keyboard shortcuts dialog + */ + fun AnkiActivity.showKeyboardShortcutsDialog() = compat.showKeyboardShortcutsDialog(this) } } diff --git a/AnkiDroid/src/main/java/com/ichi2/compat/CompatV23.kt b/AnkiDroid/src/main/java/com/ichi2/compat/CompatV23.kt index 9112fbbf48a1..b2fb3844237a 100644 --- a/AnkiDroid/src/main/java/com/ichi2/compat/CompatV23.kt +++ b/AnkiDroid/src/main/java/com/ichi2/compat/CompatV23.kt @@ -30,6 +30,7 @@ import android.os.Bundle import android.os.Environment import android.os.Vibrator import android.provider.MediaStore +import android.view.KeyboardShortcutGroup import android.view.View import androidx.appcompat.widget.TooltipCompat import androidx.core.view.OnReceiveContentListener @@ -160,6 +161,17 @@ open class CompatV23 : Compat { // No implementation possible. } + // Until API 24 + override fun showKeyboardShortcutsDialog(activity: Activity) { + // No implementation available + } + + // Until API 24 + override fun getAllShortcuts(activity: Activity): List { + // No implementation available + return mutableListOf() + } + // Until API 26 @Throws(IOException::class) override fun deleteFile(file: File) { diff --git a/AnkiDroid/src/main/java/com/ichi2/compat/CompatV24.kt b/AnkiDroid/src/main/java/com/ichi2/compat/CompatV24.kt index 4edcf131c034..ebbac6e0812b 100644 --- a/AnkiDroid/src/main/java/com/ichi2/compat/CompatV24.kt +++ b/AnkiDroid/src/main/java/com/ichi2/compat/CompatV24.kt @@ -18,11 +18,21 @@ package com.ichi2.compat import android.annotation.TargetApi import android.app.Activity +import android.content.Context import android.icu.util.ULocale +import android.view.KeyEvent +import android.view.KeyboardShortcutGroup +import android.view.KeyboardShortcutInfo import android.view.MotionEvent import android.view.View +import androidx.annotation.StringRes import androidx.core.view.OnReceiveContentListener import androidx.draganddrop.DropHelper +import com.ichi2.anki.CardBrowser +import com.ichi2.anki.CardTemplateEditor +import com.ichi2.anki.DeckPicker +import com.ichi2.anki.NoteEditor +import com.ichi2.anki.R import com.ichi2.anki.common.utils.android.isRobolectric import com.ichi2.utils.ClipboardUtil.MEDIA_MIME_TYPES import timber.log.Timber @@ -60,6 +70,85 @@ open class CompatV24 : CompatV23(), Compat { ) } + override fun showKeyboardShortcutsDialog(activity: Activity) { + activity.requestShowKeyboardShortcuts() + } + + override fun getAllShortcuts(activity: Activity): List { + fun List.toShortcutGroup(@StringRes labelRes: Int): KeyboardShortcutGroup { + val shortcuts = this.map { it.toShortcutInfo(activity) } + val groupLabel = activity.getString(labelRes) + return KeyboardShortcutGroup(groupLabel, shortcuts) + } + + val generalShortcutGroup = listOf( + Shortcut("Alt+K", R.string.show_keyboard_shortcuts_dialog), + Shortcut("Ctrl+Z", R.string.undo) + ).toShortcutGroup(R.string.pref_cat_general) + + val deckPickerShortcutGroup = + DeckPicker.shortcuts.toShortcutGroup(R.string.deck_picker_group) + + val noteEditorShortcutGroup = + NoteEditor.shortcuts.toShortcutGroup(R.string.note_editor_group) + + val cardBrowserShortcutGroup = + CardBrowser.shortcuts.toShortcutGroup(R.string.card_browser_context_menu) + + val cardTemplateEditorShortcutGroup = + CardTemplateEditor.shortcuts.toShortcutGroup(R.string.card_template_editor_group) + + val shortcutGroups = listOf( + generalShortcutGroup, + deckPickerShortcutGroup, + noteEditorShortcutGroup, + cardBrowserShortcutGroup, + cardTemplateEditorShortcutGroup + ) + + return shortcutGroups + } + + /** + * Data class representing a keyboard shortcut. + * + * @param shortcut The string representation of the keyboard shortcut (e.g., "Ctrl+Alt+S"). + * @param labelRes The string resource ID for the shortcut label. + */ + data class Shortcut(val shortcut: String, @StringRes val labelRes: Int) { + + /** + * Converts the shortcut string into a KeyboardShortcutInfo object. + * + * @param context The context used to retrieve the string label resource. + * @return A KeyboardShortcutInfo object representing the keyboard shortcut. + */ + fun toShortcutInfo(context: Context): KeyboardShortcutInfo { + val label: String = context.getString(labelRes) + val parts = shortcut.split("+") + val key = parts.last() + val keycode: Int = KeyEvent.keyCodeFromString(key) + val modifierFlags: Int = parts.dropLast(1).sumOf { getModifier(it) } + + return KeyboardShortcutInfo(label, keycode, modifierFlags) + } + + /** + * Maps a modifier string to its corresponding KeyEvent meta flag. + * + * @param modifier The modifier string (e.g., "Ctrl", "Alt", "Shift"). + * @return The corresponding KeyEvent meta flag. + */ + private fun getModifier(modifier: String): Int { + return when (modifier) { + "Ctrl" -> KeyEvent.META_CTRL_ON + "Alt" -> KeyEvent.META_ALT_ON + "Shift" -> KeyEvent.META_SHIFT_ON + else -> 0 + } + } + } + override val AXIS_RELATIVE_X: Int = MotionEvent.AXIS_RELATIVE_X override val AXIS_RELATIVE_Y: Int = MotionEvent.AXIS_RELATIVE_Y } diff --git a/AnkiDroid/src/main/res/values/02-strings.xml b/AnkiDroid/src/main/res/values/02-strings.xml index 8650fc5410f8..cfb2ca802e5a 100644 --- a/AnkiDroid/src/main/res/values/02-strings.xml +++ b/AnkiDroid/src/main/res/values/02-strings.xml @@ -429,4 +429,23 @@ opening the system text to speech settings fails"> Cannot Delete Card Type Deleting this card type will leave some notes without any cards. + + Show keyboard shortcuts dialog + Deck Picker + Study deck + Delete deck without confirmation + Note Editor + Deck selection dialog + Note selection spinner + Tags dialog + Perform preview + Card Template Editor + Edit front + Edit back + Edit styling + Add card + Rename card + Delete card + Copy markdown + diff --git a/AnkiDroid/src/main/res/values/06-statistics.xml b/AnkiDroid/src/main/res/values/06-statistics.xml index a9a802521c24..fb6757649605 100644 --- a/AnkiDroid/src/main/res/values/06-statistics.xml +++ b/AnkiDroid/src/main/res/values/06-statistics.xml @@ -23,5 +23,6 @@ <b>%1$.1f</b> months <b>%1$.1f</b> days <b>%1$.1f</b> hours + Open statistics diff --git a/AnkiDroid/src/main/res/values/07-cardbrowser.xml b/AnkiDroid/src/main/res/values/07-cardbrowser.xml index af85e81be132..f99b4e2c70df 100644 --- a/AnkiDroid/src/main/res/values/07-cardbrowser.xml +++ b/AnkiDroid/src/main/res/values/07-cardbrowser.xml @@ -90,4 +90,15 @@ %d card deleted %d cards deleted + + + Edit tags dialog + Export cards + Toggle mark + Reschedule cards + Reposition cards + Toggle bury cards + Toggle suspended cards + Display card info + Show order dialog \ No newline at end of file diff --git a/AnkiDroid/src/main/res/values/10-preferences.xml b/AnkiDroid/src/main/res/values/10-preferences.xml index a9e22b602fe4..0763806434ae 100644 --- a/AnkiDroid/src/main/res/values/10-preferences.xml +++ b/AnkiDroid/src/main/res/values/10-preferences.xml @@ -445,4 +445,7 @@ this formatter is used if the bind only applies to both the question and the ans Ignore display cutout Hide answer buttons Hide ‘Hard’ and ‘Easy’ buttons + + + Open settings