Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GSoC'24]: Implement Keyboard Shortcuts Helper #16880

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
david-allison marked this conversation as resolved.
Show resolved Hide resolved
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