UI polish: toolbar redesign, clipboard, snap fix, startup state
Toolbar: - Replace FilterChip with custom ToolButton (no ripple, instant response) - Reorder tools: 0.38 0.5 Line Box Eraser Select - DropdownMenus anchored to buttons via Box wrapper (drop below) - Page indicator styled as OutlinedButton (visible affordance) - Cut/Del/Copy/Paste operations (internal clipboard) Line snap fix: - Check pen position when timer fires instead of canceling on movement - Handles EMR stylus micro-tremor correctly EMR eraser button: - TOOL_TYPE_ERASER events route to eraser handler regardless of active tool - Physical eraser end of pen works as temporary eraser App state: - Startup navigates to last-visited notebook/page via SharedPreferences - Closing notebook (X button) clears last notebook so app starts at list - Updated app icon Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,32 @@
|
|||||||
package net.metacircular.engpad
|
package net.metacircular.engpad
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.core.content.edit
|
||||||
import net.metacircular.engpad.data.db.EngPadDatabase
|
import net.metacircular.engpad.data.db.EngPadDatabase
|
||||||
|
|
||||||
class EngPadApp : Application() {
|
class EngPadApp : Application() {
|
||||||
val database: EngPadDatabase by lazy { EngPadDatabase.getInstance(this) }
|
val database: EngPadDatabase by lazy { EngPadDatabase.getInstance(this) }
|
||||||
|
|
||||||
|
fun getLastNotebookId(): Long {
|
||||||
|
val prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
return prefs.getLong(KEY_LAST_NOTEBOOK, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setLastNotebookId(id: Long) {
|
||||||
|
getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit {
|
||||||
|
putLong(KEY_LAST_NOTEBOOK, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearLastNotebookId() {
|
||||||
|
getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit {
|
||||||
|
remove(KEY_LAST_NOTEBOOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val PREFS_NAME = "engpad_prefs"
|
||||||
|
private const val KEY_LAST_NOTEBOOK = "last_notebook_id"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,13 +10,18 @@ import net.metacircular.engpad.ui.theme.EngPadTheme
|
|||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
val database = (application as EngPadApp).database
|
val app = application as EngPadApp
|
||||||
|
val database = app.database
|
||||||
|
val lastNotebookId = app.getLastNotebookId()
|
||||||
setContent {
|
setContent {
|
||||||
EngPadTheme {
|
EngPadTheme {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
EngPadNavGraph(
|
EngPadNavGraph(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
database = database,
|
database = database,
|
||||||
|
lastNotebookId = lastNotebookId,
|
||||||
|
onNotebookOpened = { app.setLastNotebookId(it) },
|
||||||
|
onNotebookClosed = { app.clearLastNotebookId() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ fun EditorScreen(
|
|||||||
val canUndo by viewModel.undoManager.canUndo.collectAsState()
|
val canUndo by viewModel.undoManager.canUndo.collectAsState()
|
||||||
val canRedo by viewModel.undoManager.canRedo.collectAsState()
|
val canRedo by viewModel.undoManager.canRedo.collectAsState()
|
||||||
val hasSelection by viewModel.hasSelection.collectAsState()
|
val hasSelection by viewModel.hasSelection.collectAsState()
|
||||||
|
val canPaste by viewModel.canPaste.collectAsState()
|
||||||
val currentPageIndex by viewModel.currentPageIndex.collectAsState()
|
val currentPageIndex by viewModel.currentPageIndex.collectAsState()
|
||||||
val pages by viewModel.pages.collectAsState()
|
val pages by viewModel.pages.collectAsState()
|
||||||
|
|
||||||
@@ -108,14 +109,20 @@ fun EditorScreen(
|
|||||||
onUndo = { viewModel.undo() },
|
onUndo = { viewModel.undo() },
|
||||||
onRedo = { viewModel.redo() },
|
onRedo = { viewModel.redo() },
|
||||||
hasSelection = hasSelection,
|
hasSelection = hasSelection,
|
||||||
|
onCutSelection = {
|
||||||
|
viewModel.cutSelection()
|
||||||
|
canvasView.clearSelection()
|
||||||
|
},
|
||||||
onDeleteSelection = {
|
onDeleteSelection = {
|
||||||
viewModel.deleteSelection()
|
viewModel.deleteSelection()
|
||||||
canvasView.clearSelection()
|
canvasView.clearSelection()
|
||||||
},
|
},
|
||||||
onCopySelection = {
|
onCopySelection = {
|
||||||
viewModel.copySelection()
|
viewModel.copyToClipboard()
|
||||||
canvasView.clearSelection()
|
canvasView.clearSelection()
|
||||||
},
|
},
|
||||||
|
onPaste = { viewModel.paste() },
|
||||||
|
canPaste = canPaste,
|
||||||
onExportPdf = {
|
onExportPdf = {
|
||||||
val currentPage = pages.getOrNull(currentPageIndex) ?: return@EditorToolbar
|
val currentPage = pages.getOrNull(currentPageIndex) ?: return@EditorToolbar
|
||||||
val file = PdfExporter.exportPages(
|
val file = PdfExporter.exportPages(
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ class EditorViewModel(
|
|||||||
val hasSelection: StateFlow<Boolean> = _hasSelection
|
val hasSelection: StateFlow<Boolean> = _hasSelection
|
||||||
private var selectedIds = emptySet<Long>()
|
private var selectedIds = emptySet<Long>()
|
||||||
|
|
||||||
|
private val _canPaste = MutableStateFlow(false)
|
||||||
|
val canPaste: StateFlow<Boolean> = _canPaste
|
||||||
|
private var clipboard = emptyList<Stroke>()
|
||||||
|
|
||||||
// Page navigation
|
// Page navigation
|
||||||
private val _pages = MutableStateFlow<List<Page>>(emptyList())
|
private val _pages = MutableStateFlow<List<Page>>(emptyList())
|
||||||
val pages: StateFlow<List<Page>> = _pages
|
val pages: StateFlow<List<Page>> = _pages
|
||||||
@@ -204,6 +208,40 @@ class EditorViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun cutSelection() {
|
||||||
|
clipboard = _strokes.value.filter { it.id in selectedIds }
|
||||||
|
_canPaste.value = clipboard.isNotEmpty()
|
||||||
|
deleteSelection()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun copyToClipboard() {
|
||||||
|
clipboard = _strokes.value.filter { it.id in selectedIds }
|
||||||
|
_canPaste.value = clipboard.isNotEmpty()
|
||||||
|
clearSelection()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun paste() {
|
||||||
|
if (clipboard.isEmpty()) return
|
||||||
|
viewModelScope.launch {
|
||||||
|
val pageId = _currentPageId.value
|
||||||
|
undoManager.perform(
|
||||||
|
CopyStrokesAction(
|
||||||
|
strokes = clipboard,
|
||||||
|
pageId = pageId,
|
||||||
|
repository = pageRepository,
|
||||||
|
onExecute = { copies ->
|
||||||
|
_strokes.value = _strokes.value + copies
|
||||||
|
copies.forEach { onStrokeAdded?.invoke(it) }
|
||||||
|
},
|
||||||
|
onUndo = { ids ->
|
||||||
|
_strokes.value = _strokes.value.filter { it.id !in ids }
|
||||||
|
ids.forEach { onStrokeRemoved?.invoke(it) }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun copySelection() {
|
fun copySelection() {
|
||||||
val toCopy = _strokes.value.filter { it.id in selectedIds }
|
val toCopy = _strokes.value.filter { it.id in selectedIds }
|
||||||
if (toCopy.isEmpty()) return
|
if (toCopy.isEmpty()) return
|
||||||
|
|||||||
@@ -46,10 +46,11 @@ class PadCanvasView(context: Context) : View(context) {
|
|||||||
// --- Line snap ---
|
// --- Line snap ---
|
||||||
private var strokeOriginX = 0f
|
private var strokeOriginX = 0f
|
||||||
private var strokeOriginY = 0f
|
private var strokeOriginY = 0f
|
||||||
private var maxDistFromOrigin = 0f
|
private var lastStylusX = 0f // Current pen position for snap check
|
||||||
|
private var lastStylusY = 0f
|
||||||
private var isSnappedToLine = false
|
private var isSnappedToLine = false
|
||||||
private val handler = Handler(Looper.getMainLooper())
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
private val snapRunnable = Runnable { snapToLine() }
|
private val snapRunnable = Runnable { trySnapToLine() }
|
||||||
|
|
||||||
// --- Box drawing ---
|
// --- Box drawing ---
|
||||||
private var boxStartX = 0f
|
private var boxStartX = 0f
|
||||||
@@ -342,7 +343,8 @@ class PadCanvasView(context: Context) : View(context) {
|
|||||||
@Suppress("ClickableViewAccessibility")
|
@Suppress("ClickableViewAccessibility")
|
||||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||||
return when (event.getToolType(0)) {
|
return when (event.getToolType(0)) {
|
||||||
MotionEvent.TOOL_TYPE_STYLUS, MotionEvent.TOOL_TYPE_ERASER -> handleStylusInput(event)
|
MotionEvent.TOOL_TYPE_STYLUS -> handleStylusInput(event)
|
||||||
|
MotionEvent.TOOL_TYPE_ERASER -> handleEraserInput(event) // EMR pen eraser button
|
||||||
MotionEvent.TOOL_TYPE_FINGER -> handleFingerInput(event)
|
MotionEvent.TOOL_TYPE_FINGER -> handleFingerInput(event)
|
||||||
else -> super.onTouchEvent(event)
|
else -> super.onTouchEvent(event)
|
||||||
}
|
}
|
||||||
@@ -372,31 +374,25 @@ class PadCanvasView(context: Context) : View(context) {
|
|||||||
val pt = screenToCanonical(event.x, event.y)
|
val pt = screenToCanonical(event.x, event.y)
|
||||||
strokeOriginX = pt[0]
|
strokeOriginX = pt[0]
|
||||||
strokeOriginY = pt[1]
|
strokeOriginY = pt[1]
|
||||||
maxDistFromOrigin = 0f
|
lastStylusX = pt[0]
|
||||||
|
lastStylusY = pt[1]
|
||||||
isSnappedToLine = false
|
isSnappedToLine = false
|
||||||
currentPoints.clear()
|
currentPoints.clear()
|
||||||
currentPoints.add(pt[0])
|
currentPoints.add(pt[0])
|
||||||
currentPoints.add(pt[1])
|
currentPoints.add(pt[1])
|
||||||
currentPath = Path().apply { moveTo(pt[0], pt[1]) }
|
currentPath = Path().apply { moveTo(pt[0], pt[1]) }
|
||||||
currentPaint = buildPaint(canvasState.penWidthPt, Color.BLACK)
|
currentPaint = buildPaint(canvasState.penWidthPt, Color.BLACK)
|
||||||
// Start snap timer
|
// Start snap timer — will check distance when it fires
|
||||||
handler.postDelayed(snapRunnable, LINE_SNAP_DELAY_MS)
|
handler.postDelayed(snapRunnable, LINE_SNAP_DELAY_MS)
|
||||||
invalidate()
|
invalidate()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
MotionEvent.ACTION_MOVE -> {
|
MotionEvent.ACTION_MOVE -> {
|
||||||
val path = currentPath ?: return true
|
val path = currentPath ?: return true
|
||||||
// Track max distance from origin; cancel snap if pen moved far
|
// Track current position for snap check
|
||||||
if (!isSnappedToLine) {
|
val curPt = screenToCanonical(event.x, event.y)
|
||||||
val pt = screenToCanonical(event.x, event.y)
|
lastStylusX = curPt[0]
|
||||||
val dx = pt[0] - strokeOriginX
|
lastStylusY = curPt[1]
|
||||||
val dy = pt[1] - strokeOriginY
|
|
||||||
val dist = Math.sqrt((dx * dx + dy * dy).toDouble()).toFloat()
|
|
||||||
if (dist > maxDistFromOrigin) maxDistFromOrigin = dist
|
|
||||||
if (maxDistFromOrigin > LINE_SNAP_MOVE_THRESHOLD) {
|
|
||||||
handler.removeCallbacks(snapRunnable)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSnappedToLine) {
|
if (isSnappedToLine) {
|
||||||
// In snap mode: draw straight line from origin to current point
|
// In snap mode: draw straight line from origin to current point
|
||||||
@@ -445,10 +441,17 @@ class PadCanvasView(context: Context) : View(context) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun snapToLine() {
|
/**
|
||||||
if (currentPath != null) {
|
* Called by the snap timer. Checks if pen is still near origin —
|
||||||
|
* if so, activate line snap. If pen has moved far, do nothing.
|
||||||
|
*/
|
||||||
|
private fun trySnapToLine() {
|
||||||
|
if (currentPath == null) return
|
||||||
|
val dx = lastStylusX - strokeOriginX
|
||||||
|
val dy = lastStylusY - strokeOriginY
|
||||||
|
val dist = Math.sqrt((dx * dx + dy * dy).toDouble()).toFloat()
|
||||||
|
if (dist <= LINE_SNAP_MOVE_THRESHOLD) {
|
||||||
isSnappedToLine = true
|
isSnappedToLine = true
|
||||||
// Reset path to just origin — next MOVE will draw the straight line
|
|
||||||
currentPath?.reset()
|
currentPath?.reset()
|
||||||
currentPath?.moveTo(strokeOriginX, strokeOriginY)
|
currentPath?.moveTo(strokeOriginX, strokeOriginY)
|
||||||
currentPoints.clear()
|
currentPoints.clear()
|
||||||
@@ -456,6 +459,7 @@ class PadCanvasView(context: Context) : View(context) {
|
|||||||
currentPoints.add(strokeOriginY)
|
currentPoints.add(strokeOriginY)
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
// If pen has moved far, don't snap — just let freehand continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Box drawing ---
|
// --- Box drawing ---
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
package net.metacircular.engpad.ui.editor
|
package net.metacircular.engpad.ui.editor
|
||||||
|
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.combinedClickable
|
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.FilterChip
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -21,9 +26,39 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool button — simple Surface with border. No ripple animation for fast
|
||||||
|
* e-ink response. Selected state shown with filled background.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun ToolButton(
|
||||||
|
label: String,
|
||||||
|
selected: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
onClick = onClick,
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
color = if (selected) Color.Black else Color.White,
|
||||||
|
contentColor = if (selected) Color.White else Color.Black,
|
||||||
|
border = BorderStroke(1.dp, Color.Black),
|
||||||
|
modifier = modifier.padding(end = 4.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun EditorToolbar(
|
fun EditorToolbar(
|
||||||
currentTool: Tool,
|
currentTool: Tool,
|
||||||
@@ -33,8 +68,11 @@ fun EditorToolbar(
|
|||||||
onUndo: () -> Unit,
|
onUndo: () -> Unit,
|
||||||
onRedo: () -> Unit,
|
onRedo: () -> Unit,
|
||||||
hasSelection: Boolean,
|
hasSelection: Boolean,
|
||||||
|
onCutSelection: () -> Unit,
|
||||||
onDeleteSelection: () -> Unit,
|
onDeleteSelection: () -> Unit,
|
||||||
onCopySelection: () -> Unit,
|
onCopySelection: () -> Unit,
|
||||||
|
onPaste: () -> Unit,
|
||||||
|
canPaste: Boolean,
|
||||||
onExportPdf: () -> Unit,
|
onExportPdf: () -> Unit,
|
||||||
onExportJpg: () -> Unit,
|
onExportJpg: () -> Unit,
|
||||||
onClose: () -> Unit,
|
onClose: () -> Unit,
|
||||||
@@ -46,7 +84,6 @@ fun EditorToolbar(
|
|||||||
onLineStyleChanged: (LineStyle) -> Unit,
|
onLineStyleChanged: (LineStyle) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
var showBinderMenu by remember { mutableStateOf(false) }
|
|
||||||
var showGoToPageDialog by remember { mutableStateOf(false) }
|
var showGoToPageDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
@@ -54,11 +91,17 @@ fun EditorToolbar(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
// Close button
|
// Close button
|
||||||
TextButton(onClick = onClose) { Text("X") }
|
ToolButton(label = "X", selected = false, onClick = onClose)
|
||||||
|
|
||||||
// Binder dropdown
|
// Page indicator / binder — outlined button style
|
||||||
TextButton(onClick = { showBinderMenu = true }) {
|
Box {
|
||||||
Text("p$currentPageNumber/$totalPages")
|
var showBinderMenu by remember { mutableStateOf(false) }
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { showBinderMenu = true },
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
border = BorderStroke(1.dp, Color.Black),
|
||||||
|
) {
|
||||||
|
Text("p$currentPageNumber/$totalPages", fontSize = 13.sp)
|
||||||
}
|
}
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
expanded = showBinderMenu,
|
expanded = showBinderMenu,
|
||||||
@@ -72,58 +115,93 @@ fun EditorToolbar(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text("Go to page...") },
|
text = { Text("Go to page\u2026") },
|
||||||
onClick = {
|
onClick = {
|
||||||
showBinderMenu = false
|
showBinderMenu = false
|
||||||
showGoToPageDialog = true
|
showGoToPageDialog = true
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
||||||
// Tool chips
|
// Tools: 0.38 0.5 line box eraser select
|
||||||
FilterChip(
|
ToolButton("0.38", currentTool == Tool.PEN_FINE, { onToolSelected(Tool.PEN_FINE) })
|
||||||
selected = currentTool == Tool.PEN_FINE,
|
ToolButton("0.5", currentTool == Tool.PEN_MEDIUM, { onToolSelected(Tool.PEN_MEDIUM) })
|
||||||
onClick = { onToolSelected(Tool.PEN_FINE) },
|
|
||||||
label = { Text("0.38") },
|
// Line button with long-press for style
|
||||||
modifier = Modifier.padding(end = 4.dp),
|
Box {
|
||||||
|
var showLineMenu by remember { mutableStateOf(false) }
|
||||||
|
val lineLabel = when (lineStyle) {
|
||||||
|
LineStyle.PLAIN -> "Line"
|
||||||
|
LineStyle.ARROW -> "\u2192"
|
||||||
|
LineStyle.DOUBLE_ARROW -> "\u2194"
|
||||||
|
LineStyle.DASHED -> "- -"
|
||||||
|
}
|
||||||
|
Surface(
|
||||||
|
onClick = { onToolSelected(Tool.LINE) },
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
color = if (currentTool == Tool.LINE) Color.Black else Color.White,
|
||||||
|
contentColor = if (currentTool == Tool.LINE) Color.White else Color.Black,
|
||||||
|
border = BorderStroke(1.dp, Color.Black),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 4.dp)
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = { onToolSelected(Tool.LINE) },
|
||||||
|
onLongClick = { showLineMenu = true },
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = lineLabel,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
|
||||||
)
|
)
|
||||||
FilterChip(
|
}
|
||||||
selected = currentTool == Tool.PEN_MEDIUM,
|
DropdownMenu(
|
||||||
onClick = { onToolSelected(Tool.PEN_MEDIUM) },
|
expanded = showLineMenu,
|
||||||
label = { Text("0.5") },
|
onDismissRequest = { showLineMenu = false },
|
||||||
modifier = Modifier.padding(end = 4.dp),
|
) {
|
||||||
)
|
LineStyle.entries.forEach { style ->
|
||||||
FilterChip(
|
val name = when (style) {
|
||||||
selected = currentTool == Tool.ERASER,
|
LineStyle.PLAIN -> "Plain line"
|
||||||
onClick = { onToolSelected(Tool.ERASER) },
|
LineStyle.ARROW -> "Arrow \u2192"
|
||||||
label = { Text("Eraser") },
|
LineStyle.DOUBLE_ARROW -> "Double \u2194"
|
||||||
modifier = Modifier.padding(end = 4.dp),
|
LineStyle.DASHED -> "Dashed"
|
||||||
)
|
}
|
||||||
FilterChip(
|
DropdownMenuItem(
|
||||||
selected = currentTool == Tool.SELECT,
|
text = { Text(name) },
|
||||||
onClick = { onToolSelected(Tool.SELECT) },
|
onClick = {
|
||||||
label = { Text("Select") },
|
showLineMenu = false
|
||||||
modifier = Modifier.padding(end = 4.dp),
|
onLineStyleChanged(style)
|
||||||
)
|
onToolSelected(Tool.LINE)
|
||||||
FilterChip(
|
},
|
||||||
selected = currentTool == Tool.BOX,
|
|
||||||
onClick = { onToolSelected(Tool.BOX) },
|
|
||||||
label = { Text("Box") },
|
|
||||||
modifier = Modifier.padding(end = 4.dp),
|
|
||||||
)
|
|
||||||
LineToolChip(
|
|
||||||
selected = currentTool == Tool.LINE,
|
|
||||||
lineStyle = lineStyle,
|
|
||||||
onSelect = { onToolSelected(Tool.LINE) },
|
|
||||||
onStyleChanged = onLineStyleChanged,
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolButton("Box", currentTool == Tool.BOX, { onToolSelected(Tool.BOX) })
|
||||||
|
ToolButton("Eraser", currentTool == Tool.ERASER, { onToolSelected(Tool.ERASER) })
|
||||||
|
ToolButton("Select", currentTool == Tool.SELECT, { onToolSelected(Tool.SELECT) })
|
||||||
|
|
||||||
|
// Selection operations: cut / del / copy / paste
|
||||||
|
if (hasSelection || canPaste) {
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
if (hasSelection) {
|
if (hasSelection) {
|
||||||
|
TextButton(onClick = onCutSelection) { Text("Cut") }
|
||||||
TextButton(onClick = onDeleteSelection) { Text("Del") }
|
TextButton(onClick = onDeleteSelection) { Text("Del") }
|
||||||
TextButton(onClick = onCopySelection) { Text("Copy") }
|
TextButton(onClick = onCopySelection) { Text("Copy") }
|
||||||
}
|
}
|
||||||
|
if (canPaste) {
|
||||||
|
TextButton(onClick = onPaste) { Text("Paste") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
|
// Export dropdown — anchored to its button
|
||||||
|
Box {
|
||||||
var showExportMenu by remember { mutableStateOf(false) }
|
var showExportMenu by remember { mutableStateOf(false) }
|
||||||
TextButton(onClick = { showExportMenu = true }) { Text("Export") }
|
TextButton(onClick = { showExportMenu = true }) { Text("Export") }
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
@@ -131,27 +209,25 @@ fun EditorToolbar(
|
|||||||
onDismissRequest = { showExportMenu = false },
|
onDismissRequest = { showExportMenu = false },
|
||||||
) {
|
) {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text("Export as PDF") },
|
text = { Text("PDF") },
|
||||||
onClick = {
|
onClick = {
|
||||||
showExportMenu = false
|
showExportMenu = false
|
||||||
onExportPdf()
|
onExportPdf()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text("Export as JPG") },
|
text = { Text("JPG") },
|
||||||
onClick = {
|
onClick = {
|
||||||
showExportMenu = false
|
showExportMenu = false
|
||||||
onExportJpg()
|
onExportJpg()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
TextButton(onClick = onUndo, enabled = canUndo) {
|
|
||||||
Text("Undo")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TextButton(onClick = onUndo, enabled = canUndo) { Text("Undo") }
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
TextButton(onClick = onRedo, enabled = canRedo) {
|
TextButton(onClick = onRedo, enabled = canRedo) { Text("Redo") }
|
||||||
Text("Redo")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showGoToPageDialog) {
|
if (showGoToPageDialog) {
|
||||||
@@ -207,53 +283,3 @@ private fun GoToPageDialog(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
|
||||||
@Composable
|
|
||||||
private fun LineToolChip(
|
|
||||||
selected: Boolean,
|
|
||||||
lineStyle: LineStyle,
|
|
||||||
onSelect: () -> Unit,
|
|
||||||
onStyleChanged: (LineStyle) -> Unit,
|
|
||||||
) {
|
|
||||||
var showStyleMenu by remember { mutableStateOf(false) }
|
|
||||||
val label = when (lineStyle) {
|
|
||||||
LineStyle.PLAIN -> "Line"
|
|
||||||
LineStyle.ARROW -> "Arrow"
|
|
||||||
LineStyle.DOUBLE_ARROW -> "\u2194" // ↔
|
|
||||||
LineStyle.DASHED -> "Dash"
|
|
||||||
}
|
|
||||||
|
|
||||||
FilterChip(
|
|
||||||
selected = selected,
|
|
||||||
onClick = { onSelect() },
|
|
||||||
label = { Text(label) },
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(end = 4.dp)
|
|
||||||
.combinedClickable(
|
|
||||||
onClick = { onSelect() },
|
|
||||||
onLongClick = { showStyleMenu = true },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
DropdownMenu(
|
|
||||||
expanded = showStyleMenu,
|
|
||||||
onDismissRequest = { showStyleMenu = false },
|
|
||||||
) {
|
|
||||||
LineStyle.entries.forEach { style ->
|
|
||||||
val styleName = when (style) {
|
|
||||||
LineStyle.PLAIN -> "Plain line"
|
|
||||||
LineStyle.ARROW -> "Single arrow \u2192"
|
|
||||||
LineStyle.DOUBLE_ARROW -> "Double arrow \u2194"
|
|
||||||
LineStyle.DASHED -> "Dashed line"
|
|
||||||
}
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(styleName) },
|
|
||||||
onClick = {
|
|
||||||
showStyleMenu = false
|
|
||||||
onStyleChanged(style)
|
|
||||||
onSelect()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -33,12 +33,25 @@ object Routes {
|
|||||||
fun EngPadNavGraph(
|
fun EngPadNavGraph(
|
||||||
navController: NavHostController,
|
navController: NavHostController,
|
||||||
database: EngPadDatabase,
|
database: EngPadDatabase,
|
||||||
|
lastNotebookId: Long = 0,
|
||||||
|
onNotebookOpened: (Long) -> Unit = {},
|
||||||
|
onNotebookClosed: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
|
// Auto-navigate to last notebook on startup
|
||||||
|
var autoNavigated by remember { mutableStateOf(false) }
|
||||||
|
LaunchedEffect(lastNotebookId) {
|
||||||
|
if (!autoNavigated && lastNotebookId > 0) {
|
||||||
|
autoNavigated = true
|
||||||
|
navController.navigate(Routes.editor(lastNotebookId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
NavHost(navController = navController, startDestination = Routes.NOTEBOOKS) {
|
NavHost(navController = navController, startDestination = Routes.NOTEBOOKS) {
|
||||||
composable(Routes.NOTEBOOKS) {
|
composable(Routes.NOTEBOOKS) {
|
||||||
NotebookListScreen(
|
NotebookListScreen(
|
||||||
database = database,
|
database = database,
|
||||||
onNotebookClick = { notebookId ->
|
onNotebookClick = { notebookId ->
|
||||||
|
onNotebookOpened(notebookId)
|
||||||
navController.navigate(Routes.editor(notebookId))
|
navController.navigate(Routes.editor(notebookId))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -59,6 +72,7 @@ fun EngPadNavGraph(
|
|||||||
var initialPageId by remember { mutableStateOf<Long?>(null) }
|
var initialPageId by remember { mutableStateOf<Long?>(null) }
|
||||||
|
|
||||||
LaunchedEffect(notebookId) {
|
LaunchedEffect(notebookId) {
|
||||||
|
onNotebookOpened(notebookId)
|
||||||
val notebook = notebookRepo.getById(notebookId) ?: return@LaunchedEffect
|
val notebook = notebookRepo.getById(notebookId) ?: return@LaunchedEffect
|
||||||
pageSize = PageSize.fromString(notebook.pageSize)
|
pageSize = PageSize.fromString(notebook.pageSize)
|
||||||
// Use last page or fall back to first page
|
// Use last page or fall back to first page
|
||||||
@@ -90,6 +104,7 @@ fun EngPadNavGraph(
|
|||||||
initialPageId = pid,
|
initialPageId = pid,
|
||||||
database = database,
|
database = database,
|
||||||
onClose = {
|
onClose = {
|
||||||
|
onNotebookClosed()
|
||||||
navController.popBackStack(Routes.NOTEBOOKS, false)
|
navController.popBackStack(Routes.NOTEBOOKS, false)
|
||||||
},
|
},
|
||||||
onViewAllPages = {
|
onViewAllPages = {
|
||||||
|
|||||||
@@ -4,7 +4,13 @@
|
|||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
android:viewportWidth="108"
|
android:viewportWidth="108"
|
||||||
android:viewportHeight="108">
|
android:viewportHeight="108">
|
||||||
|
<!-- White background -->
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFFFF"
|
android:fillColor="#F5F5F5"
|
||||||
android:pathData="M0,0h108v108H0z" />
|
android:pathData="M0,0h108v108H0z" />
|
||||||
|
<!-- Subtle grid lines evoking graph paper / blueprint -->
|
||||||
|
<path
|
||||||
|
android:strokeColor="#E0E0E0"
|
||||||
|
android:strokeWidth="0.5"
|
||||||
|
android:pathData="M18,0 V108 M30,0 V108 M42,0 V108 M54,0 V108 M66,0 V108 M78,0 V108 M90,0 V108 M0,18 H108 M0,30 H108 M0,42 H108 M0,54 H108 M0,66 H108 M0,78 H108 M0,90 H108" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -4,8 +4,31 @@
|
|||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
android:viewportWidth="108"
|
android:viewportWidth="108"
|
||||||
android:viewportHeight="108">
|
android:viewportHeight="108">
|
||||||
<!-- Simple pen icon -->
|
<!--
|
||||||
|
Architecture node graph: three connected nodes in a triangle.
|
||||||
|
All content within the 66dp adaptive-icon safe zone (21–87).
|
||||||
|
Node centers: top (54,35), bottom-left (37,67), bottom-right (71,67).
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- Edges (drawn first, behind nodes) -->
|
||||||
<path
|
<path
|
||||||
android:fillColor="#000000"
|
android:strokeColor="#2C2C2C"
|
||||||
android:pathData="M54,24 L62,72 L54,80 L46,72 Z" />
|
android:strokeWidth="2"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M54,35 L37,67 M54,35 L71,67 M37,67 L71,67" />
|
||||||
|
|
||||||
|
<!-- Top node -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#2C2C2C"
|
||||||
|
android:pathData="M48,29 L60,29 Q63,29 63,32 L63,38 Q63,41 60,41 L48,41 Q45,41 45,38 L45,32 Q45,29 48,29 Z" />
|
||||||
|
|
||||||
|
<!-- Bottom-left node -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#2C2C2C"
|
||||||
|
android:pathData="M31,61 L43,61 Q46,61 46,64 L46,70 Q46,73 43,73 L31,73 Q28,73 28,70 L28,64 Q28,61 31,61 Z" />
|
||||||
|
|
||||||
|
<!-- Bottom-right node -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#2C2C2C"
|
||||||
|
android:pathData="M65,61 L77,61 Q80,61 80,64 L80,70 Q80,73 77,73 L65,73 Q62,73 62,70 L62,64 Q62,61 65,61 Z" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
Reference in New Issue
Block a user