Skip to content

Commit

Permalink
feat: keyboard shortcuts helper
Browse files Browse the repository at this point in the history
  • Loading branch information
SanjaySargam committed Aug 20, 2024
1 parent 7fbfcae commit 0c326ac
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 0 deletions.
26 changes: 26 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/AnkiActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -102,6 +107,27 @@ open class AnkiActivity : AppCompatActivity, SimpleMessageDialogListener {
}
}

override fun onProvideKeyboardShortcuts(
data: MutableList<KeyboardShortcutGroup>,
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)
Expand Down
13 changes: 13 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/compat/Compat.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -217,6 +218,18 @@ interface Compat {
onReceiveContentListener: OnReceiveContentListener
)

/**
* @see CompatHelper.showKeyboardShortcutsDialog
*/
fun showKeyboardShortcutsDialog(
activity: Activity
)

/**
* Get all keyboard shortcuts
*/
fun getAllShortcuts(activity: Activity): List<KeyboardShortcutGroup>

/**
* Converts a locale to a 'two letter' code (ISO-639-1 + ISO 3166-1 alpha-2)
* Locale("spa", "MEX", "001") => Locale("es", "MX", "001")
Expand Down
6 changes: 6 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/compat/CompatHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
}
}
12 changes: 12 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/compat/CompatV23.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<KeyboardShortcutGroup> {
// No implementation available
return mutableListOf()
}

// Until API 26
@Throws(IOException::class)
override fun deleteFile(file: File) {
Expand Down
152 changes: 152 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/compat/CompatV24.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,17 @@ 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.R
import com.ichi2.anki.common.utils.android.isRobolectric
import com.ichi2.utils.ClipboardUtil.MEDIA_MIME_TYPES
import timber.log.Timber
Expand Down Expand Up @@ -60,6 +66,152 @@ open class CompatV24 : CompatV23(), Compat {
)
}

override fun showKeyboardShortcutsDialog(activity: Activity) {
activity.requestShowKeyboardShortcuts()
}

override fun getAllShortcuts(activity: Activity): List<KeyboardShortcutGroup> {
fun List<Shortcut>.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 = 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)
).toShortcutGroup(R.string.deck_picker_group)

val noteEditorShortcutGroup = listOf(
Shortcut("Ctrl+ENTER", R.string.save_note),
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)
).toShortcutGroup(R.string.note_editor_group)

val cardBrowserShortcutGroup =
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)
).toShortcutGroup(R.string.card_browser_context_menu)

val cardTemplateEditorShortcutGroup = 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_note),
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)
).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
}
31 changes: 31 additions & 0 deletions AnkiDroid/src/main/res/values/02-strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -429,4 +429,35 @@ opening the system text to speech settings fails">
<string name="orphan_note_title">Cannot Delete Card Type</string>
<string name="orphan_note_message">Deleting this card type will leave some notes without any cards.</string>

<!--Keyboard Shortcut Dialog-->
<string name="show_keyboard_shortcuts_dialog">Show keyboard shortcuts dialog</string>
<string name="deck_picker_group">Deck Picker</string>
<string name="study_deck">Study deck</string>
<string name="open_statistics">Open statistics</string>
<string name="delete_deck_without_confirmation">Delete deck without confirmation</string>
<string name="open_settings">Open settings</string>
<string name="note_editor_group">Note Editor</string>
<string name="save_note">Save note</string>
<string name="deck_selection_dialog">Deck selection dialog</string>
<string name="note_selection_spinner">Note selection spinner</string>
<string name="tags_dialog">Tags dialog</string>
<string name="perform_preview">Perform preview</string>
<string name="edit_tags_dialog">Edit tags dialog</string>
<string name="export_cards">Export cards</string>
<string name="toggle_mark">Toggle mark</string>
<string name="reschedule_cards">Reschedule cards</string>
<string name="reposition_cards">Reposition cards</string>
<string name="toggle_bury_cards">Toggle bury cards</string>
<string name="toggle_suspended_cards">Toggle suspended cards</string>
<string name="display_card_info">Display card info</string>
<string name="show_order_dialog">Show order dialog</string>
<string name="card_template_editor_group">Card Template Editor</string>
<string name="edit_front">Edit front</string>
<string name="edit_back">Edit back</string>
<string name="edit_styling">Edit styling</string>
<string name="add_card">Add card</string>
<string name="rename_card">Rename card</string>
<string name="delete_card">Delete card</string>
<string name="copy_markdown">Copy markdown</string>

</resources>

0 comments on commit 0c326ac

Please sign in to comment.