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 22, 2024
1 parent 7fbfcae commit 278d9e9
Show file tree
Hide file tree
Showing 13 changed files with 262 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
33 changes: 33 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/CardTemplateEditor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
)
}
}
22 changes: 22 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/NoteEditor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
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
89 changes: 89 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,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
Expand Down Expand Up @@ -60,6 +70,85 @@ 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 =
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
}
19 changes: 19 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,23 @@ 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 shortcuts dialog-->
<string name="show_keyboard_shortcuts_dialog" comment="Description of the shortcut that shows the keyboard shortcuts dialog">Show keyboard shortcuts dialog</string>
<string name="deck_picker_group" comment="Label for the group of shortcuts related to the deck picker">Deck Picker</string>
<string name="study_deck" comment="Description of the shortcut that starts studying the selected deck">Study deck</string>
<string name="delete_deck_without_confirmation" comment="Description of the shortcut that deletes the deck without asking for confirmation">Delete deck without confirmation</string>
<string name="note_editor_group" comment="Label for the group of shortcuts related to the note editor">Note Editor</string>
<string name="deck_selection_dialog" comment="Description of the shortcut that opens the deck selection dialog">Deck selection dialog</string>
<string name="note_selection_spinner" comment="Description of the shortcut that opens the note selection spinner">Note selection spinner</string>
<string name="tags_dialog" comment="Description of the shortcut that opens the tags dialog">Tags dialog</string>
<string name="perform_preview" comment="Description of the shortcut that previews the current note">Perform preview</string>
<string name="card_template_editor_group" comment="Label for the group of shortcuts related to the card template editor">Card Template Editor</string>
<string name="edit_front" comment="Description of the shortcut that shows the front side of the card template">Edit front</string>
<string name="edit_back" comment="Description of the shortcut that shows the back side of the card template">Edit back</string>
<string name="edit_styling" comment="Description of the shortcut that shows the styling side of the card template">Edit styling</string>
<string name="add_card" comment="Description of the shortcut that adds a new card to the template">Add card</string>
<string name="rename_card" comment="Description of the shortcut that renames the selected card">Rename card</string>
<string name="delete_card" comment="Description of the shortcut that deletes the selected card">Delete card</string>
<string name="copy_markdown" comment="Description of the shortcut that copies the markdown template to the clipboard">Copy markdown</string>

</resources>
1 change: 1 addition & 0 deletions AnkiDroid/src/main/res/values/06-statistics.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@
<string name="stats_overview_months">&lt;b&gt;%1$.1f&lt;/b&gt; months</string>
<string name="stats_overview_days">&lt;b&gt;%1$.1f&lt;/b&gt; days</string>
<string name="stats_overview_hours">&lt;b&gt;%1$.1f&lt;/b&gt; hours</string>
<string name="open_statistics" comment="Description of the shortcut that opens the statistics page">Open statistics</string>

</resources>
Loading

0 comments on commit 278d9e9

Please sign in to comment.