Skip to content

Commit 78cc842

Browse files
snowtimeglassdavid-allison
authored andcommitted
feat: add eraser action
- Add eraser action, by re-using the existing code for stylus eraser - Use an eraser icon for the erase feature instead of "Undo stroke" - Replace the eraser icon for "Undo stroke" with a U-turn shape arrow icon - Keep the eraser button's ripple visible while the eraser mode is activated (to represent the eraser mode is active) - Add the action to Control settings
1 parent 131a3ad commit 78cc842

File tree

16 files changed

+184
-17
lines changed

16 files changed

+184
-17
lines changed

AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1709,6 +1709,11 @@ abstract class AbstractFlashcardViewer :
17091709
true
17101710
}
17111711

1712+
ViewerCommand.TOGGLE_ERASER -> {
1713+
toggleEraser()
1714+
true
1715+
}
1716+
17121717
ViewerCommand.CLEAR_WHITEBOARD -> {
17131718
clearWhiteboard()
17141719
true
@@ -1776,6 +1781,10 @@ abstract class AbstractFlashcardViewer :
17761781
// intentionally blank
17771782
}
17781783

1784+
protected open fun toggleEraser() {
1785+
// intentionally blank
1786+
}
1787+
17791788
protected open fun clearWhiteboard() {
17801789
// intentionally blank
17811790
}

AnkiDroid/src/main/java/com/ichi2/anki/NavigationDrawerActivity.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ abstract class NavigationDrawerActivity :
6565
private var navButtonGoesBack = false
6666

6767
// Navigation drawer list item entries
68-
private lateinit var drawerLayout: DrawerLayout
68+
lateinit var drawerLayout: DrawerLayout
6969
private var navigationView: NavigationView? = null
7070
lateinit var drawerToggle: ActionBarDrawerToggle
7171
private set

AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt

Lines changed: 100 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import android.content.SharedPreferences
2727
import android.content.pm.PackageManager
2828
import android.os.Bundle
2929
import android.os.Handler
30+
import android.os.Looper
3031
import android.os.Message
3132
import android.os.Parcelable
3233
import android.text.SpannableString
@@ -147,6 +148,7 @@ open class Reviewer :
147148
private var prefFullscreenReview = false
148149
private lateinit var colorPalette: LinearLayout
149150
private var toggleStylus = false
151+
private var isEraserMode = false
150152

151153
// A flag that determines if the SchedulingStates in CurrentQueueState are
152154
// safe to persist in the database when answering a card. This is used to
@@ -484,6 +486,10 @@ open class Reviewer :
484486
setWhiteboardVisibility(!showWhiteboard)
485487
refreshActionBar()
486488
}
489+
R.id.action_toggle_eraser -> { // toggle eraser mode
490+
Timber.i("Reviewer:: Eraser button pressed")
491+
toggleEraser()
492+
}
487493
R.id.action_toggle_stylus -> { // toggle stylus mode
488494
Timber.i("Reviewer:: Stylus set to %b", !toggleStylus)
489495
toggleStylus = !toggleStylus
@@ -541,6 +547,67 @@ open class Reviewer :
541547
refreshActionBar()
542548
}
543549

550+
public override fun toggleEraser() {
551+
val whiteboardIsShownAndHasStrokes = showWhiteboard && whiteboard?.undoEmpty() == false
552+
if (whiteboardIsShownAndHasStrokes) {
553+
Timber.i("Reviewer:: Whiteboard eraser mode set to %b", !isEraserMode)
554+
isEraserMode = !isEraserMode
555+
whiteboard?.reviewerEraserModeIsToggledOn = isEraserMode
556+
557+
refreshActionBar() // Switch the eraser item's title
558+
559+
// Keep ripple effect on the eraser button after the eraser mode is activated.
560+
toolbar.post {
561+
val eraserButtonView = toolbar.findViewById<View>(R.id.action_toggle_eraser)
562+
eraserButtonView?.apply {
563+
isPressed = isEraserMode
564+
isActivated = isEraserMode
565+
}
566+
}
567+
568+
if (isEraserMode) {
569+
startMonitoringEraserButtonRipple()
570+
showSnackbar(getString(R.string.white_board_eraser_enabled), 1000)
571+
} else {
572+
stopMonitoringEraserButtonRipple()
573+
showSnackbar(getString(R.string.white_board_eraser_disabled), 1000)
574+
}
575+
}
576+
}
577+
578+
private val handler = Handler(Looper.getMainLooper())
579+
580+
/**
581+
* The eraser button ripple should be shown while the eraser mode is activated,
582+
* but the ripple gets removed after some timings
583+
* (e.g., when the three dot menu opens,
584+
* when the side drawer opens & closes,
585+
* when the button is long-pressed)
586+
* In such timings, this function re-press the button to re-display the ripple.
587+
*/
588+
private val checkEraserButtonRippleRunnable =
589+
object : Runnable {
590+
override fun run() {
591+
val eraserButtonView = toolbar.findViewById<View>(R.id.action_toggle_eraser)
592+
if (isEraserMode && eraserButtonView?.isPressed == false) {
593+
Timber.d("eraser button ripple monitoring: unpressed status detected, and re-pressed")
594+
eraserButtonView.isPressed = true
595+
eraserButtonView.isActivated = true
596+
}
597+
handler.postDelayed(this, 100) // monitor every 100ms
598+
}
599+
}
600+
601+
private fun startMonitoringEraserButtonRipple() {
602+
Timber.d("eraser button ripple monitoring: started")
603+
handler.post(checkEraserButtonRippleRunnable)
604+
}
605+
606+
private fun stopMonitoringEraserButtonRipple() {
607+
Timber.d("eraser button ripple monitoring: stopped")
608+
handler.removeCallbacks(checkEraserButtonRippleRunnable)
609+
}
610+
544611
public override fun clearWhiteboard() {
545612
if (whiteboard != null) {
546613
whiteboard!!.clear()
@@ -756,29 +823,43 @@ open class Reviewer :
756823
val undoEnabled: Boolean
757824
val whiteboardIsShownAndHasStrokes = showWhiteboard && whiteboard?.undoEmpty() == false
758825
if (whiteboardIsShownAndHasStrokes) {
759-
undoIconId = R.drawable.eraser
826+
undoIconId = R.drawable.ic_arrow_u_left_top
760827
undoEnabled = true
761828
} else {
762829
undoIconId = R.drawable.ic_undo_white
763830
undoEnabled = colIsOpenUnsafe() && getColUnsafe.undoAvailable()
831+
this.isEraserMode = false
764832
}
765833
val alphaUndo = Themes.ALPHA_ICON_ENABLED_LIGHT
766834
val undoIcon = menu.findItem(R.id.action_undo)
767835
undoIcon.setIcon(undoIconId)
768836
undoIcon.setEnabled(undoEnabled).iconAlpha = alphaUndo
769837
undoIcon.actionView!!.isEnabled = undoEnabled
838+
val toggleEraserIcon = menu.findItem((R.id.action_toggle_eraser))
770839
if (colIsOpenUnsafe()) { // Required mostly because there are tests where `col` is null
771840
if (whiteboardIsShownAndHasStrokes) {
841+
// Make Undo action title to whiteboard Undo specific one
772842
undoIcon.title = resources.getString(R.string.undo_action_whiteboard_last_stroke)
773-
} else if (getColUnsafe.undoAvailable()) {
774-
undoIcon.title = getColUnsafe.undoLabel()
775-
// e.g. Undo Bury, Undo Change Deck, Undo Update Note
843+
844+
// Show whiteboard Eraser action button
845+
if (!actionButtons.status.toggleEraserIsDisabled()) {
846+
toggleEraserIcon.isVisible = true
847+
}
776848
} else {
777-
// In this case, there is no object word for the verb, "Undo",
778-
// so in some languages such as Japanese, which have pre/post-positional particle with the object,
779-
// we need to use the string for just "Undo" instead of the string for "Undo %s".
780-
undoIcon.title = resources.getString(R.string.undo)
781-
undoIcon.iconAlpha = Themes.ALPHA_ICON_DISABLED_LIGHT
849+
// Disable whiteboard eraser action button
850+
isEraserMode = false
851+
whiteboard?.reviewerEraserModeIsToggledOn = isEraserMode
852+
853+
if (getColUnsafe.undoAvailable()) {
854+
// e.g. Undo Bury, Undo Change Deck, Undo Update Note
855+
undoIcon.title = getColUnsafe.undoLabel()
856+
} else {
857+
// In this case, there is no object word for the verb, "Undo",
858+
// so in some languages such as Japanese, which have pre/post-positional particle with the object,
859+
// we need to use the string for just "Undo" instead of the string for "Undo %s".
860+
undoIcon.title = resources.getString(R.string.undo)
861+
undoIcon.iconAlpha = Themes.ALPHA_ICON_DISABLED_LIGHT
862+
}
782863
}
783864
menu.findItem(R.id.action_redo)?.apply {
784865
if (getColUnsafe.redoAvailable()) {
@@ -820,11 +901,21 @@ open class Reviewer :
820901
val whiteboardIcon = ContextCompat.getDrawable(applicationContext, R.drawable.ic_gesture_white)!!.mutate()
821902
val stylusIcon = ContextCompat.getDrawable(this, R.drawable.ic_gesture_stylus)!!.mutate()
822903
val whiteboardColorPaletteIcon = ContextCompat.getDrawable(applicationContext, R.drawable.ic_color_lens_white_24dp)!!.mutate()
904+
val eraserIcon = ContextCompat.getDrawable(applicationContext, R.drawable.ic_eraser)!!.mutate()
823905
if (showWhiteboard) {
906+
// "hide whiteboard" icon
824907
whiteboardIcon.alpha = Themes.ALPHA_ICON_ENABLED_LIGHT
825908
hideWhiteboardIcon.icon = whiteboardIcon
826909
hideWhiteboardIcon.setTitle(R.string.hide_whiteboard)
827910
whiteboardColorPaletteIcon.alpha = Themes.ALPHA_ICON_ENABLED_LIGHT
911+
// eraser icon
912+
toggleEraserIcon.icon = eraserIcon
913+
if (isEraserMode) {
914+
toggleEraserIcon.setTitle(R.string.disable_eraser)
915+
} else { // default
916+
toggleEraserIcon.setTitle(R.string.enable_eraser)
917+
}
918+
// whiteboard editor icon
828919
changePenColorIcon.icon = whiteboardColorPaletteIcon
829920
if (toggleStylus) {
830921
toggleStylusIcon.setTitle(R.string.disable_stylus)

AnkiDroid/src/main/java/com/ichi2/anki/Whiteboard.kt

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ class Whiteboard(
8080
private var secondFingerPointerId = 0
8181
private var secondFingerWithinTapTolerance = false
8282

83+
var reviewerEraserModeIsToggledOn = false
8384
var toggleStylus = false
8485
var isCurrentlyDrawing = false
8586
private set
@@ -120,8 +121,8 @@ class Whiteboard(
120121
private fun handleDrawEvent(event: MotionEvent): Boolean {
121122
val x = event.x
122123
val y = event.y
123-
if (event.getToolType(event.actionIndex) == MotionEvent.TOOL_TYPE_ERASER) {
124-
stylusErase(event)
124+
if (event.getToolType(event.actionIndex) == MotionEvent.TOOL_TYPE_ERASER || reviewerEraserModeIsToggledOn) {
125+
eraseTouchedStroke(event)
125126
return true
126127
}
127128
if (event.getToolType(event.actionIndex) != MotionEvent.TOOL_TYPE_STYLUS && toggleStylus) {
@@ -130,7 +131,7 @@ class Whiteboard(
130131
return when (event.actionMasked) {
131132
MotionEvent.ACTION_DOWN -> {
132133
if (event.buttonState == MotionEvent.BUTTON_STYLUS_PRIMARY) {
133-
stylusErase(event)
134+
eraseTouchedStroke(event)
134135
} else {
135136
drawStart(x, y)
136137
invalidate()
@@ -139,7 +140,7 @@ class Whiteboard(
139140
}
140141
MotionEvent.ACTION_MOVE -> {
141142
if (event.buttonState == MotionEvent.BUTTON_STYLUS_PRIMARY) {
142-
stylusErase(event)
143+
eraseTouchedStroke(event)
143144
return true
144145
}
145146
if (isCurrentlyDrawing) {
@@ -168,7 +169,7 @@ class Whiteboard(
168169
}
169170
211, 213 -> {
170171
if (event.buttonState == MotionEvent.BUTTON_STYLUS_PRIMARY) {
171-
stylusErase(event)
172+
eraseTouchedStroke(event)
172173
}
173174
true
174175
}
@@ -193,9 +194,12 @@ class Whiteboard(
193194
}
194195

195196
/**
196-
* Erase with stylus pen.(By using the eraser button on the stylus pen or by using the digital eraser)
197+
* Erase touched stroke (= path or point)
198+
* (by toggling the eraser action button on
199+
* or by using the eraser button on the stylus pen
200+
* or by using the digital eraser)
197201
*/
198-
private fun stylusErase(event: MotionEvent) {
202+
private fun eraseTouchedStroke(event: MotionEvent) {
199203
if (!undoEmpty()) {
200204
val didErase = undo.erase(event.x.toInt(), event.y.toInt())
201205
if (didErase) {

AnkiDroid/src/main/java/com/ichi2/anki/analytics/UsageAnalytics.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,7 @@ object UsageAnalytics {
550550
R.string.replay_voice_command_key,
551551
R.string.save_voice_command_key,
552552
R.string.toggle_whiteboard_command_key,
553+
R.string.toggle_eraser_command_key,
553554
R.string.clear_whiteboard_command_key,
554555
R.string.change_whiteboard_pen_color_command_key,
555556
R.string.toggle_auto_advance_command_key,
@@ -604,6 +605,7 @@ object UsageAnalytics {
604605
R.string.custom_button_suspend_key,
605606
R.string.custom_button_delete_key,
606607
R.string.custom_button_enable_whiteboard_key,
608+
R.string.custom_button_toggle_eraser_key,
607609
R.string.custom_button_toggle_stylus_key,
608610
R.string.custom_button_save_whiteboard_key,
609611
R.string.custom_button_whiteboard_pen_color_key,

AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/ViewerCommand.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ enum class ViewerCommand : MappableAction<ReviewerBinding> {
6060
SAVE_VOICE,
6161
REPLAY_VOICE,
6262
TOGGLE_WHITEBOARD,
63+
TOGGLE_ERASER,
6364
CLEAR_WHITEBOARD,
6465
CHANGE_WHITEBOARD_PEN_COLOR,
6566
SHOW_HINT,
@@ -178,6 +179,7 @@ enum class ViewerCommand : MappableAction<ReviewerBinding> {
178179
TAG,
179180
CARD_INFO,
180181
TOGGLE_WHITEBOARD,
182+
TOGGLE_ERASER,
181183
CLEAR_WHITEBOARD,
182184
CHANGE_WHITEBOARD_PEN_COLOR,
183185
RESCHEDULE_NOTE,

AnkiDroid/src/main/java/com/ichi2/anki/reviewer/ActionButtonStatus.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ class ActionButtonStatus {
5353
setupButton(preferences, R.id.action_delete, "customButtonDelete", SHOW_AS_ACTION_NEVER)
5454
setupButton(preferences, R.id.action_toggle_mic_tool_bar, "customButtonToggleMicToolBar", SHOW_AS_ACTION_NEVER)
5555
setupButton(preferences, R.id.action_toggle_whiteboard, "customButtonEnableWhiteboard", SHOW_AS_ACTION_NEVER)
56+
setupButton(preferences, R.id.action_toggle_eraser, "customButtonToggleEraser", SHOW_AS_ACTION_ALWAYS)
5657
setupButton(preferences, R.id.action_toggle_stylus, "customButtonToggleStylus", SHOW_AS_ACTION_IF_ROOM)
5758
setupButton(preferences, R.id.action_save_whiteboard, "customButtonSaveWhiteboard", SHOW_AS_ACTION_NEVER)
5859
setupButton(preferences, R.id.action_change_whiteboard_pen_color, "customButtonWhiteboardPenColor", SHOW_AS_ACTION_IF_ROOM)
@@ -95,6 +96,8 @@ class ActionButtonStatus {
9596

9697
fun hideWhiteboardIsDisabled(): Boolean = customButtons[R.id.action_hide_whiteboard] == MENU_DISABLED
9798

99+
fun toggleEraserIsDisabled(): Boolean = customButtons[R.id.action_toggle_eraser] == MENU_DISABLED
100+
98101
fun toggleStylusIsDisabled(): Boolean = customButtons[R.id.action_toggle_stylus] == MENU_DISABLED
99102

100103
fun clearWhiteboardIsDisabled(): Boolean = customButtons[R.id.action_clear_whiteboard] == MENU_DISABLED
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!--
2+
Based on: https://pictogrammers.com/library/mdi/icon/arrow-u-left-top/
3+
Change: * Replace arrowhead from L-shape (←) with filled triangle (◀-) (for consistency with the existing undo icon)
4+
* Make U-shape line thicker (for consistency with the existing undo icon)
5+
-->
6+
7+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
8+
android:width="24dp"
9+
android:height="24dp"
10+
android:viewportWidth="24"
11+
android:viewportHeight="24"
12+
android:tint="?attr/colorControlNormal">
13+
<path
14+
android:fillColor="#FFFFFF"
15+
android:pathData="M10.014,2.391 L3.639,8.744 9.99,15.119 10,10.012L13.5,10.012C15.86,10.012 17.738,11.89 17.738,14.25 17.738,16.61 15.86,18.488 13.5,18.488L5.738,18.488L5.738,21.012L13.5,21.012C17.232,21.012 20.262,17.982 20.262,14.25 20.262,10.518 17.232,7.488 13.5,7.488L10.004,7.488L10.004,7.482Z"/>
16+
</vector>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!-- source: None (Originally created for AnkiDroid) -->
2+
3+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
4+
android:tint="?attr/colorControlNormal"
5+
android:width="24dp"
6+
android:height="24dp"
7+
android:viewportWidth="24"
8+
android:viewportHeight="24">
9+
<path
10+
android:pathData="M11.565,19.444C12.306,20.185 13.542,20.185 14.283,19.444L22.05,11.677 13.221,2.849 5.454,10.616C4.713,11.357 4.715,12.594 5.456,13.335ZM12.924,17.975 L6.924,11.975 9.045,9.853 15.045,15.853ZM16.459,14.439 L10.459,8.439 13.221,5.677 19.221,11.677ZM5.995,17 L2,16.999L2,19.999L7.995,20L8.995,20C8.995,20 8.487,19.508 8.003,19.008Z"
11+
android:fillColor="#ffffff"/>
12+
</vector>

AnkiDroid/src/main/res/menu/reviewer.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@
1717
android:title="@string/redo"
1818
ankidroid:actionProviderClass="com.ichi2.ui.RtlCompliantActionProvider"
1919
ankidroid:showAsAction="always"/>
20+
<item
21+
android:id="@+id/action_toggle_eraser"
22+
android:icon="@drawable/ic_eraser"
23+
android:title="@string/enable_eraser"
24+
android:visible="false"
25+
ankidroid:showAsAction="always"/>
2026
<item
2127
android:id="@+id/action_clear_whiteboard"
2228
android:title="@string/clear_whiteboard"

0 commit comments

Comments
 (0)