From cd071339c4f9e0fd686aa52a4913fd2bc58c977c Mon Sep 17 00:00:00 2001 From: bluewhaleyt Date: Mon, 19 Feb 2024 21:46:42 +0800 Subject: [PATCH 1/3] add docs for code editor in jetpack compose --- docs/.vitepress/config/en.ts | 7 ++ docs/guide/code-editor-in-compose.md | 150 +++++++++++++++++++++++++++ package-lock.json | 4 +- 3 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 docs/guide/code-editor-in-compose.md diff --git a/docs/.vitepress/config/en.ts b/docs/.vitepress/config/en.ts index 2a59bef..59893ff 100644 --- a/docs/.vitepress/config/en.ts +++ b/docs/.vitepress/config/en.ts @@ -88,6 +88,13 @@ function guideReference(): DefaultTheme.SidebarItem[] { } ] }, + { + text: 'Jetpack Compose', + collapsed: false, + items: [ + { text: 'CodeEditor in Compose', link: 'code-editor-in-compose'} + ] + }, { text: 'API Reference', link: '../reference/xml-attributes' diff --git a/docs/guide/code-editor-in-compose.md b/docs/guide/code-editor-in-compose.md new file mode 100644 index 0000000..b69f31a --- /dev/null +++ b/docs/guide/code-editor-in-compose.md @@ -0,0 +1,150 @@ +--- +outline: deep +--- + +# CodeEditor in Compose + +Jetpack Compose is a new framework for Android development. If you are attempting to use Sora Editor, while working on apps built with Jetpack Compose. This documentation might help you. + +> The guide and code, and my english perhaps are not good, please correct them if there's any. Thank you. + +## Create a State holder + +First, we will define a `CodeEditorState` which wraps the states of the `CodeEditor`. + +```kotlin +data class CodeEditorState( + val editor: CodeEditor? = null, + val initialContent: Content = Content() +) { + var content by mutableStateOf(content) +} +``` + +You can add many states as you want. + +::: tip NOTE +If you are not using `ViewModel` and want to make a `remember*()` composable function, you can do following: +```kotlin +@Composable +fun rememberCodeEditorState( + initialContent: Content = Content() +) = remember { + CodeEditorState( + initialContent = initialContent + ) +} +``` +::: + +## Create `CodeEditor` composable + +Now, we will create `CodeEditor` composable, which will be composed with `AndroidView`. In this composable, it will accept a `state` parameter which is associated with `CodeEditorState`. + +```kotlin +@Composable +fun CodeEditor( + modifier: Modifier = Modifier, + state: CodeEditorState +) { + // ... +} +``` + +### Set the factory for `CodeEditor` + +We will need a `Context` to define a `CodeEditor`. + +```kotlin +private fun setCodeEditorFactory( + context: Context, + state: CodeEditorState +): CodeEditor { + val editor = CodeEditor(context) + editor.apply { + setText(state.content) + // ... + } + state.editor = editor + return editor +} +``` + +Once we finished creating the factory, we now can define it with `remember` composable function. + +```kotlin +@Composable +fun CodeEditor( + modifier: Modifier = Modifier, + state: CodeEditorState +) { + val context = LocalContext.current + val editor = remember { + setCodeEditorFactory( + context = context, + state = state + ) + } + AndroidView( + factory = { editor }, + modifier = modifier, + onRelease = { + it.release() + } + ) + // ... +} +``` + +### Set `LaunchedEffect` for `CodeEditor`'s states + +We need to use `LaunchedEffect` to trigger when there are states of `CodeEditor` changed. + +```kotlin +LaunchedEffect(key1 = state.content) { + state.editor?.setText(state.content) +} +``` + +## Using the `CodeEditor` composable + +After we finished implementing the `CodeEditor` composable, we can use it in our apps now. First of all, **it is highly recommend** to create a `CodeEditorState` in the `ViewModel`. + +For example, in the `MainScreen`, we will create `MainViewModel`, in this viewmodel, we will define the `CodeEditorState` here. + +```kotlin +class MainViewModel : ViewModel() { + val editorState by mutableStateOf( + CodeEditorState() + ) +} +``` + +Then, for the `MainScreen` composable, make sure you need to adjust the `Modifier` of `CodeEditor` as it is neccessary. + +```kotlin +@Composable +fun MainScreen( + viewModel: MainViewModel = viewModel() +) { + Column { + CodeEditor( + modifier = Modifier + .fillMaxSize(), + state = viewModel.editorState + ) + } +} +``` + +::: warning WARNING +If there are composables on the bottom of the `CodeEditor`, please set `Modifier.weight(1f)` instead of `Modifier.fillMaxSize()`. Otherwise, the `CodeEditor` will just dominate entire screen. +::: + +## End + +`CodeEditor` is the only widget that we need to use `AndroidView`. As for the other widgets like `SymbolInputView`, they can be fully implemented with composables. + +--- + +That's all. I don't know if this is a good practice to attempt Sora Editor's `CodeEditor` in Jetpack Compose, but I hope you will be inspired through this guide. Thanks for reading. \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e151099..b84ef16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sora-editor-docs", - "version": "0.23.3", + "version": "0.23.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sora-editor-docs", - "version": "0.23.3", + "version": "0.23.4", "devDependencies": { "markdown-it-mathjax3": "^4.3.2", "open-cli": "^8.0.0", From cf3a7bcc2f5bde4ce92e19fed0c8c1658979199c Mon Sep 17 00:00:00 2001 From: bluewhaleyt Date: Mon, 19 Feb 2024 22:13:01 +0800 Subject: [PATCH 2/3] fix typo --- docs/guide/code-editor-in-compose.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/code-editor-in-compose.md b/docs/guide/code-editor-in-compose.md index b69f31a..0a6afef 100644 --- a/docs/guide/code-editor-in-compose.md +++ b/docs/guide/code-editor-in-compose.md @@ -17,7 +17,7 @@ data class CodeEditorState( val editor: CodeEditor? = null, val initialContent: Content = Content() ) { - var content by mutableStateOf(content) + var content by mutableStateOf(initialContent) } ``` From 8feb5017c4435c9d5f013a468b78da562783c2f7 Mon Sep 17 00:00:00 2001 From: bluewhaleyt Date: Sat, 1 Jun 2024 19:50:27 +0800 Subject: [PATCH 3/3] add docs for using composeview in popupwindow components --- docs/.vitepress/config/en.ts | 3 +- .../guide/using-composeview-in-popupwindow.md | 144 ++++++++++++++++++ 2 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 docs/guide/using-composeview-in-popupwindow.md diff --git a/docs/.vitepress/config/en.ts b/docs/.vitepress/config/en.ts index 59893ff..df000f2 100644 --- a/docs/.vitepress/config/en.ts +++ b/docs/.vitepress/config/en.ts @@ -92,7 +92,8 @@ function guideReference(): DefaultTheme.SidebarItem[] { text: 'Jetpack Compose', collapsed: false, items: [ - { text: 'CodeEditor in Compose', link: 'code-editor-in-compose'} + { text: 'CodeEditor in Compose', link: 'code-editor-in-compose'}, + { text: 'Using ComposeView in PopupWindow', link: 'using-composeview-in-popupwindow'}, ] }, { diff --git a/docs/guide/using-composeview-in-popupwindow.md b/docs/guide/using-composeview-in-popupwindow.md new file mode 100644 index 0000000..3252cdf --- /dev/null +++ b/docs/guide/using-composeview-in-popupwindow.md @@ -0,0 +1,144 @@ +--- +outline: deep +--- + +# Using ComposeView in PopupWindow + +`CodeEditor` supports a number of components namely `EditorAutoCompletion`, `EditorTextActionWindow` etc, when you want to customize the layout of them, you will have two approaches: + +1. Using legacy XML to define the layouts +2. Using Compose to define the layout with `ComposeView` + +In this documentation, we will dive into the approach of using Compose to define the layout for `EditorTextActionWindow`. + +## Challenges of attempting `ComposeView` in `PopupWindow` + +As the `EditorTextActionWindow` internally uses `PopupWindow`, and provides +`setContentView()` to apply the view to the component. + +::: danger ERROR +However, if you directly put the Compose content in the `PopupWindow`, an error will throw. +```kotlin +java.lang.IllegalStateException: ViewTreeLifecycleOwner not found from android.widget.PopupWindow$PopupDecorView{9dfea2f V.E...... R.....I. 0,0-0,0} + at androidx.compose.ui.platform.WindowRecomposer_androidKt.createLifecycleAwareViewTreeRecomposer(WindowRecomposer.android.kt:242) + at androidx.compose.ui.platform.WindowRecomposer_androidKt.access$createLifecycleAwareViewTreeRecomposer(WindowRecomposer.android.kt:1) + ... +``` +::: + +By default, the `PopupWindow` cannot be worked with Compose. To solve this, we need a `FrameLayout` to be the parent layout of the `PopupWindow`, we then use this `FrameLayout` to contain the Compose content, and apply the `ViewTreeLifecycleOwner` and `ViewTreeSavedStateRegistryOwner` to the `FrameLayout`. + +::: tip TIP +We can directly retrieve the `ViewTreeLifecycleOwner` and `ViewTreeSavedStateRegistryOwner` via the `CompositionLocal`. + +```kotlin +val viewTreeLifecycleOwner = LocalViewTreeLifecycleOwner.current +val viewTreeSavedStateRegistryOwner = LocalViewTreeSavedStateRegistry.current +``` +::: + +## Define a `FrameLayout` + +We will use `android.R.id.content` for the content child of the `View` as it is neccessary to let Compose find the content child. + +```kotlin +val composeView = ComposeView(context).apply { + setContent { + // the Compose content... + } +} +val parentView = FrameLayout(context).apply { + id = android.R.id.content + setViewTreeLifecycleOwner(viewTreeLifecycleOwner) + setViewTreeSavedStateRegistryOwner(viewTreeSavedStateRegistryOwner) + layoutParams = FrameLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + addView(composeView) +} +``` + +## Complete the `EditorTextActionWindow` layout + +Here is the example of customizing the layout for `EditorTextActionWindow` in Compose. + +```kotlin +data class EditorTextActionItem( + val label: String, + val icon: ImageVector +) +``` + +```kotlin +val actionItems = listOf( + EditorTextActionItem( + label = "Select all", + icon = /* ... */ + ), + EditorTextActionItem( + label = "Copy", + icon = /* ... */ + ) + EditorTextActionItem( + label = "Paste", + icon = /* ... */ + ) + // ... +) +``` + +```kotlin +@Composable +fun EditorTextActionWindow( + modifier: Modifier = Modifier, + items: List, + onItemClick: (EditorTextActionItem) -> Unit +): FrameLayout { + val context = LocalContext.current + val viewTreeLifecycleOwner = LocalViewTreeLifecycleOwner.current + val viewTreeSavedStateRegistryOwner = LocalViewTreeSavedStateRegistry.current + val composeView = ComposeView(context).apply { + setContent { + EditorTextActionContent(modifier, items, onItemClick) + } + } + val parentView = FrameLayout(context).apply { + id = android.R.id.content + setViewTreeLifecycleOwner(viewTreeLifecycleOwner) + setViewTreeSavedStateRegistryOwner(viewTreeSavedStateRegistryOwner) + layoutParams = FrameLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + addView(composeView) + } + return parentView +} + +@Composable +private fun EditorTextActionContent( + modifier: Modifier = Modifier, + items: List, + onItemClick: (EditorTextActionItem) -> Unit +) { + Row(modifier) { + items.forEach { item -> + IconButton( + onClick = { onItemClick(item) } + ) { + Icon( + imageVector = item.icon, + contentDescription = item.label + ) + } + } + } +} +``` + +Finally, apply the layout into `EditorTextActionWindow`. + +```kotlin +editor.getComponent().setContentView(parentView) +``` \ No newline at end of file