Added stylus-only mode for PDF drawing and annotations. (#27)

This change introduces a "Stylus Only Mode" toggle in the annotation dock, allowing users to restrict drawing input to stylus pens while maintaining touch support for scrolling and navigation.

Key changes include:
- Added `isStylusOnlyMode` state and persistence logic in `PdfViewerScreen`.
- Updated `AnnotationDock` with a toggle button for the new mode.
- Modified pointer input handling in `PdfPageComposable` and `PdfVerticalReader` to filter touch events when stylus-only mode is active.
- Adjusted tap gesture detection to ensure scrolling and page clicks still function correctly under various input modes.
This commit is contained in:
Aryan 2026-03-04 13:18:26 +05:30 committed by GitHub
parent d459535ffd
commit b47b6a9164
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 97 additions and 25 deletions

View file

@ -21,10 +21,10 @@ This allows the open-source version to thrive while keeping the project sustaina
## How to Submit a Pull Request
1. Fork the repository and create your branch from `main`.
1. Fork the repository.
2. Make your changes and commit them with clear, descriptive messages.
3. Open a Pull Request!
4. **Sign the CLA:** When you open your first PR, the [CLA Assistant](https://cla-assistant.io/) bot will automatically comment with a link. Simply click the link, sign in with GitHub, and click "I agree." **This is a one-time process** once signed, it covers all your future contributions to Episteme.
4. **Sign the CLA:** When you open your first PR, the [CLA Assistant](https://cla-assistant.io/) bot will automatically comment with a link. Simply click the link, sign in with GitHub, and click "I agree." **This is a one-time process**, once signed, it covers all your future contributions to Episteme.
## Questions?

View file

@ -35,6 +35,8 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Redo
import androidx.compose.material.icons.automirrored.filled.Undo
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.DoNotTouch
import androidx.compose.material.icons.filled.TouchApp
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.Icon
@ -68,7 +70,9 @@ fun AnnotationDock(
modifier: Modifier = Modifier,
isSticky: Boolean = false,
isMinimized: Boolean,
onToggleMinimize: () -> Unit
onToggleMinimize: () -> Unit,
isStylusOnlyMode: Boolean,
onToggleStylusOnlyMode: () -> Unit
) {
val showFullDock = isSticky || !isMinimized
@ -141,6 +145,29 @@ fun AnnotationDock(
horizontalArrangement = Arrangement.spacedBy(spacing),
modifier = Modifier.alpha(toolsAlpha)
) {
// Stylus Only Toggle
if (!isMinimized) {
val iconVector = if (isStylusOnlyMode) Icons.Default.DoNotTouch else Icons.Default.TouchApp
val iconTint = if (isStylusOnlyMode) Color(0xFFE57373) else Color.White
Box(
modifier = Modifier
.size(buttonSize)
.clip(CircleShape)
.background(Color.White.copy(alpha = if (isStylusOnlyMode) 0.15f else 0f))
.clickable(onClick = onToggleStylusOnlyMode),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = iconVector,
contentDescription = "Stylus Only Mode",
tint = iconTint,
modifier = Modifier.size(iconSize)
)
}
}
// Pen Group
val isPenActive = !isMinimized && (selectedTool == InkType.PEN ||
selectedTool == InkType.FOUNTAIN_PEN ||

View file

@ -81,6 +81,7 @@ import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.PointerEventTimeoutCancellationException
import androidx.compose.ui.input.pointer.PointerType
import androidx.compose.ui.input.pointer.changedToUp
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.positionChanged
@ -416,6 +417,7 @@ internal fun PdfPageComposable(
draggingBoxId: String? = null,
isScrollLocked: Boolean = false,
isVisible: Boolean = true,
isStylusOnlyMode: Boolean = false
) {
SideEffect { Timber.tag("PdfDrawPerf").v("PdfPageComposable Recompose: Page $pageIndex") }
val pdfDocumentItem = pdfDocument.item
@ -2237,9 +2239,14 @@ internal fun PdfPageComposable(
isZoomEnabled,
isVerticalScroll,
isEditMode,
selectedTool
selectedTool,
isStylusOnlyMode
) {
if (isEditMode && selectedTool != InkType.TEXT) return@pointerInput
val isTapDetectionAllowed = !isEditMode ||
selectedTool == InkType.TEXT ||
isStylusOnlyMode
if (!isTapDetectionAllowed) return@pointerInput
detectTapGestures(onTap = { tapOffset ->
Timber.d(
@ -2600,7 +2607,8 @@ internal fun PdfPageComposable(
offset,
isScrolling,
isVerticalScroll,
selectedTool
selectedTool,
isStylusOnlyMode
) {
val canDraw = isEditMode && selectedTool != InkType.TEXT && !isScrolling && !isVerticalScroll && actualBitmapWidthPx > 0 && actualBitmapHeightPx > 0
@ -2611,6 +2619,13 @@ internal fun PdfPageComposable(
try {
awaitEachGesture {
val down = awaitFirstDown(requireUnconsumed = false)
Timber.tag("PointerTypeDebug").d("Page $pageIndex: Input Type detected: ${down.type}")
if (isStylusOnlyMode && down.type == PointerType.Touch) {
return@awaitEachGesture
}
val dragPointerId = down.id
val startPos = down.position
var dragStarted = false

View file

@ -88,6 +88,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.PointerType
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.positionChanged
import androidx.compose.ui.input.pointer.util.VelocityTracker
@ -216,7 +217,8 @@ internal fun PdfVerticalReader(
isAutoScrollTempPaused: Boolean = false,
isScrollLocked: Boolean = false,
autoScrollSpeed: Float = 1.0f,
onInteractionListener: () -> Unit = {}
onInteractionListener: () -> Unit = {},
isStylusOnlyMode: Boolean = false
) {
SideEffect { Timber.tag("PdfDrawPerf").v("LIST: PdfVerticalReader Recomposing.") }
var globalEraserPosition by remember { mutableStateOf<Offset?>(null) }
@ -726,13 +728,17 @@ internal fun PdfVerticalReader(
}
}
val globalDrawingModifier = Modifier.pointerInput(isEditMode, layoutInfo, selectedTool) {
val globalDrawingModifier = Modifier.pointerInput(isEditMode, layoutInfo, selectedTool, isStylusOnlyMode) {
if (!isEditMode) return@pointerInput
if (selectedTool == InkType.TEXT) return@pointerInput
awaitEachGesture {
val down = awaitFirstDown(requireUnconsumed = false)
if (isStylusOnlyMode && down.type == PointerType.Touch) {
return@awaitEachGesture
}
fun getPageAndPoint(screenOffset: Offset): Pair<Int, PdfPoint>? {
val zoom = zoomAnimatable.value
val panX = panXAnimatable.value
@ -814,31 +820,31 @@ internal fun PdfVerticalReader(
modifier = Modifier
.fillMaxSize()
.then(globalDrawingModifier)
.pointerInput(isEditMode, selectedTool) {
.pointerInput(isEditMode, selectedTool, isStylusOnlyMode) {
Timber.tag("PdfTouchDebug").v(
"VerticalReader: TapPointerInput init. isEditMode=$isEditMode"
)
if (isEditMode && selectedTool != InkType.TEXT) return@pointerInput
val isTapDetectionAllowed = !isEditMode ||
selectedTool == InkType.TEXT ||
isStylusOnlyMode
if (!isTapDetectionAllowed) return@pointerInput
detectTapGestures(onTap = {
if (!isEditMode) {
Timber.tag("PdfTouchDebug").d(
"VerticalReader: Tap detected (Toggling bars)"
)
Timber.tag("PdfTouchDebug").d("VerticalReader: Tap detected")
selectionClearTrigger++
onPageClick()
} else if (selectedTool == InkType.TEXT) {
onPageClick()
}
}, onDoubleTap = { offset ->
if (!isEditMode) {
Timber.tag("PdfTouchDebug").d("VerticalReader: DoubleTap detected")
onDoubleTapToZoom(offset)
}
Timber.tag("PdfTouchDebug").d("VerticalReader: DoubleTap detected")
onDoubleTapToZoom(offset)
})
}
.pointerInput(totalDocHeight, isEditMode, selectedTool, isScrollLocked) {
.pointerInput(totalDocHeight, isEditMode, selectedTool, isScrollLocked, isStylusOnlyMode) {
val tracker = VelocityTracker()
val decay = exponentialDecay<Float>()
val touchSlop = viewConfiguration.touchSlop
@ -849,11 +855,14 @@ internal fun PdfVerticalReader(
)
val down = awaitFirstDown(requireUnconsumed = false)
Timber.tag("PdfTouchDebug").v(
"VerticalReader: DOWN received. Pressed=${down.pressed}, Consumed=${down.isConsumed}"
)
if (isEditMode && selectedTool != InkType.TEXT && down.pressed) {
Timber.tag("PointerTypeDebug").d("VerticalReader: Input Type detected: ${down.type}")
val isDrawingGesture = isEditMode &&
selectedTool != InkType.TEXT &&
(!isStylusOnlyMode || down.type != PointerType.Touch)
if (isDrawingGesture && down.pressed) {
val event = awaitPointerEvent()
Timber.tag("PdfTouchDebug").v(
"VerticalReader: EditMode check. Event changes: ${event.changes.size}"
@ -959,8 +968,9 @@ internal fun PdfVerticalReader(
}
}
val shouldScroll =
(panLocked || gestureDisambiguationMode != 0) && (!isEditMode || selectedTool == InkType.TEXT || isMultiTouch)
val isTouchInput = event.changes.all { it.type == PointerType.Touch }
val shouldScroll = (panLocked || gestureDisambiguationMode != 0) &&
(!isEditMode || selectedTool == InkType.TEXT || isMultiTouch || (isStylusOnlyMode && isTouchInput))
if (shouldScroll) {
if (!isInteracting) {
@ -1432,6 +1442,7 @@ internal fun PdfVerticalReader(
onOcrModelDownloading = onOcrModelDownloading,
selectedTool = selectedTool,
richTextController = richTextController,
isStylusOnlyMode = isStylusOnlyMode,
textBoxes = textBoxes.filter { it.pageIndex == page.index },
selectedTextBoxId = selectedTextBoxId,
onTextBoxChange = onTextBoxChange,

View file

@ -291,12 +291,23 @@ private const val PDF_AUTO_SCROLL_LOCKED_KEY = "pdf_auto_scroll_locked"
private const val PDF_AUTO_SCROLL_USE_SLIDER_KEY = "pdf_auto_scroll_use_slider"
private const val PDF_AUTO_SCROLL_MIN_SPEED_KEY = "pdf_auto_scroll_min_speed"
private const val PDF_AUTO_SCROLL_MAX_SPEED_KEY = "pdf_auto_scroll_max_speed"
private const val STYLUS_ONLY_MODE_KEY = "stylus_only_mode"
private fun savePdfAutoScrollMinSpeed(context: Context, speed: Float) {
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
prefs.edit { putFloat(PDF_AUTO_SCROLL_MIN_SPEED_KEY, speed) }
}
private fun saveStylusOnlyMode(context: Context, isEnabled: Boolean) {
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
prefs.edit { putBoolean(STYLUS_ONLY_MODE_KEY, isEnabled) }
}
private fun loadStylusOnlyMode(context: Context): Boolean {
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
return prefs.getBoolean(STYLUS_ONLY_MODE_KEY, false)
}
private fun loadPdfAutoScrollMinSpeed(context: Context): Float {
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
return prefs.getFloat(PDF_AUTO_SCROLL_MIN_SPEED_KEY, 0.1f)
@ -649,6 +660,7 @@ fun PdfViewerScreen(
var isAutoScrollLocked by remember { mutableStateOf(loadPdfAutoScrollLocked(context)) }
var autoScrollUseSlider by remember { mutableStateOf(loadPdfAutoScrollUseSlider(context)) }
var isStylusOnlyMode by remember { mutableStateOf(loadStylusOnlyMode(context)) }
var currentTtsMode by remember { mutableStateOf(loadTtsMode(context)) }
var showTtsSettingsSheet by remember { mutableStateOf(false) }
@ -3298,6 +3310,7 @@ fun PdfViewerScreen(
}
},
richTextController = richTextController,
isStylusOnlyMode = isStylusOnlyMode,
isEditMode = isDrawingActive,
textBoxes = textBoxes.filter { it.pageIndex == pageIndex },
selectedTextBoxId = selectedTextBoxId,
@ -3649,6 +3662,7 @@ fun PdfViewerScreen(
},
selectedTool = selectedTool,
richTextController = richTextController,
isStylusOnlyMode = isStylusOnlyMode,
isEditMode = isDrawingActive,
textBoxes = textBoxes,
selectedTextBoxId = selectedTextBoxId,
@ -4894,6 +4908,11 @@ fun PdfViewerScreen(
activePenColor = dockPenColor,
activeHighlighterColor = dockHighlighterColor,
lastPenTool = lastPenTool,
isStylusOnlyMode = isStylusOnlyMode,
onToggleStylusOnlyMode = {
isStylusOnlyMode = !isStylusOnlyMode
saveStylusOnlyMode(context, isStylusOnlyMode)
},
onToolClick = { clickedTool ->
if (clickedTool == InkType.ERASER) {
annotationSettingsRepo.updateSelectedTool(