diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/reviewer/ReviewerMenuSettingsAdapter.kt b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/reviewer/ReviewerMenuSettingsAdapter.kt new file mode 100644 index 000000000000..9181eebd439b --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/reviewer/ReviewerMenuSettingsAdapter.kt @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2024 Brayan Oliveira + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +package com.ichi2.anki.preferences.reviewer + +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.appcompat.widget.AppCompatImageView +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.textview.MaterialTextView +import com.ichi2.anki.R + +class ReviewerMenuSettingsAdapter( + private val items: List, +) : RecyclerView.Adapter() { + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int, + ): RecyclerView.ViewHolder { + val inflater = LayoutInflater.from(parent.context) + return when (viewType) { + ReviewerMenuSettingsRecyclerItem.ACTION_VIEW_TYPE -> { + val itemView = inflater.inflate(R.layout.reviewer_menu_item, parent, false) + ActionViewHolder(itemView) + } + ReviewerMenuSettingsRecyclerItem.DISPLAY_TYPE_VIEW_TYPE -> { + val itemView = inflater.inflate(R.layout.reviewer_menu_display_type, parent, false) + DisplayTypeViewHolder(itemView) + } + else -> throw IllegalArgumentException("Unexpected viewType") + } + } + + override fun onBindViewHolder( + holder: RecyclerView.ViewHolder, + position: Int, + ) { + val item = items[position] + when (holder) { + is ActionViewHolder -> holder.bind((item as ReviewerMenuSettingsRecyclerItem.Action).viewerAction) + is DisplayTypeViewHolder -> holder.bind((item as ReviewerMenuSettingsRecyclerItem.DisplayType).menuDisplayType) + } + } + + override fun getItemCount(): Int = items.size + + override fun getItemViewType(position: Int): Int = items[position].viewType + + private var onDragListener: ((RecyclerView.ViewHolder) -> Unit)? = null + + fun setOnDragListener(onDragListener: (RecyclerView.ViewHolder) -> Unit) { + this.onDragListener = onDragListener + } + + private inner class ActionViewHolder( + itemView: View, + ) : RecyclerView.ViewHolder(itemView) { + /** @see [R.layout.reviewer_menu_item] */ + fun bind(action: ViewerAction) { + action.titleRes.let { itemView.findViewById(R.id.title).setText(it) } + action.drawableRes?.let { itemView.findViewById(R.id.icon).setBackgroundResource(it) } + + itemView.findViewById(R.id.drag_handle).setOnTouchListener { _, event -> + if (event.action == MotionEvent.ACTION_DOWN) { + onDragListener?.invoke(this) + } + return@setOnTouchListener false + } + } + } + + private class DisplayTypeViewHolder( + itemView: View, + ) : RecyclerView.ViewHolder(itemView) { + /** @see [R.layout.reviewer_menu_display_type] */ + fun bind(displayCategory: MenuDisplayType) { + itemView.findViewById(R.id.title).setText(displayCategory.title) + } + } +} + +/** + * @param viewType type to be returned at [RecyclerView.Adapter.getItemViewType] + */ +sealed class ReviewerMenuSettingsRecyclerItem( + val viewType: Int, +) { + data class Action( + val viewerAction: ViewerAction, + ) : ReviewerMenuSettingsRecyclerItem(ACTION_VIEW_TYPE) + + data class DisplayType( + val menuDisplayType: MenuDisplayType, + ) : ReviewerMenuSettingsRecyclerItem(DISPLAY_TYPE_VIEW_TYPE) + + companion object { + const val ACTION_VIEW_TYPE = 0 + const val DISPLAY_TYPE_VIEW_TYPE = 1 + } +} diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/reviewer/ReviewerMenuSettingsFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/reviewer/ReviewerMenuSettingsFragment.kt index 51e7208caa97..0d46e74c0a60 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/reviewer/ReviewerMenuSettingsFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/reviewer/ReviewerMenuSettingsFragment.kt @@ -15,7 +15,109 @@ */ package com.ichi2.anki.preferences.reviewer +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import androidx.appcompat.widget.ActionMenuView import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.appbar.MaterialToolbar +import com.google.android.material.snackbar.Snackbar import com.ichi2.anki.R +import com.ichi2.anki.snackbar.showSnackbar +import com.ichi2.anki.utils.ext.sharedPrefs +import kotlinx.coroutines.launch -class ReviewerMenuSettingsFragment : Fragment(R.layout.preferences_reviewer_menu) +class ReviewerMenuSettingsFragment : + Fragment(R.layout.preferences_reviewer_menu), + OnClearViewListener, + ActionMenuView.OnMenuItemClickListener { + override fun onViewCreated( + view: View, + savedInstanceState: Bundle?, + ) { + super.onViewCreated(view, savedInstanceState) + + setupRecyclerView(view) + + view.findViewById(R.id.toolbar).setNavigationOnClickListener { + requireActivity().finish() + } + view.findViewById(R.id.reviewer_menu_view).apply { + setOnMenuItemClickListener(this@ReviewerMenuSettingsFragment) + } + } + + private fun setupRecyclerView(view: View) { + val menuItems = MenuDisplayType.getMenuItems(sharedPrefs()) + + fun section(displayType: MenuDisplayType): List = + listOf(ReviewerMenuSettingsRecyclerItem.DisplayType(displayType)) + + menuItems.getValue(displayType).map { ReviewerMenuSettingsRecyclerItem.Action(it) } + + val recyclerViewItems = MenuDisplayType.entries.flatMap { section(it) } + + val callback = ReviewerMenuSettingsTouchHelperCallback(recyclerViewItems) + callback.setOnClearViewListener(this) + val itemTouchHelper = ItemTouchHelper(callback) + + val adapter = + ReviewerMenuSettingsAdapter(recyclerViewItems).apply { + setOnDragListener { viewHolder -> + itemTouchHelper.startDrag(viewHolder) + } + } + view.findViewById(R.id.recycler_view).apply { + layoutManager = LinearLayoutManager(requireContext()) + this.adapter = adapter + itemTouchHelper.attachToRecyclerView(this) + } + } + + override fun onClearView(items: List) { + val menuOnlyItemsIndex = + items.indexOfFirst { + it is ReviewerMenuSettingsRecyclerItem.DisplayType && it.menuDisplayType == MenuDisplayType.MENU_ONLY + } + val disabledItemsIndex = + items.indexOfFirst { + it is ReviewerMenuSettingsRecyclerItem.DisplayType && it.menuDisplayType == MenuDisplayType.DISABLED + } + + val alwaysShowItems = + items + .subList(1, menuOnlyItemsIndex) + .mapNotNull { (it as? ReviewerMenuSettingsRecyclerItem.Action)?.viewerAction } + val menuOnlyItems = + items + .subList(menuOnlyItemsIndex, disabledItemsIndex) + .mapNotNull { (it as? ReviewerMenuSettingsRecyclerItem.Action)?.viewerAction } + val disabledItems = + items + .subList(disabledItemsIndex, items.lastIndex) + .mapNotNull { (it as? ReviewerMenuSettingsRecyclerItem.Action)?.viewerAction } + + val preferences = sharedPrefs() + MenuDisplayType.ALWAYS.setPreferenceValue(preferences, alwaysShowItems) + MenuDisplayType.MENU_ONLY.setPreferenceValue(preferences, menuOnlyItems) + MenuDisplayType.DISABLED.setPreferenceValue(preferences, disabledItems) + + lifecycleScope.launch { + val menu = requireView().findViewById(R.id.reviewer_menu_view) + menu.clear() + menu.addActions(alwaysShowItems, menuOnlyItems) + menu.setFlagTitles() + } + } + + override fun onMenuItemClick(item: MenuItem): Boolean { + val action = ViewerAction.fromId(item.itemId) + if (action.isSubMenu()) return false + + item.title?.let { showSnackbar(it, Snackbar.LENGTH_SHORT) } + return true + } +} diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/reviewer/ReviewerMenuSettingsTouchHelperCallback.kt b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/reviewer/ReviewerMenuSettingsTouchHelperCallback.kt new file mode 100644 index 000000000000..4e0b6a5860da --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/reviewer/ReviewerMenuSettingsTouchHelperCallback.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2024 Brayan Oliveira + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +package com.ichi2.anki.preferences.reviewer + +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView +import java.util.Collections + +/** + * A [ItemTouchHelper.Callback] for the [ReviewerMenuSettingsAdapter]. + * + * It allows drag and dropping of [ReviewerMenuSettingsAdapter.ActionViewHolder], but not of + * [ReviewerMenuSettingsAdapter.DisplayTypeViewHolder], or any kind of swipe. + * + * [setOnClearViewListener] can be used to set an action to run after the user interaction has ended + * (see [clearView]). + */ +class ReviewerMenuSettingsTouchHelperCallback( + private val items: List, +) : ItemTouchHelper.Callback() { + private val movementFlags = makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0) + + override fun getMovementFlags( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + ): Int = + if (viewHolder.itemViewType == ReviewerMenuSettingsRecyclerItem.DISPLAY_TYPE_VIEW_TYPE) { + 0 + } else { + movementFlags + } + + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder, + ): Boolean { + val fromPosition = viewHolder.absoluteAdapterPosition + val toPosition = target.absoluteAdapterPosition + + // `Always show` should always be the first element, so don't allow moving above it + if (toPosition == 0) return false + + Collections.swap(items, fromPosition, toPosition) + recyclerView.adapter?.notifyItemMoved(fromPosition, toPosition) + return true + } + + override fun clearView( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + ) { + super.clearView(recyclerView, viewHolder) + onClearViewListener?.onClearView(items) + } + + override fun isLongPressDragEnabled(): Boolean = false + + private var onClearViewListener: OnClearViewListener? = null + + /** Sets a listener to be called after [clearView] */ + fun setOnClearViewListener(listener: OnClearViewListener) { + onClearViewListener = listener + } + + override fun onSwiped( + viewHolder: RecyclerView.ViewHolder, + direction: Int, + ) { + // do nothing + } +} + +fun interface OnClearViewListener { + fun onClearView(items: List) +} diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/reviewer/ReviewerMenuView.kt b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/reviewer/ReviewerMenuView.kt new file mode 100644 index 000000000000..d0c2d698fd8d --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/reviewer/ReviewerMenuView.kt @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2024 Brayan Oliveira + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +package com.ichi2.anki.preferences.reviewer + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuItem +import android.view.ViewTreeObserver.OnGlobalLayoutListener +import android.widget.HorizontalScrollView +import android.widget.LinearLayout +import androidx.appcompat.view.menu.MenuBuilder +import androidx.appcompat.view.menu.MenuItemImpl +import androidx.appcompat.widget.ActionMenuView +import androidx.lifecycle.findViewTreeLifecycleOwner +import androidx.lifecycle.lifecycleScope +import com.ichi2.anki.Flag +import com.ichi2.anki.R +import com.ichi2.anki.preferences.sharedPrefs +import kotlinx.coroutines.launch + +/** + * View for displaying the reviewer menu actions. + * + * It works like an [ActionMenuView], but the visible action items are horizontally scrollable by + * using an [ActionMenuView] inside a [HorizontalScrollView], and another one for the overflown + * actions. + * + * It also initializes itself, which makes possible to see it in Android Studio layout previews. + * + * @see [R.layout.reviewer_menu_item] + */ +class ReviewerMenuView + @JvmOverloads + constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + ) : LinearLayout(context, attrs, defStyleAttr) { + private val frontMenu: Menu + private val overflowMenu: Menu + + init { + val inflater = LayoutInflater.from(context).inflate(R.layout.reviewer_menu_view, this, true) + frontMenu = inflater.findViewById(R.id.front_menu_view).menu + overflowMenu = + inflater.findViewById(R.id.overflow_menu_view).menu.apply { + (this as? MenuBuilder)?.setOptionalIconsVisible(true) + } + setupMenus() + } + + fun clear() { + frontMenu.clear() + overflowMenu.clear() + } + + fun findItem(id: Int): MenuItemImpl? = (frontMenu.findItem(id) ?: overflowMenu.findItem(id)) as? MenuItemImpl + + fun setOnMenuItemClickListener(listener: ActionMenuView.OnMenuItemClickListener) { + findViewById(R.id.front_menu_view).setOnMenuItemClickListener(listener) + findViewById(R.id.overflow_menu_view).setOnMenuItemClickListener(listener) + } + + fun addActions( + alwaysShow: List, + menuOnly: List, + ) { + addActionsToMenu(frontMenu, alwaysShow, MenuItem.SHOW_AS_ACTION_ALWAYS) + addActionsToMenu(overflowMenu, menuOnly, MenuItem.SHOW_AS_ACTION_NEVER) + + val submenuActions = ViewerAction.entries.filter { it.parentMenu != null } + for (action in submenuActions) { + val subMenu = findItem(action.parentMenu!!.menuId)?.subMenu ?: continue + val title = resources.getString(action.titleRes) + subMenu.add(Menu.NONE, action.menuId, Menu.NONE, title)?.apply { + action.drawableRes?.let { setIcon(it) } + } + } + } + + suspend fun setFlagTitles() { + val submenu = findItem(R.id.action_flag)?.subMenu ?: return + for ((flag, name) in Flag.queryDisplayNames()) { + submenu.findItem(flag.id)?.title = name + } + } + + private fun addActionsToMenu( + menu: Menu, + actions: List, + menuActionType: Int, + ) { + val subMenus = ViewerAction.getSubMenus() + for (action in actions) { + val title = resources.getString(action.titleRes) + val menuItem = + if (action in subMenus) { + menu.addSubMenu(Menu.NONE, action.menuId, Menu.NONE, title).item + } else { + menu.add(Menu.NONE, action.menuId, Menu.NONE, title) + } + with(menuItem) { + action.drawableRes?.let { setIcon(it) } + setShowAsAction(menuActionType) + } + } + } + + private fun setupMenus() { + val menuItems = MenuDisplayType.getMenuItems(context.sharedPrefs(), MenuDisplayType.ALWAYS, MenuDisplayType.MENU_ONLY) + addActions(menuItems.getValue(MenuDisplayType.ALWAYS), menuItems.getValue(MenuDisplayType.MENU_ONLY)) + // wait until attached to a fragment or activity to launch the coroutine to setup flags + viewTreeObserver.addOnGlobalLayoutListener( + object : OnGlobalLayoutListener { + override fun onGlobalLayout() { + findViewTreeLifecycleOwner()?.lifecycleScope?.launch { + setFlagTitles() + } + viewTreeObserver.removeOnGlobalLayoutListener(this) + } + }, + ) + } + } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/ReviewerFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/ReviewerFragment.kt index b5e72ecd3ca9..f6c1cdaedbb5 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/ReviewerFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/ReviewerFragment.kt @@ -27,8 +27,8 @@ import android.webkit.WebView import android.widget.FrameLayout import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.StringRes -import androidx.appcompat.view.menu.MenuBuilder -import androidx.appcompat.widget.Toolbar +import androidx.appcompat.view.menu.SubMenuBuilder +import androidx.appcompat.widget.ActionMenuView import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat @@ -48,6 +48,40 @@ import com.ichi2.anki.NoteEditor import com.ichi2.anki.R import com.ichi2.anki.cardviewer.CardMediaPlayer import com.ichi2.anki.noteeditor.NoteEditorLauncher +import com.ichi2.anki.preferences.reviewer.ReviewerMenuView +import com.ichi2.anki.preferences.reviewer.ViewerAction +import com.ichi2.anki.preferences.reviewer.ViewerAction.ADD_NOTE +import com.ichi2.anki.preferences.reviewer.ViewerAction.BURY_CARD +import com.ichi2.anki.preferences.reviewer.ViewerAction.BURY_MENU +import com.ichi2.anki.preferences.reviewer.ViewerAction.BURY_NOTE +import com.ichi2.anki.preferences.reviewer.ViewerAction.CARD_INFO +import com.ichi2.anki.preferences.reviewer.ViewerAction.DECK_OPTIONS +import com.ichi2.anki.preferences.reviewer.ViewerAction.DELETE +import com.ichi2.anki.preferences.reviewer.ViewerAction.EDIT_NOTE +import com.ichi2.anki.preferences.reviewer.ViewerAction.FLAG_BLUE +import com.ichi2.anki.preferences.reviewer.ViewerAction.FLAG_GREEN +import com.ichi2.anki.preferences.reviewer.ViewerAction.FLAG_MENU +import com.ichi2.anki.preferences.reviewer.ViewerAction.FLAG_ORANGE +import com.ichi2.anki.preferences.reviewer.ViewerAction.FLAG_PINK +import com.ichi2.anki.preferences.reviewer.ViewerAction.FLAG_PURPLE +import com.ichi2.anki.preferences.reviewer.ViewerAction.FLAG_RED +import com.ichi2.anki.preferences.reviewer.ViewerAction.FLAG_TURQUOISE +import com.ichi2.anki.preferences.reviewer.ViewerAction.MARK +import com.ichi2.anki.preferences.reviewer.ViewerAction.REDO +import com.ichi2.anki.preferences.reviewer.ViewerAction.SUSPEND_CARD +import com.ichi2.anki.preferences.reviewer.ViewerAction.SUSPEND_MENU +import com.ichi2.anki.preferences.reviewer.ViewerAction.SUSPEND_NOTE +import com.ichi2.anki.preferences.reviewer.ViewerAction.UNDO +import com.ichi2.anki.preferences.reviewer.ViewerAction.UNSET_FLAG +import com.ichi2.anki.preferences.reviewer.ViewerAction.USER_ACTION_1 +import com.ichi2.anki.preferences.reviewer.ViewerAction.USER_ACTION_2 +import com.ichi2.anki.preferences.reviewer.ViewerAction.USER_ACTION_3 +import com.ichi2.anki.preferences.reviewer.ViewerAction.USER_ACTION_4 +import com.ichi2.anki.preferences.reviewer.ViewerAction.USER_ACTION_5 +import com.ichi2.anki.preferences.reviewer.ViewerAction.USER_ACTION_6 +import com.ichi2.anki.preferences.reviewer.ViewerAction.USER_ACTION_7 +import com.ichi2.anki.preferences.reviewer.ViewerAction.USER_ACTION_8 +import com.ichi2.anki.preferences.reviewer.ViewerAction.USER_ACTION_9 import com.ichi2.anki.previewer.CardViewerActivity import com.ichi2.anki.previewer.CardViewerFragment import com.ichi2.anki.snackbar.BaseSnackbarBuilderProvider @@ -55,15 +89,16 @@ import com.ichi2.anki.snackbar.SnackbarBuilder import com.ichi2.anki.snackbar.showSnackbar import com.ichi2.anki.utils.ext.collectIn import com.ichi2.anki.utils.ext.collectLatestIn +import com.ichi2.anki.utils.ext.menu +import com.ichi2.anki.utils.ext.removeSubMenu import com.ichi2.anki.utils.ext.sharedPrefs import com.ichi2.libanki.sched.Counts -import com.ichi2.utils.increaseHorizontalPaddingOfOverflowMenuIcons import kotlinx.coroutines.launch class ReviewerFragment : CardViewerFragment(R.layout.reviewer2), BaseSnackbarBuilderProvider, - Toolbar.OnMenuItemClickListener { + ActionMenuView.OnMenuItemClickListener { override val viewModel: ReviewerViewModel by viewModels { ReviewerViewModel.factory(CardMediaPlayer()) } @@ -88,20 +123,15 @@ class ReviewerFragment : ) { super.onViewCreated(view, savedInstanceState) - setupImmersiveMode(view) - setupAnswerButtons(view) - setupCounts(view) - view.findViewById(R.id.toolbar).apply { - setOnMenuItemClickListener(this@ReviewerFragment) setNavigationOnClickListener { requireActivity().onBackPressedDispatcher.onBackPressed() } - (menu as? MenuBuilder)?.let { - setupMenuItems(it) - it.setOptionalIconsVisible(true) - requireContext().increaseHorizontalPaddingOfOverflowMenuIcons(it) - } } + setupImmersiveMode(view) + setupAnswerButtons(view) + setupCounts(view) + setupMenu(view) + viewModel.actionFeedbackFlow .flowWithLifecycle(lifecycle) .collectIn(lifecycleScope) { message -> @@ -126,36 +156,41 @@ class ReviewerFragment : // TODO override fun onMenuItemClick(item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_add_note -> launchAddNote() - R.id.action_bury_card -> viewModel.buryCard() - R.id.action_bury_note -> viewModel.buryNote() - R.id.action_card_info -> launchCardInfo() - R.id.action_delete -> viewModel.deleteNote() - R.id.action_edit -> launchEditNote() - R.id.action_mark -> viewModel.toggleMark() - R.id.action_open_deck_options -> launchDeckOptions() - R.id.action_redo -> viewModel.redo() - R.id.action_suspend_card -> viewModel.suspendCard() - R.id.action_suspend_note -> viewModel.suspendNote() - R.id.action_undo -> viewModel.undo() - R.id.flag_none -> viewModel.setFlag(Flag.NONE) - R.id.flag_red -> viewModel.setFlag(Flag.RED) - R.id.flag_orange -> viewModel.setFlag(Flag.ORANGE) - R.id.flag_green -> viewModel.setFlag(Flag.GREEN) - R.id.flag_blue -> viewModel.setFlag(Flag.BLUE) - R.id.flag_pink -> viewModel.setFlag(Flag.PINK) - R.id.flag_turquoise -> viewModel.setFlag(Flag.TURQUOISE) - R.id.flag_purple -> viewModel.setFlag(Flag.PURPLE) - R.id.user_action_1 -> viewModel.userAction(1) - R.id.user_action_2 -> viewModel.userAction(2) - R.id.user_action_3 -> viewModel.userAction(3) - R.id.user_action_4 -> viewModel.userAction(4) - R.id.user_action_5 -> viewModel.userAction(5) - R.id.user_action_6 -> viewModel.userAction(6) - R.id.user_action_7 -> viewModel.userAction(7) - R.id.user_action_8 -> viewModel.userAction(8) - R.id.user_action_9 -> viewModel.userAction(9) + if (item.hasSubMenu()) return false + val action = ViewerAction.fromId(item.itemId) + when (action) { + ADD_NOTE -> launchAddNote() + CARD_INFO -> launchCardInfo() + DECK_OPTIONS -> launchDeckOptions() + EDIT_NOTE -> launchEditNote() + DELETE -> viewModel.deleteNote() + MARK -> viewModel.toggleMark() + REDO -> viewModel.redo() + UNDO -> viewModel.undo() + BURY_NOTE -> viewModel.buryNote() + BURY_CARD -> viewModel.buryCard() + SUSPEND_NOTE -> viewModel.suspendNote() + SUSPEND_CARD -> viewModel.suspendCard() + UNSET_FLAG -> viewModel.setFlag(Flag.NONE) + FLAG_RED -> viewModel.setFlag(Flag.RED) + FLAG_ORANGE -> viewModel.setFlag(Flag.ORANGE) + FLAG_BLUE -> viewModel.setFlag(Flag.BLUE) + FLAG_GREEN -> viewModel.setFlag(Flag.GREEN) + FLAG_PINK -> viewModel.setFlag(Flag.PINK) + FLAG_TURQUOISE -> viewModel.setFlag(Flag.TURQUOISE) + FLAG_PURPLE -> viewModel.setFlag(Flag.PURPLE) + USER_ACTION_1 -> viewModel.userAction(1) + USER_ACTION_2 -> viewModel.userAction(2) + USER_ACTION_3 -> viewModel.userAction(3) + USER_ACTION_4 -> viewModel.userAction(4) + USER_ACTION_5 -> viewModel.userAction(5) + USER_ACTION_6 -> viewModel.userAction(6) + USER_ACTION_7 -> viewModel.userAction(7) + USER_ACTION_8 -> viewModel.userAction(8) + USER_ACTION_9 -> viewModel.userAction(9) + SUSPEND_MENU -> viewModel.suspendCard() + BURY_MENU -> viewModel.buryCard() + FLAG_MENU -> return false } return true } @@ -250,82 +285,87 @@ class ReviewerFragment : } } - private fun setupFlagMenu(menu: Menu) { - val submenu = menu.findItem(R.id.action_flag).subMenu - lifecycleScope.launch { - for ((flag, name) in Flag.queryDisplayNames()) { - submenu - ?.add(Menu.NONE, flag.id, Menu.NONE, name) - ?.setIcon(flag.drawableRes) + private fun setupBury(menu: ReviewerMenuView) { + val menuItem = menu.findItem(BURY_MENU.menuId) ?: return + val flow = viewModel.canBuryNoteFlow.flowWithLifecycle(lifecycle) + flow.collectLatestIn(lifecycleScope) { canBuryNote -> + if (canBuryNote) { + if (menuItem.hasSubMenu()) return@collectLatestIn + menuItem.setTitle(BURY_MENU.titleRes) + val submenu = + SubMenuBuilder(menu.context, menuItem.menu, menuItem).apply { + add(Menu.NONE, BURY_NOTE.menuId, Menu.NONE, BURY_NOTE.titleRes) + add(Menu.NONE, BURY_CARD.menuId, Menu.NONE, BURY_CARD.titleRes) + } + menuItem.setSubMenu(submenu) + } else { + menuItem.removeSubMenu() + menuItem.setTitle(BURY_CARD.titleRes) } } + } + + private fun setupSuspend(menu: ReviewerMenuView) { + val menuItem = menu.findItem(SUSPEND_MENU.menuId) ?: return + val flow = viewModel.canSuspendNoteFlow.flowWithLifecycle(lifecycle) + flow.collectLatestIn(lifecycleScope) { canSuspendNote -> + if (canSuspendNote) { + if (menuItem.hasSubMenu()) return@collectLatestIn + menuItem.setTitle(SUSPEND_MENU.titleRes) + val submenu = + SubMenuBuilder(menu.context, menuItem.menu, menuItem).apply { + add(Menu.NONE, SUSPEND_NOTE.menuId, Menu.NONE, SUSPEND_NOTE.titleRes) + add(Menu.NONE, SUSPEND_CARD.menuId, Menu.NONE, SUSPEND_CARD.titleRes) + } + menuItem.setSubMenu(submenu) + } else { + menuItem.removeSubMenu() + menuItem.setTitle(SUSPEND_CARD.titleRes) + } + } + } + + private fun setupMenu(view: View) { + val menu = view.findViewById(R.id.reviewer_menu_view) + menu.setOnMenuItemClickListener(this) viewModel.flagFlow .flowWithLifecycle(lifecycle) .collectLatestIn(lifecycleScope) { flagCode -> - menu.findItem(R.id.action_flag).setIcon(flagCode.drawableRes) + menu.findItem(FLAG_MENU.menuId)?.setIcon(flagCode.drawableRes) } - } - private fun setupMenuItems(menu: Menu) { - setupFlagMenu(menu) + setupBury(menu) + setupSuspend(menu) // TODO show that the card is marked somehow when the menu item is overflowed or not shown - val markItem = menu.findItem(R.id.action_mark) + val markItem = menu.findItem(MARK.menuId) viewModel.isMarkedFlow .flowWithLifecycle(lifecycle) .collectLatestIn(lifecycleScope) { isMarked -> if (isMarked) { - markItem.setIcon(R.drawable.ic_star) - markItem.setTitle(R.string.menu_unmark_note) - } else { - markItem.setIcon(R.drawable.ic_star_border_white) - markItem.setTitle(R.string.menu_mark_note) - } - } - - val buryItem = menu.findItem(R.id.action_bury) - val buryCardItem = menu.findItem(R.id.action_bury_card) - viewModel.canBuryNoteFlow - .flowWithLifecycle(lifecycle) - .collectLatestIn(lifecycleScope) { canBuryNote -> - if (canBuryNote) { - buryItem.isVisible = true - buryCardItem.isVisible = false - } else { - buryItem.isVisible = false - buryCardItem.isVisible = true - } - } - - val suspendItem = menu.findItem(R.id.action_suspend) - val suspendCardItem = menu.findItem(R.id.action_suspend_card) - viewModel.canSuspendNoteFlow - .flowWithLifecycle(lifecycle) - .collectLatestIn(lifecycleScope) { canSuspendNote -> - if (canSuspendNote) { - suspendItem.isVisible = true - suspendCardItem.isVisible = false + markItem?.setIcon(R.drawable.ic_star) + markItem?.setTitle(R.string.menu_unmark_note) } else { - suspendItem.isVisible = false - suspendItem.isVisible = true + markItem?.setIcon(R.drawable.ic_star_border_white) + markItem?.setTitle(R.string.menu_mark_note) } } - val undoItem = menu.findItem(R.id.action_undo) + val undoItem = menu.findItem(UNDO.menuId) viewModel.undoLabelFlow .flowWithLifecycle(lifecycle) .collectLatestIn(lifecycleScope) { label -> - undoItem.title = label ?: CollectionManager.TR.undoUndo() - undoItem.isEnabled = label != null + undoItem?.title = label ?: CollectionManager.TR.undoUndo() + undoItem?.isEnabled = label != null } - val redoItem = menu.findItem(R.id.action_redo) + val redoItem = menu.findItem(REDO.menuId) viewModel.redoLabelFlow .flowWithLifecycle(lifecycle) .collectLatestIn(lifecycleScope) { label -> - redoItem.title = label ?: CollectionManager.TR.undoRedo() - redoItem.isEnabled = label != null + redoItem?.title = label ?: CollectionManager.TR.undoRedo() + redoItem?.isEnabled = label != null } } diff --git a/AnkiDroid/src/main/res/drawable/ic_drag_indicator_24.xml b/AnkiDroid/src/main/res/drawable/ic_drag_indicator_24.xml new file mode 100644 index 000000000000..0221f274933e --- /dev/null +++ b/AnkiDroid/src/main/res/drawable/ic_drag_indicator_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/AnkiDroid/src/main/res/layout/preferences_reviewer_menu.xml b/AnkiDroid/src/main/res/layout/preferences_reviewer_menu.xml index 70dd14465fe0..2a3596c9d299 100644 --- a/AnkiDroid/src/main/res/layout/preferences_reviewer_menu.xml +++ b/AnkiDroid/src/main/res/layout/preferences_reviewer_menu.xml @@ -12,7 +12,16 @@ android:layout_height="?attr/actionBarSize" app:navigationIcon="?attr/homeAsUpIndicator" app:navigationContentDescription="@string/abc_action_bar_up_description" - /> + > + + + + @@ -46,9 +44,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="?attr/learnCountColor" - app:layout_constraintStart_toEndOf="@id/new_count" - app:layout_constraintEnd_toStartOf="@id/rev_count" - android:paddingEnd="5dp" + android:paddingEnd="6dp" tools:text="81" /> @@ -57,11 +53,17 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="?attr/reviewCountColor" - app:layout_constraintStart_toEndOf="@id/lrn_count" tools:text="54" + android:paddingEnd="10dp" /> - + diff --git a/AnkiDroid/src/main/res/layout/reviewer_menu_display_type.xml b/AnkiDroid/src/main/res/layout/reviewer_menu_display_type.xml new file mode 100644 index 000000000000..10135665cade --- /dev/null +++ b/AnkiDroid/src/main/res/layout/reviewer_menu_display_type.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/AnkiDroid/src/main/res/layout/reviewer_menu_item.xml b/AnkiDroid/src/main/res/layout/reviewer_menu_item.xml new file mode 100644 index 000000000000..8065fe776ad8 --- /dev/null +++ b/AnkiDroid/src/main/res/layout/reviewer_menu_item.xml @@ -0,0 +1,50 @@ + + + + + + + + + diff --git a/AnkiDroid/src/main/res/layout/reviewer_menu_view.xml b/AnkiDroid/src/main/res/layout/reviewer_menu_view.xml new file mode 100644 index 000000000000..2b3fd4ed8daf --- /dev/null +++ b/AnkiDroid/src/main/res/layout/reviewer_menu_view.xml @@ -0,0 +1,37 @@ + + + + + + + + + \ No newline at end of file diff --git a/AnkiDroid/src/main/res/menu/reviewer2.xml b/AnkiDroid/src/main/res/menu/reviewer2.xml deleted file mode 100644 index f58f78c5a4d8..000000000000 --- a/AnkiDroid/src/main/res/menu/reviewer2.xml +++ /dev/null @@ -1,162 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/ConstantUniquenessTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/ConstantUniquenessTest.kt index a2bee7ac9239..fa5fa38c9d73 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/ConstantUniquenessTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/ConstantUniquenessTest.kt @@ -17,6 +17,7 @@ package com.ichi2.anki import com.ichi2.anki.notifications.NotificationId +import com.ichi2.anki.preferences.reviewer.ReviewerMenuSettingsRecyclerItem import com.ichi2.anki.worker.UniqueWorkNames import org.junit.Test import kotlin.reflect.KClass @@ -31,6 +32,7 @@ class ConstantUniquenessTest { fun testConstantUniqueness() { assertConstantUniqueness(NotificationId::class) assertConstantUniqueness(UniqueWorkNames::class) + assertConstantUniqueness(ReviewerMenuSettingsRecyclerItem.Companion::class) } companion object {