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

improvement(custom-study): disable unusable items #17709

Merged
merged 3 commits into from
Jan 4, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,13 @@ import android.annotation.SuppressLint
import android.app.Dialog
import android.content.res.Resources
import android.os.Bundle
import android.util.TypedValue
import android.view.WindowManager
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView
import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AlertDialog
Expand All @@ -36,11 +40,11 @@ import com.ichi2.anki.CollectionManager.withCol
import com.ichi2.anki.R
import com.ichi2.anki.analytics.AnalyticsDialogFragment
import com.ichi2.anki.dialogs.customstudy.CustomStudyDialog.ContextMenuOption
import com.ichi2.anki.dialogs.customstudy.CustomStudyDialog.ContextMenuOption.EXTEND_NEW
import com.ichi2.anki.dialogs.customstudy.CustomStudyDialog.ContextMenuOption.EXTEND_REV
import com.ichi2.anki.dialogs.customstudy.CustomStudyDialog.ContextMenuOption.STUDY_AHEAD
import com.ichi2.anki.dialogs.customstudy.CustomStudyDialog.ContextMenuOption.STUDY_FORGOT
import com.ichi2.anki.dialogs.customstudy.CustomStudyDialog.ContextMenuOption.STUDY_NEW
import com.ichi2.anki.dialogs.customstudy.CustomStudyDialog.ContextMenuOption.STUDY_PREVIEW
import com.ichi2.anki.dialogs.customstudy.CustomStudyDialog.ContextMenuOption.STUDY_REV
import com.ichi2.anki.dialogs.customstudy.CustomStudyDialog.ContextMenuOption.STUDY_TAGS
import com.ichi2.anki.dialogs.customstudy.CustomStudyDialog.CustomStudyDefaults.Companion.toDomainModel
import com.ichi2.anki.dialogs.tags.TagsDialog
Expand All @@ -65,9 +69,10 @@ import com.ichi2.utils.BundleUtils.getNullableInt
import com.ichi2.utils.KotlinCleanup
import com.ichi2.utils.cancelable
import com.ichi2.utils.customView
import com.ichi2.utils.listItems
import com.ichi2.utils.dp
import com.ichi2.utils.negativeButton
import com.ichi2.utils.positiveButton
import com.ichi2.utils.setPaddingRelative
import com.ichi2.utils.textAsIntOrNull
import com.ichi2.utils.title
import net.ankiweb.rsdroid.exceptions.BackendDeckIsFilteredException
Expand Down Expand Up @@ -167,44 +172,81 @@ class CustomStudyDialog(
}
}

/**
* Handles selecting an item from the main menu of the dialog
*/
private fun onMenuItemSelected(item: ContextMenuOption) =
when (item) {
STUDY_TAGS -> {
/*
* This is a special Dialog for CUSTOM STUDY, where instead of only collecting a
* number, it is necessary to collect a list of tags. This case handles the creation
* of that Dialog.
*/
val dialogFragment =
TagsDialog().withArguments(
context = requireContext(),
type = TagsDialog.DialogType.CUSTOM_STUDY_TAGS,
checkedTags = ArrayList(),
allTags = ArrayList(collection.tags.byDeck(dialogDeckId)),
)
requireActivity().showDialogFragment(dialogFragment)
}
EXTEND_NEW,
EXTEND_REV,
STUDY_FORGOT,
STUDY_AHEAD,
STUDY_PREVIEW,
-> {
// User asked for a standard custom study option
val d =
CustomStudyDialog(collection, customStudyListener)
.withArguments(dialogDeckId, item)
requireActivity().showDialogFragment(d)
}
}

private fun buildContextMenu(): AlertDialog {
val listIds = getListIds()
val customMenuView = ScrollView(requireContext())
val container =
LinearLayout(requireContext())
.apply {
orientation = LinearLayout.VERTICAL
setPaddingRelative(9.dp.toPx(requireContext()))
}
customMenuView.addView(
container,
FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT,
),
)
val ta = TypedValue()
requireContext().theme.resolveAttribute(android.R.attr.selectableItemBackground, ta, true)
ContextMenuOption.entries
.map {
when (it) {
EXTEND_NEW -> Pair(it, defaults.extendNew.isUsable)
EXTEND_REV -> Pair(it, defaults.extendReview.isUsable)
else -> Pair(it, true)
}
}.forEach { (menuItem, isItemEnabled) ->
(layoutInflater.inflate(android.R.layout.simple_list_item_1, container, false) as TextView)
.apply {
text = menuItem.getTitle(requireContext().resources)
isEnabled = isItemEnabled
setBackgroundResource(ta.resourceId)
setTextAppearance(android.R.style.TextAppearance_Material_Body1)
setOnClickListener { onMenuItemSelected(menuItem) }
}.also { container.addView(it) }
}

return AlertDialog
.Builder(requireActivity())
.title(text = TR.actionsCustomStudy().toSentenceCase(this, R.string.sentence_custom_study))
.cancelable(true)
.listItems(items = listIds.map { it.getTitle(resources) }) { _, index ->
when (listIds[index]) {
STUDY_TAGS -> {
/*
* This is a special Dialog for CUSTOM STUDY, where instead of only collecting a
* number, it is necessary to collect a list of tags. This case handles the creation
* of that Dialog.
*/
val dialogFragment =
TagsDialog().withArguments(
context = requireContext(),
type = TagsDialog.DialogType.CUSTOM_STUDY_TAGS,
checkedTags = ArrayList(),
allTags = ArrayList(collection.tags.byDeck(dialogDeckId)),
)
requireActivity().showDialogFragment(dialogFragment)
}
STUDY_NEW,
STUDY_REV,
STUDY_FORGOT,
STUDY_AHEAD,
STUDY_PREVIEW,
-> {
// User asked for a standard custom study option
val d =
CustomStudyDialog(collection, customStudyListener)
.withArguments(dialogDeckId, listIds[index])
requireActivity().showDialogFragment(d)
}
}
}.create()
.customView(customMenuView)
.create()
}

/**
Expand Down Expand Up @@ -233,7 +275,7 @@ class CustomStudyDialog(
setSelectAllOnFocus(true)
requestFocus()
// a user may enter a negative value when extending limits
if (contextMenuOption == STUDY_NEW || contextMenuOption == STUDY_REV) {
if (contextMenuOption == EXTEND_NEW || contextMenuOption == EXTEND_REV) {
inputType = EditorInfo.TYPE_CLASS_NUMBER or EditorInfo.TYPE_NUMBER_FLAG_SIGNED
}
}
Expand Down Expand Up @@ -273,8 +315,8 @@ class CustomStudyDialog(
customStudyRequest {
deckId = dialogDeckId
when (contextMenuOption) {
STUDY_NEW -> newLimitDelta = userEntry
STUDY_REV -> reviewLimitDelta = userEntry
EXTEND_NEW -> newLimitDelta = userEntry
EXTEND_REV -> reviewLimitDelta = userEntry
STUDY_FORGOT -> forgotDays = userEntry
STUDY_AHEAD -> reviewAheadDays = userEntry
STUDY_PREVIEW -> previewDays = userEntry
Expand All @@ -287,7 +329,7 @@ class CustomStudyDialog(
collection.sched.customStudy(request)
}
when (contextMenuOption) {
STUDY_NEW, STUDY_REV ->
EXTEND_NEW, EXTEND_REV ->
customStudyListener?.onExtendStudyLimits()
STUDY_FORGOT, STUDY_AHEAD, STUDY_PREVIEW -> customStudyListener?.onCreateCustomStudySession()
STUDY_TAGS -> TODO("This branch has not been covered before")
Expand All @@ -301,7 +343,7 @@ class CustomStudyDialog(
STUDY_FORGOT -> sharedPrefs().edit { putInt("forgottenDays", userEntry) }
STUDY_AHEAD -> sharedPrefs().edit { putInt("aheadDays", userEntry) }
STUDY_PREVIEW -> sharedPrefs().edit { putInt("previewDays", userEntry) }
STUDY_NEW, STUDY_REV -> {
EXTEND_NEW, EXTEND_REV -> {
// Nothing to do in ankidroid. The default value is provided by the backend.
}
STUDY_TAGS -> TODO("This branch has not been covered before")
Expand Down Expand Up @@ -335,29 +377,12 @@ class CustomStudyDialog(
)
}

/**
* Retrieve the list of ids to put in the context menu list
* @return the ids of which values to show
*/
private fun getListIds(): List<ContextMenuOption> {
// Standard context menu
return mutableListOf(STUDY_FORGOT, STUDY_AHEAD, STUDY_PREVIEW, STUDY_TAGS).apply {
if (defaults.extendReview.isUsable) {
this.add(0, STUDY_REV)
}
// We want 'Extend new' above 'Extend review' if both appear
if (defaults.extendNew.isUsable) {
this.add(0, STUDY_NEW)
}
}
}

/** Line 1 of the number entry dialog */
private val text1: String
get() =
when (selectedSubDialog) {
STUDY_NEW -> defaults.labelForNewQueueAvailable()
STUDY_REV -> defaults.labelForReviewQueueAvailable()
EXTEND_NEW -> defaults.labelForNewQueueAvailable()
EXTEND_REV -> defaults.labelForReviewQueueAvailable()
STUDY_FORGOT,
STUDY_AHEAD,
STUDY_PREVIEW,
Expand All @@ -371,8 +396,8 @@ class CustomStudyDialog(
get() {
val res = resources
return when (selectedSubDialog) {
STUDY_NEW -> res.getString(R.string.custom_study_new_extend)
STUDY_REV -> res.getString(R.string.custom_study_rev_extend)
EXTEND_NEW -> res.getString(R.string.custom_study_new_extend)
EXTEND_REV -> res.getString(R.string.custom_study_rev_extend)
STUDY_FORGOT -> res.getString(R.string.custom_study_forgotten)
STUDY_AHEAD -> res.getString(R.string.custom_study_ahead)
STUDY_PREVIEW -> res.getString(R.string.custom_study_preview)
Expand All @@ -387,8 +412,8 @@ class CustomStudyDialog(
get() {
val prefs = requireActivity().sharedPrefs()
return when (selectedSubDialog) {
STUDY_NEW -> defaults.extendNew.initialValue.toString()
STUDY_REV -> defaults.extendReview.initialValue.toString()
EXTEND_NEW -> defaults.extendNew.initialValue.toString()
EXTEND_REV -> defaults.extendReview.initialValue.toString()
STUDY_FORGOT -> prefs.getInt("forgottenDays", 1).toString()
STUDY_AHEAD -> prefs.getInt("aheadDays", 1).toString()
STUDY_PREVIEW -> prefs.getInt("previewDays", 1).toString()
Expand Down Expand Up @@ -460,17 +485,17 @@ class CustomStudyDialog(
}

/**
* Possible context menu options that could be shown in the custom study dialog.
* Context menu options shown in the custom study dialog.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
enum class ContextMenuOption(
val getTitle: Resources.() -> String,
) {
/** Increase today's new card limit */
STUDY_NEW({ TR.customStudyIncreaseTodaysNewCardLimit() }),
EXTEND_NEW({ TR.customStudyIncreaseTodaysNewCardLimit() }),

/** Increase today's review card limit */
STUDY_REV({ TR.customStudyIncreaseTodaysReviewCardLimit() }),
EXTEND_REV({ TR.customStudyIncreaseTodaysReviewCardLimit() }),

/** Review forgotten cards */
STUDY_FORGOT({ TR.customStudyReviewForgottenCards() }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ class SetDueDateDialog : DialogFragment() {

companion object {
const val ARG_CARD_IDS = "ARGS_CARD_IDS"
const val MAX_WIDTH_DP = 450
const val MAX_WIDTH_DP = 450f

@CheckResult
fun newInstance(cardIds: List<CardId>) =
Expand Down Expand Up @@ -309,5 +309,5 @@ private fun AnkiActivity.updateDueDate(viewModel: SetDueDateViewModel) =
showSnackbar(TR.schedulingSetDueDateDone(cardsUpdated), Snackbar.LENGTH_SHORT)
}

// TODO: See if we can turn this to a `val` when context parameters are back
fun Int.dpToPx(context: Context): Int = (this * context.resources.displayMetrics.density + 0.5f).toInt()
// TODO: better to use 16.dp ... toPx(context)
fun Float.dpToPx(context: Context): Int = (this * context.resources.displayMetrics.density + 0.5f).toInt()
55 changes: 55 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/utils/ViewUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@
package com.ichi2.utils

import android.app.Activity
import android.content.Context
import android.graphics.Rect
import android.view.MotionEvent
import android.view.View
import androidx.core.view.OnReceiveContentListener
import androidx.draganddrop.DropHelper
import com.ichi2.anki.scheduling.dpToPx

/** @see View.performClick */
fun View.performClickIfEnabled() {
Expand Down Expand Up @@ -66,3 +68,56 @@ fun View.configureView(
onReceiveContentListener,
)
}

/**
* Sets the relative padding for all dimensions of the view
*
* The view may add on the space required to display the scrollbars,
* depending on the style and visibility of the scrollbars.
* So the values returned from `getPadding` calls
* may be different from the values set in this call.
*/
fun View.setPaddingRelative(px: Int) = setPaddingRelative(px, px, px, px)

/**
* Sets the relative padding for all dimensions of the view
*
* The view may add on the space required to display the scrollbars,
* depending on the style and visibility of the scrollbars.
* So the values returned from `getPadding` calls
* may be different from the values set in this call.
*/
@Suppress("unused")
fun View.setPaddingRelative(dp: Dp) = setPaddingRelative(dp.toPx(context))

/**
* Sets the relative padding
*
* The view may add on the space required to display the scrollbars,
* depending on the style and visibility of the scrollbars.
* So the values returned from `getPadding` calls
* may be different from the values set in this call.
*/
// Since we're in Kotlin, this now allows named arguments!
@Suppress("unused")
fun View.setPaddingRelative(
start: Dp,
top: Dp,
end: Dp,
bottom: Dp,
) = setPaddingRelative(start.toPx(context), top.toPx(context), end.toPx(context), bottom.toPx(context))

/** Returns a [Dp] instance equal to this [Int] number of display pixels. */
val Int.dp
get() = Dp(dp = this.toFloat())

/**
* Helper for 'display pixels' to 'pixels' conversions
*/
@JvmInline
value class Dp(
val dp: Float,
) {
// TODO: improve once we have context parameters
fun toPx(context: Context) = dp.dpToPx(context)
}
Loading
Loading