Skip to content

Commit

Permalink
Fix header style on adding/removing text
Browse files Browse the repository at this point in the history
  • Loading branch information
cp-radhika-s committed Oct 18, 2023
1 parent b1301d4 commit 04ad789
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 70 deletions.
14 changes: 7 additions & 7 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ plugins {

android {
namespace 'com.example.texteditor'
compileSdk 33
compileSdk 34

defaultConfig {
applicationId "com.example.texteditor"
minSdk 24
targetSdk 33
targetSdk 34
versionCode 1
versionName "1.0"

Expand Down Expand Up @@ -48,11 +48,11 @@ android {

dependencies {

implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.core:core-ktx:1.12.0'
implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.0')
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
implementation 'androidx.activity:activity-compose:1.7.2'
implementation platform('androidx.compose:compose-bom:2022.10.00')
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2'
implementation 'androidx.activity:activity-compose:1.8.0'
implementation platform('androidx.compose:compose-bom:2023.10.00')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-graphics'
implementation 'androidx.compose.ui:ui-tooling-preview'
Expand All @@ -61,7 +61,7 @@ dependencies {
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation platform('androidx.compose:compose-bom:2022.10.00')
androidTestImplementation platform('androidx.compose:compose-bom:2023.10.00')
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
debugImplementation 'androidx.compose.ui:ui-tooling'
debugImplementation 'androidx.compose.ui:ui-test-manifest'
Expand Down
57 changes: 19 additions & 38 deletions app/src/main/java/com/example/texteditor/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.example.texteditor

import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
Expand All @@ -21,6 +22,7 @@ import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
Expand All @@ -40,6 +42,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.PopupProperties
import com.canopas.editor.ui.data.RichEditorState
import com.canopas.editor.ui.ui.RichEditor
import com.canopas.editor.ui.ui.rememberEditorState
Expand Down Expand Up @@ -135,43 +138,20 @@ fun StyleContainer(
value = state,
)

// IconButton(
// modifier = Modifier
// .padding(2.dp)
// .size(48.dp),
// onClick = {
// Log.d("XXX", "Json ${state.toJson()} ")
//
// },
// ) {
// Icon(
// painter = painterResource(id = R.drawable.ic_save), contentDescription = null,
// modifier = Modifier.size(24.dp)
// )
// }

// IconButton(
// modifier = Modifier
// .padding(2.dp)
// .size(48.dp),
// onClick = {
// var json = state.toJson()
// // state.setJson("")
// // scope.launch {
// // delay(5000)
// // withContext(Dispatchers.Main) {
// Log.d("XXX", "Json ${json} ")
// // state.setJson(json)
// // }
// // }
//
// },
// ) {
// Icon(
// Icons.Default.Add, contentDescription = null,
// modifier = Modifier.size(24.dp)
// )
// }
IconButton(
modifier = Modifier
.padding(2.dp)
.size(48.dp),
onClick = {
// Log.d("XXX", "Json ${state.toJson()} "
state.reset()
},
) {
Icon(
Icons.Default.Refresh, contentDescription = null,
modifier = Modifier.size(24.dp)
)
}

}
}
Expand Down Expand Up @@ -206,7 +186,8 @@ fun TitleStyleButton(
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
modifier = Modifier.wrapContentSize()
modifier = Modifier.wrapContentSize(),
properties = PopupProperties(false)
) {

DropDownItem(text = "Text",
Expand Down
16 changes: 8 additions & 8 deletions editor/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ plugins {

android {
namespace 'com.canopas.editor'
compileSdk 33
compileSdk 34

defaultConfig {
minSdk 24
targetSdk 33
targetSdk 34

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
Expand Down Expand Up @@ -46,20 +46,20 @@ android {

dependencies {

implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
implementation 'androidx.activity:activity-compose:1.7.2'
implementation platform('androidx.compose:compose-bom:2023.05.01')
implementation 'com.google.android.material:material:1.10.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2'
implementation 'androidx.activity:activity-compose:1.8.0'
implementation platform('androidx.compose:compose-bom:2023.10.00')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-graphics'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.compose.material3:material3'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation platform('androidx.compose:compose-bom:2022.10.00')
androidTestImplementation platform('androidx.compose:compose-bom:2023.10.00')
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
debugImplementation 'androidx.compose.ui:ui-tooling'
debugImplementation 'androidx.compose.ui:ui-test-manifest'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,4 +212,9 @@ class RichEditorState internal constructor(
return JsonEditorParser.decode(this)
}

fun reset() {
setContent(emptyList())
focusedAttributeIndexState = 0
}

}
117 changes: 100 additions & 17 deletions editor/src/main/java/com/canopas/editor/ui/data/RichTextState.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.canopas.editor.ui.data

import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
Expand All @@ -12,6 +13,10 @@ import androidx.compose.ui.text.input.OffsetMapping
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.TransformedText
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.substring
import com.canopas.editor.ui.parser.json.toSpansString
import com.canopas.editor.ui.utils.isDefault
import com.canopas.editor.ui.utils.isHeaderStyle
import kotlin.math.max
import kotlin.math.min

Expand Down Expand Up @@ -122,6 +127,11 @@ class RichTextState internal constructor(
currentStyles.clear()
currentStyles.add(style)

if ((style.isHeaderStyle() || style.isDefault())) {
handleAddHeaderStyle(style)
updateTextFieldValue()
return
}
if (!selection.collapsed) {
applyStylesToSelectedText(style)
}
Expand All @@ -132,6 +142,10 @@ class RichTextState internal constructor(
currentStyles.add(style)
}

if ((style.isHeaderStyle() || style.isDefault()) && selection.collapsed) {
handleAddHeaderStyle(style)
updateTextFieldValue()
}
if (!selection.collapsed) {
applyStylesToSelectedText(style)
}
Expand Down Expand Up @@ -265,15 +279,43 @@ class RichTextState internal constructor(
currentStyles.remove(style)
}

if (!selection.collapsed)
if (!selection.collapsed) {
removeStylesFromSelectedText(style)
updateTextFieldValue()
}
}

private fun removeStylesFromSelectedText(style: SpanStyle) {
if (textFieldValue.selection.collapsed) {
return
private fun removeStylesFromSelectedText(fromIndex: Int, toIndex: Int) {
val selectedParts = spans.filter { part ->
part.from < toIndex && part.to >= fromIndex
}

selectedParts.forEach { part ->
val index = spans.indexOf(part)
if (index !in spans.indices) return@forEach

if (part.from < fromIndex && part.to >= toIndex) {
spans[index] = part.copy(to = fromIndex - 1)
spans.add(
index + 1,
part.copy(
from = toIndex,
)
)
} else if (part.from < fromIndex) {
spans[index] = part.copy(
to = fromIndex - 1
)
} else {
spans.removeAt(index)
}
}

}


private fun removeStylesFromSelectedText(style: SpanStyle) {

val fromIndex = textFieldValue.selection.min
val toIndex = textFieldValue.selection.max

Expand Down Expand Up @@ -303,7 +345,7 @@ class RichTextState internal constructor(
spans.removeAt(index)
}
}
updateTextFieldValue()

}

fun onTextFieldValueChange(newTextFieldValue: TextFieldValue) {
Expand All @@ -329,13 +371,41 @@ class RichTextState internal constructor(
}
}

private fun handleAddHeaderStyle(style: SpanStyle, textField: TextFieldValue = textFieldValue) {
val text = textField.text

val fromIndex = textField.selection.min
val toIndex =
if (textField.selection.collapsed) fromIndex else textField.selection.max


val startIndex: Int = max(0, text.lastIndexOf("\n", fromIndex - 1))
var endIndex: Int = text.indexOf("\n", toIndex)

if (endIndex == -1) endIndex = text.length - 1

removeStylesFromSelectedText(startIndex, endIndex)

spans.add(
RichTextSpan(
from = startIndex,
to = endIndex,
style = style
)
)
}

private fun handleAddingCharacters(newValue: TextFieldValue) {
val typedChars = newValue.text.length - textFieldValue.text.length
val startTypeIndex = newValue.selection.min - typedChars

if (newValue.text.getOrNull(startTypeIndex) == '\n') {
removeTitleStylesIfAny()
}
val selectedStyles = currentStyles.toMutableList()

moveSpans(startTypeIndex, typedChars)

val selectedStyles = currentStyles.toMutableList()
val startParts = spans.filter { startTypeIndex - 1 in it.from..it.to }
val endParts = spans.filter { startTypeIndex in it.from..it.to }
val commonParts = startParts.intersect(endParts.toSet())
Expand Down Expand Up @@ -365,6 +435,12 @@ class RichTextState internal constructor(
}
}

private fun removeTitleStylesIfAny() {
if (currentStyles.any { it.isHeaderStyle() }) {
currentStyles.clear()
}
}

private fun processSpan(
richTextSpan: RichTextSpan,
typedChars: Int,
Expand Down Expand Up @@ -429,11 +505,23 @@ class RichTextState internal constructor(
private fun handleRemovingCharacters(
newTextFieldValue: TextFieldValue
) {
val removedChars = textFieldValue.text.length - newTextFieldValue.text.length
val startRemoveIndex = newTextFieldValue.selection.min + removedChars
val removedCharsCount = textFieldValue.text.length - newTextFieldValue.text.length
val startRemoveIndex = newTextFieldValue.selection.min + removedCharsCount
val endRemoveIndex = newTextFieldValue.selection.min
val removeRange = endRemoveIndex until startRemoveIndex

val newLineIndex = textFieldValue.text.substring(
endRemoveIndex,
startRemoveIndex
).indexOf("\n")

if (currentStyles.any { it.isHeaderStyle() } && newLineIndex != -1) {
val style = currentStyles.first { it.isHeaderStyle() }
handleAddHeaderStyle(style, newTextFieldValue)
updateTextFieldValue(newTextFieldValue)
return
}

val iterator = spans.iterator()

val partsCopy = spans.toMutableList()
Expand All @@ -444,19 +532,19 @@ class RichTextState internal constructor(

if (removeRange.last < part.from) {
partsCopy[index] = part.copy(
from = part.from - removedChars,
to = part.to - removedChars
from = part.from - removedCharsCount,
to = part.to - removedCharsCount
)
} else if (removeRange.first <= part.from && removeRange.last >= part.to) {
// Remove the element from the copy.
partsCopy.removeAt(index)
} else if (removeRange.first <= part.from) {
partsCopy[index] = part.copy(
from = max(0, removeRange.first),
to = min(newTextFieldValue.text.length, part.to - removedChars)
to = min(newTextFieldValue.text.length, part.to - removedCharsCount)
)
} else if (removeRange.last <= part.to) {
partsCopy[index] = part.copy(to = part.to - removedChars)
partsCopy[index] = part.copy(to = part.to - removedCharsCount)
} else if (removeRange.first < part.to) {
partsCopy[index] = part.copy(to = removeRange.first)
}
Expand All @@ -480,11 +568,6 @@ class RichTextState internal constructor(
part.from < toIndex && part.to >= fromIndex
}

// if(selectedParts.isEmpty()){
// currentStyles.first {
//
// }
// }
selectedParts.forEach { part ->
val index = spans.indexOf(part)
if (index !in spans.indices) return@forEach
Expand Down
Loading

0 comments on commit 04ad789

Please sign in to comment.