Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
BrayanDSO committed Dec 7, 2024
1 parent 76eb3a7 commit 303a346
Show file tree
Hide file tree
Showing 7 changed files with 407 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,113 @@
*/
package com.ichi2.anki.preferences

import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.appcompat.view.menu.MenuBuilder
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.appbar.MaterialToolbar
import com.ichi2.anki.R
import com.ichi2.anki.preferences.reviewer.MenuDisplayType
import com.ichi2.anki.preferences.reviewer.OnClearViewListener
import com.ichi2.anki.preferences.reviewer.ReviewerMenuSettingsAdapter
import com.ichi2.anki.preferences.reviewer.ReviewerMenuSettingsRecyclerItem
import com.ichi2.anki.preferences.reviewer.ReviewerMenuSettingsTouchHelperCallback
import com.ichi2.anki.preferences.reviewer.ViewerAction
import com.ichi2.anki.utils.ext.sharedPrefs

class ReviewerMenuSettingsFragment : Fragment(R.layout.preferences_reviewer_menu)
class ReviewerMenuSettingsFragment :
Fragment(R.layout.preferences_reviewer_menu),
OnClearViewListener {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

val menuItems = MenuDisplayType.getMenuItems(sharedPrefs())
setupRecyclerView(view, menuItems)

val toolbar = view.findViewById<MaterialToolbar>(R.id.toolbar)
val menu = toolbar.menu
(menu as? MenuBuilder)?.setOptionalIconsVisible(true)
setupMenu(toolbar.menu, menuItems)
}

private fun setupRecyclerView(view: View, menuItems: MenuDisplayType.Items) {
fun toRecyclerItems(items: List<ViewerAction>): Array<ReviewerMenuSettingsRecyclerItem> =
items.map { ReviewerMenuSettingsRecyclerItem.Action(it) }.toTypedArray()

val recyclerViewItems = listOf(
ReviewerMenuSettingsRecyclerItem.DisplayType(MenuDisplayType.ALWAYS),
*toRecyclerItems(menuItems.alwaysShow),
ReviewerMenuSettingsRecyclerItem.DisplayType(MenuDisplayType.MENU_ONLY),
*toRecyclerItems(menuItems.menuOnly),
ReviewerMenuSettingsRecyclerItem.DisplayType(MenuDisplayType.DISABLED),
*toRecyclerItems(menuItems.disabled)
)

val callback = ReviewerMenuSettingsTouchHelperCallback(recyclerViewItems)
callback.setOnClearViewListener(this)
val itemTouchHelper = ItemTouchHelper(callback)

val adapter = ReviewerMenuSettingsAdapter(recyclerViewItems).apply {
setOnDragListener { viewHolder ->
itemTouchHelper.startDrag(viewHolder)
}
}
view.findViewById<RecyclerView>(R.id.recycler_view).apply {
layoutManager = LinearLayoutManager(requireContext())
this.adapter = adapter
itemTouchHelper.attachToRecyclerView(this)
}
}

override fun onClearView(items: List<ReviewerMenuSettingsRecyclerItem>) {
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 onMenuItems = 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, onMenuItems)
MenuDisplayType.DISABLED.setPreferenceValue(preferences, disabledItems)
}

private fun addActionsToMenu(
menu: Menu,
actions: List<ViewerAction>,
menuActionType: Int
) {
val menuActions = ViewerAction.entries.mapNotNull { it.parentMenu }
for (action in actions) {
val title = action.titleRes?.let { getString(it) } ?: ""
val menuItem = if (action in menuActions) {
menu.addSubMenu(Menu.NONE, action.menuId, Menu.NONE, title)
menu.findItem(action.menuId)
} else {
menu.add(Menu.NONE, action.menuId, Menu.NONE, title)
}
with(menuItem) {
action.drawableRes?.let { setIcon(it) }
setShowAsAction(menuActionType)
}
}
}

private fun setupMenu(menu: Menu, menuItems: MenuDisplayType.Items) {
addActionsToMenu(menu, menuItems.alwaysShow, MenuItem.SHOW_AS_ACTION_ALWAYS)
addActionsToMenu(menu, menuItems.menuOnly, MenuItem.SHOW_AS_ACTION_NEVER)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
*/
package com.ichi2.anki.preferences.reviewer

import android.content.SharedPreferences
import androidx.annotation.StringRes
import androidx.core.content.edit
import com.ichi2.anki.R

enum class MenuDisplayType(@StringRes val title: Int) {
Expand All @@ -24,4 +26,55 @@ enum class MenuDisplayType(@StringRes val title: Int) {
DISABLED(R.string.disabled);

private val preferenceKey get() = "ReviewerMenuDisplayType_$name"

/**
* @return the configured actions for this menu display type.
*/
private fun getConfiguredActions(preferences: SharedPreferences): List<ViewerAction> {
val prefValue = preferences.getString(preferenceKey, null)
?: return emptyList()

val actionsNames = prefValue.split(SEPARATOR)
return actionsNames.mapNotNull { name ->
ViewerAction.entries.firstOrNull { it.name == name }
}
}

fun setPreferenceValue(preferences: SharedPreferences, actions: List<ViewerAction>) {
val prefValue = actions.joinToString(SEPARATOR) { it.name }
preferences.edit { putString(preferenceKey, prefValue) }
}

data class Items(
val alwaysShow: List<ViewerAction>,
val menuOnly: List<ViewerAction>,
val disabled: List<ViewerAction>
)

companion object {
private const val SEPARATOR = ","

/**
* @return actions that weren't configured yet. May happen if the user hasn't configured
* any of the menu actions, or if a new action was implemented but not configured yet.
*/
private fun getAllNotConfiguredActions(prefs: SharedPreferences): List<ViewerAction> {
val mappedActions = MenuDisplayType.entries.flatMap { it.getConfiguredActions(prefs) }
return ViewerAction.entries.filter {
it.defaultDisplayType != null && it !in mappedActions
}
}

fun getMenuItems(prefs: SharedPreferences): Items {
val notConfiguredActions = getAllNotConfiguredActions(prefs)
val alwaysShowActions = ALWAYS.getConfiguredActions(prefs) +
notConfiguredActions.filter { it.defaultDisplayType == ALWAYS }
val menuOnlyActions = MENU_ONLY.getConfiguredActions(prefs) +
notConfiguredActions.filter { it.defaultDisplayType == MENU_ONLY }
val disabledActions = DISABLED.getConfiguredActions(prefs) +
notConfiguredActions.filter { it.defaultDisplayType == DISABLED }

return Items(alwaysShowActions, menuOnlyActions, disabledActions)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright (c) 2024 Brayan Oliveira <[email protected]>
*
* 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 <http://www.gnu.org/licenses/>.
*/
package com.ichi2.anki.preferences.reviewer

import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.AppCompatImageView
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.textview.MaterialTextView
import com.ichi2.anki.R
import com.ichi2.ui.FixedTextView

class ReviewerMenuSettingsAdapter(private val items: List<ReviewerMenuSettingsRecyclerItem>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
ReviewerMenuSettingsRecyclerItem.ACTION_VIEW_TYPE -> {
val itemView = LayoutInflater.from(parent.context)
.inflate(R.layout.reviewer_menu_item, parent, false)
ActionViewHolder(itemView)
}
ReviewerMenuSettingsRecyclerItem.DISPLAY_TYPE_VIEW_TYPE -> {
val itemView = LayoutInflater.from(parent.context)
.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<FixedTextView>(R.id.title).setText(it) }
action.drawableRes?.let { itemView.findViewById<AppCompatImageView>(R.id.icon).setBackgroundResource(it) }

itemView.findViewById<AppCompatImageView>(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<MaterialTextView>(R.id.title).setText(displayCategory.title)
}
}
}

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
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright (c) 2024 Brayan Oliveira <[email protected]>
*
* 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 <http://www.gnu.org/licenses/>.
*/
package com.ichi2.anki.preferences.reviewer

import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import java.util.Collections

class ReviewerMenuSettingsTouchHelperCallback(private val items: List<ReviewerMenuSettingsRecyclerItem>) : ItemTouchHelper.Callback() {
private val movementFlags = makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0)
private var onClearViewListener: OnClearViewListener? = null

override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
return 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` is always 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 onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
// do nothing
}

override fun clearView(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
) {
super.clearView(recyclerView, viewHolder)
onClearViewListener?.onClearView(items)
}

override fun isLongPressDragEnabled(): Boolean = false

/** Sets a listener to be called after [clearView] */
fun setOnClearViewListener(listener: OnClearViewListener) {
onClearViewListener = listener
}
}

fun interface OnClearViewListener {
fun onClearView(items: List<ReviewerMenuSettingsRecyclerItem>)
}
5 changes: 5 additions & 0 deletions AnkiDroid/src/main/res/drawable/ic_drag_indicator_24.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">

<path android:fillColor="@android:color/white" android:pathData="M11,18c0,1.1 -0.9,2 -2,2s-2,-0.9 -2,-2 0.9,-2 2,-2 2,0.9 2,2zM9,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM9,4c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM15,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM15,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM15,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>

</vector>
23 changes: 23 additions & 0 deletions AnkiDroid/src/main/res/layout/reviewer_menu_display_type.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:orientation="horizontal"
android:baselineAligned="false"
android:layout_marginTop="16dp"
android:padding="8dp">

<com.google.android.material.textview.MaterialTextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:textAlignment="viewStart"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:textColor="?attr/preferenceCategoryTitleTextColor"
tools:text="Menu only"/>

</LinearLayout>
Loading

0 comments on commit 303a346

Please sign in to comment.