diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..eddb7ad --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,46 @@ +apply plugin: 'com.android.application' + +apply plugin: 'kotlin-android' + +apply plugin: 'kotlin-android-extensions' + +android { + compileSdkVersion rootProject.ext.compileSdkVersion + buildToolsVersion rootProject.ext.buildToolsVersion + + defaultConfig { + applicationId "de.markusressel.kodeeditor" + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode rootProject.ext.versionCode + versionName rootProject.ext.versionName + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + + multiDexEnabled true + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation project(':library') + + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + + implementation "com.android.support:appcompat-v7:$supportLibVersion" + + testImplementation 'junit:junit:4.12' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' +} + +configurations.all { + resolutionStrategy.force "com.android.support:support-v4:$supportLibVersion" + resolutionStrategy.force "com.android.support:appcompat-v7:$supportLibVersion" + resolutionStrategy.force "com.android.support:design:$supportLibVersion" +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..3544dbc --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/de/markusressel/kodeeditor/MainActivity.kt b/app/src/main/java/de/markusressel/kodeeditor/MainActivity.kt new file mode 100644 index 0000000..4f03ace --- /dev/null +++ b/app/src/main/java/de/markusressel/kodeeditor/MainActivity.kt @@ -0,0 +1,18 @@ +package de.markusressel.kodeeditor + +import android.os.Bundle +import android.support.v7.app.AppCompatActivity +import kotlinx.android.synthetic.main.activity_main.* + +class MainActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super + .onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + codeEditorView + .setText("# Topic 1\n" + "\n" + "This is a *simple* demo text written in **Markdown**") + + } +} diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..c7bd21d --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..d5fccc5 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..a849a26 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,25 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..a2f5908 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..1b52399 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..ff10afd Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..115a4c7 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..dcd3cd8 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..459ca60 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..8ca12fe Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..8e19b41 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..b824ebd Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..4c19a13 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..3ab3e9c --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #3F51B5 + #303F9F + #FF4081 + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..a96ba16 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + KodeEditor + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..5885930 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..eb31ba9 --- /dev/null +++ b/build.gradle @@ -0,0 +1,54 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + ext.kotlin_version = '1.2.61' + + ext { + gradle_plugin_version = '3.1.3' + + minSdkVersion = 16 + versionName = "0.0.1" + versionCode = 1 + + compileSdkVersion = 27 + targetSdkVersion = 27 + buildToolsVersion = "27.0.3" + + // DEPENDENCIES + + androidKtxVersion = "0.3" + supportLibVersion = "27.1.1" + + javaxAnnotationVersion = "10.0-b28" + aboutlibrariesVersion = "6.0.9" + materialdrawerVersion = "6.0.8" + iconicsVersion = "3.0.4" + + rxKotlinVersion = "2.3.0" + rxBindingVersion = "2.1.1" + rxLifecycleVersion = "2.2.1" + + timberKtVersion = "1.5.1" + } + + + repositories { + google() + jcenter() + } + dependencies { + classpath "com.android.tools.build:gradle:$gradle_plugin_version" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..743d692 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,13 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..6e2838d --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sat Aug 25 17:52:39 CEST 2018 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip diff --git a/library/build.gradle b/library/build.gradle new file mode 100644 index 0000000..5572a35 --- /dev/null +++ b/library/build.gradle @@ -0,0 +1,62 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion rootProject.ext.compileSdkVersion + buildToolsVersion rootProject.ext.buildToolsVersion + + defaultConfig { + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode rootProject.ext.versionCode + versionName rootProject.ext.versionName + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +// api 'org.jetbrains.kotlinx:kotlinx-coroutines-android:0.25.0' + + implementation "com.android.support:appcompat-v7:$supportLibVersion" + + // RxJava (RxKotlin actually) + implementation "io.reactivex.rxjava2:rxjava:2.2.0" + implementation 'io.reactivex.rxjava2:rxandroid:2.0.2' + implementation "io.reactivex.rxjava2:rxkotlin:${rootProject.ext.rxKotlinVersion}" + + // RxBinding + // RxJava binding APIs for Android UI widgets from the platform and support libraries. + implementation "com.jakewharton.rxbinding2:rxbinding:${rootProject.ext.rxBindingVersion}" + implementation "com.jakewharton.rxbinding2:rxbinding-support-v4:${rootProject.ext.rxBindingVersion}" + implementation "com.jakewharton.rxbinding2:rxbinding-design:${rootProject.ext.rxBindingVersion}" + + // RxLifecycle to prevent memory leaks + def rxLifecycleVersion = rootProject.ext.rxLifecycleVersion + implementation "com.trello.rxlifecycle2:rxlifecycle:$rxLifecycleVersion" + implementation "com.trello.rxlifecycle2:rxlifecycle-kotlin:$rxLifecycleVersion" + implementation "com.trello.rxlifecycle2:rxlifecycle-android:$rxLifecycleVersion" + implementation "com.trello.rxlifecycle2:rxlifecycle-android-lifecycle-kotlin:$rxLifecycleVersion" + + // Zoom Layout Container + api 'com.otaliastudios:zoomlayout:1.3.0' + + testImplementation 'junit:junit:4.12' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' +} +repositories { + mavenCentral() +} +kotlin { + experimental { + coroutines "enable" + } +} diff --git a/library/proguard-rules.pro b/library/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/library/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml new file mode 100644 index 0000000..692fb76 --- /dev/null +++ b/library/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/library/src/main/java/de/markusressel/kodeeditor/library/syntaxhighlighter/SyntaxHighlighter.kt b/library/src/main/java/de/markusressel/kodeeditor/library/syntaxhighlighter/SyntaxHighlighter.kt new file mode 100644 index 0000000..7a1c19a --- /dev/null +++ b/library/src/main/java/de/markusressel/kodeeditor/library/syntaxhighlighter/SyntaxHighlighter.kt @@ -0,0 +1,95 @@ +package de.markusressel.kodeeditor.library.syntaxhighlighter + +import android.text.Editable +import android.text.Spannable +import android.text.style.CharacterStyle +import de.markusressel.kodeeditor.library.syntaxhighlighter.colorscheme.SyntaxColorScheme + +/** + * Interface for a SyntaxHighlighter + */ +interface SyntaxHighlighter { + + val appliedStyles: MutableSet + + /** + * The currently active color scheme + */ + var colorScheme: SyntaxColorScheme + + /** + * Get a set of rules for this highlighter + */ + fun getRules(): Set + + /** + * Get the default color scheme to use for this highlighter + */ + fun getDefaultColorScheme(): SyntaxColorScheme + + /** + * Highlight the given text + */ + fun highlight(editable: Editable) { + // cleanup previously applied styles + // clear(editable) + clearAppliedStyles(editable) + + // reapply + getRules() + .forEach { + val sectionType = it + .getSectionType() + it + .findMatches(editable) + .forEach { + val start = it + .range + .start + val end = it.range.endInclusive + 1 + + // needs to be called for each result + // so multiple spans are created and applied + val styles = colorScheme + .getStyles(sectionType) + + highlight(editable, start, end, styles) + } + } + } + + /** + * Apply a set of styles to a specific part of an editable + * + * @param editable the editable to highlight + * @param start the starting position + * @param end the end position (inclusive) + * @param styles the styles to apply + */ + private fun highlight(editable: Editable, start: Int, end: Int, styles: Set<() -> CharacterStyle>) { + styles + .forEach { + val style = it() + editable + .setSpan(style, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + + // remember which styles were applied + appliedStyles + .add(style) + } + } + + /** + * Clear any modifications the syntax highlighter may have made to a given editable + */ + fun clearAppliedStyles(editable: Editable) { + appliedStyles + .forEach { + editable + .removeSpan(it) + } + appliedStyles + .clear() + } + +} \ No newline at end of file diff --git a/library/src/main/java/de/markusressel/kodeeditor/library/syntaxhighlighter/SyntaxHighlighterBase.kt b/library/src/main/java/de/markusressel/kodeeditor/library/syntaxhighlighter/SyntaxHighlighterBase.kt new file mode 100644 index 0000000..9bf82cd --- /dev/null +++ b/library/src/main/java/de/markusressel/kodeeditor/library/syntaxhighlighter/SyntaxHighlighterBase.kt @@ -0,0 +1,12 @@ +package de.markusressel.kodeeditor.library.syntaxhighlighter + +import android.text.style.CharacterStyle +import de.markusressel.kodeeditor.library.syntaxhighlighter.colorscheme.SyntaxColorScheme + +abstract class SyntaxHighlighterBase : SyntaxHighlighter { + + override val appliedStyles: MutableSet = mutableSetOf() + + override var colorScheme: SyntaxColorScheme = getDefaultColorScheme() + +} \ No newline at end of file diff --git a/library/src/main/java/de/markusressel/kodeeditor/library/syntaxhighlighter/SyntaxHighlighterRule.kt b/library/src/main/java/de/markusressel/kodeeditor/library/syntaxhighlighter/SyntaxHighlighterRule.kt new file mode 100644 index 0000000..e149613 --- /dev/null +++ b/library/src/main/java/de/markusressel/kodeeditor/library/syntaxhighlighter/SyntaxHighlighterRule.kt @@ -0,0 +1,18 @@ +package de.markusressel.kodeeditor.library.syntaxhighlighter + +import android.text.Editable +import de.markusressel.kodeeditor.library.syntaxhighlighter.colorscheme.SectionTypeEnum + +interface SyntaxHighlighterRule { + + /** + * Get the type of section this rule is meant for + */ + fun getSectionType(): SectionTypeEnum + + /** + * Find segments in the editable that are affected by this rule + */ + fun findMatches(editable: Editable): Sequence + +} \ No newline at end of file diff --git a/library/src/main/java/de/markusressel/kodeeditor/library/syntaxhighlighter/colorscheme/SectionTypeEnum.kt b/library/src/main/java/de/markusressel/kodeeditor/library/syntaxhighlighter/colorscheme/SectionTypeEnum.kt new file mode 100644 index 0000000..bae7479 --- /dev/null +++ b/library/src/main/java/de/markusressel/kodeeditor/library/syntaxhighlighter/colorscheme/SectionTypeEnum.kt @@ -0,0 +1,8 @@ +package de.markusressel.kodeeditor.library.syntaxhighlighter.colorscheme + +/** + * Text section types that may occur and can be styled individually + */ +enum class SectionTypeEnum { + BoldText, Heading, ItalicText, Link, SourceCode, StrikedText +} \ No newline at end of file diff --git a/library/src/main/java/de/markusressel/kodeeditor/library/syntaxhighlighter/colorscheme/SyntaxColorScheme.kt b/library/src/main/java/de/markusressel/kodeeditor/library/syntaxhighlighter/colorscheme/SyntaxColorScheme.kt new file mode 100644 index 0000000..7e14cef --- /dev/null +++ b/library/src/main/java/de/markusressel/kodeeditor/library/syntaxhighlighter/colorscheme/SyntaxColorScheme.kt @@ -0,0 +1,15 @@ +package de.markusressel.kodeeditor.library.syntaxhighlighter.colorscheme + +import android.text.style.CharacterStyle + +/** + * A color scheme for a syntax highlighter + */ +interface SyntaxColorScheme { + + /** + * Get a set of styles to apply for a specific text/section type + */ + fun getStyles(type: SectionTypeEnum): Set<() -> CharacterStyle> + +} diff --git a/library/src/main/java/de/markusressel/kodeeditor/library/view/CodeEditText.kt b/library/src/main/java/de/markusressel/kodeeditor/library/view/CodeEditText.kt new file mode 100644 index 0000000..b51379a --- /dev/null +++ b/library/src/main/java/de/markusressel/kodeeditor/library/view/CodeEditText.kt @@ -0,0 +1,87 @@ +package de.markusressel.kodeeditor.library.view + +import android.content.Context +import android.support.v7.widget.AppCompatEditText +import android.util.AttributeSet +import android.util.Log +import com.jakewharton.rxbinding2.widget.RxTextView +import com.trello.rxlifecycle2.kotlin.bindToLifecycle +import de.markusressel.kodeeditor.library.syntaxhighlighter.SyntaxHighlighter +import de.markusressel.mkdocseditor.syntaxhighlighter.markdown.MarkdownSyntaxHighlighter +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.rxkotlin.subscribeBy +import io.reactivex.schedulers.Schedulers +import java.util.concurrent.TimeUnit + +class CodeEditText : AppCompatEditText { + + var syntaxHighlighter: SyntaxHighlighter = MarkdownSyntaxHighlighter() + private var highlightingTimeout = 50L to TimeUnit.MILLISECONDS + + private var highlightingDisposable: Disposable? = null + + constructor(context: Context) : super(context) { + reinit() + } + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { + reinit() + } + + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { + reinit() + } + + private fun reinit() { + highlightingDisposable + ?.dispose() + + highlightingDisposable = RxTextView + .afterTextChangeEvents(this) + .debounce(highlightingTimeout.first, highlightingTimeout.second) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .bindToLifecycle(this) + .subscribeBy(onNext = { + // syntax highlighting + refreshSyntaxHighlighting() + }, onError = { + Log + .e(TAG, "Error while refreshing syntax highlighting", it) + }) + } + + /** + * Set the timeout before new text is highlighted after the user has stopped typing + * + * @param timeout arbitrary value + * @param timeUnit the timeunit to use + */ + @Suppress("unused") + fun setHighlightingTimeout(timeout: Long, timeUnit: TimeUnit) { + highlightingTimeout = timeout to timeUnit + reinit() + } + + /** + * Get the current timeout in milliseconds + */ + @Suppress("unused") + fun getHighlightingTimeout(): Long { + return highlightingTimeout + .second + .toMillis(highlightingTimeout.first) + } + + @Synchronized + fun refreshSyntaxHighlighting() { + syntaxHighlighter + .highlight(text) + } + + companion object { + const val TAG = "CodeEditText" + } + +} \ No newline at end of file diff --git a/library/src/main/java/de/markusressel/kodeeditor/library/view/CodeEditorView.kt b/library/src/main/java/de/markusressel/kodeeditor/library/view/CodeEditorView.kt new file mode 100644 index 0000000..91cd697 --- /dev/null +++ b/library/src/main/java/de/markusressel/kodeeditor/library/view/CodeEditorView.kt @@ -0,0 +1,255 @@ +package de.markusressel.kodeeditor.library.view + +import android.content.Context +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.support.annotation.StringRes +import android.support.v4.view.ViewCompat +import android.util.AttributeSet +import android.util.Log +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.widget.LinearLayout +import android.widget.TextView +import com.jakewharton.rxbinding2.widget.RxTextView +import com.otaliastudios.zoom.ZoomLayout +import com.trello.rxlifecycle2.kotlin.bindToLifecycle +import de.markusressel.kodeeditor.library.syntaxhighlighter.SyntaxHighlighter +import de.markusressel.library.R +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.rxkotlin.subscribeBy +import io.reactivex.schedulers.Schedulers +import java.util.concurrent.TimeUnit +import kotlin.math.roundToInt + + +class CodeEditorView : ZoomLayout { + + private lateinit var contentLayout: LinearLayout + lateinit var lineNumberView: TextView + lateinit var editTextView: CodeEditText + + var moveWithCursorEnabled = false + + private var currentLineCount = -1 + + constructor(context: Context) : super(context) { + initialize(null) + } + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { + initialize(attrs) + } + + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { + initialize(attrs) + } + + private fun initialize(attrs: AttributeSet?) { + readParameters(attrs) + + inflateViews(LayoutInflater.from(context)) + addView(contentLayout) + + setListeners() + + editTextView + .setViewBackgroundWithoutResettingPadding(null) + + editTextView + .post { + editTextView + .setSelection(0) + } + } + + private fun View.setViewBackgroundWithoutResettingPadding(background: Drawable?) { + val paddingBottom = this + .paddingBottom + val paddingStart = ViewCompat + .getPaddingStart(this) + val paddingEnd = ViewCompat + .getPaddingEnd(this) + val paddingTop = this + .paddingTop + ViewCompat + .setBackground(this, background) + ViewCompat + .setPaddingRelative(this, paddingStart, paddingTop, paddingEnd, paddingBottom) + } + + + private fun readParameters(attrs: AttributeSet?) { + + } + + private fun inflateViews(inflater: LayoutInflater) { + contentLayout = inflater.inflate(R.layout.view_code_editor__inner_layout, null) as LinearLayout + lineNumberView = contentLayout.findViewById(R.id.codeLinesView) as TextView + editTextView = contentLayout.findViewById(R.id.codeEditText) as CodeEditText + + post { + val displayMetrics = context + .resources + .displayMetrics + + contentLayout + .minimumHeight = displayMetrics + .heightPixels + contentLayout + .minimumWidth = displayMetrics + .widthPixels + } + } + + private fun setListeners() { + setOnTouchListener { view, motionEvent -> + when (motionEvent.action) { + MotionEvent.ACTION_MOVE -> { + moveWithCursorEnabled = false + } + } + false + } + + editTextView + .setOnClickListener { + moveWithCursorEnabled = true + } + + Observable + .interval(250, TimeUnit.MILLISECONDS) + .filter { moveWithCursorEnabled } + .bindToLifecycle(this) + .subscribeBy(onNext = { + moveScreenWithCursorIfNecessary() + }, onError = { + Log + .e(TAG, "Error moving screen with cursor", it) + }) + + RxTextView + .textChanges(editTextView) + .debounce(50, TimeUnit.MILLISECONDS) + .filter { + moveWithCursorEnabled = true + editTextView.lineCount != currentLineCount + } + .subscribeOn(Schedulers.computation()) + .observeOn(AndroidSchedulers.mainThread()) + .bindToLifecycle(this) + .subscribeBy(onNext = { + updateLineNumbers(editTextView.lineCount) + }, onError = { + Log + .e(TAG, "Error updating line numbers", it) + }) + } + + private fun moveScreenWithCursorIfNecessary() { + val pos = editTextView + .selectionStart + val layout = editTextView + .layout + + if (layout != null) { + val line = layout + .getLineForOffset(pos) + val baseline = layout + .getLineBaseline(line) + val ascent = layout + .getLineAscent(line) + val x = layout + .getPrimaryHorizontal(pos) + val y = (baseline + ascent) + .toFloat() + + val zoomLayoutRect = Rect() + getLocalVisibleRect(zoomLayoutRect) + + val transformedX = x * realZoom + panX * realZoom + lineNumberView.width * realZoom + val transformedY = y * realZoom + panY * realZoom + + if (!zoomLayoutRect.contains(transformedX.roundToInt(), transformedY.roundToInt())) { + + var newX = panX + var newY = panY + + if (transformedX < zoomLayoutRect.left || transformedX > zoomLayoutRect.right) { + newX = -x + } + + if (transformedY < zoomLayoutRect.top || transformedY > zoomLayoutRect.bottom) { + newY = -y + } + + moveTo(zoom, newX, newY, false) + } + } + } + + private fun updateLineNumbers(lines: Int) { + currentLineCount = lines + + val linesToDraw = if (lines < MIN_LINES) { + MIN_LINES + } else { + lines + } + + val sb = StringBuilder() + for (i in 1..linesToDraw) { + sb + .append("$i:\n") + } + lineNumberView + .text = sb + .toString() + } + + /** + * @param editable true = user can type, false otherwise + */ + fun setEditable(editable: Boolean) { + editTextView + .isEnabled = editable + } + + /** + * Set the text in the editor + */ + fun setText(text: CharSequence) { + editTextView + .setText(text, TextView.BufferType.EDITABLE) + editTextView + .refreshSyntaxHighlighting() + } + + /** + * Set the text in the editor + */ + @Suppress("unused") + fun setText(@StringRes text: Int) { + editTextView + .setText(text, TextView.BufferType.EDITABLE) + editTextView + .refreshSyntaxHighlighting() + } + + /** + * Set the syntax highlighter to use for this CodeEditor + */ + @Suppress("unused") + fun setSyntaxHighlighter(syntaxHighlighter: SyntaxHighlighter) { + editTextView + .syntaxHighlighter = syntaxHighlighter + } + + companion object { + const val TAG = "CodeEditorView" + const val MIN_LINES = 1 + } + +} diff --git a/library/src/main/res/layout/view_code_editor__inner_layout.xml b/library/src/main/res/layout/view_code_editor__inner_layout.xml new file mode 100644 index 0000000..53d841c --- /dev/null +++ b/library/src/main/res/layout/view_code_editor__inner_layout.xml @@ -0,0 +1,31 @@ + + + + + + + + diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..3306997 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':app', ':library'