Skip to content

Commit

Permalink
feat: keyboard shortcuts helper
Browse files Browse the repository at this point in the history
Alt + K shows the keyboard shortcuts dialog
  • Loading branch information
SanjaySargam authored and david-allison committed Oct 6, 2024
1 parent a1d3a5e commit 75d9e56
Show file tree
Hide file tree
Showing 15 changed files with 400 additions and 59 deletions.
43 changes: 42 additions & 1 deletion AnkiDroid/src/main/java/com/ichi2/anki/AnkiActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,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 @@ -44,6 +47,7 @@ import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import com.google.android.material.color.MaterialColors
import com.google.android.material.snackbar.Snackbar
import com.ichi2.anim.ActivityTransitionAnimation
import com.ichi2.anim.ActivityTransitionAnimation.Direction
import com.ichi2.anim.ActivityTransitionAnimation.Direction.DEFAULT
Expand All @@ -60,7 +64,10 @@ import com.ichi2.anki.receiver.SdCardReceiver
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.registerReceiverCompat
import com.ichi2.compat.CompatV24
import com.ichi2.compat.ShortcutGroupProvider
import com.ichi2.compat.customtabs.CustomTabActivityHelper
import com.ichi2.compat.customtabs.CustomTabsFallback
import com.ichi2.compat.customtabs.CustomTabsHelper
Expand All @@ -73,7 +80,7 @@ import androidx.browser.customtabs.CustomTabsIntent.Builder as CustomTabsIntentB

@UiThread
@KotlinCleanup("set activityName")
open class AnkiActivity : AppCompatActivity, SimpleMessageDialogListener {
open class AnkiActivity : AppCompatActivity, SimpleMessageDialogListener, ShortcutGroupProvider, AnkiActivityProvider {

/**
* Receiver that informs us when a broadcast listen in [broadcastsActions] is received.
Expand All @@ -86,6 +93,7 @@ open class AnkiActivity : AppCompatActivity, SimpleMessageDialogListener {
/** The name of the parent class (example: 'Reviewer') */
private val activityName: String
val dialogHandler = DialogHandler(this)
override val ankiActivity = this

private val customTabActivityHelper: CustomTabActivityHelper = CustomTabActivityHelper()

Expand Down Expand Up @@ -624,6 +632,32 @@ open class AnkiActivity : AppCompatActivity, SimpleMessageDialogListener {
finish()
}

override fun onProvideKeyboardShortcuts(
data: MutableList<KeyboardShortcutGroup>,
menu: Menu?,
deviceId: Int
) {
val shortcutGroups = CompatHelper.compat.getShortcuts(this)
data.addAll(shortcutGroups)
super.onProvideKeyboardShortcuts(data, menu, deviceId)
}

override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
if (event.isAltPressed && keyCode == KeyEvent.KEYCODE_K) {
CompatHelper.compat.showKeyboardShortcutsDialog(this)
return true
}

val done = super.onKeyUp(keyCode, event)

// Show snackbar only if the current activity have shortcuts, a modifier key is pressed and the keyCode is an unmapped alphabet key
if (!done && shortcuts != null && (event.isCtrlPressed || event.isAltPressed || event.isMetaPressed) && (keyCode in KeyEvent.KEYCODE_A..KeyEvent.KEYCODE_Z) || (keyCode in KeyEvent.KEYCODE_NUMPAD_0..KeyEvent.KEYCODE_NUMPAD_9)) {
showSnackbar(R.string.show_shortcuts_message, Snackbar.LENGTH_SHORT)
return true
}
return false
}

/**
* If storage permissions are not granted, shows a toast message and finishes the activity.
*
Expand All @@ -641,6 +675,9 @@ open class AnkiActivity : AppCompatActivity, SimpleMessageDialogListener {
return false
}

override val shortcuts
get(): CompatV24.ShortcutGroup? = null

companion object {
const val DIALOG_FRAGMENT_TAG = "dialog"

Expand Down Expand Up @@ -674,3 +711,7 @@ fun Fragment.requireAnkiActivity(): AnkiActivity {
return requireActivity() as? AnkiActivity?
?: throw java.lang.IllegalStateException("Fragment $this not attached to an AnkiActivity.")
}

interface AnkiActivityProvider {
val ankiActivity: AnkiActivity
}
10 changes: 8 additions & 2 deletions AnkiDroid/src/main/java/com/ichi2/anki/AnkiFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import com.ichi2.async.CollectionLoader
import com.ichi2.compat.CompatV24
import com.ichi2.libanki.Collection
import com.ichi2.utils.increaseHorizontalPaddingOfOverflowMenuIcons
import com.ichi2.utils.tintOverflowMenuIcons
Expand All @@ -47,12 +48,12 @@ import timber.log.Timber
*/
// TODO: Consider refactoring to create AnkiInterface to consolidate common implementations between AnkiFragment and AnkiActivity.
// This could help reduce code repetition and improve maintainability.
open class AnkiFragment(@LayoutRes layout: Int) : Fragment(layout) {
open class AnkiFragment(@LayoutRes layout: Int) : Fragment(layout), AnkiActivityProvider {

val getColUnsafe: Collection
get() = CollectionManager.getColUnsafe()

val ankiActivity: AnkiActivity
override val ankiActivity: AnkiActivity
get() = requireAnkiActivity()

val mainToolbar: Toolbar
Expand Down Expand Up @@ -218,4 +219,9 @@ open class AnkiFragment(@LayoutRes layout: Int) : Fragment(layout) {
requireActivity().finish()
return false
}

/**
* Lists of shortcuts for this fragment, and the IdRes of the name of this shortcut group.
*/
open val shortcuts: CompatV24.ShortcutGroup? = null
}
44 changes: 44 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ import com.ichi2.anki.utils.roundedTimeSpanUnformatted
import com.ichi2.anki.widgets.DeckDropDownAdapter.SubtitleListener
import com.ichi2.annotations.NeedsTest
import com.ichi2.async.renderBrowserQA
import com.ichi2.compat.CompatHelper
import com.ichi2.compat.CompatV24
import com.ichi2.compat.shortcut
import com.ichi2.libanki.Card
import com.ichi2.libanki.CardId
import com.ichi2.libanki.ChangeManager
Expand Down Expand Up @@ -144,6 +147,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import net.ankiweb.rsdroid.RustCleanup
import net.ankiweb.rsdroid.Translations
import timber.log.Timber
import kotlin.math.abs
import kotlin.math.ceil
Expand Down Expand Up @@ -672,6 +676,10 @@ open class CardBrowser :
Timber.i("Ctrl+K: Toggle Mark")
toggleMark()
return true
} else if (event.isAltPressed) {
Timber.i("Alt+K: Show keyboard shortcuts dialog")
CompatHelper.compat.showKeyboardShortcutsDialog(this)
return true
}
}
KeyEvent.KEYCODE_R -> {
Expand Down Expand Up @@ -2363,6 +2371,42 @@ open class CardBrowser :
}
}

override val shortcuts
get() = CompatV24.ShortcutGroup(
listOf(
shortcut("Ctrl+Shift+A", R.string.edit_tags_dialog),
shortcut("Ctrl+A", R.string.card_browser_select_all),
shortcut("Ctrl+Shift+E", Translations::exportingExport),
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", Translations::browsingToggleMark),
shortcut("Ctrl+Alt+R", Translations::browsingReschedule),
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("Ctrl+T", R.string.card_browser_search_by_tag),
shortcut("Ctrl+Shift+S", Translations::actionsReposition),
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", Translations::browsingToggleBury),
shortcut("Ctrl+J", Translations::browsingToggleSuspend),
shortcut("Ctrl+Shift+I", Translations::actionsCardInfo),
shortcut("Ctrl+O", R.string.show_order_dialog),
shortcut("Ctrl+M", R.string.card_browser_show_marked),
shortcut("Esc", R.string.card_browser_select_none),
shortcut("Ctrl+1", R.string.gesture_flag_red),
shortcut("Ctrl+2", R.string.gesture_flag_orange),
shortcut("Ctrl+3", R.string.gesture_flag_green),
shortcut("Ctrl+4", R.string.gesture_flag_blue),
shortcut("Ctrl+5", R.string.gesture_flag_pink),
shortcut("Ctrl+6", R.string.gesture_flag_turquoise),
shortcut("Ctrl+7", R.string.gesture_flag_purple)
),
R.string.card_browser_context_menu
)

companion object {
/**
* Argument key to add on change deck dialog,
Expand Down
129 changes: 76 additions & 53 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,8 @@ 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
import com.ichi2.compat.shortcut
import com.ichi2.libanki.Collection
import com.ichi2.libanki.Note
import com.ichi2.libanki.NoteId
Expand All @@ -86,6 +88,7 @@ import com.ichi2.ui.FixedTextView
import com.ichi2.utils.KotlinCleanup
import com.ichi2.utils.copyToClipboard
import com.ichi2.utils.jsonObjectIterable
import net.ankiweb.rsdroid.Translations
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
Expand Down Expand Up @@ -325,61 +328,62 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener {

override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
val currentFragment = currentFragment ?: return super.onKeyUp(keyCode, event)
if (event.isCtrlPressed) {
when (keyCode) {
KeyEvent.KEYCODE_P -> {
Timber.i("Ctrl+P: Perform preview from keypress")
currentFragment.performPreview()
}
KeyEvent.KEYCODE_1 -> {
Timber.i("Ctrl+1: Edit front template from keypress")
currentFragment.bottomNavigation.selectedItemId = R.id.front_edit
}
KeyEvent.KEYCODE_2 -> {
Timber.i("Ctrl+2: Edit back template from keypress")
currentFragment.bottomNavigation.selectedItemId = R.id.back_edit
}
KeyEvent.KEYCODE_3 -> {
Timber.i("Ctrl+3: Edit styling from keypress")
currentFragment.bottomNavigation.selectedItemId = R.id.styling_edit
}
KeyEvent.KEYCODE_S -> {
Timber.i("Ctrl+S: Save note from keypress")
currentFragment.saveNoteType()
}
KeyEvent.KEYCODE_I -> {
Timber.i("Ctrl+I: Insert field from keypress")
currentFragment.showInsertFieldDialog()
}
KeyEvent.KEYCODE_A -> {
Timber.i("Ctrl+A: Add card template from keypress")
currentFragment.addCardTemplate()
}
KeyEvent.KEYCODE_R -> {
Timber.i("Ctrl+R: Rename card from keypress")
currentFragment.showRenameDialog()
}
KeyEvent.KEYCODE_B -> {
Timber.i("Ctrl+B: Open browser appearance from keypress")
currentFragment.openBrowserAppearance()
}
KeyEvent.KEYCODE_D -> {
Timber.i("Ctrl+D: Delete card from keypress")
currentFragment.deleteCardTemplate()
}
KeyEvent.KEYCODE_O -> {
Timber.i("Ctrl+O: Display deck override dialog from keypress")
currentFragment.displayDeckOverrideDialog(currentFragment.tempModel)
}
KeyEvent.KEYCODE_M -> {
Timber.i("Ctrl+M: Copy markdown from keypress")
currentFragment.copyMarkdownTemplateToClipboard()
}
else -> return super.onKeyUp(keyCode, event)
if (!event.isCtrlPressed) { return super.onKeyUp(keyCode, event) }
when (keyCode) {
KeyEvent.KEYCODE_P -> {
Timber.i("Ctrl+P: Perform preview from keypress")
currentFragment.performPreview()
}
KeyEvent.KEYCODE_1 -> {
Timber.i("Ctrl+1: Edit front template from keypress")
currentFragment.bottomNavigation.selectedItemId = R.id.front_edit
}
KeyEvent.KEYCODE_2 -> {
Timber.i("Ctrl+2: Edit back template from keypress")
currentFragment.bottomNavigation.selectedItemId = R.id.back_edit
}
KeyEvent.KEYCODE_3 -> {
Timber.i("Ctrl+3: Edit styling from keypress")
currentFragment.bottomNavigation.selectedItemId = R.id.styling_edit
}
KeyEvent.KEYCODE_S -> {
Timber.i("Ctrl+S: Save note from keypress")
currentFragment.saveNoteType()
}
KeyEvent.KEYCODE_I -> {
Timber.i("Ctrl+I: Insert field from keypress")
currentFragment.showInsertFieldDialog()
}
KeyEvent.KEYCODE_A -> {
Timber.i("Ctrl+A: Add card template from keypress")
currentFragment.addCardTemplate()
}
KeyEvent.KEYCODE_R -> {
Timber.i("Ctrl+R: Rename card from keypress")
currentFragment.showRenameDialog()
}
KeyEvent.KEYCODE_B -> {
Timber.i("Ctrl+B: Open browser appearance from keypress")
currentFragment.openBrowserAppearance()
}
KeyEvent.KEYCODE_D -> {
Timber.i("Ctrl+D: Delete card from keypress")
currentFragment.deleteCardTemplate()
}
KeyEvent.KEYCODE_O -> {
Timber.i("Ctrl+O: Display deck override dialog from keypress")
currentFragment.displayDeckOverrideDialog(currentFragment.tempModel)
}
KeyEvent.KEYCODE_M -> {
Timber.i("Ctrl+M: Copy markdown from keypress")
currentFragment.copyMarkdownTemplateToClipboard()
}
else -> {
return super.onKeyUp(keyCode, event)
}
return true
}
return super.onKeyUp(keyCode, event)
// We reach this only if we didn't reach the `else` case.
return true
}

@get:VisibleForTesting
Expand Down Expand Up @@ -424,6 +428,25 @@ open class CardTemplateEditor : AnkiActivity(), DeckSelectionListener {
}
}

override val shortcuts
get() = CompatV24.ShortcutGroup(
listOf(
shortcut("Ctrl+P", R.string.card_editor_preview_card),
shortcut("Ctrl+1", R.string.edit_front_template),
shortcut("Ctrl+2", R.string.edit_back_template),
shortcut("Ctrl+3", R.string.edit_styling),
shortcut("Ctrl+S", R.string.save),
shortcut("Ctrl+I", R.string.card_template_editor_insert_field),
shortcut("Ctrl+A", Translations::cardTemplatesAddCardType),
shortcut("Ctrl+R", Translations::cardTemplatesRenameCardType),
shortcut("Ctrl+B", R.string.edit_browser_appearance),
shortcut("Ctrl+D", Translations::cardTemplatesRemoveCardType),
shortcut("Ctrl+O", Translations::cardTemplatesDeckOverride),
shortcut("Ctrl+M", R.string.copy_the_template)
),
R.string.card_template_editor_group
)

class CardTemplateFragment : Fragment() {
private val refreshFragmentHandler = Handler(Looper.getMainLooper())
private var currentEditorTitle: FixedTextView? = null
Expand Down
Loading

0 comments on commit 75d9e56

Please sign in to comment.