Skip to content

Commit 7d3a24f

Browse files
authored
Merge pull request #23 from alexdremov/minor-refactor
Support Text Block
2 parents 1c218b9 + cb3f880 commit 7d3a24f

43 files changed

Lines changed: 2696 additions & 942 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/test.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,14 @@ jobs:
2828
uses: gradle/actions/setup-gradle@v4
2929

3030
- name: Run Tests with Coverage
31-
run: ./gradlew jacocoTestReport
31+
run: ./gradlew jacocoTestReport --info
32+
33+
- name: Upload Snapshot Artifacts
34+
if: failure()
35+
uses: actions/upload-artifact@v4
36+
with:
37+
name: snapshot-mismatches
38+
path: app/build/outputs/snapshots/
3239

3340
- name: Add Coverage PR Comment
3441
uses: Madrapps/jacoco-report@v1.7.1

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,5 @@ local.properties
1616
memory_dumps
1717
.idea
1818
profile_memory.sh
19+
FEATUREPLAN.md
20+

app/build.gradle.kts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,14 @@ dependencies {
190190
implementation("com.google.api-client:google-api-client-android:2.8.1")
191191
implementation("com.google.apis:google-api-services-drive:v3-rev20251210-2.0.0")
192192

193+
// Markwon (Markdown Rendering & Editing)
194+
implementation("io.noties.markwon:core:4.6.2")
195+
implementation("io.noties.markwon:editor:4.6.2")
196+
implementation("io.noties.markwon:ext-strikethrough:4.6.2")
197+
implementation("io.noties.markwon:ext-tables:4.6.2")
198+
implementation("io.noties.markwon:ext-tasklist:4.6.2")
199+
implementation("io.noties.markwon:syntax-highlight:4.6.2")
200+
193201
// Vulnerability force fixes
194202
constraints {
195203
// See: https://github.com/alexdremov/notate/security/dependabot/2
@@ -216,4 +224,10 @@ dependencies {
216224
}
217225
}
218226
}
227+
228+
modules {
229+
module("org.jetbrains:annotations-java5") {
230+
replacedBy("org.jetbrains:annotations", "annotations-java5 is a subset of annotations and causes duplicate classes")
231+
}
232+
}
219233
}

app/src/main/java/com/alexdremov/notate/CanvasActivity.kt

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ class CanvasActivity : AppCompatActivity() {
172172
ProcessLifecycleOwner.get().lifecycleScope.launch {
173173
viewModel.closeSession(path, finalMetadata)
174174
}
175-
175+
176176
// Finish after data capture
177177
finish()
178178
}
@@ -330,14 +330,6 @@ class CanvasActivity : AppCompatActivity() {
330330
}
331331
binding.toolbarContainer.addView(composeToolbar)
332332

333-
binding.toolbarContainer.setOnTouchListener { _, event ->
334-
if (event.action == android.view.MotionEvent.ACTION_DOWN) {
335-
lifecycleScope.launch { binding.canvasView.getController().clearSelection() }
336-
binding.canvasView.dismissActionPopup()
337-
}
338-
false
339-
}
340-
341333
sidebarController =
342334
SettingsSidebarController(
343335
this,
@@ -634,11 +626,22 @@ class CanvasActivity : AppCompatActivity() {
634626
targetRect: android.graphics.Rect,
635627
) {
636628
Logger.d("NotateDebug", "handleToolClick ID=$toolId")
637-
lifecycleScope.launch { binding.canvasView.getController().clearSelection() }
629+
630+
val item = viewModel.toolbarItems.value.find { it.id == toolId }
631+
val isSelectionSafeTool =
632+
when (item) {
633+
is ToolbarItem.Pen -> item.penTool.type == ToolType.TEXT
634+
is ToolbarItem.Select -> true
635+
else -> false
636+
}
637+
638+
// If clicking same tool (opening settings) OR switching to TEXT/SELECT, preserve selection.
639+
if (viewModel.activeToolId.value != toolId && !isSelectionSafeTool) {
640+
lifecycleScope.launch { binding.canvasView.getController().clearSelection() }
641+
}
638642
binding.canvasView.dismissActionPopup()
639643

640644
if (viewModel.activeToolId.value == toolId) {
641-
val item = viewModel.toolbarItems.value.find { it.id == toolId }
642645
val tool =
643646
when (item) {
644647
is ToolbarItem.Pen -> item.penTool
@@ -651,7 +654,17 @@ class CanvasActivity : AppCompatActivity() {
651654
com.alexdremov.notate.ui.dialog.PenSettingsPopup(
652655
this,
653656
tool,
654-
onUpdate = { updatedTool -> viewModel.updateTool(updatedTool) },
657+
onUpdate = { updatedTool ->
658+
viewModel.updateTool(updatedTool)
659+
if (updatedTool.type == ToolType.TEXT) {
660+
lifecycleScope.launch {
661+
binding.canvasView.getController().updateSelectedTextStyle(
662+
fontSize = updatedTool.width,
663+
color = updatedTool.color,
664+
)
665+
}
666+
}
667+
},
655668
onRemove = { toolToRemove -> viewModel.removePen(toolToRemove.id) },
656669
onDismiss = {
657670
com.alexdremov.notate.util.EpdFastModeController

app/src/main/java/com/alexdremov/notate/data/CanvasSerializer.kt

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import com.alexdremov.notate.config.CanvasConfig
55
import com.alexdremov.notate.model.BackgroundStyle
66
import com.alexdremov.notate.model.Stroke
77
import com.alexdremov.notate.model.Tag
8+
import com.alexdremov.notate.model.TextItem
89
import com.alexdremov.notate.util.Logger
910
import com.alexdremov.notate.util.StrokeGeometry
1011
import com.onyx.android.sdk.api.device.epd.EpdController
@@ -58,6 +59,28 @@ object CanvasSerializer {
5859
opacity = item.opacity,
5960
)
6061

62+
fun toTextItemData(item: TextItem): TextItemData =
63+
TextItemData(
64+
text = item.text,
65+
x = item.bounds.left,
66+
y = item.bounds.top,
67+
width = item.bounds.width(),
68+
height = item.bounds.height(),
69+
fontSize = item.fontSize,
70+
color = item.color,
71+
zIndex = item.zIndex,
72+
order = item.order,
73+
rotation = item.rotation,
74+
opacity = item.opacity,
75+
alignment =
76+
when (item.alignment) {
77+
android.text.Layout.Alignment.ALIGN_OPPOSITE -> 1
78+
android.text.Layout.Alignment.ALIGN_CENTER -> 2
79+
else -> 0
80+
},
81+
backgroundColor = item.backgroundColor,
82+
)
83+
6184
fun toData(
6285
canvasType: CanvasType,
6386
pageWidth: Float,
@@ -141,6 +164,25 @@ object CanvasSerializer {
141164
)
142165
}
143166

167+
fun fromTextItemData(tData: TextItemData): TextItem =
168+
TextItem(
169+
text = tData.text,
170+
fontSize = tData.fontSize,
171+
color = tData.color,
172+
bounds = RectF(tData.x, tData.y, tData.x + tData.width, tData.y + tData.height),
173+
alignment =
174+
when (tData.alignment) {
175+
1 -> android.text.Layout.Alignment.ALIGN_OPPOSITE
176+
2 -> android.text.Layout.Alignment.ALIGN_CENTER
177+
else -> android.text.Layout.Alignment.ALIGN_NORMAL
178+
},
179+
backgroundColor = tData.backgroundColor,
180+
zIndex = tData.zIndex,
181+
order = tData.order,
182+
rotation = tData.rotation,
183+
opacity = tData.opacity,
184+
)
185+
144186
data class LoadedCanvasState(
145187
val quadtree: com.alexdremov.notate.util.Quadtree,
146188
val contentBounds: RectF,

app/src/main/java/com/alexdremov/notate/data/PreferencesManager.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,13 @@ object PreferencesManager {
371371
.Select(tool),
372372
)
373373
}
374+
375+
com.alexdremov.notate.model.ToolType.TEXT -> {
376+
items.add(
377+
com.alexdremov.notate.model.ToolbarItem
378+
.Pen(tool), // Text is treated as a Pen tool in ToolbarItem for now or needs a new wrapper
379+
)
380+
}
374381
}
375382
}
376383

app/src/main/java/com/alexdremov/notate/data/SerializationModels.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ data class RegionProto(
5252
@ProtoNumber(2) val idY: Int,
5353
@ProtoNumber(3) val strokes: List<StrokeData> = emptyList(),
5454
@ProtoNumber(4) val images: List<CanvasImageData> = emptyList(),
55+
@ProtoNumber(5) val texts: List<TextItemData> = emptyList(),
5556
)
5657

5758
@Serializable
@@ -86,6 +87,23 @@ data class CanvasImageData(
8687
val opacity: Float = 1.0f,
8788
)
8889

90+
@Serializable
91+
data class TextItemData(
92+
@ProtoNumber(1) val text: String,
93+
@ProtoNumber(2) val x: Float,
94+
@ProtoNumber(3) val y: Float,
95+
@ProtoNumber(4) val width: Float,
96+
@ProtoNumber(5) val height: Float,
97+
@ProtoNumber(6) val fontSize: Float,
98+
@ProtoNumber(7) val color: Int,
99+
@ProtoNumber(8) val zIndex: Float,
100+
@ProtoNumber(9) val order: Long,
101+
@ProtoNumber(10) val rotation: Float = 0f,
102+
@ProtoNumber(11) val opacity: Float = 1.0f,
103+
@ProtoNumber(12) val alignment: Int = 0, // 0: Normal, 1: Opposite, 2: Center
104+
@ProtoNumber(13) val backgroundColor: Int = 0,
105+
)
106+
89107
@Serializable
90108
data class StrokeData(
91109
@ProtoNumber(2)

app/src/main/java/com/alexdremov/notate/data/region/RegionManager.kt

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -501,11 +501,19 @@ class RegionManager(
501501
if (toRemove.isNotEmpty()) {
502502
resizingId = id
503503
regionCache.remove(id)
504-
resizingId = null
505-
506504
region.items.removeAll(toRemove)
507-
toRemove.forEach { region.quadtree?.remove(it) }
508-
505+
toRemove.forEach { item ->
506+
var removedCount = 0
507+
while (region.quadtree?.remove(item) == true) {
508+
removedCount++
509+
}
510+
if (removedCount > 1) {
511+
Logger.w(
512+
"RegionManager",
513+
"Removed item ${item.order} from Quadtree $removedCount times",
514+
)
515+
}
516+
}
509517
region.contentBounds.setEmpty()
510518
region.items.forEach {
511519
if (region.contentBounds.isEmpty) {

app/src/main/java/com/alexdremov/notate/data/region/RegionStorage.kt

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.alexdremov.notate.data.CanvasSerializer
66
import com.alexdremov.notate.data.RegionBoundsProto
77
import com.alexdremov.notate.data.RegionProto
88
import com.alexdremov.notate.data.StrokeData
9+
import com.alexdremov.notate.data.TextItemData
910
import com.alexdremov.notate.model.Stroke
1011
import com.alexdremov.notate.util.Logger
1112
import kotlinx.serialization.ExperimentalSerializationApi
@@ -101,6 +102,7 @@ class RegionStorage(
101102
fun saveRegion(data: RegionData): Boolean {
102103
val strokeData = ArrayList<StrokeData>()
103104
val imageData = ArrayList<CanvasImageData>()
105+
val textData = ArrayList<TextItemData>()
104106

105107
for (item in data.items) {
106108
when (item) {
@@ -116,10 +118,14 @@ class RegionStorage(
116118
val relativeItem = item.copy(uri = uri)
117119
imageData.add(CanvasSerializer.toCanvasImageData(relativeItem))
118120
}
121+
122+
is com.alexdremov.notate.model.TextItem -> {
123+
textData.add(CanvasSerializer.toTextItemData(item))
124+
}
119125
}
120126
}
121127

122-
val proto = RegionProto(data.id.x, data.id.y, strokeData, imageData)
128+
val proto = RegionProto(data.id.x, data.id.y, strokeData, imageData, textData)
123129
val file = getRegionFile(data.id)
124130

125131
return try {
@@ -184,7 +190,13 @@ class RegionStorage(
184190
data.items.add(image)
185191
}
186192

187-
Logger.d(TAG, "Loaded region $id (${data.items.size} items)")
193+
// Convert Text
194+
proto.texts.forEach { tData ->
195+
val textItem = CanvasSerializer.fromTextItemData(tData)
196+
data.items.add(textItem)
197+
}
198+
199+
Logger.d("RegionStorage", "Loaded region $id (${data.items.size} items)")
188200
data
189201
} catch (e: Exception) {
190202
Logger.e(TAG, "Failed to load region $id (File: ${file.absolutePath}, Size: ${file.length()})", e)

0 commit comments

Comments
 (0)