Skip to content

Conversation

@Ethran
Copy link
Owner

@Ethran Ethran commented Feb 9, 2026

CanvasEventBus - Extract the companion object from DrawCanvas.kt

All observer logic from `DrawCanvas.kt` has been extracted into a new `CanvasObserverRegistry.kt` class. This improves separation of concerns by dedicating a class to manage the various event bus and state flow observers.

The `DrawCanvas` now initializes and registers these observers through the new registry, cleaning up its own implementation. The visibility of several helper methods in `DrawCanvas` has been changed from `private` to `fun` to allow access from the new registry class.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the editor canvas signaling/observer plumbing by extracting DrawCanvas’s former companion-object event flows into a dedicated CanvasEventBus, and moving the observer wiring into a standalone registry. This reduces the responsibilities of DrawCanvas and updates the rest of the app to use the new event bus.

Changes:

  • Introduces CanvasEventBus to host shared flows/state previously on DrawCanvas.Companion.
  • Extracts DrawCanvas.registerObservers() implementation into CanvasObserverRegistry.
  • Updates multiple UI/editor components to emit/collect via CanvasEventBus (including page change events formerly on EditorControlTower).

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
app/src/main/java/com/ethran/notable/ui/dialogs/BackgroundSelector.kt Switches refresh signaling from DrawCanvas to CanvasEventBus.
app/src/main/java/com/ethran/notable/ui/components/QuickNav.kt Uses CanvasEventBus for save/preview/restore and page-change signaling.
app/src/main/java/com/ethran/notable/ui/Router.kt Uses CanvasEventBus.isDrawing to toggle drawing state around QuickNav.
app/src/main/java/com/ethran/notable/editor/utils/Select.kt Uses CanvasEventBus.refreshUi after selection updates.
app/src/main/java/com/ethran/notable/editor/ui/toolbar/ToolbarMenu.kt Routes clear-page action through CanvasEventBus.clearPageSignal.
app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt Routes image insert + refresh through CanvasEventBus.
app/src/main/java/com/ethran/notable/editor/ui/EditorGestureReceiver.kt Emits gesture selection/force update via CanvasEventBus.
app/src/main/java/com/ethran/notable/editor/state/history.kt Moves history commit coordination to CanvasEventBus flows/deferred.
app/src/main/java/com/ethran/notable/editor/drawing/pageDrawing.kt Clears inserted-image state via CanvasEventBus.addImageByUri.
app/src/main/java/com/ethran/notable/editor/PageView.kt Triggers redraws via CanvasEventBus instead of DrawCanvas flows.
app/src/main/java/com/ethran/notable/editor/EditorControlTower.kt Collects page-change events from CanvasEventBus.changePage.
app/src/main/java/com/ethran/notable/editor/DrawCanvas.kt Removes companion event bus + inlines observer hookup via new registry.
app/src/main/java/com/ethran/notable/editor/CanvasObserverRegistry.kt New: encapsulates all canvas-related flow observers/handlers.
app/src/main/java/com/ethran/notable/editor/CanvasEventBus.kt New: centralized shared flows/state for canvas/editor events.
app/src/main/java/com/ethran/notable/data/PageDataManager.kt Emits canvas redraw on background-file invalidation via CanvasEventBus.
app/src/main/java/com/ethran/notable/MainActivity.kt Uses CanvasEventBus for restart/focus/refresh signaling.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 406 to 408
private val observers = CanvasObserverRegistry(
coroutineScope, this, page, state, history, touchHelper
)
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CanvasObserverRegistry is constructed with touchHelper, which forces evaluation of the touchHelper lazy during DrawCanvas construction. That changes the lifecycle/initialization timing of TouchHelper.create(...) (it now happens before init()/surface callbacks). Consider making observers lazy as well, or passing a getter (or TouchHelper? provider) so touchHelper stays lazily initialized when first needed.

Copilot uses AI. Check for mistakes.
Moved the `selectRectangle` function from `DrawCanvas.kt` to `editor/utils/Select.kt` for better code organization. The function's call site in `CanvasObserverRegistry.kt` has been updated to reflect this change.
--AI--
- **Extracted `CanvasRefreshManager`:** UI refresh and canvas drawing logic has been moved from `DrawCanvas` into a new `CanvasRefreshManager` class. This centralizes responsibility for screen updates.
- **Updated Method Calls:** `OnyxInputHandler`, `DrawCanvas`, and `CanvasObserverRegistry` now delegate UI refresh calls to the new `CanvasRefreshManager` instance.
- **Moved History Committing:** The logic for batching and committing stroke history has been moved from `DrawCanvas` to `CanvasObserverRegistry`.
- All classes directly related to the drawing canvas, such as `DrawCanvas`, `CanvasEventBus`, `CanvasRefreshManager`, and `OnyxInputHandler`, have been moved into a new `com.ethran.notable.editor.canvas` package.
- Imports have been updated across the application to reflect the new package structure.
All call sites have been updated to use the new location in `CanvasEventBus`.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 24 out of 24 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +69 to +121
val zoneToRedraw = dirtyRect ?: Rect(0, 0, page.viewWidth, page.viewHeight)
var canvas: Canvas? = null
try {
// Lock the canvas only for the dirtyRect region
canvas = drawCanvas.holder.lockCanvas(zoneToRedraw) ?: return

canvas.drawBitmap(page.windowedBitmap, zoneToRedraw, zoneToRedraw, Paint())

if (drawCanvas.getActualState().mode == Mode.Select) {
// render selection, but only within dirtyRect
drawCanvas.getActualState().selectionState.firstPageCut?.let { cutPoints ->
log.i("render cut")
val path = pointsToPath(cutPoints.map {
SimplePointF(
it.x - page.scroll.x, it.y - page.scroll.y
)
})
canvas.drawPath(path, selectPaint)
}
}
} catch (e: IllegalStateException) {
log.w("Surface released during draw", e)
// ignore — surface is gone
} finally {
try {
if (canvas != null) {
drawCanvas.holder.unlockCanvasAndPost(canvas)
}
} catch (e: IllegalStateException) {
log.w("Surface released during unlock", e)
}
}
}


fun restoreCanvas(dirtyRect: Rect, bitmap: Bitmap = page.windowedBitmap) {
drawCanvas.post {
val holder = drawCanvas.holder
var surfaceCanvas: Canvas? = null
try {
surfaceCanvas = holder.lockCanvas(dirtyRect)
// Draw the preview bitmap scaled to fit the dirty rect
surfaceCanvas.drawBitmap(bitmap, dirtyRect, dirtyRect, null)
} catch (e: Exception) {
Log.e("DrawCanvas", "Canvas lock failed: ${e.message}")
} finally {
if (surfaceCanvas != null) {
holder.unlockCanvasAndPost(surfaceCanvas)
}
// Trigger partial refresh
refreshScreenRegion(drawCanvas, dirtyRect)
}
}
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CanvasRefreshManager.drawCanvasToView(...) duplicates drawing logic that also exists in DrawCanvas.drawCanvasToView(...) (and similarly for restoreCanvas). Keeping two copies increases the chance they drift. Consider delegating to a single implementation (either have DrawCanvas delegate to CanvasRefreshManager or remove the duplicate methods from the manager).

Suggested change
val zoneToRedraw = dirtyRect ?: Rect(0, 0, page.viewWidth, page.viewHeight)
var canvas: Canvas? = null
try {
// Lock the canvas only for the dirtyRect region
canvas = drawCanvas.holder.lockCanvas(zoneToRedraw) ?: return
canvas.drawBitmap(page.windowedBitmap, zoneToRedraw, zoneToRedraw, Paint())
if (drawCanvas.getActualState().mode == Mode.Select) {
// render selection, but only within dirtyRect
drawCanvas.getActualState().selectionState.firstPageCut?.let { cutPoints ->
log.i("render cut")
val path = pointsToPath(cutPoints.map {
SimplePointF(
it.x - page.scroll.x, it.y - page.scroll.y
)
})
canvas.drawPath(path, selectPaint)
}
}
} catch (e: IllegalStateException) {
log.w("Surface released during draw", e)
// ignore — surface is gone
} finally {
try {
if (canvas != null) {
drawCanvas.holder.unlockCanvasAndPost(canvas)
}
} catch (e: IllegalStateException) {
log.w("Surface released during unlock", e)
}
}
}
fun restoreCanvas(dirtyRect: Rect, bitmap: Bitmap = page.windowedBitmap) {
drawCanvas.post {
val holder = drawCanvas.holder
var surfaceCanvas: Canvas? = null
try {
surfaceCanvas = holder.lockCanvas(dirtyRect)
// Draw the preview bitmap scaled to fit the dirty rect
surfaceCanvas.drawBitmap(bitmap, dirtyRect, dirtyRect, null)
} catch (e: Exception) {
Log.e("DrawCanvas", "Canvas lock failed: ${e.message}")
} finally {
if (surfaceCanvas != null) {
holder.unlockCanvasAndPost(surfaceCanvas)
}
// Trigger partial refresh
refreshScreenRegion(drawCanvas, dirtyRect)
}
}
// Delegate drawing logic to DrawCanvas to avoid duplication.
drawCanvas.drawCanvasToView(dirtyRect)
}
fun restoreCanvas(dirtyRect: Rect, bitmap: Bitmap = page.windowedBitmap) {
// Delegate restore logic to DrawCanvas to avoid duplication.
drawCanvas.restoreCanvas(dirtyRect, bitmap)

Copilot uses AI. Check for mistakes.
Extract drawing and erasing logic from `onRawDrawingTouchPointListReceived` into their own dedicated functions, `onRawDrawingList` and `onRawErasingList`, respectively. This improves code organization and readability.
@Ethran Ethran merged commit 5b34398 into main Feb 9, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant