diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/ChannelLinkSpan.kt b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/ChannelLinkSpan.kt new file mode 100644 index 000000000..7e9be6eb1 --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/ChannelLinkSpan.kt @@ -0,0 +1,47 @@ +/* + * Quasseldroid - Quassel client for Android + * + * Copyright (c) 2019 Janne Koschinski + * Copyright (c) 2019 The Quassel Project + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 as published + * by the Free Software Foundation. + * + * 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 . + */ + +package de.kuschku.quasseldroid.util.irc.format + +import android.text.TextPaint +import android.text.style.ClickableSpan +import android.view.View +import de.kuschku.libquassel.protocol.NetworkId +import de.kuschku.quasseldroid.ui.chat.ChatActivity + +class ChannelLinkSpan( + private val networkId: NetworkId, + private val text: String, + private val highlight: Boolean +) : ClickableSpan() { + override fun updateDrawState(ds: TextPaint?) { + if (ds != null) { + if (!highlight) ds.color = ds.linkColor + ds.isUnderlineText = true + } + } + + override fun onClick(widget: View) { + ChatActivity.launch( + widget.context, + networkId = networkId, + channel = text + ) + } +} diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/ContentFormatter.kt b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/ContentFormatter.kt index 116acfa76..7c0ae3c14 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/ContentFormatter.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/ContentFormatter.kt @@ -22,20 +22,17 @@ package de.kuschku.quasseldroid.util.irc.format import android.content.Context import android.graphics.Typeface import android.text.SpannableString -import android.text.Spanned -import android.text.TextPaint -import android.text.style.* -import android.view.View +import android.text.style.ForegroundColorSpan +import android.text.style.StyleSpan import androidx.annotation.ColorInt import de.kuschku.libquassel.protocol.NetworkId import de.kuschku.libquassel.util.irc.HostmaskHelper import de.kuschku.libquassel.util.irc.SenderColorUtil import de.kuschku.quasseldroid.R import de.kuschku.quasseldroid.settings.MessageSettings -import de.kuschku.quasseldroid.ui.chat.ChatActivity import de.kuschku.quasseldroid.util.helper.styledAttributes -import de.kuschku.quasseldroid.util.irc.format.spans.IrcBackgroundColorSpan -import de.kuschku.quasseldroid.util.irc.format.spans.IrcForegroundColorSpan +import de.kuschku.quasseldroid.util.irc.format.model.FormatInfo +import de.kuschku.quasseldroid.util.irc.format.model.IrcFormat import de.kuschku.quasseldroid.util.ui.SpanFormatter import org.intellij.lang.annotations.Language import javax.inject.Inject @@ -80,59 +77,28 @@ class ContentFormatter @Inject constructor( getColor(0, 0) } - class QuasselURLSpan(text: String, private val highlight: Boolean) : URLSpan(text) { - override fun updateDrawState(ds: TextPaint?) { - if (ds != null) { - if (!highlight) ds.color = ds.linkColor - ds.isUnderlineText = true - } - } - } - - class ChannelLinkSpan(private val networkId: NetworkId, private val text: String, - private val highlight: Boolean) : ClickableSpan() { - override fun updateDrawState(ds: TextPaint?) { - if (ds != null) { - if (!highlight) ds.color = ds.linkColor - ds.isUnderlineText = true - } - } - - override fun onClick(widget: View) { - ChatActivity.launch( - widget.context, - networkId = networkId, - channel = text - ) - } - } - fun formatContent(content: String, highlight: Boolean = false, showSpoilers: Boolean = false, networkId: NetworkId?): CharSequence { - val formattedText = ircFormatDeserializer.formatString(content, messageSettings.colorizeMirc) - val text = SpannableString(formattedText) + val spans = mutableListOf() + val formattedText = SpannableString( + ircFormatDeserializer.formatString( + content, + messageSettings.colorizeMirc, + spans + ) + ) if (showSpoilers) { - val spans = mutableMapOf, MutableList>() - for (span in text.getSpans(0, text.length, IrcForegroundColorSpan::class.java)) { - val from = text.getSpanStart(span) - val to = text.getSpanEnd(span) - spans.getOrPut(Triple(from, to, span.getForegroundColor()), ::mutableListOf).add(span) - } - for (span in text.getSpans(0, text.length, IrcBackgroundColorSpan::class.java)) { - val from = text.getSpanStart(span) - val to = text.getSpanEnd(span) - spans.getOrPut(Triple(from, to, span.getBackgroundColor()), ::mutableListOf).add(span) - } - for (group in spans.values) { - if (group.size > 1 && - group.any { it is ForegroundColorSpan } && - group.any { it is BackgroundColorSpan }) { - for (span in group) { - text.removeSpan(span) - } + spans.removeAll { + when { + it.format is IrcFormat.Color -> + it.format.foreground == it.format.background + it.format is IrcFormat.Hex -> + it.format.foreground == it.format.background + else -> + false } } } @@ -140,11 +106,11 @@ class ContentFormatter @Inject constructor( for (result in urlPattern.findAll(formattedText)) { val group = result.groups[1] if (group != null) { - text.setSpan( - QuasselURLSpan(group.value, highlight), group.range.start, + spans.add(FormatInfo( + group.range.start, group.range.start + group.value.length, - Spanned.SPAN_INCLUSIVE_EXCLUSIVE - ) + IrcFormat.Url(group.value, highlight) + )) } } @@ -152,15 +118,21 @@ class ContentFormatter @Inject constructor( for (result in channelPattern.findAll(formattedText)) { val group = result.groups[1] if (group != null) { - text.setSpan(ChannelLinkSpan(networkId, group.value, highlight), - group.range.start, - group.range.endInclusive + 1, - Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + spans.add(FormatInfo( + group.range.start, + group.range.start + group.value.length, + IrcFormat.Channel(networkId, group.value, highlight) + )) } } } - return text + spans.reverse() + for (span in spans) { + span.apply(formattedText) + } + + return formattedText } private fun formatNickNickImpl(nick: String, self: Boolean, colorize: Boolean, diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/IrcFormatDeserializer.kt b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/IrcFormatDeserializer.kt index a390248ae..03e3aff35 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/IrcFormatDeserializer.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/IrcFormatDeserializer.kt @@ -21,11 +21,12 @@ package de.kuschku.quasseldroid.util.irc.format import android.content.Context import android.text.SpannableStringBuilder -import android.text.Spanned import de.kuschku.quasseldroid.R import de.kuschku.quasseldroid.util.compatibility.AndroidCrashFixer import de.kuschku.quasseldroid.util.helper.getColorCompat -import de.kuschku.quasseldroid.util.irc.format.spans.* +import de.kuschku.quasseldroid.util.irc.format.model.FormatDescription +import de.kuschku.quasseldroid.util.irc.format.model.FormatInfo +import de.kuschku.quasseldroid.util.irc.format.model.IrcFormat import javax.inject.Inject /** @@ -70,19 +71,28 @@ class IrcFormatDeserializer(private val mircColors: IntArray) { * @param content mIRC formatted String * @return a CharSequence with Android’s span format representing the input string */ - fun formatString(content: String?, colorize: Boolean): CharSequence { + fun formatString(content: String?, colorize: Boolean, + output: MutableList? = null): CharSequence { if (content == null) return "" val str = AndroidCrashFixer.removeCrashableCharacters(content) val plainText = SpannableStringBuilder() - var bold: FormatDescription? = null - var italic: FormatDescription? = null - var underline: FormatDescription? = null - var strikethrough: FormatDescription? = null - var monospace: FormatDescription? = null - var color: FormatDescription? = null - var hexColor: FormatDescription? = null + var bold: FormatDescription? = null + var italic: FormatDescription? = null + var underline: FormatDescription? = null + var strikethrough: FormatDescription? = null + var monospace: FormatDescription? = null + var color: FormatDescription? = null + var hexColor: FormatDescription? = null + + fun applyFormat(desc: FormatDescription) { + if (output != null) { + output.add(FormatInfo(desc.start, plainText.length, desc.format)) + } else { + desc.apply(plainText, plainText.length) + } + } // Iterating over every character var normalCount = 0 @@ -96,11 +106,12 @@ class IrcFormatDeserializer(private val mircColors: IntArray) { // If there is an element on stack with the same code, close it bold = if (bold != null) { - if (colorize) bold.apply(plainText, plainText.length) + if (colorize) applyFormat(bold) null // Otherwise create a new one } else { - FormatDescription(plainText.length, BoldIrcFormat()) + FormatDescription(plainText.length, + IrcFormat.Bold) } } CODE_ITALIC -> { @@ -109,11 +120,12 @@ class IrcFormatDeserializer(private val mircColors: IntArray) { // If there is an element on stack with the same code, close it italic = if (italic != null) { - if (colorize) italic.apply(plainText, plainText.length) + if (colorize) applyFormat(italic) null // Otherwise create a new one } else { - FormatDescription(plainText.length, ItalicIrcFormat()) + FormatDescription(plainText.length, + IrcFormat.Italic) } } CODE_UNDERLINE -> { @@ -122,11 +134,12 @@ class IrcFormatDeserializer(private val mircColors: IntArray) { // If there is an element on stack with the same code, close it underline = if (underline != null) { - if (colorize) underline.apply(plainText, plainText.length) + if (colorize) applyFormat(underline) null // Otherwise create a new one } else { - FormatDescription(plainText.length, UnderlineIrcFormat()) + FormatDescription(plainText.length, + IrcFormat.Underline) } } CODE_STRIKETHROUGH -> { @@ -135,11 +148,12 @@ class IrcFormatDeserializer(private val mircColors: IntArray) { // If there is an element on stack with the same code, close it strikethrough = if (strikethrough != null) { - if (colorize) strikethrough.apply(plainText, plainText.length) + if (colorize) applyFormat(strikethrough) null // Otherwise create a new one } else { - FormatDescription(plainText.length, StrikethroughIrcFormat()) + FormatDescription(plainText.length, + IrcFormat.Strikethrough) } } CODE_MONOSPACE -> { @@ -148,11 +162,12 @@ class IrcFormatDeserializer(private val mircColors: IntArray) { // If there is an element on stack with the same code, close it monospace = if (monospace != null) { - if (colorize) monospace.apply(plainText, plainText.length) + if (colorize) applyFormat(monospace) null // Otherwise create a new one } else { - FormatDescription(plainText.length, MonospaceIrcFormat()) + FormatDescription(plainText.length, + IrcFormat.Monospace) } } CODE_COLOR -> { @@ -175,14 +190,14 @@ class IrcFormatDeserializer(private val mircColors: IntArray) { // 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) + if (colorize) applyFormat(color) // Reuse old background, if possible if (background.toInt() == -1) background = color.format.background } // Add new format color = FormatDescription( - plainText.length, ColorIrcFormat(foreground, background, this.mircColors) + plainText.length, IrcFormat.Color(foreground, background, this.mircColors) ) // i points in front of the next character @@ -190,7 +205,7 @@ class IrcFormatDeserializer(private val mircColors: IntArray) { // Otherwise assume this is a closing tag } else if (color != null) { - if (colorize) color.apply(plainText, plainText.length) + if (colorize) applyFormat(color) color = null } } @@ -214,20 +229,23 @@ class IrcFormatDeserializer(private val mircColors: IntArray) { // If previous element was also a color element, try to reuse background if (hexColor != null) { // Apply old format - if (colorize) hexColor.apply(plainText, plainText.length) + if (colorize) applyFormat(hexColor) // Reuse old background, if possible if (background == -1) background = hexColor.format.background } // Add new format - hexColor = FormatDescription(plainText.length, HexIrcFormat(foreground, background)) + hexColor = FormatDescription(plainText.length, + IrcFormat.Hex( + 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 (hexColor != null) { - if (colorize) hexColor.apply(plainText, plainText.length) + if (colorize) applyFormat(hexColor) hexColor = null } } @@ -237,7 +255,7 @@ class IrcFormatDeserializer(private val mircColors: IntArray) { // 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) + if (colorize) applyFormat(color) color = FormatDescription( plainText.length, color.format.copySwapped() ) @@ -249,23 +267,23 @@ class IrcFormatDeserializer(private val mircColors: IntArray) { // End all formatting tags if (bold != null) { - if (colorize) bold.apply(plainText, plainText.length) + if (colorize) applyFormat(bold) bold = null } if (italic != null) { - if (colorize) italic.apply(plainText, plainText.length) + if (colorize) applyFormat(italic) italic = null } if (underline != null) { - if (colorize) underline.apply(plainText, plainText.length) + if (colorize) applyFormat(underline) underline = null } if (color != null) { - if (colorize) color.apply(plainText, plainText.length) + if (colorize) applyFormat(color) color = null } if (hexColor != null) { - if (colorize) hexColor.apply(plainText, plainText.length) + if (colorize) applyFormat(hexColor) hexColor = null } } @@ -281,108 +299,29 @@ class IrcFormatDeserializer(private val mircColors: IntArray) { // End all formatting tags if (bold != null) { - if (colorize) bold.apply(plainText, plainText.length) + if (colorize) applyFormat(bold) } if (italic != null) { - if (colorize) italic.apply(plainText, plainText.length) + if (colorize) applyFormat(italic) } if (underline != null) { - if (colorize) underline.apply(plainText, plainText.length) + if (colorize) applyFormat(underline) } if (strikethrough != null) { - if (colorize) strikethrough.apply(plainText, plainText.length) + if (colorize) applyFormat(strikethrough) } if (monospace != null) { - if (colorize) monospace.apply(plainText, plainText.length) + if (colorize) applyFormat(monospace) } if (color != null) { - if (colorize) color.apply(plainText, plainText.length) + if (colorize) applyFormat(color) } if (hexColor != null) { - if (colorize) hexColor.apply(plainText, plainText.length) + if (colorize) applyFormat(hexColor) } return plainText } - private interface IrcFormat { - fun applyTo(editable: SpannableStringBuilder, from: Int, to: Int) - } - - private class FormatDescription(val start: Int, val format: U) { - 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) - } - } - - private class UnderlineIrcFormat : IrcFormat { - override fun applyTo(editable: SpannableStringBuilder, from: Int, to: Int) { - editable.setSpan(IrcUnderlineSpan(), from, to, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) - } - } - - private class StrikethroughIrcFormat : IrcFormat { - override fun applyTo(editable: SpannableStringBuilder, from: Int, to: Int) { - editable.setSpan(IrcStrikethroughSpan(), from, to, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) - } - } - - private class MonospaceIrcFormat : IrcFormat { - override fun applyTo(editable: SpannableStringBuilder, from: Int, to: Int) { - editable.setSpan(IrcMonospaceSpan(), from, to, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) - } - } - - private class BoldIrcFormat : IrcFormat { - override fun applyTo(editable: SpannableStringBuilder, from: Int, to: Int) { - editable.setSpan(IrcBoldSpan(), from, to, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) - } - } - - private inner class HexIrcFormat(val foreground: Int, val background: Int) : IrcFormat { - override fun applyTo(editable: SpannableStringBuilder, from: Int, to: Int) { - if (foreground >= 0) { - editable.setSpan( - IrcForegroundColorSpan.HEX(foreground or 0xFFFFFF.inv()), from, to, - Spanned.SPAN_INCLUSIVE_EXCLUSIVE - ) - } - if (background >= 0) { - editable.setSpan( - IrcBackgroundColorSpan.HEX(background or 0xFFFFFF.inv()), from, to, - Spanned.SPAN_INCLUSIVE_EXCLUSIVE - ) - } - } - } - - private inner class ColorIrcFormat(val foreground: Byte, val background: Byte, - val mircColors: IntArray) : IrcFormat { - override fun applyTo(editable: SpannableStringBuilder, from: Int, to: Int) { - if (foreground.toInt() >= 0 && foreground.toInt() < mircColors.size) { - editable.setSpan( - IrcForegroundColorSpan.MIRC(foreground.toInt(), mircColors[foreground.toInt()]), from, to, - Spanned.SPAN_INCLUSIVE_EXCLUSIVE - ) - } - if (background.toInt() >= 0 && background.toInt() < mircColors.size) { - editable.setSpan( - IrcBackgroundColorSpan.MIRC(background.toInt(), mircColors[background.toInt()]), from, to, - Spanned.SPAN_INCLUSIVE_EXCLUSIVE - ) - } - } - - fun copySwapped(): ColorIrcFormat { - return ColorIrcFormat(background, foreground, mircColors) - } - } - companion object { private const val CODE_BOLD = 0x02.toChar() private const val CODE_COLOR = 0x03.toChar() diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/QuasselURLSpan.kt b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/QuasselURLSpan.kt new file mode 100644 index 000000000..14921b3c7 --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/QuasselURLSpan.kt @@ -0,0 +1,32 @@ +/* + * Quasseldroid - Quassel client for Android + * + * Copyright (c) 2019 Janne Koschinski + * Copyright (c) 2019 The Quassel Project + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 as published + * by the Free Software Foundation. + * + * 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 . + */ + +package de.kuschku.quasseldroid.util.irc.format + +import android.text.TextPaint +import android.text.style.URLSpan + +class QuasselURLSpan(text: String, private val highlight: Boolean) : URLSpan(text) { + override fun updateDrawState(ds: TextPaint?) { + if (ds != null) { + if (!highlight) ds.color = ds.linkColor + ds.isUnderlineText = true + } + } +} diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/model/FormatDescription.kt b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/model/FormatDescription.kt new file mode 100644 index 000000000..add0deab4 --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/model/FormatDescription.kt @@ -0,0 +1,28 @@ +/* + * Quasseldroid - Quassel client for Android + * + * Copyright (c) 2019 Janne Koschinski + * Copyright (c) 2019 The Quassel Project + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 as published + * by the Free Software Foundation. + * + * 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 . + */ + +package de.kuschku.quasseldroid.util.irc.format.model + +import android.text.SpannableStringBuilder + +class FormatDescription(val start: Int, val format: U) { + fun apply(editable: SpannableStringBuilder, end: Int) { + format.applyTo(editable, start, end) + } +} diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/model/FormatInfo.kt b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/model/FormatInfo.kt new file mode 100644 index 000000000..76822446d --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/model/FormatInfo.kt @@ -0,0 +1,28 @@ +/* + * Quasseldroid - Quassel client for Android + * + * Copyright (c) 2019 Janne Koschinski + * Copyright (c) 2019 The Quassel Project + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 as published + * by the Free Software Foundation. + * + * 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 . + */ + +package de.kuschku.quasseldroid.util.irc.format.model + +import android.text.Spannable + +data class FormatInfo(val start: Int, val end: Int, val format: IrcFormat) { + fun apply(editable: Spannable) { + format.applyTo(editable, start, end) + } +} diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/model/IrcFormat.kt b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/model/IrcFormat.kt new file mode 100644 index 000000000..9a5c5e35a --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/model/IrcFormat.kt @@ -0,0 +1,150 @@ +/* + * Quasseldroid - Quassel client for Android + * + * Copyright (c) 2019 Janne Koschinski + * Copyright (c) 2019 The Quassel Project + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 as published + * by the Free Software Foundation. + * + * 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 . + */ + +package de.kuschku.quasseldroid.util.irc.format.model + +import android.text.Spannable +import android.text.Spanned +import de.kuschku.libquassel.protocol.NetworkId +import de.kuschku.quasseldroid.util.irc.format.ChannelLinkSpan +import de.kuschku.quasseldroid.util.irc.format.QuasselURLSpan +import de.kuschku.quasseldroid.util.irc.format.spans.* + +sealed class IrcFormat { + abstract fun applyTo(editable: Spannable, from: Int, to: Int) + + object Italic : IrcFormat() { + override fun applyTo(editable: Spannable, from: Int, to: Int) { + editable.setSpan(IrcItalicSpan(), from, to, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + } + } + + object Underline : IrcFormat() { + override fun applyTo(editable: Spannable, from: Int, to: Int) { + editable.setSpan(IrcUnderlineSpan(), from, to, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + } + } + + object Strikethrough : IrcFormat() { + override fun applyTo(editable: Spannable, from: Int, to: Int) { + editable.setSpan(IrcStrikethroughSpan(), from, to, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + } + } + + object Monospace : IrcFormat() { + override fun applyTo(editable: Spannable, from: Int, to: Int) { + editable.setSpan(IrcMonospaceSpan(), from, to, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + } + } + + object Bold : IrcFormat() { + override fun applyTo(editable: Spannable, from: Int, to: Int) { + editable.setSpan(IrcBoldSpan(), from, to, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + } + } + + data class Hex(val foreground: Int, val background: Int) : IrcFormat() { + override fun applyTo(editable: Spannable, from: Int, to: Int) { + if (foreground >= 0) { + editable.setSpan( + IrcForegroundColorSpan.HEX(foreground or 0xFFFFFF.inv()), from, to, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE + ) + } + if (background >= 0) { + editable.setSpan( + IrcBackgroundColorSpan.HEX(background or 0xFFFFFF.inv()), from, to, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE + ) + } + } + } + + data class Color(val foreground: Byte, val background: Byte, + private val mircColors: IntArray) : IrcFormat() { + override fun applyTo(editable: Spannable, from: Int, to: Int) { + if (foreground.toInt() >= 0 && foreground.toInt() < mircColors.size) { + editable.setSpan( + IrcForegroundColorSpan.MIRC(foreground.toInt(), + mircColors[foreground.toInt()]), + from, + to, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE + ) + } + if (background.toInt() >= 0 && background.toInt() < mircColors.size) { + editable.setSpan( + IrcBackgroundColorSpan.MIRC(background.toInt(), + mircColors[background.toInt()]), + from, + to, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE + ) + } + } + + fun copySwapped(): Color { + return Color(background, foreground, mircColors) + } + + override fun toString(): String { + return "Color(foreground=$foreground, background=$background)" + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Color + + if (foreground != other.foreground) return false + if (background != other.background) return false + + return true + } + + override fun hashCode(): Int { + var result = foreground.toInt() + result = 31 * result + background + return result + } + } + + data class Url(val target: String, val highlight: Boolean) : IrcFormat() { + override fun applyTo(editable: Spannable, from: Int, to: Int) { + editable.setSpan( + QuasselURLSpan(target, highlight), from, to, Spanned.SPAN_INCLUSIVE_EXCLUSIVE + ) + } + } + + data class Channel(val networkId: NetworkId, val target: String, val highlight: Boolean) : + IrcFormat() { + override fun applyTo(editable: Spannable, from: Int, to: Int) { + editable.setSpan( + ChannelLinkSpan(networkId, target, highlight), from, to, Spanned.SPAN_INCLUSIVE_EXCLUSIVE + ) + } + } +} diff --git a/app/src/test/java/de/kuschku/quasseldroid/util/irc/format/IrcFormatDeserializerTest.kt b/app/src/test/java/de/kuschku/quasseldroid/util/irc/format/IrcFormatDeserializerTest.kt index 93e7a8575..e980b77a5 100644 --- a/app/src/test/java/de/kuschku/quasseldroid/util/irc/format/IrcFormatDeserializerTest.kt +++ b/app/src/test/java/de/kuschku/quasseldroid/util/irc/format/IrcFormatDeserializerTest.kt @@ -19,11 +19,9 @@ package de.kuschku.quasseldroid.util.irc.format -import android.text.Spanned -import android.text.SpannedString import de.kuschku.quasseldroid.QuasseldroidTest -import de.kuschku.quasseldroid.util.irc.format.spans.IrcBackgroundColorSpan -import de.kuschku.quasseldroid.util.irc.format.spans.IrcForegroundColorSpan +import de.kuschku.quasseldroid.util.irc.format.model.FormatInfo +import de.kuschku.quasseldroid.util.irc.format.model.IrcFormat import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test @@ -34,7 +32,7 @@ import org.robolectric.annotation.Config @Config(application = QuasseldroidTest::class) @RunWith(RobolectricTestRunner::class) class IrcFormatDeserializerTest { - lateinit var deserializer: IrcFormatDeserializer + private lateinit var deserializer: IrcFormatDeserializer @Before fun setUp() { @@ -43,46 +41,25 @@ class IrcFormatDeserializerTest { @Test fun testMissingEndTag() { - val text = SpannedString.valueOf(deserializer.formatString( - "\u000301,01weeeeeeeeee", - colorize = true - )) + val spans = mutableListOf() + val text = deserializer.formatString( + content = "\u000301,01weeeeeeeeee", + colorize = true, + output = spans + ) + assertEquals("weeeeeeeeee", text.toString()) assertEquals( listOf( - SpanInfo( - from = 0, - to = 11, - flags = Spanned.SPAN_INCLUSIVE_EXCLUSIVE, - span = IrcForegroundColorSpan.MIRC(mircColor = 1, color = colors[1]) - ), - SpanInfo( - from = 0, - to = 11, - flags = Spanned.SPAN_INCLUSIVE_EXCLUSIVE, - span = IrcBackgroundColorSpan.MIRC(mircColor = 1, color = colors[1]) + FormatInfo( + start = 0, + end = 11, + format = IrcFormat.Color(1, 1, colors) ) ), - text.allSpans() + spans ) } - inline fun Spanned.allSpans(): List = - getSpans(0, length, T::class.java).map { - SpanInfo( - from = getSpanStart(it), - to = getSpanEnd(it), - flags = getSpanFlags(it), - span = it - ) - } - - data class SpanInfo( - val from: Int, - val to: Int, - val flags: Int, - val span: Any? - ) - companion object { val colors = intArrayOf( 0x00ffffff,