From 096a402b7ea09df7d10baa5bedf4df96747b2833 Mon Sep 17 00:00:00 2001 From: Janne Koschinski <janne@kuschku.de> Date: Sun, 18 Feb 2018 22:35:43 +0100 Subject: [PATCH] Port mIRC Color handling from -NG --- .../util/helper/ThemeHelper.kt | 7 +- .../util/irc/format/IrcFormatDeserializer.kt | 331 ++++++++++++++++++ .../util/irc/format/IrcFormatSerializer.kt | 144 ++++++++ .../util/irc/format/spans/Copyable.kt | 5 + .../format/spans/IrcBackgroundColorSpan.kt | 11 + .../util/irc/format/spans/IrcBoldSpan.kt | 8 + .../format/spans/IrcForegroundColorSpan.kt | 11 + .../util/irc/format/spans/IrcItalicSpan.kt | 8 + .../util/irc/format/spans/IrcUnderlineSpan.kt | 7 + 9 files changed, 528 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/IrcFormatDeserializer.kt create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/IrcFormatSerializer.kt create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/Copyable.kt create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcBackgroundColorSpan.kt create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcBoldSpan.kt create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcForegroundColorSpan.kt create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcItalicSpan.kt create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcUnderlineSpan.kt diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/ThemeHelper.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/ThemeHelper.kt index 452086db1..137e10e6d 100644 --- a/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/ThemeHelper.kt +++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/ThemeHelper.kt @@ -3,10 +3,9 @@ package de.kuschku.quasseldroid_ng.util.helper import android.content.res.Resources import android.content.res.TypedArray -inline fun Resources.Theme.styledAttributes(vararg attributes: Int, f: TypedArray.() -> Unit) { - this.obtainStyledAttributes(attributes).use { - it.apply(f) - } +inline fun <R> Resources.Theme.styledAttributes(vararg attributes: Int, f: TypedArray.() -> R) + = this.obtainStyledAttributes(attributes).run { + f() } inline fun <R> TypedArray.use(block: (TypedArray) -> R): R { diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/IrcFormatDeserializer.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/IrcFormatDeserializer.kt new file mode 100644 index 000000000..4a7f9048b --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/IrcFormatDeserializer.kt @@ -0,0 +1,331 @@ +/* + * QuasselDroid - Quassel client for Android + * Copyright (C) 2016 Janne Koschinski + * Copyright (C) 2016 Ken Børge Viktil + * Copyright (C) 2016 Magnus Fjell + * Copyright (C) 2016 Martin Sandsmark <martin.sandsmark@kde.org> + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.kuschku.quasseldroid_ng.util.irc.format + + +import android.content.Context +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.style.UnderlineSpan +import de.kuschku.quasseldroid_ng.R +import de.kuschku.quasseldroid_ng.util.helper.styledAttributes +import de.kuschku.quasseldroid_ng.util.irc.format.spans.IrcBackgroundColorSpan +import de.kuschku.quasseldroid_ng.util.irc.format.spans.IrcBoldSpan +import de.kuschku.quasseldroid_ng.util.irc.format.spans.IrcForegroundColorSpan +import de.kuschku.quasseldroid_ng.util.irc.format.spans.IrcItalicSpan +import java.util.* + +/** + * A helper class to turn mIRC formatted Strings into Android’s SpannableStrings with the same + * color and format codes + */ +class IrcFormatDeserializer(private val context: Context) { + + /** + * Function to handle mIRC formatted strings + * + * @param str mIRC formatted String + * @return a CharSequence with Android’s span format representing the input string + */ + fun formatString(str: String?, colorize: Boolean): CharSequence { + if (str == null) return "" + + val plainText = SpannableStringBuilder() + var bold: FormatDescription? = null + var italic: FormatDescription? = null + var underline: FormatDescription? = null + var color: FormatDescription? = null + + // Iterating over every character + var normalCount = 0 + var i = 0 + while (i < str.length) { + val character = str[i] + when (character) { + CODE_BOLD -> { + plainText.append(str.substring(i - normalCount, i)) + normalCount = 0 + + // If there is an element on stack with the same code, close it + if (bold != null) { + if (colorize) bold.apply(plainText, plainText.length) + bold = null + // Otherwise create a new one + } else { + val format = fromId(character) + bold = FormatDescription(plainText.length, format!!) + } + } + CODE_ITALIC -> { + plainText.append(str.substring(i - normalCount, i)) + normalCount = 0 + + // If there is an element on stack with the same code, close it + if (italic != null) { + if (colorize) italic.apply(plainText, plainText.length) + italic = null + // Otherwise create a new one + } else { + val format = fromId(character) + italic = FormatDescription(plainText.length, format!!) + } + } + CODE_UNDERLINE -> { + plainText.append(str.substring(i - normalCount, i)) + normalCount = 0 + + // If there is an element on stack with the same code, close it + if (underline != null) { + if (colorize) underline.apply(plainText, plainText.length) + underline = null + // Otherwise create a new one + } else { + val format = fromId(character) + underline = FormatDescription(plainText.length, format!!) + } + } + CODE_COLOR -> { + plainText.append(str.substring(i - normalCount, i)) + normalCount = 0 + + val foregroundStart = i + 1 + val foregroundEnd = findEndOfNumber(str, foregroundStart) + // If we have a foreground element + if (foregroundEnd > foregroundStart) { + val foreground = readNumber(str, foregroundStart, foregroundEnd) + + var background: Byte = -1 + var backgroundEnd = -1 + // If we have a background code, read it + if (str.length > foregroundEnd && str[foregroundEnd] == ',') { + backgroundEnd = findEndOfNumber(str, foregroundEnd + 1) + background = readNumber(str, foregroundEnd + 1, backgroundEnd) + } + // If previous element was also a color element, try to reuse background + if (color != null) { + // Apply old format + if (colorize) color.apply(plainText, plainText.length) + // Reuse old background, if possible + if (background.toInt() == -1) + background = (color.format as ColorIrcFormat).background + } + // Add new format + color = FormatDescription(plainText.length, ColorIrcFormat(foreground, background)) + + // i points in front of the next character + i = (if (backgroundEnd == -1) foregroundEnd else backgroundEnd) - 1 + + // Otherwise assume this is a closing tag + } else if (color != null) { + if (colorize) color.apply(plainText, plainText.length) + color = null + } + } + CODE_SWAP -> { + plainText.append(str.substring(i - normalCount, i)) + normalCount = 0 + + // If we have a color tag before, apply it, and create a new one with swapped colors + if (color != null) { + if (colorize) color.apply(plainText, plainText.length) + color = FormatDescription( + plainText.length, (color.format as ColorIrcFormat).copySwapped() + ) + } + } + CODE_RESET -> { + plainText.append(str.substring(i - normalCount, i)) + normalCount = 0 + + // End all formatting tags + if (bold != null) { + if (colorize) bold.apply(plainText, plainText.length) + bold = null + } + if (italic != null) { + if (colorize) italic.apply(plainText, plainText.length) + italic = null + } + if (underline != null) { + if (colorize) underline.apply(plainText, plainText.length) + underline = null + } + if (color != null) { + if (colorize) color.apply(plainText, plainText.length) + color = null + } + } + else -> { + // Just append it, if it’s not special + normalCount++ + } + } + i++ + } + + // End all formatting tags + if (bold != null) { + if (colorize) bold.apply(plainText, plainText.length) + } + if (italic != null) { + if (colorize) italic.apply(plainText, plainText.length) + } + if (underline != null) { + if (colorize) underline.apply(plainText, plainText.length) + } + if (color != null) { + if (colorize) color.apply(plainText, plainText.length) + } + plainText.append(str.substring(str.length - normalCount, str.length)) + return plainText + } + + private interface IrcFormat { + fun applyTo(editable: SpannableStringBuilder, from: Int, to: Int) + + fun id(): Byte + } + + private class FormatDescription(val start: Int, val format: IrcFormat) { + + fun apply(editable: SpannableStringBuilder, end: Int) { + format.applyTo(editable, start, end) + } + } + + private class ItalicIrcFormat : IrcFormat { + override fun applyTo(editable: SpannableStringBuilder, from: Int, to: Int) { + editable.setSpan(IrcItalicSpan(), from, to, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + } + + override fun id(): Byte { + return CODE_ITALIC.toByte() + } + } + + private class UnderlineIrcFormat : IrcFormat { + override fun applyTo(editable: SpannableStringBuilder, from: Int, to: Int) { + editable.setSpan(UnderlineSpan(), from, to, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + } + + override fun id(): Byte { + return CODE_UNDERLINE.toByte() + } + } + + private class BoldIrcFormat : IrcFormat { + override fun applyTo(editable: SpannableStringBuilder, from: Int, to: Int) { + editable.setSpan(IrcBoldSpan(), from, to, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + } + + override fun id(): Byte { + return CODE_BOLD.toByte() + } + } + + private inner class ColorIrcFormat(val foreground: Byte, val background: Byte) : IrcFormat { + + override fun applyTo(editable: SpannableStringBuilder, from: Int, to: Int) { + val mircColors = context.theme.styledAttributes( + R.attr.mircColor0, R.attr.mircColor1, R.attr.mircColor2, R.attr.mircColor3, + R.attr.mircColor4, R.attr.mircColor5, R.attr.mircColor6, R.attr.mircColor7, + R.attr.mircColor8, R.attr.mircColor9, R.attr.mircColorA, R.attr.mircColorB, + R.attr.mircColorC, R.attr.mircColorD, R.attr.mircColorE, R.attr.mircColorF + ) { + IntArray(16) { + getColor(it, 0) + } + } + + if (foreground.toInt() != -1 && foreground.toInt() != 99) { + editable.setSpan( + IrcForegroundColorSpan(foreground.toInt(), mircColors[foreground % 16]), from, to, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE + ) + } + if (background.toInt() != -1 && background.toInt() != 99) { + editable.setSpan( + IrcBackgroundColorSpan(background.toInt(), mircColors[background % 16]), from, to, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE + ) + } + } + + fun copySwapped(): ColorIrcFormat { + return ColorIrcFormat(background, foreground) + } + + override fun id(): Byte { + return CODE_COLOR.toByte() + } + } + + companion object { + val CODE_BOLD = 0x02.toChar() + val CODE_COLOR = 0x03.toChar() + val CODE_ITALIC = 0x1D.toChar() + val CODE_UNDERLINE = 0x1F.toChar() + val CODE_SWAP = 0x16.toChar() + val CODE_RESET = 0x0F.toChar() + + /** + * Try to read a number from a String in specified bounds + * + * @param str String to be read from + * @param start Start index (inclusive) + * @param end End index (exclusive) + * @return The byte represented by the digits read from the string + */ + fun readNumber(str: String, start: Int, end: Int): Byte { + val result = str.substring(start, end) + return if (result.isEmpty()) + -1 + else + Integer.parseInt(result, 10).toByte() + } + + /** + * @param str String to be searched in + * @param start Start position (inclusive) + * @return Index of first character that is not a digit + */ + private fun findEndOfNumber(str: String, start: Int): Int { + val validCharCodes = HashSet(Arrays.asList('0', '1', '2', '3', '4', '5', '6', '7', '8', '9')) + val searchFrame = str.substring(start) + var i = 0 + while (i < 2 && i < searchFrame.length) { + if (!validCharCodes.contains(searchFrame[i])) { + break + } + i++ + } + return start + i + } + + private fun fromId(id: Char) = when (id) { + CODE_BOLD -> BoldIrcFormat() + CODE_ITALIC -> ItalicIrcFormat() + CODE_UNDERLINE -> UnderlineIrcFormat() + else -> null + } + } +} diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/IrcFormatSerializer.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/IrcFormatSerializer.kt new file mode 100644 index 000000000..5914434b0 --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/IrcFormatSerializer.kt @@ -0,0 +1,144 @@ +package de.kuschku.quasseldroid_ng.util.irc.format + +import android.content.Context +import android.text.Spanned +import android.text.style.BackgroundColorSpan +import android.text.style.CharacterStyle +import android.text.style.ForegroundColorSpan +import android.text.style.UnderlineSpan +import de.kuschku.quasseldroid_ng.R +import de.kuschku.quasseldroid_ng.util.helper.styledAttributes +import de.kuschku.quasseldroid_ng.util.irc.format.spans.IrcBackgroundColorSpan +import de.kuschku.quasseldroid_ng.util.irc.format.spans.IrcBoldSpan +import de.kuschku.quasseldroid_ng.util.irc.format.spans.IrcForegroundColorSpan +import de.kuschku.quasseldroid_ng.util.irc.format.spans.IrcItalicSpan +import java.util.* + +class IrcFormatSerializer internal constructor(private val context: Context) { + + fun toEscapeCodes(text: Spanned): String { + val out = StringBuilder() + withinParagraph(out, text, 0, text.length) + return out.toString() + } + + private fun withinParagraph(out: StringBuilder, text: Spanned, + start: Int, end: Int) { + var next: Int + var foreground = -1 + var background = -1 + var bold = false + var underline = false + var italic = false + + var i = start + while (i < end) { + next = text.nextSpanTransition(i, end, CharacterStyle::class.java) + val style = text.getSpans(i, next, CharacterStyle::class.java) + + var afterForeground = -1 + var afterBackground = -1 + var afterBold = false + var afterUnderline = false + var afterItalic = false + + for (aStyle in style) { + if (text.getSpanFlags(aStyle) and Spanned.SPAN_COMPOSING != 0) + continue + + if (aStyle is IrcBoldSpan) { + afterBold = true + } else if (aStyle is IrcItalicSpan) { + afterItalic = true + } else if (aStyle is UnderlineSpan) { + afterUnderline = true + } else if (aStyle is IrcForegroundColorSpan) { + afterForeground = aStyle.mircColor + } else if (aStyle is IrcBackgroundColorSpan) { + afterBackground = aStyle.mircColor + } else if (aStyle is ForegroundColorSpan) { + afterForeground = 0 + } else if (aStyle is BackgroundColorSpan) { + afterBackground = 0 + } + } + + if (afterBold != bold) { + out.append(CODE_BOLD) + } + + if (afterUnderline != underline) { + out.append(CODE_UNDERLINE) + } + + if (afterItalic != italic) { + out.append(CODE_ITALIC) + } + + if (afterForeground != foreground || afterBackground != background) { + if (afterForeground == background && afterBackground == foreground) { + out.append(CODE_SWAP) + } else { + out.append(CODE_COLOR) + if (afterBackground == -1) { + if (afterForeground == -1) { + // Foreground changed from a value to null, we don’t set any new foreground + // Background changed from a value to null, we don’t set any new background + } else { + out.append(CODE_COLOR) + out.append(String.format(Locale.US, "%02d", afterForeground)) + } + } else if (background == afterBackground) { + if (afterForeground == -1) { + out.append( + String.format( + Locale.US, "%02d", + context.theme.styledAttributes(R.attr.colorForegroundMirc) { + getColor(0, 0) + } + ) + ) + } else { + out.append(String.format(Locale.US, "%02d", afterForeground)) + } + } else { + if (afterForeground == -1) { + out.append( + String.format( + Locale.US, "%02d,%02d", + context.theme.styledAttributes(R.attr.colorForegroundMirc) { + getColor(0, 0) + }, + afterBackground + ) + ) + } else { + out.append(String.format(Locale.US, "%02d,%02d", afterForeground, afterBackground)) + } + } + } + } + + out.append(text.subSequence(i, next)) + + bold = afterBold + italic = afterItalic + underline = afterUnderline + background = afterBackground + foreground = afterForeground + i = next + } + + if (bold || italic || underline || background != -1 || foreground != -1) + out.append(CODE_RESET) + } + + companion object { + val CODE_BOLD: Char = 0x02.toChar() + val CODE_COLOR: Char = 0x03.toChar() + val CODE_ITALIC: Char = 0x1D.toChar() + val CODE_UNDERLINE: Char = 0x1F.toChar() + val CODE_SWAP: Char = 0x16.toChar() + val CODE_RESET: Char = 0x0F.toChar() + } +} diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/Copyable.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/Copyable.kt new file mode 100644 index 000000000..dd103d016 --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/Copyable.kt @@ -0,0 +1,5 @@ +package de.kuschku.quasseldroid_ng.util.irc.format.spans + +interface Copyable<out T> { + fun copy(): T +} diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcBackgroundColorSpan.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcBackgroundColorSpan.kt new file mode 100644 index 000000000..5927da7b0 --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcBackgroundColorSpan.kt @@ -0,0 +1,11 @@ +package de.kuschku.quasseldroid_ng.util.irc.format.spans + +import android.support.annotation.ColorInt +import android.text.style.BackgroundColorSpan + +class IrcBackgroundColorSpan( + val mircColor: Int, + @ColorInt color: Int +) : BackgroundColorSpan(color), Copyable<IrcBackgroundColorSpan> { + override fun copy() = IrcBackgroundColorSpan(mircColor, backgroundColor) +} diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcBoldSpan.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcBoldSpan.kt new file mode 100644 index 000000000..ddaeec36b --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcBoldSpan.kt @@ -0,0 +1,8 @@ +package de.kuschku.quasseldroid_ng.util.irc.format.spans + +import android.graphics.Typeface +import android.text.style.StyleSpan + +class IrcBoldSpan : StyleSpan(Typeface.BOLD), Copyable<IrcBoldSpan> { + override fun copy() = IrcBoldSpan() +} diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcForegroundColorSpan.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcForegroundColorSpan.kt new file mode 100644 index 000000000..70461675a --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcForegroundColorSpan.kt @@ -0,0 +1,11 @@ +package de.kuschku.quasseldroid_ng.util.irc.format.spans + +import android.support.annotation.ColorInt +import android.text.style.ForegroundColorSpan + +class IrcForegroundColorSpan( + val mircColor: Int, + @ColorInt color: Int +) : ForegroundColorSpan(color), Copyable<IrcForegroundColorSpan> { + override fun copy() = IrcForegroundColorSpan(mircColor, foregroundColor) +} diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcItalicSpan.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcItalicSpan.kt new file mode 100644 index 000000000..d38f195e5 --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcItalicSpan.kt @@ -0,0 +1,8 @@ +package de.kuschku.quasseldroid_ng.util.irc.format.spans + +import android.graphics.Typeface +import android.text.style.StyleSpan + +class IrcItalicSpan : StyleSpan(Typeface.ITALIC), Copyable<IrcItalicSpan> { + override fun copy() = IrcItalicSpan() +} diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcUnderlineSpan.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcUnderlineSpan.kt new file mode 100644 index 000000000..447a11bd7 --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcUnderlineSpan.kt @@ -0,0 +1,7 @@ +package de.kuschku.quasseldroid_ng.util.irc.format.spans + +import android.text.style.UnderlineSpan + +class IrcUnderlineSpan : UnderlineSpan(), Copyable<IrcUnderlineSpan> { + override fun copy() = IrcUnderlineSpan() +} -- GitLab