From ed3016be78ce25648addc99d4d6d109035e1f3ce Mon Sep 17 00:00:00 2001 From: Wiktor Wichrowski Date: Mon, 9 Mar 2026 20:57:14 +0100 Subject: [PATCH 01/10] migrate editor state management to `EditorViewModel` - Relocated editor state (mode, pen, eraser, pen settings, and selection) from `EditorState` to `EditorViewModel`. - Refactored `EditorState` into a thin wrapper for backward compatibility with canvas components. - Introduced `CanvasCommand` and `EditorUiEvent` channels in `EditorViewModel` to decouple UI actions from side effects. - Unified toolbar and editor UI state into a single `ToolbarUiState` flow. - Moved persistence of editor settings into `EditorViewModel` using `LaunchedEffect` in `EditorView`. - Updated `EditorView`, `EditorSurface`, and `ScrollIndicator` to consume state directly from `EditorViewModel`. - Moved `Mode`, `PlacementMode`, and `Clipboard` definitions to `EditorViewModel.kt`. --- .../datastore/EditorSettingCacheManager.kt | 2 +- .../notable/editor/EditorControlTower.kt | 27 +- .../com/ethran/notable/editor/EditorView.kt | 133 ++--- .../ethran/notable/editor/EditorViewModel.kt | 483 ++++++++++++------ .../editor/canvas/CanvasObserverRegistry.kt | 4 +- .../editor/canvas/CanvasRefreshManager.kt | 2 +- .../notable/editor/canvas/DrawCanvas.kt | 2 +- .../notable/editor/canvas/OnyxInputHandler.kt | 2 +- .../notable/editor/state/EditorState.kt | 160 ++---- .../notable/editor/state/SelectionState.kt | 7 +- .../ethran/notable/editor/ui/EditorSurface.kt | 12 +- .../notable/editor/ui/ScrollIndicator.kt | 13 +- .../notable/editor/ui/toolbar/Toolbar.kt | 2 +- .../notable/editor/ui/toolbar/ToolbarMenu.kt | 2 +- .../notable/editor/utils/ImageHandler.kt | 2 +- .../com/ethran/notable/editor/utils/Select.kt | 2 +- 16 files changed, 466 insertions(+), 389 deletions(-) diff --git a/app/src/main/java/com/ethran/notable/data/datastore/EditorSettingCacheManager.kt b/app/src/main/java/com/ethran/notable/data/datastore/EditorSettingCacheManager.kt index fdaff115..56ebbe65 100644 --- a/app/src/main/java/com/ethran/notable/data/datastore/EditorSettingCacheManager.kt +++ b/app/src/main/java/com/ethran/notable/data/datastore/EditorSettingCacheManager.kt @@ -2,7 +2,7 @@ package com.ethran.notable.data.datastore import com.ethran.notable.data.db.Kv import com.ethran.notable.data.db.KvRepository -import com.ethran.notable.editor.state.Mode +import com.ethran.notable.editor.Mode import com.ethran.notable.editor.utils.Eraser import com.ethran.notable.editor.utils.NamedSettings import com.ethran.notable.editor.utils.Pen diff --git a/app/src/main/java/com/ethran/notable/editor/EditorControlTower.kt b/app/src/main/java/com/ethran/notable/editor/EditorControlTower.kt index 9a2b922c..b016d65c 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorControlTower.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorControlTower.kt @@ -8,9 +8,7 @@ import com.ethran.notable.editor.canvas.CanvasEventBus import com.ethran.notable.editor.state.EditorState import com.ethran.notable.editor.state.History import com.ethran.notable.editor.state.HistoryBusActions -import com.ethran.notable.editor.state.Mode import com.ethran.notable.editor.state.Operation -import com.ethran.notable.editor.state.PlacementMode import com.ethran.notable.editor.state.SelectionState import com.ethran.notable.editor.state.UndoRedoType import com.ethran.notable.editor.utils.divideStrokesFromCut @@ -90,19 +88,9 @@ class EditorControlTower( * @param id The unique identifier of the page to switch to. */ private suspend fun switchPage(id: String) { - // TODO: Check if this is a problem: - //`switchPage()` now calls `page.changePage(id)` inside `withContext(Dispatchers.IO)`, - // but `PageView.changePage()` mutates in-memory state (e.g., `currentPageId`, - // `zoomLevel.value`) before it does its own IO work. Calling it from IO risks - // off-main state mutations and also makes it easier for callers to accidentally - // invoke `page.changePage()` twice (which currently happens in `registerObservers()`). - // Prefer calling `page.changePage(id)` from the main thread (or let - // `PageView.changePage()` manage its own dispatching) and ensure callers don’t call - // it again after `switchPage()`. - // Switch to Main thread for Compose state mutations withContext(Dispatchers.Main) { - state.changePage(id) + state.viewModel.changePage(id) history.cleanHistory() } @@ -117,15 +105,15 @@ class EditorControlTower( logEditorControlTower.w("IsDrawing already set to $value") return } - state.isDrawing = value + state.viewModel.isDrawing = value } fun toggleTool() { - state.mode = if (state.mode == Mode.Draw) Mode.Erase else Mode.Draw + state.viewModel.onToolbarAction(ToolbarAction.ChangeMode(if (state.mode == Mode.Draw) Mode.Erase else Mode.Draw)) } fun toggleZen() { - state.isToolbarOpen = !state.isToolbarOpen + state.viewModel.onToolbarAction(ToolbarAction.ToggleToolbar) } fun getSnapshotOfSelectionState(): SelectionState { @@ -139,7 +127,7 @@ class EditorControlTower( fun goToNextPage() { scope.launch(Dispatchers.IO) { logEditorControlTower.i("Going to next page") - val next = state.getNextPage() + val next = state.viewModel.getNextPage() if (next != null) switchPage(next) } @@ -148,7 +136,7 @@ class EditorControlTower( fun goToPreviousPage() { scope.launch(Dispatchers.IO) { logEditorControlTower.i("Going to previous page") - val previous = state.getPreviousPage() + val previous = state.viewModel.getPreviousPage() if (previous != null) switchPage(previous) } @@ -250,7 +238,7 @@ class EditorControlTower( fun deleteSelection() { val operationList = state.selectionState.deleteSelection(page) history.addOperationsToHistory(operationList) - state.isDrawing = true + state.viewModel.isDrawing = true scope.launch { CanvasEventBus.refreshUi.emit(Unit) } @@ -326,4 +314,3 @@ class EditorControlTower( showHint("Pasted content from clipboard", scope) } } - diff --git a/app/src/main/java/com/ethran/notable/editor/EditorView.kt b/app/src/main/java/com/ethran/notable/editor/EditorView.kt index a5c28149..dfc9077f 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorView.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorView.kt @@ -90,7 +90,7 @@ fun EditorView( if (!exists) { // TODO: check if it is correct, and remove exeption throwing - throw Exception("Page does not exist") +// throw Exception("Page does not exist") if (bookId != null) { // clean the book log.i("Could not find page, Cleaning book") @@ -113,7 +113,6 @@ fun EditorView( val height = convertDpToPixel(this.maxHeight, context).toInt() val width = convertDpToPixel(this.maxWidth, context).toInt() - val page = remember { PageView( context = context, @@ -126,37 +125,28 @@ fun EditorView( ) } - val editorState = remember { - EditorState( - appRepository = appRepository, - bookId = bookId, - pageId = pageId, - pageView = page, - persistedEditorSettings = editorSettingCacheManager.getEditorSettings(), - onPageChange = onPageChange - ) - } - val history = remember { History(page) } + + // Create EditorState wrapper for backward compatibility + val editorState = remember(viewModel, page) { + EditorState(viewModel, page) + } + + // Initialize ViewModel with persisted settings on first composition + LaunchedEffect(Unit) { + viewModel.initFromPersistedSettings(editorSettingCacheManager.getEditorSettings()) + } + val editorControlTower = remember { EditorControlTower(scope, page, history, editorState).apply { registerObservers() } } - // Collect UI Events from ViewModel + // Collect UI Events from ViewModel (navigation and snackbars) LaunchedEffect(Unit) { viewModel.uiEvents.collect { event -> when (event) { - is EditorUiEvent.Undo -> editorControlTower.undo() - is EditorUiEvent.Redo -> editorControlTower.redo() - is EditorUiEvent.Paste -> editorControlTower.pasteFromClipboard() - is EditorUiEvent.ResetView -> editorControlTower.resetZoomAndScroll() - is EditorUiEvent.ClearAllStrokes -> { - CanvasEventBus.clearPageSignal.emit(Unit) - snackManager.displaySnack(SnackConf(text = "Cleared all strokes")) - } - is EditorUiEvent.NavigateToLibrary -> { navController.navigate(LibraryDestination.createRoute(event.folderId)) } @@ -172,35 +162,29 @@ fun EditorView( is EditorUiEvent.ShowSnackbar -> { snackManager.displaySnack(SnackConf(text = event.message)) } + } + } + } - is EditorUiEvent.CopyImageToCanvas -> { - CanvasEventBus.addImageByUri.value = event.uri + // Collect Canvas Commands from ViewModel + LaunchedEffect(Unit) { + viewModel.canvasCommands.collect { command -> + when (command) { + CanvasCommand.Undo -> editorControlTower.undo() + CanvasCommand.Redo -> editorControlTower.redo() + CanvasCommand.Paste -> editorControlTower.pasteFromClipboard() + CanvasCommand.ResetView -> editorControlTower.resetZoomAndScroll() + CanvasCommand.ClearAllStrokes -> { + CanvasEventBus.clearPageSignal.emit(Unit) + snackManager.displaySnack(SnackConf(text = "Cleared all strokes")) } - EditorUiEvent.RefreshCanvas -> { + CanvasCommand.RefreshCanvas -> { CanvasEventBus.reloadFromDb.emit(Unit) } - is EditorUiEvent.ModeChanged -> { - editorState.mode = event.mode - } - - is EditorUiEvent.PenChanged -> { - editorState.pen = event.pen - } - - is EditorUiEvent.PenSettingChanged -> { - val newSettings = editorState.penSettings.toMutableMap() - newSettings[event.pen.penName] = event.setting - editorState.penSettings = newSettings - } - - is EditorUiEvent.EraserChanged -> { - editorState.eraser = event.eraser - } - - is EditorUiEvent.ToolbarVisibilityChanged -> { - editorState.isToolbarOpen = event.visible + is CanvasCommand.CopyImageToCanvas -> { + CanvasEventBus.addImageByUri.value = command.uri } } } @@ -222,38 +206,24 @@ fun EditorView( } } - // Sync legacy state to ViewModel for Toolbar rendering + // Sync PageView state to ViewModel for Toolbar rendering val zoomLevel by page.zoomLevel.collectAsState() - val selectionActive = editorState.selectionState.isNonEmpty() + val selectionActive = viewModel.selectionState.isNonEmpty() LaunchedEffect( zoomLevel, page.scroll, - editorState.clipboard, - editorState.isToolbarOpen, - editorState.mode, - editorState.pen, - editorState.eraser, - editorState.penSettings, + viewModel.clipboard, selectionActive ) { - viewModel.setHasClipboard(editorState.clipboard != null) - viewModel.setShowResetView(zoomLevel != 1.0f) // page.scroll != Offset.Zero + viewModel.setHasClipboard(viewModel.clipboard != null) + viewModel.setShowResetView(zoomLevel != 1.0f) viewModel.setSelectionActive(selectionActive) - viewModel.updateToolbarSettings( - ToolbarUiState( - isToolbarOpen = editorState.isToolbarOpen, - mode = editorState.mode, - pen = editorState.pen, - eraser = editorState.eraser, - penSettings = editorState.penSettings - ) - ) } DisposableEffect(Unit) { onDispose { // finish selection operation - editorState.selectionState.applySelectionDisplace(page) + viewModel.selectionState.applySelectionDisplace(page) if (bookId != null) exportToLinkedFile( exportEngine, bookId, @@ -263,32 +233,31 @@ fun EditorView( } } - // TODO put in editorSetting class + // Persist editor settings when they change LaunchedEffect( - editorState.isToolbarOpen, - editorState.pen, - editorState.penSettings, - editorState.mode, - editorState.eraser + viewModel.isToolbarOpen, + viewModel.pen, + viewModel.penSettings, + viewModel.mode, + viewModel.eraser ) { - log.i("EditorView: saving") + log.i("EditorView: saving editor settings") editorSettingCacheManager.setEditorSettings( EditorSettingCacheManager.EditorSettings( - isToolbarOpen = editorState.isToolbarOpen, - mode = editorState.mode, - pen = editorState.pen, - eraser = editorState.eraser, - penSettings = editorState.penSettings + isToolbarOpen = viewModel.isToolbarOpen, + mode = viewModel.mode, + pen = viewModel.pen, + eraser = viewModel.eraser, + penSettings = viewModel.penSettings ) ) } - InkaTheme { EditorGestureReceiver(controlTower = editorControlTower) EditorSurface( - appRepository = appRepository, state = editorState, page = page, history = history + appRepository = appRepository, viewModel = viewModel, page = page, history = history ) SelectedBitmap( context = context, controlTower = editorControlTower @@ -299,11 +268,11 @@ fun EditorView( .fillMaxHeight() ) { Spacer(modifier = Modifier.weight(1f)) - ScrollIndicator(state = editorState) + ScrollIndicator(viewModel = viewModel, page = page) } PositionedToolbar( viewModel = viewModel, onDrawingStateCheck = { viewModel.updateDrawingState() }) - HorizontalScrollIndicator(state = editorState) + HorizontalScrollIndicator(viewModel = viewModel, page = page) } } } diff --git a/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt b/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt index 72ec1db9..1efa1f79 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt @@ -1,45 +1,99 @@ package com.ethran.notable.editor import android.content.Context +import android.graphics.Color import android.net.Uri +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.core.net.toUri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.ethran.notable.data.AppRepository import com.ethran.notable.data.copyImageToDatabase +import com.ethran.notable.data.datastore.EditorSettingCacheManager import com.ethran.notable.data.datastore.GlobalAppSettings import com.ethran.notable.data.db.getPageIndex import com.ethran.notable.data.db.getParentFolder import com.ethran.notable.data.model.BackgroundType import com.ethran.notable.editor.canvas.CanvasEventBus -import com.ethran.notable.editor.state.Mode +import com.ethran.notable.editor.state.ClipboardContent +import com.ethran.notable.editor.state.SelectionState import com.ethran.notable.editor.utils.Eraser import com.ethran.notable.editor.utils.Pen import com.ethran.notable.editor.utils.PenSetting import com.ethran.notable.io.ExportEngine import com.ethran.notable.io.ExportFormat import com.ethran.notable.io.ExportTarget +import com.ethran.notable.ui.SnackConf +import com.ethran.notable.ui.SnackState import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext +import io.shipbook.shipbooksdk.Log import io.shipbook.shipbooksdk.ShipBook import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject - private val log = ShipBook.getLogger("EditorViewModel") +// -------------------------------------------------------- +// 1. UI STATE +// -------------------------------------------------------- /** - * Toolbar Actions (Intents) representing user interactions. + * Flat toolbar/editor UI state exposed to Compose. + * Also used as `EditorUiState` via typealias for backward compatibility. */ +data class ToolbarUiState( + // Document info + val notebookId: String? = null, + val pageId: String? = null, + val isBookActive: Boolean = false, + val pageNumberInfo: String = "1/1", + val currentPageNumber: Int = 0, + + // Background + val backgroundType: String = "native", + val backgroundPath: String = "blank", + val backgroundPageNumber: Int = 0, + + // Toolbar visibility & menus + val isToolbarOpen: Boolean = false, + val isMenuOpen: Boolean = false, + val isStrokeSelectionOpen: Boolean = false, + val isBackgroundSelectorModalOpen: Boolean = false, + val showResetView: Boolean = false, + + // Canvas / drawing + val mode: Mode = Mode.Draw, + val pen: Pen = Pen.BALLPEN, + val eraser: Eraser = Eraser.PEN, + val penSettings: Map = emptyMap(), + val isSelectionActive: Boolean = false, + val hasClipboard: Boolean = false, +) { + val isDrawingAllowed: Boolean + get() = mode == Mode.Draw && + !isMenuOpen && + !isStrokeSelectionOpen && + !isBackgroundSelectorModalOpen && + !isSelectionActive +} + +/** Backward-compatible alias used by ToolbarMenu and other components. */ +typealias EditorUiState = ToolbarUiState + +// -------------------------------------------------------- +// 2. USER ACTIONS (Intents) +// -------------------------------------------------------- + sealed class ToolbarAction { object ToggleToolbar : ToolbarAction() data class ChangeMode(val mode: Mode) : ToolbarAction() @@ -51,20 +105,17 @@ sealed class ToolbarAction { data class ToggleBackgroundSelector(val isOpen: Boolean) : ToolbarAction() data class ToggleScribbleToErase(val enabled: Boolean) : ToolbarAction() - // Actions that trigger side effects in ControlTower or other components object Undo : ToolbarAction() object Redo : ToolbarAction() object Paste : ToolbarAction() object ResetView : ToolbarAction() object ClearAllStrokes : ToolbarAction() - // Complex Actions data class ImagePicked(val uri: Uri) : ToolbarAction() data class ExportPage(val format: ExportFormat) : ToolbarAction() data class ExportBook(val format: ExportFormat) : ToolbarAction() data class BackgroundChanged(val type: String, val path: String?) : ToolbarAction() - // Navigation object NavigateToLibrary : ToolbarAction() object NavigateToBugReport : ToolbarAction() object NavigateToPages : ToolbarAction() @@ -73,61 +124,37 @@ sealed class ToolbarAction { object CloseAllMenus : ToolbarAction() } -/** - * UI Events for one-time side effects (navigation, snackbars, etc.) - */ +/** Backward-compatible alias used by ToolbarMenu. */ +typealias EditorAction = ToolbarAction + +// -------------------------------------------------------- +// 3. CANVAS COMMANDS (Imperative drawing actions) +// -------------------------------------------------------- + +sealed class CanvasCommand { + object Undo : CanvasCommand() + object Redo : CanvasCommand() + object Paste : CanvasCommand() + object ResetView : CanvasCommand() + object ClearAllStrokes : CanvasCommand() + object RefreshCanvas : CanvasCommand() + data class CopyImageToCanvas(val uri: Uri) : CanvasCommand() +} + +// -------------------------------------------------------- +// 4. UI EVENTS (Navigation, Snackbars) +// -------------------------------------------------------- + sealed class EditorUiEvent { data class ShowSnackbar(val message: String) : EditorUiEvent() data class NavigateToLibrary(val folderId: String?) : EditorUiEvent() data class NavigateToPages(val bookId: String) : EditorUiEvent() object NavigateToBugReport : EditorUiEvent() - - // Side effects back to drawing logic/engine - object Undo : EditorUiEvent() - object Redo : EditorUiEvent() - object Paste : EditorUiEvent() - object ResetView : EditorUiEvent() - object ClearAllStrokes : EditorUiEvent() - object RefreshCanvas : EditorUiEvent() - data class CopyImageToCanvas(val uri: Uri) : EditorUiEvent() - - // Sync state back to EditorState - data class ModeChanged(val mode: Mode) : EditorUiEvent() - data class PenChanged(val pen: Pen) : EditorUiEvent() - data class PenSettingChanged(val pen: Pen, val setting: PenSetting) : EditorUiEvent() - data class EraserChanged(val eraser: Eraser) : EditorUiEvent() - data class ToolbarVisibilityChanged(val visible: Boolean) : EditorUiEvent() } -/** - * UI State for the Toolbar rendering. - */ -data class ToolbarUiState( - val isToolbarOpen: Boolean = true, - val mode: Mode = Mode.Draw, - val pen: Pen = Pen.BALLPEN, - val penSettings: Map = emptyMap(), - val eraser: Eraser = Eraser.PEN, - val isMenuOpen: Boolean = false, - val isStrokeSelectionOpen: Boolean = false, - val isBackgroundSelectorModalOpen: Boolean = false, - val pageNumberInfo: String = "1/1", - val hasClipboard: Boolean = false, - val showResetView: Boolean = false, - val isSelectionActive: Boolean = false, - - // Context needed for visibility rules in UI - val notebookId: String? = null, - val pageId: String? = null, - val isBookActive: Boolean = false, - - // TODO: check correctness - // Internal data for BackgroundSelector rendering if it remains stateless - val backgroundType: String = "native", - val backgroundPath: String = "blank", - val backgroundPageNumber: Int = 0, - val currentPageNumber: Int = 0 -) +// -------------------------------------------------------- +// 5. VIEW MODEL +// -------------------------------------------------------- @HiltViewModel class EditorViewModel @Inject constructor( @@ -136,29 +163,89 @@ class EditorViewModel @Inject constructor( private val exportEngine: ExportEngine ) : ViewModel() { + // ---- Toolbar / UI State (single flat flow) ---- private val _toolbarState = MutableStateFlow(ToolbarUiState()) val toolbarState: StateFlow = _toolbarState.asStateFlow() - private val _uiEvents = MutableSharedFlow() - val uiEvents: SharedFlow = _uiEvents.asSharedFlow() + // ---- One-Time Events (Channels) ---- + private val uiEventChannel = Channel() + val uiEvents = uiEventChannel.receiveAsFlow() + + private val canvasCommandChannel = Channel() + val canvasCommands = canvasCommandChannel.receiveAsFlow() + + // ---- Internal document context ---- + private var bookId: String? = null + + // ---- Editor state (from EditorState) ---- + var currentPageId by mutableStateOf("") + private set + + var mode by mutableStateOf(Mode.Draw) + private set + + var pen by mutableStateOf(Pen.BALLPEN) + private set + + var eraser by mutableStateOf(Eraser.PEN) + private set + + var isDrawing by mutableStateOf(true) + + var isToolbarOpen by mutableStateOf(false) + private set + + var penSettings by mutableStateOf(DEFAULT_PEN_SETTINGS) + private set + + val selectionState = SelectionState() + + private var _clipboard by mutableStateOf(Clipboard.content) + var clipboard + get() = _clipboard + set(value) { + _clipboard = value + // The clipboard content must survive the EditorState, so we store a copy in + // a singleton that lives outside of the EditorState + Clipboard.content = value + } + + // -------------------------------------------------------- + // Initialization from persisted settings + // -------------------------------------------------------- + + /** + * Restores editor settings from the persisted cache. + * Should be called once when EditorView is composed. + */ + fun initFromPersistedSettings(settings: EditorSettingCacheManager.EditorSettings?) { + mode = settings?.mode ?: Mode.Draw + pen = settings?.pen ?: Pen.BALLPEN + eraser = settings?.eraser ?: Eraser.PEN + isToolbarOpen = settings?.isToolbarOpen ?: false + penSettings = settings?.penSettings ?: DEFAULT_PEN_SETTINGS + + syncToolbarState() + } - // Internal context - private var currentBookId: String? = null - private var currentPageId: String = "" + // -------------------------------------------------------- + // Toolbar Action Dispatch + // -------------------------------------------------------- fun onToolbarAction(action: ToolbarAction) { when (action) { is ToolbarAction.ToggleToolbar -> { - val newVisible = !_toolbarState.value.isToolbarOpen - _toolbarState.update { it.copy(isToolbarOpen = newVisible) } - sendUiEvent(EditorUiEvent.ToolbarVisibilityChanged(newVisible)) + isToolbarOpen = !isToolbarOpen + syncToolbarState() updateDrawingState() } + is ToolbarAction.ChangeMode -> { - _toolbarState.update { it.copy(mode = action.mode) } - sendUiEvent(EditorUiEvent.ModeChanged(action.mode)) + mode = action.mode + syncToolbarState() updateDrawingState() } + is ToolbarAction.ChangePen -> handlePenChange(action.pen) is ToolbarAction.ChangePenSetting -> handlePenSettingChange(action.pen, action.setting) is ToolbarAction.ChangeEraser -> handleEraserChange(action.eraser) @@ -166,10 +253,12 @@ class EditorViewModel @Inject constructor( _toolbarState.update { it.copy(isMenuOpen = !it.isMenuOpen) } updateDrawingState() } + is ToolbarAction.UpdateMenuOpenTo -> { _toolbarState.update { it.copy(isStrokeSelectionOpen = action.isOpen) } updateDrawingState() } + is ToolbarAction.ToggleBackgroundSelector -> { _toolbarState.update { it.copy(isBackgroundSelectorModalOpen = action.isOpen) } updateDrawingState() @@ -177,17 +266,22 @@ class EditorViewModel @Inject constructor( is ToolbarAction.ToggleScribbleToErase -> updateScribbleToErase(action.enabled) is ToolbarAction.ImagePicked -> handleImagePicked(action.uri) - is ToolbarAction.ExportPage -> handleExport(ExportTarget.Page(currentPageId), action.format) + is ToolbarAction.ExportPage -> handleExport( + ExportTarget.Page(currentPageId), + action.format + ) + is ToolbarAction.ExportBook -> { - currentBookId?.let { handleExport(ExportTarget.Book(it), action.format) } + bookId?.let { handleExport(ExportTarget.Book(it), action.format) } } + is ToolbarAction.BackgroundChanged -> handleBackgroundChange(action.type, action.path) - ToolbarAction.Undo -> sendUiEvent(EditorUiEvent.Undo) - ToolbarAction.Redo -> sendUiEvent(EditorUiEvent.Redo) - ToolbarAction.Paste -> sendUiEvent(EditorUiEvent.Paste) - ToolbarAction.ResetView -> sendUiEvent(EditorUiEvent.ResetView) - ToolbarAction.ClearAllStrokes -> sendUiEvent(EditorUiEvent.ClearAllStrokes) + ToolbarAction.Undo -> sendCanvasCommand(CanvasCommand.Undo) + ToolbarAction.Redo -> sendCanvasCommand(CanvasCommand.Redo) + ToolbarAction.Paste -> sendCanvasCommand(CanvasCommand.Paste) + ToolbarAction.ResetView -> sendCanvasCommand(CanvasCommand.ResetView) + ToolbarAction.ClearAllStrokes -> sendCanvasCommand(CanvasCommand.ClearAllStrokes) ToolbarAction.NavigateToLibrary -> handleNavigateToLibrary() ToolbarAction.NavigateToBugReport -> sendUiEvent(EditorUiEvent.NavigateToBugReport) @@ -198,38 +292,35 @@ class EditorViewModel @Inject constructor( } } - private fun sendUiEvent(event: EditorUiEvent) { - viewModelScope.launch { _uiEvents.emit(event) } - } + // -------------------------------------------------------- + // Toolbar Action Handlers (private) + // -------------------------------------------------------- private fun handlePenChange(pen: Pen) { - var penChanged = false - var modeChanged = false - - _toolbarState.update { state -> - if (state.mode == Mode.Draw && state.pen == pen) { - state.copy(isStrokeSelectionOpen = true) - } else { - penChanged = true - if (state.mode != Mode.Draw) { - modeChanged = true - } - state.copy(mode = Mode.Draw, pen = pen) + if (mode == Mode.Draw && this.pen == pen) { + _toolbarState.update { it.copy(isStrokeSelectionOpen = true) } + } else { + this.pen = pen + if (mode != Mode.Draw) { + mode = Mode.Draw } + syncToolbarState() } - // Fire side-effects outside the update block - if (penChanged) sendUiEvent(EditorUiEvent.PenChanged(pen)) - if (modeChanged) sendUiEvent(EditorUiEvent.ModeChanged(Mode.Draw)) - updateDrawingState() } private fun handleEraserChange(eraser: Eraser) { - _toolbarState.update { it.copy(eraser = eraser) } - sendUiEvent(EditorUiEvent.EraserChanged(eraser)) + this.eraser = eraser + syncToolbarState() updateDrawingState() } + private fun handlePenSettingChange(pen: Pen, setting: PenSetting) { + val newSettings = penSettings.toMutableMap() + newSettings[pen.penName] = setting + penSettings = newSettings + syncToolbarState() + } private fun handleCloseAllMenus() { log.d("Closing all menus in EditorViewModel") @@ -243,36 +334,6 @@ class EditorViewModel @Inject constructor( updateDrawingState() } - /** - * Re-evaluates whether drawing should be enabled based on menu and selection states. - * Also handles switching back to drawing mode when menus are closed. - */ - fun updateDrawingState() { - val state = _toolbarState.value - val anyMenuOpen = - state.isMenuOpen || state.isStrokeSelectionOpen || state.isBackgroundSelectorModalOpen - val shouldBeDrawing = !anyMenuOpen && !_toolbarState.value.isSelectionActive - log.d("Drawing state: $shouldBeDrawing") - viewModelScope.launch { - CanvasEventBus.isDrawing.emit(shouldBeDrawing) - } - } - - fun onFocusChanged(isFocused: Boolean) { - if (isFocused) { - updateDrawingState() - } - } - - private fun handlePenSettingChange(pen: Pen, setting: PenSetting) { - _toolbarState.update { state -> - val newSettings = state.penSettings.toMutableMap() - newSettings[pen.penName] = setting - state.copy(penSettings = newSettings) - } - sendUiEvent(EditorUiEvent.PenSettingChanged(pen, setting)) - } - private fun updateScribbleToErase(enabled: Boolean) { viewModelScope.launch(Dispatchers.IO) { appRepository.kvProxy.setAppSettings( @@ -285,9 +346,9 @@ class EditorViewModel @Inject constructor( viewModelScope.launch(Dispatchers.IO) { try { val copiedFile = copyImageToDatabase(context, uri) - _uiEvents.emit(EditorUiEvent.CopyImageToCanvas(copiedFile.toUri())) + sendCanvasCommand(CanvasCommand.CopyImageToCanvas(copiedFile.toUri())) } catch (e: Exception) { - _uiEvents.emit(EditorUiEvent.ShowSnackbar("Image import failed: ${e.message}")) + sendUiEvent(EditorUiEvent.ShowSnackbar("Image import failed: ${e.message}")) } } } @@ -296,9 +357,9 @@ class EditorViewModel @Inject constructor( viewModelScope.launch(Dispatchers.IO) { try { val result = exportEngine.export(target, format) - _uiEvents.emit(EditorUiEvent.ShowSnackbar(result)) + sendUiEvent(EditorUiEvent.ShowSnackbar(result)) } catch (e: Exception) { - _uiEvents.emit(EditorUiEvent.ShowSnackbar("Export failed: ${e.message}")) + sendUiEvent(EditorUiEvent.ShowSnackbar("Export failed: ${e.message}")) } } } @@ -317,20 +378,20 @@ class EditorViewModel @Inject constructor( val bgPageNum = when (val bgTypeObj = BackgroundType.fromKey(type)) { is BackgroundType.Pdf -> bgTypeObj.page is BackgroundType.AutoPdf -> { - currentBookId?.let { appRepository.getPageNumber(it, currentPageId) } ?: 0 + bookId?.let { appRepository.getPageNumber(it, currentPageId) } ?: 0 } else -> 0 } - _toolbarState.update { + _toolbarState.update { it.copy( backgroundType = updatedPage.backgroundType, backgroundPath = updatedPage.background, backgroundPageNumber = bgPageNum ) } - _uiEvents.emit(EditorUiEvent.RefreshCanvas) + sendCanvasCommand(CanvasCommand.RefreshCanvas) } } @@ -338,29 +399,56 @@ class EditorViewModel @Inject constructor( viewModelScope.launch(Dispatchers.IO) { val page = appRepository.pageRepository.getById(currentPageId) val parentFolder = page?.getParentFolder(appRepository.bookRepository) - _uiEvents.emit(EditorUiEvent.NavigateToLibrary(parentFolder)) + sendUiEvent(EditorUiEvent.NavigateToLibrary(parentFolder)) } } private fun handleNavigateToPages() { - currentBookId?.let { bookId -> - viewModelScope.launch { - _uiEvents.emit(EditorUiEvent.NavigateToPages(bookId)) - } + bookId?.let { id -> + sendUiEvent(EditorUiEvent.NavigateToPages(id)) + } + } + + // -------------------------------------------------------- + // Drawing State + // -------------------------------------------------------- + + /** + * Re-evaluates whether drawing should be enabled based on menu and selection states. + */ + fun updateDrawingState() { + val state = _toolbarState.value + val anyMenuOpen = + state.isMenuOpen || state.isStrokeSelectionOpen || state.isBackgroundSelectorModalOpen + val shouldBeDrawing = !anyMenuOpen && !state.isSelectionActive + isDrawing = shouldBeDrawing + log.d("Drawing state: $shouldBeDrawing") + viewModelScope.launch { + CanvasEventBus.isDrawing.emit(shouldBeDrawing) + } + } + + fun onFocusChanged(isFocused: Boolean) { + if (isFocused) { + updateDrawingState() } } + // -------------------------------------------------------- + // Book / Page Data + // -------------------------------------------------------- + /** * Loads context data for the toolbar (page number, background info, etc.) */ fun loadBookData(bookId: String?, pageId: String) { - currentBookId = bookId - currentPageId = pageId + this.bookId = bookId + this.currentPageId = pageId viewModelScope.launch(Dispatchers.IO) { val page = appRepository.pageRepository.getById(pageId) val book = bookId?.let { appRepository.bookRepository.getById(it) } - + val pageIndex = book?.getPageIndex(pageId) ?: 0 val totalPages = book?.pageIds?.size ?: 1 @@ -373,7 +461,7 @@ class EditorViewModel @Inject constructor( else -> 0 } - + _toolbarState.update { it.copy( notebookId = bookId, @@ -389,6 +477,57 @@ class EditorViewModel @Inject constructor( } } + // -------------------------------------------------------- + // Page Navigation (from EditorState) + // -------------------------------------------------------- + + suspend fun getNextPage(): String? { + return if (bookId != null) { + appRepository.getNextPageIdFromBookAndPageOrCreate( + pageId = currentPageId, notebookId = bookId!! + ) + } else null + } + + suspend fun getPreviousPage(): String? { + return if (bookId != null) { + appRepository.getPreviousPageIdFromBookAndPage( + pageId = currentPageId, notebookId = bookId!! + ) + } else null + } + + suspend fun updateOpenedPage(newPageId: String) { + Log.d("EditorView", "Update open page to $newPageId") + if (bookId != null) { + appRepository.bookRepository.setOpenPageId(bookId!!, newPageId) + } + if (newPageId != currentPageId) { + Log.d("EditorView", "Page changed") + currentPageId = newPageId + } else { + Log.d("EditorView", "Tried to change to same page!") + SnackState.globalSnackFlow.tryEmit( + SnackConf(text = "Tried to change to same page!", duration = 3000) + ) + } + } + + /** + * Changes the current page to the one with the specified [id]. + * + * @param id The unique identifier of the page to switch to. + */ + suspend fun changePage(id: String) { + log.d("Changing page to $id, from $currentPageId") + updateOpenedPage(id) + selectionState.reset() + } + + // -------------------------------------------------------- + // Toolbar State Sync Helpers + // -------------------------------------------------------- + fun setHasClipboard(hasClipboard: Boolean) { _toolbarState.update { it.copy(hasClipboard = hasClipboard) } } @@ -404,15 +543,63 @@ class EditorViewModel @Inject constructor( } } - fun updateToolbarSettings(state: ToolbarUiState) { - _toolbarState.update { + /** + * Pushes the current mutableState editor fields into the toolbar StateFlow + * so that Compose UI sees a single consistent snapshot. + */ + private fun syncToolbarState() { + _toolbarState.update { it.copy( - isToolbarOpen = state.isToolbarOpen, - mode = state.mode, - pen = state.pen, - eraser = state.eraser, - penSettings = state.penSettings + isToolbarOpen = isToolbarOpen, + mode = mode, + pen = pen, + eraser = eraser, + penSettings = penSettings ) } } + + // -------------------------------------------------------- + // Event / Command Helpers + // -------------------------------------------------------- + + private fun sendUiEvent(event: EditorUiEvent) { + viewModelScope.launch { uiEventChannel.send(event) } + } + + private fun sendCanvasCommand(command: CanvasCommand) { + viewModelScope.launch { canvasCommandChannel.send(command) } + } + + companion object { + val DEFAULT_PEN_SETTINGS = mapOf( + Pen.BALLPEN.penName to PenSetting(5f, Color.BLACK), + Pen.REDBALLPEN.penName to PenSetting(5f, Color.RED), + Pen.BLUEBALLPEN.penName to PenSetting(5f, Color.BLUE), + Pen.GREENBALLPEN.penName to PenSetting(5f, Color.GREEN), + Pen.PENCIL.penName to PenSetting(5f, Color.BLACK), + Pen.BRUSH.penName to PenSetting(5f, Color.BLACK), + Pen.MARKER.penName to PenSetting(40f, Color.LTGRAY), + Pen.FOUNTAIN.penName to PenSetting(5f, Color.BLACK) + ) + } +} + + +// -------------------------------------------------------- +// Enums (from EditorState) +// -------------------------------------------------------- + +enum class Mode { + Draw, Erase, Select, Line } + + +// if state is Move then applySelectionDisplace() will delete original strokes and images +enum class PlacementMode { + Move, Paste +} + +object Clipboard { + var content: ClipboardContent? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/ethran/notable/editor/canvas/CanvasObserverRegistry.kt b/app/src/main/java/com/ethran/notable/editor/canvas/CanvasObserverRegistry.kt index b541c793..f898c025 100644 --- a/app/src/main/java/com/ethran/notable/editor/canvas/CanvasObserverRegistry.kt +++ b/app/src/main/java/com/ethran/notable/editor/canvas/CanvasObserverRegistry.kt @@ -126,7 +126,7 @@ class CanvasObserverRegistry( coroutineScope.launch { CanvasEventBus.isDrawing.collect { logCanvasObserver.v("drawing state changed to $it!") - state.isDrawing = it + state.viewModel.isDrawing = it } } } @@ -298,5 +298,3 @@ class CanvasObserverRegistry( } - - diff --git a/app/src/main/java/com/ethran/notable/editor/canvas/CanvasRefreshManager.kt b/app/src/main/java/com/ethran/notable/editor/canvas/CanvasRefreshManager.kt index c5a4f2d4..2719e173 100644 --- a/app/src/main/java/com/ethran/notable/editor/canvas/CanvasRefreshManager.kt +++ b/app/src/main/java/com/ethran/notable/editor/canvas/CanvasRefreshManager.kt @@ -6,10 +6,10 @@ import android.graphics.Paint import android.graphics.Rect import android.os.Looper import com.ethran.notable.data.model.SimplePointF +import com.ethran.notable.editor.Mode import com.ethran.notable.editor.PageView import com.ethran.notable.editor.drawing.selectPaint import com.ethran.notable.editor.state.EditorState -import com.ethran.notable.editor.state.Mode import com.ethran.notable.editor.utils.pointsToPath import com.ethran.notable.editor.utils.refreshScreenRegion import com.ethran.notable.editor.utils.resetScreenFreeze diff --git a/app/src/main/java/com/ethran/notable/editor/canvas/DrawCanvas.kt b/app/src/main/java/com/ethran/notable/editor/canvas/DrawCanvas.kt index d816d3b1..a8ff007c 100644 --- a/app/src/main/java/com/ethran/notable/editor/canvas/DrawCanvas.kt +++ b/app/src/main/java/com/ethran/notable/editor/canvas/DrawCanvas.kt @@ -11,12 +11,12 @@ import android.view.SurfaceHolder import android.view.SurfaceView import com.ethran.notable.data.AppRepository import com.ethran.notable.data.model.SimplePointF +import com.ethran.notable.editor.Mode import com.ethran.notable.editor.PageView import com.ethran.notable.editor.drawing.OpenGLRenderer import com.ethran.notable.editor.drawing.selectPaint import com.ethran.notable.editor.state.EditorState import com.ethran.notable.editor.state.History -import com.ethran.notable.editor.state.Mode import com.ethran.notable.editor.state.Operation import com.ethran.notable.editor.utils.DeviceCompat import com.ethran.notable.editor.utils.onSurfaceChanged diff --git a/app/src/main/java/com/ethran/notable/editor/canvas/OnyxInputHandler.kt b/app/src/main/java/com/ethran/notable/editor/canvas/OnyxInputHandler.kt index c07f6d4c..d39acd3c 100644 --- a/app/src/main/java/com/ethran/notable/editor/canvas/OnyxInputHandler.kt +++ b/app/src/main/java/com/ethran/notable/editor/canvas/OnyxInputHandler.kt @@ -7,10 +7,10 @@ import android.util.Log import androidx.compose.ui.unit.dp import androidx.core.graphics.toRect import com.ethran.notable.data.datastore.GlobalAppSettings +import com.ethran.notable.editor.Mode import com.ethran.notable.editor.PageView import com.ethran.notable.editor.state.EditorState import com.ethran.notable.editor.state.History -import com.ethran.notable.editor.state.Mode import com.ethran.notable.editor.utils.DeviceCompat import com.ethran.notable.editor.utils.Eraser import com.ethran.notable.editor.utils.Pen diff --git a/app/src/main/java/com/ethran/notable/editor/state/EditorState.kt b/app/src/main/java/com/ethran/notable/editor/state/EditorState.kt index 0f271951..56a09315 100644 --- a/app/src/main/java/com/ethran/notable/editor/state/EditorState.kt +++ b/app/src/main/java/com/ethran/notable/editor/state/EditorState.kt @@ -1,138 +1,68 @@ package com.ethran.notable.editor.state -import android.graphics.Color import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import com.ethran.notable.data.AppRepository -import com.ethran.notable.data.datastore.EditorSettingCacheManager +import com.ethran.notable.editor.EditorViewModel +import com.ethran.notable.editor.Mode import com.ethran.notable.editor.PageView +import com.ethran.notable.editor.ToolbarAction import com.ethran.notable.editor.utils.Eraser import com.ethran.notable.editor.utils.Pen import com.ethran.notable.editor.utils.PenSetting -import com.ethran.notable.ui.SnackConf -import com.ethran.notable.ui.SnackState -import io.shipbook.shipbooksdk.Log -import io.shipbook.shipbooksdk.ShipBook -enum class Mode { - Draw, Erase, Select, Line -} - - // TODO: move to EditorViewModel, or somewhere else, this code shouldnt be here. +/** + * Wrapper around EditorViewModel for backward compatibility with canvas components + * (DrawCanvas, OnyxInputHandler, CanvasRefreshManager, etc. that still expect EditorState). + * This is a thin adapter that delegates to the ViewModel. + */ +@Stable class EditorState( - val bookId: String? = null, - val pageId: String, - val pageView: PageView, - val appRepository: AppRepository, - persistedEditorSettings: EditorSettingCacheManager.EditorSettings?, - val onPageChange: (String) -> Unit + val viewModel: EditorViewModel, + val pageView: PageView ) { - var currentPageId by mutableStateOf(pageId) - private set - - - suspend fun getNextPage(): String? { - return if (bookId != null) { - val newPageId = appRepository.getNextPageIdFromBookAndPageOrCreate( - pageId = currentPageId, notebookId = bookId - ) - newPageId - } else null - } - - suspend fun getPreviousPage(): String? { - return if (bookId != null) { - val newPageId = appRepository.getPreviousPageIdFromBookAndPage( - pageId = currentPageId, notebookId = bookId - ) - newPageId - } else null - } - - suspend fun updateOpenedPage(newPageId: String) { - Log.d("EditorView", "Update open page to $newPageId") - if (bookId != null) { - appRepository.bookRepository.setOpenPageId(bookId, newPageId) - } - if (newPageId != currentPageId) { - Log.d("EditorView", "Page changed") - onPageChange(newPageId) - currentPageId = newPageId - } else { - Log.d("EditorView", "Tried to change to same page!") - SnackState.globalSnackFlow.tryEmit( - SnackConf(text = "Tried to change to same page!", duration = 3000) - ) + // Delegate to ViewModel + var mode: Mode + get() = viewModel.mode + set(value) { + viewModel.onToolbarAction(ToolbarAction.ChangeMode(value)) } - } + var pen: Pen + get() = viewModel.pen + set(value) { + viewModel.onToolbarAction(ToolbarAction.ChangePen(value)) + } - private val log = ShipBook.getLogger("EditorState") + val eraser: Eraser + get() = viewModel.eraser - var mode by mutableStateOf(persistedEditorSettings?.mode ?: Mode.Draw) // should save - var pen by mutableStateOf(persistedEditorSettings?.pen ?: Pen.BALLPEN) // should save - var eraser by mutableStateOf(persistedEditorSettings?.eraser ?: Eraser.PEN) // should save - var isDrawing by mutableStateOf(true) // gives information if pen touch will be drawn or not - // For debugging: -// var isDrawing: Boolean -// get() = _isDrawing -// set(value) { -// if (_isDrawing != value) { -// Log.d(TAG, "isDrawing modified from ${_isDrawing} to $value") -// logCallStack("isDrawing modification") -// _isDrawing = value -// } -// } + val penSettings: Map + get() = viewModel.penSettings - var isToolbarOpen by mutableStateOf( - persistedEditorSettings?.isToolbarOpen ?: false - ) // should save - var penSettings by mutableStateOf( - persistedEditorSettings?.penSettings ?: mapOf( - Pen.BALLPEN.penName to PenSetting(5f, Color.BLACK), - Pen.REDBALLPEN.penName to PenSetting(5f, Color.RED), - Pen.BLUEBALLPEN.penName to PenSetting(5f, Color.BLUE), - Pen.GREENBALLPEN.penName to PenSetting(5f, Color.GREEN), - Pen.PENCIL.penName to PenSetting(5f, Color.BLACK), - Pen.BRUSH.penName to PenSetting(5f, Color.BLACK), - Pen.MARKER.penName to PenSetting(40f, Color.LTGRAY), - Pen.FOUNTAIN.penName to PenSetting(5f, Color.BLACK) - ) - ) + var isToolbarOpen: Boolean + get() = viewModel.isToolbarOpen + set(value) { + if (value != viewModel.isToolbarOpen) { + viewModel.onToolbarAction(ToolbarAction.ToggleToolbar) + } + } - val selectionState = SelectionState() + val selectionState: SelectionState + get() = viewModel.selectionState - private var _clipboard by mutableStateOf(Clipboard.content) - var clipboard - get() = _clipboard + var clipboard: ClipboardContent? + get() = viewModel.clipboard set(value) { - this._clipboard = value - // The clipboard content must survive the EditorState, so we store a copy in - // a singleton that lives outside of the EditorState - Clipboard.content = value + viewModel.clipboard = value } + var isDrawing: Boolean + get() = viewModel.isDrawing + set(value) { + viewModel.isDrawing = value + } - /** - * Changes the current page to the one with the specified [id]. - * - * @param id The unique identifier of the page to switch to. - */ - suspend fun changePage(id: String) { - log.d("Changing page to $id, from $currentPageId") - updateOpenedPage(id) - selectionState.reset() - } -} - -// if state is Move then applySelectionDisplace() will delete original strokes and images -enum class PlacementMode { - Move, Paste + suspend fun getNextPage(): String? = viewModel.getNextPage() + suspend fun getPreviousPage(): String? = viewModel.getPreviousPage() + suspend fun changePage(id: String) = viewModel.changePage(id) } - -object Clipboard { - var content: ClipboardContent? = null -} \ No newline at end of file diff --git a/app/src/main/java/com/ethran/notable/editor/state/SelectionState.kt b/app/src/main/java/com/ethran/notable/editor/state/SelectionState.kt index 1500e07d..65316ce3 100644 --- a/app/src/main/java/com/ethran/notable/editor/state/SelectionState.kt +++ b/app/src/main/java/com/ethran/notable/editor/state/SelectionState.kt @@ -15,6 +15,7 @@ import com.ethran.notable.data.db.Image import com.ethran.notable.data.db.Stroke import com.ethran.notable.data.model.SimplePointF import com.ethran.notable.editor.PageView +import com.ethran.notable.editor.PlacementMode import com.ethran.notable.editor.drawing.drawImage import com.ethran.notable.editor.utils.imageBoundsInt import com.ethran.notable.editor.utils.offsetImage @@ -168,8 +169,8 @@ class SelectionState { } // move the selection a bit, to show the copy selectionDisplaceOffset = IntOffset( - x = selectionDisplaceOffset!!.x + 50, - y = selectionDisplaceOffset!!.y + 50, + x = (selectionDisplaceOffset?.x ?: 0) + 50, + y = (selectionDisplaceOffset?.y ?: 0) + 50, ) } @@ -251,4 +252,4 @@ class SelectionState { images = images ?: emptyList() ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/ethran/notable/editor/ui/EditorSurface.kt b/app/src/main/java/com/ethran/notable/editor/ui/EditorSurface.kt index bce55452..16c43467 100644 --- a/app/src/main/java/com/ethran/notable/editor/ui/EditorSurface.kt +++ b/app/src/main/java/com/ethran/notable/editor/ui/EditorSurface.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.viewinterop.AndroidView import com.ethran.notable.data.AppRepository +import com.ethran.notable.editor.EditorViewModel import com.ethran.notable.editor.PageView import com.ethran.notable.editor.canvas.DrawCanvas import com.ethran.notable.editor.state.EditorState @@ -17,18 +18,23 @@ private val log = ShipBook.getLogger("EditorSurface") @Composable fun EditorSurface( appRepository: AppRepository, - state: EditorState, page: PageView, history: History + viewModel: EditorViewModel, page: PageView, history: History ) { val coroutineScope = rememberCoroutineScope() log.i("recompose surface") + // Create EditorState wrapper for backward compatibility with DrawCanvas + val editorState = EditorState(viewModel, page) + AndroidView( factory = { ctx -> DrawCanvas( context = ctx, appRepository = appRepository, - state = state, page = page, history = history, - coroutineScope = coroutineScope + coroutineScope = coroutineScope, + state = editorState, + page = page, + history = history ).apply { init() registerObservers() diff --git a/app/src/main/java/com/ethran/notable/editor/ui/ScrollIndicator.kt b/app/src/main/java/com/ethran/notable/editor/ui/ScrollIndicator.kt index e27d3f4f..c285900a 100644 --- a/app/src/main/java/com/ethran/notable/editor/ui/ScrollIndicator.kt +++ b/app/src/main/java/com/ethran/notable/editor/ui/ScrollIndicator.kt @@ -16,7 +16,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp -import com.ethran.notable.editor.state.EditorState +import com.ethran.notable.editor.EditorViewModel +import com.ethran.notable.editor.PageView import com.ethran.notable.ui.convertDpToPixel import kotlin.math.max @@ -25,14 +26,13 @@ import kotlin.math.max * Uses page.scroll.y (IntOffset) for the vertical position. */ @Composable -fun ScrollIndicator(state: EditorState) { +fun ScrollIndicator(viewModel: EditorViewModel, page: PageView) { BoxWithConstraints( modifier = Modifier .width(5.dp) .fillMaxHeight() ) { val viewportHeightPx = convertDpToPixel(this.maxHeight, LocalContext.current).toInt() - val page = state.pageView // Total scrollable height approximation: // page.height is the total content height (page coordinates) @@ -43,7 +43,7 @@ fun ScrollIndicator(state: EditorState) { val indicatorSizeDp = (viewportHeightPx / virtualHeight.toFloat()) * this.maxHeight.value val indicatorPositionDp = (page.scroll.y / virtualHeight.toFloat()) * this.maxHeight.value - if (!state.isToolbarOpen) return@BoxWithConstraints + if (!viewModel.isToolbarOpen) return@BoxWithConstraints Box( modifier = Modifier @@ -62,7 +62,7 @@ fun ScrollIndicator(state: EditorState) { * Uses page.scroll.x (IntOffset) for the horizontal position. */ @Composable -fun HorizontalScrollIndicator(state: EditorState) { +fun HorizontalScrollIndicator(viewModel: EditorViewModel, page: PageView) { Column(Modifier.fillMaxSize()) { Spacer(Modifier.weight(1f)) BoxWithConstraints( @@ -71,7 +71,6 @@ fun HorizontalScrollIndicator(state: EditorState) { .fillMaxWidth() ) { val viewportWidthPx = convertDpToPixel(this.maxWidth, LocalContext.current).toInt() - val page = state.pageView // Total scrollable width approximation: // page.width is the total content width (page coordinates) @@ -82,7 +81,7 @@ fun HorizontalScrollIndicator(state: EditorState) { val indicatorSizeDp = (viewportWidthPx / virtualWidth.toFloat()) * this.maxWidth.value val indicatorPositionDp = (page.scroll.x / virtualWidth.toFloat()) * this.maxWidth.value - if (!state.isToolbarOpen) return@BoxWithConstraints + if (!viewModel.isToolbarOpen) return@BoxWithConstraints Box( modifier = Modifier diff --git a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt index c726af72..37da8a26 100644 --- a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt +++ b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt @@ -29,7 +29,7 @@ import com.ethran.notable.data.datastore.BUTTON_SIZE import com.ethran.notable.data.datastore.GlobalAppSettings import com.ethran.notable.editor.ToolbarAction import com.ethran.notable.editor.ToolbarUiState -import com.ethran.notable.editor.state.Mode +import com.ethran.notable.editor.Mode import com.ethran.notable.editor.utils.Pen import com.ethran.notable.editor.utils.PenSetting import com.ethran.notable.ui.dialogs.BackgroundSelector diff --git a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/ToolbarMenu.kt b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/ToolbarMenu.kt index 0954299d..a5b23817 100644 --- a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/ToolbarMenu.kt +++ b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/ToolbarMenu.kt @@ -25,9 +25,9 @@ import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupProperties import com.ethran.notable.R import com.ethran.notable.data.datastore.BUTTON_SIZE +import com.ethran.notable.editor.Mode import com.ethran.notable.editor.ToolbarAction import com.ethran.notable.editor.ToolbarUiState -import com.ethran.notable.editor.state.Mode import com.ethran.notable.editor.utils.Pen import com.ethran.notable.editor.utils.PenSetting import com.ethran.notable.io.ExportFormat diff --git a/app/src/main/java/com/ethran/notable/editor/utils/ImageHandler.kt b/app/src/main/java/com/ethran/notable/editor/utils/ImageHandler.kt index df653391..7d5685e5 100644 --- a/app/src/main/java/com/ethran/notable/editor/utils/ImageHandler.kt +++ b/app/src/main/java/com/ethran/notable/editor/utils/ImageHandler.kt @@ -7,10 +7,10 @@ import androidx.compose.ui.graphics.asAndroidBitmap import androidx.compose.ui.graphics.asImageBitmap import com.ethran.notable.data.db.Image import com.ethran.notable.editor.PageView +import com.ethran.notable.editor.PlacementMode import com.ethran.notable.editor.canvas.CanvasEventBus import com.ethran.notable.editor.drawing.drawImage import com.ethran.notable.editor.state.EditorState -import com.ethran.notable.editor.state.PlacementMode import com.ethran.notable.io.uriToBitmap import com.ethran.notable.ui.showHint import io.shipbook.shipbooksdk.Log diff --git a/app/src/main/java/com/ethran/notable/editor/utils/Select.kt b/app/src/main/java/com/ethran/notable/editor/utils/Select.kt index 74836f7e..bf5c7819 100644 --- a/app/src/main/java/com/ethran/notable/editor/utils/Select.kt +++ b/app/src/main/java/com/ethran/notable/editor/utils/Select.kt @@ -13,10 +13,10 @@ import com.ethran.notable.data.db.Stroke import com.ethran.notable.data.model.SimplePointF import com.ethran.notable.editor.canvas.CanvasEventBus import com.ethran.notable.editor.PageView +import com.ethran.notable.editor.PlacementMode import com.ethran.notable.editor.drawing.drawImage import com.ethran.notable.editor.drawing.drawStroke import com.ethran.notable.editor.state.EditorState -import com.ethran.notable.editor.state.PlacementMode import com.ethran.notable.ui.SnackConf import com.ethran.notable.ui.SnackState import io.shipbook.shipbooksdk.ShipBook From 0817c81e065de9857a4f2a86172db7830af9143e Mon Sep 17 00:00:00 2001 From: Wiktor Wichrowski Date: Mon, 9 Mar 2026 21:44:46 +0100 Subject: [PATCH 02/10] cleanup --- app/src/main/java/com/ethran/notable/editor/EditorView.kt | 2 +- .../main/java/com/ethran/notable/editor/EditorViewModel.kt | 4 ---- .../java/com/ethran/notable/editor/state/EditorState.kt | 6 ------ .../main/java/com/ethran/notable/editor/ui/EditorSurface.kt | 2 +- 4 files changed, 2 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/ethran/notable/editor/EditorView.kt b/app/src/main/java/com/ethran/notable/editor/EditorView.kt index dfc9077f..368db275 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorView.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorView.kt @@ -131,7 +131,7 @@ fun EditorView( // Create EditorState wrapper for backward compatibility val editorState = remember(viewModel, page) { - EditorState(viewModel, page) + EditorState(viewModel) } // Initialize ViewModel with persisted settings on first composition diff --git a/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt b/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt index 1efa1f79..6a2f376a 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt @@ -87,8 +87,6 @@ data class ToolbarUiState( !isSelectionActive } -/** Backward-compatible alias used by ToolbarMenu and other components. */ -typealias EditorUiState = ToolbarUiState // -------------------------------------------------------- // 2. USER ACTIONS (Intents) @@ -124,8 +122,6 @@ sealed class ToolbarAction { object CloseAllMenus : ToolbarAction() } -/** Backward-compatible alias used by ToolbarMenu. */ -typealias EditorAction = ToolbarAction // -------------------------------------------------------- // 3. CANVAS COMMANDS (Imperative drawing actions) diff --git a/app/src/main/java/com/ethran/notable/editor/state/EditorState.kt b/app/src/main/java/com/ethran/notable/editor/state/EditorState.kt index 56a09315..e3d47e55 100644 --- a/app/src/main/java/com/ethran/notable/editor/state/EditorState.kt +++ b/app/src/main/java/com/ethran/notable/editor/state/EditorState.kt @@ -3,7 +3,6 @@ package com.ethran.notable.editor.state import androidx.compose.runtime.Stable import com.ethran.notable.editor.EditorViewModel import com.ethran.notable.editor.Mode -import com.ethran.notable.editor.PageView import com.ethran.notable.editor.ToolbarAction import com.ethran.notable.editor.utils.Eraser import com.ethran.notable.editor.utils.Pen @@ -17,7 +16,6 @@ import com.ethran.notable.editor.utils.PenSetting @Stable class EditorState( val viewModel: EditorViewModel, - val pageView: PageView ) { // Delegate to ViewModel @@ -61,8 +59,4 @@ class EditorState( set(value) { viewModel.isDrawing = value } - - suspend fun getNextPage(): String? = viewModel.getNextPage() - suspend fun getPreviousPage(): String? = viewModel.getPreviousPage() - suspend fun changePage(id: String) = viewModel.changePage(id) } diff --git a/app/src/main/java/com/ethran/notable/editor/ui/EditorSurface.kt b/app/src/main/java/com/ethran/notable/editor/ui/EditorSurface.kt index 16c43467..8dee01c9 100644 --- a/app/src/main/java/com/ethran/notable/editor/ui/EditorSurface.kt +++ b/app/src/main/java/com/ethran/notable/editor/ui/EditorSurface.kt @@ -24,7 +24,7 @@ fun EditorSurface( log.i("recompose surface") // Create EditorState wrapper for backward compatibility with DrawCanvas - val editorState = EditorState(viewModel, page) + val editorState = EditorState(viewModel) AndroidView( factory = { ctx -> From 604c2c404822deaf7e7caa75b478ecfbe4382c51 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Mar 2026 08:59:22 +0100 Subject: [PATCH 03/10] Refactor EditorViewModel: StateFlow single source of truth, fix page nav, type relocation (#221) * Initial plan * Refactor EditorViewModel architecture: remove mutableStateOf, add StateFlow single source of truth Co-authored-by: Ethran <77555700+Ethran@users.noreply.github.com> * Address code review: AtomicBoolean for didInitSettings, remove scroll from LaunchedEffect keys, clarify isDrawing mutability Co-authored-by: Ethran <77555700+Ethran@users.noreply.github.com> * Update app/src/main/java/com/ethran/notable/editor/state/EditorState.kt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update app/src/main/java/com/ethran/notable/editor/state/EditorState.kt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Ethran <77555700+Ethran@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../datastore/EditorSettingCacheManager.kt | 2 +- .../notable/editor/EditorControlTower.kt | 6 +- .../com/ethran/notable/editor/EditorView.kt | 39 +++--- .../ethran/notable/editor/EditorViewModel.kt | 131 +++++------------- .../editor/canvas/CanvasObserverRegistry.kt | 2 +- .../editor/canvas/CanvasRefreshManager.kt | 2 +- .../notable/editor/canvas/DrawCanvas.kt | 2 +- .../notable/editor/canvas/OnyxInputHandler.kt | 2 +- .../notable/editor/state/EditorState.kt | 76 +++++----- .../notable/editor/state/EditorTypes.kt | 23 +++ .../notable/editor/state/SelectionState.kt | 2 +- .../ethran/notable/editor/ui/EditorSurface.kt | 12 +- .../notable/editor/ui/ScrollIndicator.kt | 10 +- .../notable/editor/ui/toolbar/Toolbar.kt | 2 +- .../notable/editor/ui/toolbar/ToolbarMenu.kt | 2 +- .../notable/editor/utils/ImageHandler.kt | 2 +- .../com/ethran/notable/editor/utils/Select.kt | 2 +- 17 files changed, 149 insertions(+), 168 deletions(-) create mode 100644 app/src/main/java/com/ethran/notable/editor/state/EditorTypes.kt diff --git a/app/src/main/java/com/ethran/notable/data/datastore/EditorSettingCacheManager.kt b/app/src/main/java/com/ethran/notable/data/datastore/EditorSettingCacheManager.kt index 56ebbe65..fdaff115 100644 --- a/app/src/main/java/com/ethran/notable/data/datastore/EditorSettingCacheManager.kt +++ b/app/src/main/java/com/ethran/notable/data/datastore/EditorSettingCacheManager.kt @@ -2,7 +2,7 @@ package com.ethran.notable.data.datastore import com.ethran.notable.data.db.Kv import com.ethran.notable.data.db.KvRepository -import com.ethran.notable.editor.Mode +import com.ethran.notable.editor.state.Mode import com.ethran.notable.editor.utils.Eraser import com.ethran.notable.editor.utils.NamedSettings import com.ethran.notable.editor.utils.Pen diff --git a/app/src/main/java/com/ethran/notable/editor/EditorControlTower.kt b/app/src/main/java/com/ethran/notable/editor/EditorControlTower.kt index b016d65c..31712314 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorControlTower.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorControlTower.kt @@ -8,6 +8,8 @@ import com.ethran.notable.editor.canvas.CanvasEventBus import com.ethran.notable.editor.state.EditorState import com.ethran.notable.editor.state.History import com.ethran.notable.editor.state.HistoryBusActions +import com.ethran.notable.editor.state.Mode +import com.ethran.notable.editor.state.PlacementMode import com.ethran.notable.editor.state.Operation import com.ethran.notable.editor.state.SelectionState import com.ethran.notable.editor.state.UndoRedoType @@ -105,7 +107,7 @@ class EditorControlTower( logEditorControlTower.w("IsDrawing already set to $value") return } - state.viewModel.isDrawing = value + state.isDrawing = value } fun toggleTool() { @@ -238,7 +240,7 @@ class EditorControlTower( fun deleteSelection() { val operationList = state.selectionState.deleteSelection(page) history.addOperationsToHistory(operationList) - state.viewModel.isDrawing = true + state.isDrawing = true scope.launch { CanvasEventBus.refreshUi.emit(Unit) } diff --git a/app/src/main/java/com/ethran/notable/editor/EditorView.kt b/app/src/main/java/com/ethran/notable/editor/EditorView.kt index 368db275..fc0f70c8 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorView.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorView.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -17,6 +16,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import com.ethran.notable.data.AppRepository import com.ethran.notable.data.datastore.EditorSettingCacheManager @@ -162,6 +162,10 @@ fun EditorView( is EditorUiEvent.ShowSnackbar -> { snackManager.displaySnack(SnackConf(text = event.message)) } + + is EditorUiEvent.PageChanged -> { + onPageChange(event.pageId) + } } } } @@ -206,16 +210,19 @@ fun EditorView( } } + // Collect toolbar state and sync EditorState (keeps snapshotFlow observers in canvas alive) + val toolbarState by viewModel.toolbarState.collectAsStateWithLifecycle() + LaunchedEffect(toolbarState) { + editorState.syncFrom(toolbarState) + } + // Sync PageView state to ViewModel for Toolbar rendering - val zoomLevel by page.zoomLevel.collectAsState() + val zoomLevel by page.zoomLevel.collectAsStateWithLifecycle() val selectionActive = viewModel.selectionState.isNonEmpty() LaunchedEffect( zoomLevel, - page.scroll, - viewModel.clipboard, selectionActive ) { - viewModel.setHasClipboard(viewModel.clipboard != null) viewModel.setShowResetView(zoomLevel != 1.0f) viewModel.setSelectionActive(selectionActive) } @@ -235,20 +242,20 @@ fun EditorView( // Persist editor settings when they change LaunchedEffect( - viewModel.isToolbarOpen, - viewModel.pen, - viewModel.penSettings, - viewModel.mode, - viewModel.eraser + toolbarState.isToolbarOpen, + toolbarState.pen, + toolbarState.penSettings, + toolbarState.mode, + toolbarState.eraser ) { log.i("EditorView: saving editor settings") editorSettingCacheManager.setEditorSettings( EditorSettingCacheManager.EditorSettings( - isToolbarOpen = viewModel.isToolbarOpen, - mode = viewModel.mode, - pen = viewModel.pen, - eraser = viewModel.eraser, - penSettings = viewModel.penSettings + isToolbarOpen = toolbarState.isToolbarOpen, + mode = toolbarState.mode, + pen = toolbarState.pen, + eraser = toolbarState.eraser, + penSettings = toolbarState.penSettings ) ) } @@ -257,7 +264,7 @@ fun EditorView( InkaTheme { EditorGestureReceiver(controlTower = editorControlTower) EditorSurface( - appRepository = appRepository, viewModel = viewModel, page = page, history = history + appRepository = appRepository, state = editorState, page = page, history = history ) SelectedBitmap( context = context, controlTower = editorControlTower diff --git a/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt b/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt index 6a2f376a..f88979f0 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt @@ -3,9 +3,6 @@ package com.ethran.notable.editor import android.content.Context import android.graphics.Color import android.net.Uri -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue import androidx.core.net.toUri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -17,7 +14,7 @@ import com.ethran.notable.data.db.getPageIndex import com.ethran.notable.data.db.getParentFolder import com.ethran.notable.data.model.BackgroundType import com.ethran.notable.editor.canvas.CanvasEventBus -import com.ethran.notable.editor.state.ClipboardContent +import com.ethran.notable.editor.state.Mode import com.ethran.notable.editor.state.SelectionState import com.ethran.notable.editor.utils.Eraser import com.ethran.notable.editor.utils.Pen @@ -25,8 +22,6 @@ import com.ethran.notable.editor.utils.PenSetting import com.ethran.notable.io.ExportEngine import com.ethran.notable.io.ExportFormat import com.ethran.notable.io.ExportTarget -import com.ethran.notable.ui.SnackConf -import com.ethran.notable.ui.SnackState import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import io.shipbook.shipbooksdk.Log @@ -39,6 +34,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject private val log = ShipBook.getLogger("EditorViewModel") @@ -78,6 +74,7 @@ data class ToolbarUiState( val penSettings: Map = emptyMap(), val isSelectionActive: Boolean = false, val hasClipboard: Boolean = false, + val isDrawing: Boolean = true, ) { val isDrawingAllowed: Boolean get() = mode == Mode.Draw && @@ -146,6 +143,7 @@ sealed class EditorUiEvent { data class NavigateToLibrary(val folderId: String?) : EditorUiEvent() data class NavigateToPages(val bookId: String) : EditorUiEvent() object NavigateToBugReport : EditorUiEvent() + data class PageChanged(val pageId: String) : EditorUiEvent() } // -------------------------------------------------------- @@ -164,64 +162,42 @@ class EditorViewModel @Inject constructor( val toolbarState: StateFlow = _toolbarState.asStateFlow() // ---- One-Time Events (Channels) ---- - private val uiEventChannel = Channel() + private val uiEventChannel = Channel(Channel.BUFFERED) val uiEvents = uiEventChannel.receiveAsFlow() - private val canvasCommandChannel = Channel() + private val canvasCommandChannel = Channel(Channel.BUFFERED) val canvasCommands = canvasCommandChannel.receiveAsFlow() // ---- Internal document context ---- private var bookId: String? = null + private var currentPageId: String = "" - // ---- Editor state (from EditorState) ---- - var currentPageId by mutableStateOf("") - private set - - var mode by mutableStateOf(Mode.Draw) - private set - - var pen by mutableStateOf(Pen.BALLPEN) - private set - - var eraser by mutableStateOf(Eraser.PEN) - private set - - var isDrawing by mutableStateOf(true) - - var isToolbarOpen by mutableStateOf(false) - private set - - var penSettings by mutableStateOf(DEFAULT_PEN_SETTINGS) - private set + // ---- Init guard ---- + private val didInitSettings = AtomicBoolean(false) + // ---- Selection state (kept for drawing logic compatibility) ---- val selectionState = SelectionState() - private var _clipboard by mutableStateOf(Clipboard.content) - var clipboard - get() = _clipboard - set(value) { - _clipboard = value - // The clipboard content must survive the EditorState, so we store a copy in - // a singleton that lives outside of the EditorState - Clipboard.content = value - } - // -------------------------------------------------------- // Initialization from persisted settings // -------------------------------------------------------- /** * Restores editor settings from the persisted cache. - * Should be called once when EditorView is composed. + * Idempotent: only applies settings on first call; subsequent calls are no-ops. */ fun initFromPersistedSettings(settings: EditorSettingCacheManager.EditorSettings?) { - mode = settings?.mode ?: Mode.Draw - pen = settings?.pen ?: Pen.BALLPEN - eraser = settings?.eraser ?: Eraser.PEN - isToolbarOpen = settings?.isToolbarOpen ?: false - penSettings = settings?.penSettings ?: DEFAULT_PEN_SETTINGS + if (!didInitSettings.compareAndSet(false, true)) return - syncToolbarState() + _toolbarState.update { + it.copy( + mode = settings?.mode ?: Mode.Draw, + pen = settings?.pen ?: Pen.BALLPEN, + eraser = settings?.eraser ?: Eraser.PEN, + isToolbarOpen = settings?.isToolbarOpen ?: false, + penSettings = settings?.penSettings ?: DEFAULT_PEN_SETTINGS + ) + } } // -------------------------------------------------------- @@ -231,14 +207,12 @@ class EditorViewModel @Inject constructor( fun onToolbarAction(action: ToolbarAction) { when (action) { is ToolbarAction.ToggleToolbar -> { - isToolbarOpen = !isToolbarOpen - syncToolbarState() + _toolbarState.update { it.copy(isToolbarOpen = !it.isToolbarOpen) } updateDrawingState() } is ToolbarAction.ChangeMode -> { - mode = action.mode - syncToolbarState() + _toolbarState.update { it.copy(mode = action.mode) } updateDrawingState() } @@ -293,29 +267,26 @@ class EditorViewModel @Inject constructor( // -------------------------------------------------------- private fun handlePenChange(pen: Pen) { - if (mode == Mode.Draw && this.pen == pen) { + val state = _toolbarState.value + if (state.mode == Mode.Draw && state.pen == pen) { _toolbarState.update { it.copy(isStrokeSelectionOpen = true) } } else { - this.pen = pen - if (mode != Mode.Draw) { - mode = Mode.Draw + _toolbarState.update { + it.copy(pen = pen, mode = Mode.Draw) } - syncToolbarState() } updateDrawingState() } private fun handleEraserChange(eraser: Eraser) { - this.eraser = eraser - syncToolbarState() + _toolbarState.update { it.copy(eraser = eraser) } updateDrawingState() } private fun handlePenSettingChange(pen: Pen, setting: PenSetting) { - val newSettings = penSettings.toMutableMap() + val newSettings = _toolbarState.value.penSettings.toMutableMap() newSettings[pen.penName] = setting - penSettings = newSettings - syncToolbarState() + _toolbarState.update { it.copy(penSettings = newSettings) } } private fun handleCloseAllMenus() { @@ -417,7 +388,7 @@ class EditorViewModel @Inject constructor( val anyMenuOpen = state.isMenuOpen || state.isStrokeSelectionOpen || state.isBackgroundSelectorModalOpen val shouldBeDrawing = !anyMenuOpen && !state.isSelectionActive - isDrawing = shouldBeDrawing + _toolbarState.update { it.copy(isDrawing = shouldBeDrawing) } log.d("Drawing state: $shouldBeDrawing") viewModelScope.launch { CanvasEventBus.isDrawing.emit(shouldBeDrawing) @@ -501,11 +472,10 @@ class EditorViewModel @Inject constructor( if (newPageId != currentPageId) { Log.d("EditorView", "Page changed") currentPageId = newPageId + sendUiEvent(EditorUiEvent.PageChanged(newPageId)) } else { Log.d("EditorView", "Tried to change to same page!") - SnackState.globalSnackFlow.tryEmit( - SnackConf(text = "Tried to change to same page!", duration = 3000) - ) + sendUiEvent(EditorUiEvent.ShowSnackbar("Tried to change to same page!")) } } @@ -539,22 +509,6 @@ class EditorViewModel @Inject constructor( } } - /** - * Pushes the current mutableState editor fields into the toolbar StateFlow - * so that Compose UI sees a single consistent snapshot. - */ - private fun syncToolbarState() { - _toolbarState.update { - it.copy( - isToolbarOpen = isToolbarOpen, - mode = mode, - pen = pen, - eraser = eraser, - penSettings = penSettings - ) - } - } - // -------------------------------------------------------- // Event / Command Helpers // -------------------------------------------------------- @@ -579,23 +533,4 @@ class EditorViewModel @Inject constructor( Pen.FOUNTAIN.penName to PenSetting(5f, Color.BLACK) ) } -} - - -// -------------------------------------------------------- -// Enums (from EditorState) -// -------------------------------------------------------- - -enum class Mode { - Draw, Erase, Select, Line -} - - -// if state is Move then applySelectionDisplace() will delete original strokes and images -enum class PlacementMode { - Move, Paste -} - -object Clipboard { - var content: ClipboardContent? = null } \ No newline at end of file diff --git a/app/src/main/java/com/ethran/notable/editor/canvas/CanvasObserverRegistry.kt b/app/src/main/java/com/ethran/notable/editor/canvas/CanvasObserverRegistry.kt index f898c025..e76e906a 100644 --- a/app/src/main/java/com/ethran/notable/editor/canvas/CanvasObserverRegistry.kt +++ b/app/src/main/java/com/ethran/notable/editor/canvas/CanvasObserverRegistry.kt @@ -126,7 +126,7 @@ class CanvasObserverRegistry( coroutineScope.launch { CanvasEventBus.isDrawing.collect { logCanvasObserver.v("drawing state changed to $it!") - state.viewModel.isDrawing = it + state.isDrawing = it } } } diff --git a/app/src/main/java/com/ethran/notable/editor/canvas/CanvasRefreshManager.kt b/app/src/main/java/com/ethran/notable/editor/canvas/CanvasRefreshManager.kt index 2719e173..88260440 100644 --- a/app/src/main/java/com/ethran/notable/editor/canvas/CanvasRefreshManager.kt +++ b/app/src/main/java/com/ethran/notable/editor/canvas/CanvasRefreshManager.kt @@ -6,7 +6,7 @@ import android.graphics.Paint import android.graphics.Rect import android.os.Looper import com.ethran.notable.data.model.SimplePointF -import com.ethran.notable.editor.Mode +import com.ethran.notable.editor.state.Mode import com.ethran.notable.editor.PageView import com.ethran.notable.editor.drawing.selectPaint import com.ethran.notable.editor.state.EditorState diff --git a/app/src/main/java/com/ethran/notable/editor/canvas/DrawCanvas.kt b/app/src/main/java/com/ethran/notable/editor/canvas/DrawCanvas.kt index a8ff007c..2b69517d 100644 --- a/app/src/main/java/com/ethran/notable/editor/canvas/DrawCanvas.kt +++ b/app/src/main/java/com/ethran/notable/editor/canvas/DrawCanvas.kt @@ -11,7 +11,7 @@ import android.view.SurfaceHolder import android.view.SurfaceView import com.ethran.notable.data.AppRepository import com.ethran.notable.data.model.SimplePointF -import com.ethran.notable.editor.Mode +import com.ethran.notable.editor.state.Mode import com.ethran.notable.editor.PageView import com.ethran.notable.editor.drawing.OpenGLRenderer import com.ethran.notable.editor.drawing.selectPaint diff --git a/app/src/main/java/com/ethran/notable/editor/canvas/OnyxInputHandler.kt b/app/src/main/java/com/ethran/notable/editor/canvas/OnyxInputHandler.kt index d39acd3c..323463fb 100644 --- a/app/src/main/java/com/ethran/notable/editor/canvas/OnyxInputHandler.kt +++ b/app/src/main/java/com/ethran/notable/editor/canvas/OnyxInputHandler.kt @@ -7,7 +7,7 @@ import android.util.Log import androidx.compose.ui.unit.dp import androidx.core.graphics.toRect import com.ethran.notable.data.datastore.GlobalAppSettings -import com.ethran.notable.editor.Mode +import com.ethran.notable.editor.state.Mode import com.ethran.notable.editor.PageView import com.ethran.notable.editor.state.EditorState import com.ethran.notable.editor.state.History diff --git a/app/src/main/java/com/ethran/notable/editor/state/EditorState.kt b/app/src/main/java/com/ethran/notable/editor/state/EditorState.kt index e3d47e55..fc632834 100644 --- a/app/src/main/java/com/ethran/notable/editor/state/EditorState.kt +++ b/app/src/main/java/com/ethran/notable/editor/state/EditorState.kt @@ -1,9 +1,11 @@ package com.ethran.notable.editor.state import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import com.ethran.notable.editor.EditorViewModel -import com.ethran.notable.editor.Mode -import com.ethran.notable.editor.ToolbarAction +import com.ethran.notable.editor.ToolbarUiState import com.ethran.notable.editor.utils.Eraser import com.ethran.notable.editor.utils.Pen import com.ethran.notable.editor.utils.PenSetting @@ -11,52 +13,62 @@ import com.ethran.notable.editor.utils.PenSetting /** * Wrapper around EditorViewModel for backward compatibility with canvas components * (DrawCanvas, OnyxInputHandler, CanvasRefreshManager, etc. that still expect EditorState). - * This is a thin adapter that delegates to the ViewModel. + * + * Holds its own Compose snapshot state (mutableStateOf) so that canvas observers using + * snapshotFlow remain reactive. Call [syncFrom] whenever [EditorViewModel.toolbarState] changes. */ @Stable class EditorState( val viewModel: EditorViewModel, ) { + private val initial = viewModel.toolbarState.value - // Delegate to ViewModel - var mode: Mode - get() = viewModel.mode - set(value) { - viewModel.onToolbarAction(ToolbarAction.ChangeMode(value)) - } + var mode by mutableStateOf(initial.mode) + private set - var pen: Pen - get() = viewModel.pen - set(value) { - viewModel.onToolbarAction(ToolbarAction.ChangePen(value)) - } + var pen by mutableStateOf(initial.pen) + private set - val eraser: Eraser - get() = viewModel.eraser + var eraser by mutableStateOf(initial.eraser) + private set - val penSettings: Map - get() = viewModel.penSettings + var penSettings by mutableStateOf(initial.penSettings) + private set - var isToolbarOpen: Boolean - get() = viewModel.isToolbarOpen - set(value) { - if (value != viewModel.isToolbarOpen) { - viewModel.onToolbarAction(ToolbarAction.ToggleToolbar) - } - } + var isToolbarOpen by mutableStateOf(initial.isToolbarOpen) + private set + + // Intentionally public — canvas components (CanvasObserverRegistry, EditorControlTower, + // Select) set this directly, and CanvasObserverRegistry uses snapshotFlow { state.isDrawing }. + var isDrawing by mutableStateOf(initial.isDrawing) val selectionState: SelectionState get() = viewModel.selectionState + private var _clipboard by mutableStateOf(Clipboard.content) var clipboard: ClipboardContent? - get() = viewModel.clipboard + get() = _clipboard set(value) { - viewModel.clipboard = value + _clipboard = value + // The clipboard content must survive the EditorState, so we store a copy in + // a singleton that lives outside of the EditorState + Clipboard.content = value + viewModel.setHasClipboard(value != null) } - var isDrawing: Boolean - get() = viewModel.isDrawing - set(value) { - viewModel.isDrawing = value - } + init { + viewModel.setHasClipboard(_clipboard != null) + } + /** + * Synchronises this EditorState's mutableStateOf fields from the given [ToolbarUiState]. + * Call this from a LaunchedEffect in EditorView whenever toolbarState changes. + */ + fun syncFrom(state: ToolbarUiState) { + mode = state.mode + pen = state.pen + eraser = state.eraser + penSettings = state.penSettings + isToolbarOpen = state.isToolbarOpen + isDrawing = state.isDrawing + } } diff --git a/app/src/main/java/com/ethran/notable/editor/state/EditorTypes.kt b/app/src/main/java/com/ethran/notable/editor/state/EditorTypes.kt new file mode 100644 index 00000000..21ea22d2 --- /dev/null +++ b/app/src/main/java/com/ethran/notable/editor/state/EditorTypes.kt @@ -0,0 +1,23 @@ +package com.ethran.notable.editor.state + +/** + * Drawing mode for the editor. + */ +enum class Mode { + Draw, Erase, Select, Line +} + +/** + * Placement mode for selection operations. + * If state is Move then applySelectionDisplace() will delete original strokes and images. + */ +enum class PlacementMode { + Move, Paste +} + +/** + * Singleton holding clipboard content across EditorState instances. + */ +object Clipboard { + var content: ClipboardContent? = null +} diff --git a/app/src/main/java/com/ethran/notable/editor/state/SelectionState.kt b/app/src/main/java/com/ethran/notable/editor/state/SelectionState.kt index 65316ce3..c31cb45e 100644 --- a/app/src/main/java/com/ethran/notable/editor/state/SelectionState.kt +++ b/app/src/main/java/com/ethran/notable/editor/state/SelectionState.kt @@ -15,7 +15,7 @@ import com.ethran.notable.data.db.Image import com.ethran.notable.data.db.Stroke import com.ethran.notable.data.model.SimplePointF import com.ethran.notable.editor.PageView -import com.ethran.notable.editor.PlacementMode +import com.ethran.notable.editor.state.PlacementMode import com.ethran.notable.editor.drawing.drawImage import com.ethran.notable.editor.utils.imageBoundsInt import com.ethran.notable.editor.utils.offsetImage diff --git a/app/src/main/java/com/ethran/notable/editor/ui/EditorSurface.kt b/app/src/main/java/com/ethran/notable/editor/ui/EditorSurface.kt index 8dee01c9..66b4211b 100644 --- a/app/src/main/java/com/ethran/notable/editor/ui/EditorSurface.kt +++ b/app/src/main/java/com/ethran/notable/editor/ui/EditorSurface.kt @@ -6,7 +6,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.viewinterop.AndroidView import com.ethran.notable.data.AppRepository -import com.ethran.notable.editor.EditorViewModel import com.ethran.notable.editor.PageView import com.ethran.notable.editor.canvas.DrawCanvas import com.ethran.notable.editor.state.EditorState @@ -18,21 +17,20 @@ private val log = ShipBook.getLogger("EditorSurface") @Composable fun EditorSurface( appRepository: AppRepository, - viewModel: EditorViewModel, page: PageView, history: History + state: EditorState, + page: PageView, + history: History ) { val coroutineScope = rememberCoroutineScope() log.i("recompose surface") - // Create EditorState wrapper for backward compatibility with DrawCanvas - val editorState = EditorState(viewModel) - AndroidView( factory = { ctx -> DrawCanvas( context = ctx, appRepository = appRepository, coroutineScope = coroutineScope, - state = editorState, + state = state, page = page, history = history ).apply { @@ -42,4 +40,4 @@ fun EditorSurface( }, modifier = Modifier.fillMaxSize() ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/ethran/notable/editor/ui/ScrollIndicator.kt b/app/src/main/java/com/ethran/notable/editor/ui/ScrollIndicator.kt index c285900a..9a83c6f2 100644 --- a/app/src/main/java/com/ethran/notable/editor/ui/ScrollIndicator.kt +++ b/app/src/main/java/com/ethran/notable/editor/ui/ScrollIndicator.kt @@ -12,10 +12,12 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.ethran.notable.editor.EditorViewModel import com.ethran.notable.editor.PageView import com.ethran.notable.ui.convertDpToPixel @@ -27,6 +29,7 @@ import kotlin.math.max */ @Composable fun ScrollIndicator(viewModel: EditorViewModel, page: PageView) { + val toolbarState by viewModel.toolbarState.collectAsStateWithLifecycle() BoxWithConstraints( modifier = Modifier .width(5.dp) @@ -43,7 +46,7 @@ fun ScrollIndicator(viewModel: EditorViewModel, page: PageView) { val indicatorSizeDp = (viewportHeightPx / virtualHeight.toFloat()) * this.maxHeight.value val indicatorPositionDp = (page.scroll.y / virtualHeight.toFloat()) * this.maxHeight.value - if (!viewModel.isToolbarOpen) return@BoxWithConstraints + if (!toolbarState.isToolbarOpen) return@BoxWithConstraints Box( modifier = Modifier @@ -63,6 +66,7 @@ fun ScrollIndicator(viewModel: EditorViewModel, page: PageView) { */ @Composable fun HorizontalScrollIndicator(viewModel: EditorViewModel, page: PageView) { + val toolbarState by viewModel.toolbarState.collectAsStateWithLifecycle() Column(Modifier.fillMaxSize()) { Spacer(Modifier.weight(1f)) BoxWithConstraints( @@ -81,7 +85,7 @@ fun HorizontalScrollIndicator(viewModel: EditorViewModel, page: PageView) { val indicatorSizeDp = (viewportWidthPx / virtualWidth.toFloat()) * this.maxWidth.value val indicatorPositionDp = (page.scroll.x / virtualWidth.toFloat()) * this.maxWidth.value - if (!viewModel.isToolbarOpen) return@BoxWithConstraints + if (!toolbarState.isToolbarOpen) return@BoxWithConstraints Box( modifier = Modifier @@ -94,4 +98,4 @@ fun HorizontalScrollIndicator(viewModel: EditorViewModel, page: PageView) { ) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt index 37da8a26..c726af72 100644 --- a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt +++ b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt @@ -29,7 +29,7 @@ import com.ethran.notable.data.datastore.BUTTON_SIZE import com.ethran.notable.data.datastore.GlobalAppSettings import com.ethran.notable.editor.ToolbarAction import com.ethran.notable.editor.ToolbarUiState -import com.ethran.notable.editor.Mode +import com.ethran.notable.editor.state.Mode import com.ethran.notable.editor.utils.Pen import com.ethran.notable.editor.utils.PenSetting import com.ethran.notable.ui.dialogs.BackgroundSelector diff --git a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/ToolbarMenu.kt b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/ToolbarMenu.kt index a5b23817..8fd67021 100644 --- a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/ToolbarMenu.kt +++ b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/ToolbarMenu.kt @@ -25,7 +25,7 @@ import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupProperties import com.ethran.notable.R import com.ethran.notable.data.datastore.BUTTON_SIZE -import com.ethran.notable.editor.Mode +import com.ethran.notable.editor.state.Mode import com.ethran.notable.editor.ToolbarAction import com.ethran.notable.editor.ToolbarUiState import com.ethran.notable.editor.utils.Pen diff --git a/app/src/main/java/com/ethran/notable/editor/utils/ImageHandler.kt b/app/src/main/java/com/ethran/notable/editor/utils/ImageHandler.kt index 7d5685e5..f60ab72d 100644 --- a/app/src/main/java/com/ethran/notable/editor/utils/ImageHandler.kt +++ b/app/src/main/java/com/ethran/notable/editor/utils/ImageHandler.kt @@ -7,7 +7,7 @@ import androidx.compose.ui.graphics.asAndroidBitmap import androidx.compose.ui.graphics.asImageBitmap import com.ethran.notable.data.db.Image import com.ethran.notable.editor.PageView -import com.ethran.notable.editor.PlacementMode +import com.ethran.notable.editor.state.PlacementMode import com.ethran.notable.editor.canvas.CanvasEventBus import com.ethran.notable.editor.drawing.drawImage import com.ethran.notable.editor.state.EditorState diff --git a/app/src/main/java/com/ethran/notable/editor/utils/Select.kt b/app/src/main/java/com/ethran/notable/editor/utils/Select.kt index bf5c7819..c8c3a300 100644 --- a/app/src/main/java/com/ethran/notable/editor/utils/Select.kt +++ b/app/src/main/java/com/ethran/notable/editor/utils/Select.kt @@ -13,7 +13,7 @@ import com.ethran.notable.data.db.Stroke import com.ethran.notable.data.model.SimplePointF import com.ethran.notable.editor.canvas.CanvasEventBus import com.ethran.notable.editor.PageView -import com.ethran.notable.editor.PlacementMode +import com.ethran.notable.editor.state.PlacementMode import com.ethran.notable.editor.drawing.drawImage import com.ethran.notable.editor.drawing.drawStroke import com.ethran.notable.editor.state.EditorState From 9afa2a5217cc1972c9b72bb0ba2a1546536ced88 Mon Sep 17 00:00:00 2001 From: Wiktor Wichrowski Date: Tue, 10 Mar 2026 15:55:20 +0100 Subject: [PATCH 04/10] fix for canvas init. --- app/src/main/java/com/ethran/notable/editor/EditorView.kt | 1 + .../main/java/com/ethran/notable/editor/EditorViewModel.kt | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/ethran/notable/editor/EditorView.kt b/app/src/main/java/com/ethran/notable/editor/EditorView.kt index fc0f70c8..039ef90b 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorView.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorView.kt @@ -137,6 +137,7 @@ fun EditorView( // Initialize ViewModel with persisted settings on first composition LaunchedEffect(Unit) { viewModel.initFromPersistedSettings(editorSettingCacheManager.getEditorSettings()) + viewModel.updateDrawingState() } val editorControlTower = remember { diff --git a/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt b/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt index f88979f0..285aff60 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt @@ -13,6 +13,7 @@ import com.ethran.notable.data.datastore.GlobalAppSettings import com.ethran.notable.data.db.getPageIndex import com.ethran.notable.data.db.getParentFolder import com.ethran.notable.data.model.BackgroundType +import com.ethran.notable.editor.EditorViewModel.Companion.DEFAULT_PEN_SETTINGS import com.ethran.notable.editor.canvas.CanvasEventBus import com.ethran.notable.editor.state.Mode import com.ethran.notable.editor.state.SelectionState @@ -71,7 +72,8 @@ data class ToolbarUiState( val mode: Mode = Mode.Draw, val pen: Pen = Pen.BALLPEN, val eraser: Eraser = Eraser.PEN, - val penSettings: Map = emptyMap(), + // TODO: if it is an emptyMap(), the DrawCanvas crashes, to be fixed. + val penSettings: Map = DEFAULT_PEN_SETTINGS, val isSelectionActive: Boolean = false, val hasClipboard: Boolean = false, val isDrawing: Boolean = true, From 0a9ef640512b8e2def33c04ac1b2016cf6fb3e04 Mon Sep 17 00:00:00 2001 From: Wiktor Wichrowski Date: Tue, 10 Mar 2026 16:09:15 +0100 Subject: [PATCH 05/10] refactor page change observation to use snapshotFlow in EditorView --- .../com/ethran/notable/editor/EditorView.kt | 19 +++++++++++++++---- .../ethran/notable/editor/EditorViewModel.kt | 3 +-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/ethran/notable/editor/EditorView.kt b/app/src/main/java/com/ethran/notable/editor/EditorView.kt index 039ef90b..86252348 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorView.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorView.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel @@ -42,6 +43,9 @@ import com.ethran.notable.ui.views.LibraryDestination import com.ethran.notable.ui.views.PagesDestination import io.shipbook.shipbooksdk.ShipBook import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -163,10 +167,6 @@ fun EditorView( is EditorUiEvent.ShowSnackbar -> { snackManager.displaySnack(SnackConf(text = event.message)) } - - is EditorUiEvent.PageChanged -> { - onPageChange(event.pageId) - } } } } @@ -217,6 +217,17 @@ fun EditorView( editorState.syncFrom(toolbarState) } + // Observe pageId changes from ViewModel state for navigation + LaunchedEffect(viewModel) { + snapshotFlow { toolbarState.pageId } + .filterNotNull() + .distinctUntilChanged() + .drop(1) // Skip initial emission from loadBookData + .collect { newPageId -> + onPageChange(newPageId) + } + } + // Sync PageView state to ViewModel for Toolbar rendering val zoomLevel by page.zoomLevel.collectAsStateWithLifecycle() val selectionActive = viewModel.selectionState.isNonEmpty() diff --git a/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt b/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt index 285aff60..629f4ff2 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt @@ -145,7 +145,6 @@ sealed class EditorUiEvent { data class NavigateToLibrary(val folderId: String?) : EditorUiEvent() data class NavigateToPages(val bookId: String) : EditorUiEvent() object NavigateToBugReport : EditorUiEvent() - data class PageChanged(val pageId: String) : EditorUiEvent() } // -------------------------------------------------------- @@ -474,7 +473,7 @@ class EditorViewModel @Inject constructor( if (newPageId != currentPageId) { Log.d("EditorView", "Page changed") currentPageId = newPageId - sendUiEvent(EditorUiEvent.PageChanged(newPageId)) + _toolbarState.update { it.copy(pageId = newPageId) } } else { Log.d("EditorView", "Tried to change to same page!") sendUiEvent(EditorUiEvent.ShowSnackbar("Tried to change to same page!")) From 639ee317cdda75588950919e652cd89b046f5fbb Mon Sep 17 00:00:00 2001 From: Wiktor Wichrowski Date: Tue, 10 Mar 2026 16:20:21 +0100 Subject: [PATCH 06/10] refactor page navigation logic and state management - Move page navigation responsibility from `EditorControlTower` to `EditorViewModel`. - Update `currentPageId` to be a computed property derived from `_toolbarState`. - Implement `goToNextPage` and `goToPreviousPage` in `EditorViewModel` using `viewModelScope`. - Ensure history is cleared when navigating between pages. - Refactor `changePage` to handle its own coroutine scope and reset selection state. --- .../notable/editor/EditorControlTower.kt | 20 ++++-------- .../ethran/notable/editor/EditorViewModel.kt | 32 +++++++++++++------ 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/ethran/notable/editor/EditorControlTower.kt b/app/src/main/java/com/ethran/notable/editor/EditorControlTower.kt index 31712314..f458f694 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorControlTower.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorControlTower.kt @@ -127,21 +127,15 @@ class EditorControlTower( } fun goToNextPage() { - scope.launch(Dispatchers.IO) { - logEditorControlTower.i("Going to next page") - val next = state.viewModel.getNextPage() - if (next != null) - switchPage(next) - } + logEditorControlTower.i("Going to next page") + state.viewModel.goToNextPage() + history.cleanHistory() } fun goToPreviousPage() { - scope.launch(Dispatchers.IO) { - logEditorControlTower.i("Going to previous page") - val previous = state.viewModel.getPreviousPage() - if (previous != null) - switchPage(previous) - } + logEditorControlTower.i("Going to previous page") + state.viewModel.goToPreviousPage() + history.cleanHistory() } fun undo() { @@ -315,4 +309,4 @@ class EditorControlTower( showHint("Pasted content from clipboard", scope) } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt b/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt index 629f4ff2..3a929e05 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt @@ -171,7 +171,7 @@ class EditorViewModel @Inject constructor( // ---- Internal document context ---- private var bookId: String? = null - private var currentPageId: String = "" + private val currentPageId: String get() = _toolbarState.value.pageId.orEmpty() // ---- Init guard ---- private val didInitSettings = AtomicBoolean(false) @@ -411,7 +411,6 @@ class EditorViewModel @Inject constructor( */ fun loadBookData(bookId: String?, pageId: String) { this.bookId = bookId - this.currentPageId = pageId viewModelScope.launch(Dispatchers.IO) { val page = appRepository.pageRepository.getById(pageId) @@ -449,7 +448,7 @@ class EditorViewModel @Inject constructor( // Page Navigation (from EditorState) // -------------------------------------------------------- - suspend fun getNextPage(): String? { + private suspend fun getNextPageId(): String? { return if (bookId != null) { appRepository.getNextPageIdFromBookAndPageOrCreate( pageId = currentPageId, notebookId = bookId!! @@ -457,7 +456,7 @@ class EditorViewModel @Inject constructor( } else null } - suspend fun getPreviousPage(): String? { + private suspend fun getPreviousPageId(): String? { return if (bookId != null) { appRepository.getPreviousPageIdFromBookAndPage( pageId = currentPageId, notebookId = bookId!! @@ -465,15 +464,26 @@ class EditorViewModel @Inject constructor( } else null } - suspend fun updateOpenedPage(newPageId: String) { + fun goToNextPage() { + viewModelScope.launch(Dispatchers.IO) { + getNextPageId()?.let { changePage(it) } + } + } + + fun goToPreviousPage() { + viewModelScope.launch(Dispatchers.IO) { + getPreviousPageId()?.let { changePage(it) } + } + } + + private suspend fun updateOpenedPage(newPageId: String) { Log.d("EditorView", "Update open page to $newPageId") if (bookId != null) { appRepository.bookRepository.setOpenPageId(bookId!!, newPageId) } if (newPageId != currentPageId) { Log.d("EditorView", "Page changed") - currentPageId = newPageId - _toolbarState.update { it.copy(pageId = newPageId) } + loadBookData(bookId, newPageId) } else { Log.d("EditorView", "Tried to change to same page!") sendUiEvent(EditorUiEvent.ShowSnackbar("Tried to change to same page!")) @@ -485,10 +495,12 @@ class EditorViewModel @Inject constructor( * * @param id The unique identifier of the page to switch to. */ - suspend fun changePage(id: String) { + fun changePage(id: String) { log.d("Changing page to $id, from $currentPageId") - updateOpenedPage(id) - selectionState.reset() + viewModelScope.launch(Dispatchers.IO) { + updateOpenedPage(id) + selectionState.reset() + } } // -------------------------------------------------------- From 83a905e83d8664b71f0f1332e82970b7127c9a3a Mon Sep 17 00:00:00 2001 From: Wiktor Wichrowski Date: Tue, 10 Mar 2026 20:16:37 +0100 Subject: [PATCH 07/10] update pen and stroke initialization logic --- .../ethran/notable/editor/canvas/CanvasObserverRegistry.kt | 2 +- .../main/java/com/ethran/notable/editor/canvas/DrawCanvas.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/ethran/notable/editor/canvas/CanvasObserverRegistry.kt b/app/src/main/java/com/ethran/notable/editor/canvas/CanvasObserverRegistry.kt index e76e906a..a19af78b 100644 --- a/app/src/main/java/com/ethran/notable/editor/canvas/CanvasObserverRegistry.kt +++ b/app/src/main/java/com/ethran/notable/editor/canvas/CanvasObserverRegistry.kt @@ -173,7 +173,7 @@ class CanvasObserverRegistry( private fun observePenChanges() { coroutineScope.launch { - snapshotFlow { state.pen }.drop(1).collect { + snapshotFlow { state.pen }.drop(0).collect { logCanvasObserver.v("pen change: ${state.pen}") inputHandler.updatePenAndStroke() refreshManager.refreshUiSuspend() diff --git a/app/src/main/java/com/ethran/notable/editor/canvas/DrawCanvas.kt b/app/src/main/java/com/ethran/notable/editor/canvas/DrawCanvas.kt index 2b69517d..75cae6ab 100644 --- a/app/src/main/java/com/ethran/notable/editor/canvas/DrawCanvas.kt +++ b/app/src/main/java/com/ethran/notable/editor/canvas/DrawCanvas.kt @@ -108,8 +108,8 @@ class DrawCanvas( log.i("surface created $holder") // set up the drawing surface inputHandler.updateActiveSurface() - // Restore the correct stroke size and style. - inputHandler.updatePenAndStroke() + // Restore the correct stroke size and style, now its done in initFromPersistedSettings +// inputHandler.updatePenAndStroke() } override fun surfaceChanged( From 4e8ef6b5c12130106268de4c26433dd910f3e1d6 Mon Sep 17 00:00:00 2001 From: Wiktor Wichrowski Date: Tue, 10 Mar 2026 21:33:38 +0100 Subject: [PATCH 08/10] add logging and improve selection state handling --- .../com/ethran/notable/editor/EditorView.kt | 1 + .../ethran/notable/editor/EditorViewModel.kt | 17 ++++++++++++++++- .../notable/editor/state/SelectionState.kt | 8 +++++++- .../com/ethran/notable/editor/utils/Select.kt | 16 ++++++++++++---- 4 files changed, 36 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/ethran/notable/editor/EditorView.kt b/app/src/main/java/com/ethran/notable/editor/EditorView.kt index 86252348..12e3d2c5 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorView.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorView.kt @@ -235,6 +235,7 @@ fun EditorView( zoomLevel, selectionActive ) { + log.v("EditorView: zoomLevel=$zoomLevel, selectionActive=$selectionActive") viewModel.setShowResetView(zoomLevel != 1.0f) viewModel.setSelectionActive(selectionActive) } diff --git a/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt b/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt index 3a929e05..bd65a711 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt @@ -206,6 +206,7 @@ class EditorViewModel @Inject constructor( // -------------------------------------------------------- fun onToolbarAction(action: ToolbarAction) { + log.v("onToolbarAction: $action") when (action) { is ToolbarAction.ToggleToolbar -> { _toolbarState.update { it.copy(isToolbarOpen = !it.isToolbarOpen) } @@ -385,6 +386,7 @@ class EditorViewModel @Inject constructor( * Re-evaluates whether drawing should be enabled based on menu and selection states. */ fun updateDrawingState() { + log.v("updateDrawingState") val state = _toolbarState.value val anyMenuOpen = state.isMenuOpen || state.isStrokeSelectionOpen || state.isBackgroundSelectorModalOpen @@ -410,6 +412,7 @@ class EditorViewModel @Inject constructor( * Loads context data for the toolbar (page number, background info, etc.) */ fun loadBookData(bookId: String?, pageId: String) { + log.v("loadBookData: bookId=$bookId, pageId=$pageId") this.bookId = bookId viewModelScope.launch(Dispatchers.IO) { @@ -465,18 +468,21 @@ class EditorViewModel @Inject constructor( } fun goToNextPage() { + log.v("goToNextPage") viewModelScope.launch(Dispatchers.IO) { getNextPageId()?.let { changePage(it) } } } fun goToPreviousPage() { + log.v("goToPreviousPage") viewModelScope.launch(Dispatchers.IO) { getPreviousPageId()?.let { changePage(it) } } } private suspend fun updateOpenedPage(newPageId: String) { + log.v("updateOpenedPage: $newPageId") Log.d("EditorView", "Update open page to $newPageId") if (bookId != null) { appRepository.bookRepository.setOpenPageId(bookId!!, newPageId) @@ -496,6 +502,7 @@ class EditorViewModel @Inject constructor( * @param id The unique identifier of the page to switch to. */ fun changePage(id: String) { + log.v("changePage: $id") log.d("Changing page to $id, from $currentPageId") viewModelScope.launch(Dispatchers.IO) { updateOpenedPage(id) @@ -516,9 +523,15 @@ class EditorViewModel @Inject constructor( } fun setSelectionActive(active: Boolean) { + log.v("setSelectionActive: $active") if (_toolbarState.value.isSelectionActive != active) { + if (active) //selection is active, we can directly update it, and skip other checks + viewModelScope.launch { + CanvasEventBus.isDrawing.emit(false) + } _toolbarState.update { it.copy(isSelectionActive = active) } - updateDrawingState() + if (!active) + updateDrawingState() } } @@ -527,10 +540,12 @@ class EditorViewModel @Inject constructor( // -------------------------------------------------------- private fun sendUiEvent(event: EditorUiEvent) { + log.v("sendUiEvent: $event") viewModelScope.launch { uiEventChannel.send(event) } } private fun sendCanvasCommand(command: CanvasCommand) { + log.v("sendCanvasCommand: $command") viewModelScope.launch { canvasCommandChannel.send(command) } } diff --git a/app/src/main/java/com/ethran/notable/editor/state/SelectionState.kt b/app/src/main/java/com/ethran/notable/editor/state/SelectionState.kt index c31cb45e..e95c7299 100644 --- a/app/src/main/java/com/ethran/notable/editor/state/SelectionState.kt +++ b/app/src/main/java/com/ethran/notable/editor/state/SelectionState.kt @@ -15,7 +15,6 @@ import com.ethran.notable.data.db.Image import com.ethran.notable.data.db.Stroke import com.ethran.notable.data.model.SimplePointF import com.ethran.notable.editor.PageView -import com.ethran.notable.editor.state.PlacementMode import com.ethran.notable.editor.drawing.drawImage import com.ethran.notable.editor.utils.imageBoundsInt import com.ethran.notable.editor.utils.offsetImage @@ -46,6 +45,7 @@ class SelectionState { var placementMode by mutableStateOf(null) fun reset() { + log.v("reset") selectedStrokes = null selectedImages = null secondPageCut = null @@ -67,6 +67,7 @@ class SelectionState { } fun resizeImages(scale: Int, scope: CoroutineScope, page: PageView) { + log.v("resizeImages: scale=$scale") val selectedImagesCopy = selectedImages?.map { image -> image.copy( height = image.height + (image.height * scale / 100), @@ -113,6 +114,7 @@ class SelectionState { @Suppress("UNUSED_PARAMETER") fun resizeStrokes(scale: Int, scope: CoroutineScope, page: PageView) { + log.v("resizeStrokes: scale=$scale") //TODO: implement this } @@ -126,6 +128,7 @@ class SelectionState { * @return A list of [Operation]s that can be used to undo the deletion (e.g., re-adding the deleted items). */ fun deleteSelection(page: PageView): List { + log.v("deleteSelection: images=${selectedImages?.size}, strokes=${selectedStrokes?.size}") val operationList = mutableListOf() val selectedImagesToRemove = selectedImages if (!selectedImagesToRemove.isNullOrEmpty()) { @@ -146,6 +149,7 @@ class SelectionState { } fun duplicateSelection() { + log.v("duplicateSelection") // set operation to paste only placementMode = PlacementMode.Paste if (!selectedStrokes.isNullOrEmpty()) @@ -176,6 +180,7 @@ class SelectionState { // Moves strokes, and redraws canvas. fun applySelectionDisplace(page: PageView): List? { + log.v("applySelectionDisplace: offset=$selectionDisplaceOffset, mode=$placementMode") if (selectionDisplaceOffset == null) return null if (selectionRect == null) return null @@ -235,6 +240,7 @@ class SelectionState { } fun selectionToClipboard(scrollPos: Offset, context: Context): ClipboardContent { + log.v("selectionToClipboard: scrollPos=$scrollPos, images=${selectedImages?.size}, strokes=${selectedStrokes?.size}") val strokes = selectedStrokes?.map { offsetStroke(it, offset = -scrollPos) diff --git a/app/src/main/java/com/ethran/notable/editor/utils/Select.kt b/app/src/main/java/com/ethran/notable/editor/utils/Select.kt index c8c3a300..8448623a 100644 --- a/app/src/main/java/com/ethran/notable/editor/utils/Select.kt +++ b/app/src/main/java/com/ethran/notable/editor/utils/Select.kt @@ -11,12 +11,12 @@ import com.ethran.notable.data.PageDataManager import com.ethran.notable.data.db.Image import com.ethran.notable.data.db.Stroke import com.ethran.notable.data.model.SimplePointF -import com.ethran.notable.editor.canvas.CanvasEventBus import com.ethran.notable.editor.PageView -import com.ethran.notable.editor.state.PlacementMode +import com.ethran.notable.editor.canvas.CanvasEventBus import com.ethran.notable.editor.drawing.drawImage import com.ethran.notable.editor.drawing.drawStroke import com.ethran.notable.editor.state.EditorState +import com.ethran.notable.editor.state.PlacementMode import com.ethran.notable.ui.SnackConf import com.ethran.notable.ui.SnackState import io.shipbook.shipbooksdk.ShipBook @@ -45,7 +45,14 @@ fun selectStrokesFromPath(strokes: List, path: Path): List { return strokes.filter { strokeBounds(it).intersect(bounds) - }.filter { it.points.any { region.contains(it.x.toInt(), (it.y - bounds.top).toInt()) } } + }.filter { + it.points.any { point -> + region.contains( + point.x.toInt(), + (point.y - bounds.top).toInt() + ) + } + } } fun selectImagesFromPath(images: List, path: Path): List { @@ -61,7 +68,7 @@ fun selectImagesFromPath(images: List, path: Path): List { imageBounds(it).intersect(bounds) }.filter { // include image if all its corners are within region - imagePoints(it).all { region.contains(it.x, (it.y - bounds.top).toInt()) } + imagePoints(it).all { point -> region.contains(point.x, (point.y - bounds.top).toInt()) } } } @@ -74,6 +81,7 @@ fun selectImagesAndStrokes( imagesToSelect: List, strokesToSelect: List ) { + log.v("selectImagesAndStrokes: images=${imagesToSelect.size}, strokes=${strokesToSelect.size}") //handle selection: val pageBounds = Rect() From 5fc62269e0482c39713175ae5d8d28bbca4f23ce Mon Sep 17 00:00:00 2001 From: Wiktor Wichrowski Date: Tue, 10 Mar 2026 21:45:08 +0100 Subject: [PATCH 09/10] add logging and improve selection state handling --- .../com/ethran/notable/editor/EditorViewModel.kt | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt b/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt index bd65a711..f8d67bc1 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt @@ -79,11 +79,9 @@ data class ToolbarUiState( val isDrawing: Boolean = true, ) { val isDrawingAllowed: Boolean - get() = mode == Mode.Draw && - !isMenuOpen && - !isStrokeSelectionOpen && - !isBackgroundSelectorModalOpen && - !isSelectionActive + get() = !isSelectionActive && + !(isMenuOpen || isStrokeSelectionOpen || isBackgroundSelectorModalOpen) + } @@ -387,10 +385,7 @@ class EditorViewModel @Inject constructor( */ fun updateDrawingState() { log.v("updateDrawingState") - val state = _toolbarState.value - val anyMenuOpen = - state.isMenuOpen || state.isStrokeSelectionOpen || state.isBackgroundSelectorModalOpen - val shouldBeDrawing = !anyMenuOpen && !state.isSelectionActive + val shouldBeDrawing = _toolbarState.value.isDrawingAllowed _toolbarState.update { it.copy(isDrawing = shouldBeDrawing) } log.d("Drawing state: $shouldBeDrawing") viewModelScope.launch { From 52ce4063f172ac7b75ff62efe6a8b8df4db089b9 Mon Sep 17 00:00:00 2001 From: Wiktor Wichrowski Date: Tue, 10 Mar 2026 21:58:35 +0100 Subject: [PATCH 10/10] sync EditorViewModel with quick navigation visibility and update drawing state --- .../main/java/com/ethran/notable/editor/EditorView.kt | 10 ++++++++-- .../java/com/ethran/notable/editor/EditorViewModel.kt | 8 +++++++- .../com/ethran/notable/navigation/NotableNavHost.kt | 2 +- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/ethran/notable/editor/EditorView.kt b/app/src/main/java/com/ethran/notable/editor/EditorView.kt index 12e3d2c5..50608bdd 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorView.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorView.kt @@ -77,6 +77,7 @@ fun EditorView( appRepository: AppRepository, bookId: String?, pageId: String, + isQuickNavOpen: Boolean, onPageChange: (String) -> Unit, viewModel: EditorViewModel = hiltViewModel() ) { @@ -94,7 +95,7 @@ fun EditorView( if (!exists) { // TODO: check if it is correct, and remove exeption throwing -// throw Exception("Page does not exist") + throw Exception("Page does not exist") if (bookId != null) { // clean the book log.i("Could not find page, Cleaning book") @@ -111,6 +112,11 @@ fun EditorView( } } + // Sync isQuickNavOpen to ViewModel + LaunchedEffect(isQuickNavOpen) { + viewModel.onToolbarAction(ToolbarAction.UpdateQuickNavOpen(isQuickNavOpen)) + } + if (pageExists == null) return BoxWithConstraints { @@ -165,7 +171,7 @@ fun EditorView( } is EditorUiEvent.ShowSnackbar -> { - snackManager.displaySnack(SnackConf(text = event.message)) + snackManager.displaySnack(SnackConf(text = event.message, duration = 2000)) } } } diff --git a/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt b/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt index f8d67bc1..d94bce31 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt @@ -77,11 +77,12 @@ data class ToolbarUiState( val isSelectionActive: Boolean = false, val hasClipboard: Boolean = false, val isDrawing: Boolean = true, + val isQuickNavOpen: Boolean = false, ) { val isDrawingAllowed: Boolean get() = !isSelectionActive && !(isMenuOpen || isStrokeSelectionOpen || isBackgroundSelectorModalOpen) - + && !isQuickNavOpen } @@ -117,6 +118,7 @@ sealed class ToolbarAction { object NavigateToHome : ToolbarAction() object CloseAllMenus : ToolbarAction() + data class UpdateQuickNavOpen(val isOpen: Boolean) : ToolbarAction() } @@ -259,6 +261,10 @@ class EditorViewModel @Inject constructor( ToolbarAction.NavigateToHome -> sendUiEvent(EditorUiEvent.NavigateToLibrary(null)) ToolbarAction.CloseAllMenus -> handleCloseAllMenus() + is ToolbarAction.UpdateQuickNavOpen -> { + _toolbarState.update { it.copy(isQuickNavOpen = action.isOpen) } + updateDrawingState() + } } } diff --git a/app/src/main/java/com/ethran/notable/navigation/NotableNavHost.kt b/app/src/main/java/com/ethran/notable/navigation/NotableNavHost.kt index 5f955df1..be732c5d 100644 --- a/app/src/main/java/com/ethran/notable/navigation/NotableNavHost.kt +++ b/app/src/main/java/com/ethran/notable/navigation/NotableNavHost.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -106,6 +105,7 @@ fun NotableNavHost( navController = appNavigator.navController, bookId = bookId, pageId = currentPageId, + isQuickNavOpen = appNavigator.isQuickNavOpen, onPageChange = { newPageId -> appNavigator.onPageChange( backStackEntry,