diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/ChatActivity.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/ChatActivity.kt index beb5782ace16ab2a50ff8f4209b7f0e036d234b2..df74b7053726523bb651c79c8cb2e4d7aff50656 100644 --- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/ChatActivity.kt +++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/ChatActivity.kt @@ -15,7 +15,9 @@ import android.support.v4.widget.DrawerLayout import android.support.v7.app.ActionBarDrawerToggle import android.support.v7.widget.Toolbar import android.text.InputType +import android.text.Spanned import android.view.* +import android.view.inputmethod.EditorInfo import android.widget.EditText import android.widget.ImageButton import butterknife.BindView @@ -32,14 +34,12 @@ import de.kuschku.quasseldroid_ng.Keys import de.kuschku.quasseldroid_ng.R import de.kuschku.quasseldroid_ng.persistence.QuasselDatabase import de.kuschku.quasseldroid_ng.ui.settings.SettingsActivity +import de.kuschku.quasseldroid_ng.ui.settings.data.AppearanceSettings import de.kuschku.quasseldroid_ng.ui.settings.data.BacklogSettings import de.kuschku.quasseldroid_ng.ui.settings.data.Settings import de.kuschku.quasseldroid_ng.ui.viewmodel.QuasselViewModel import de.kuschku.quasseldroid_ng.util.AndroidHandlerThread -import de.kuschku.quasseldroid_ng.util.helper.editApply -import de.kuschku.quasseldroid_ng.util.helper.invoke -import de.kuschku.quasseldroid_ng.util.helper.let -import de.kuschku.quasseldroid_ng.util.helper.sharedPreferences +import de.kuschku.quasseldroid_ng.util.helper.* import de.kuschku.quasseldroid_ng.util.irc.format.IrcFormatSerializer import de.kuschku.quasseldroid_ng.util.service.ServiceBoundActivity import de.kuschku.quasseldroid_ng.util.ui.MaterialContentLoadingProgressBar @@ -117,6 +117,17 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc send() } + chatline.imeOptions = when (appearanceSettings.inputEnter) { + AppearanceSettings.InputEnterMode.EMOJI -> listOf( + EditorInfo.IME_ACTION_NONE, + EditorInfo.IME_FLAG_NO_EXTRACT_UI + ) + AppearanceSettings.InputEnterMode.SEND -> listOf( + EditorInfo.IME_ACTION_SEND, + EditorInfo.IME_FLAG_NO_EXTRACT_UI + ) + }.fold(0, Int::or) + chatline.setOnKeyListener { _, keyCode, event -> if (event.hasNoModifiers() && (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER)) { send() @@ -171,9 +182,13 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc viewModel.getBuffer().let { bufferId -> session.bufferSyncer?.bufferInfo(bufferId)?.also { bufferInfo -> val output = mutableListOf<IAliasManager.Command>() - session.aliasManager?.processInput( - bufferInfo, ircFormatSerializer.toEscapeCodes(text), output - ) + for (line in text.lineSequence()) { + session.aliasManager?.processInput( + bufferInfo, + if (line is Spanned) ircFormatSerializer.toEscapeCodes(line) else line.toString(), + output + ) + } for (command in output) { session.rpcHandler?.sendInput(command.buffer, command.message) } diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/messages/QuasselMessageRenderer.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/messages/QuasselMessageRenderer.kt index 993162c303b8c7085fd5c29d81366bcab8f9b229..8d141bc8ee220898ea8857be1229062b03ed48cb 100644 --- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/messages/QuasselMessageRenderer.kt +++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/messages/QuasselMessageRenderer.kt @@ -324,28 +324,38 @@ class QuasselMessageRenderer( } } - private fun formatNickImpl(sender: String, colorize: Boolean, hostmask: Boolean): CharSequence { - val nick = IrcUserUtils.nick(sender) - val content = if (hostmask) sender else nick - val spannableString = SpannableString(content) + private fun formatNickNickImpl(nick: String, colorize: Boolean): CharSequence { + val spannableString = SpannableString(nick) if (colorize) { val senderColor = IrcUserUtils.senderColor(nick) spannableString.setSpan( ForegroundColorSpan(senderColors[senderColor % senderColors.size]), 0, - content.length, + nick.length, SpannableString.SPAN_INCLUSIVE_EXCLUSIVE ) } spannableString.setSpan( StyleSpan(Typeface.BOLD), 0, - content.length, + nick.length, SpannableString.SPAN_INCLUSIVE_EXCLUSIVE ) return spannableString } + private fun formatNickImpl(sender: String, colorize: Boolean, hostmask: Boolean): CharSequence { + val nick = IrcUserUtils.nick(sender) + val mask = IrcUserUtils.mask(sender) + val formattedNick = formatNickNickImpl(nick, colorize) + + return if (hostmask) { + SpanFormatter.format("%s (%s)", formattedNick, mask) + } else { + formattedNick + } + } + private fun formatNick(sender: String, self: Boolean, highlight: Boolean, showHostmask: Boolean) = when (appearanceSettings.colorizeNicknames) { diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/settings/data/AppearanceSettings.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/settings/data/AppearanceSettings.kt index f3f53c065c5951442c63dfcd6a341397f4a3ca03..0419da8defce651b3680c11f365769b8636ee80c 100644 --- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/settings/data/AppearanceSettings.kt +++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/settings/data/AppearanceSettings.kt @@ -6,6 +6,7 @@ import de.kuschku.quasseldroid_ng.R data class AppearanceSettings( val showPrefix: ShowPrefixMode = ShowPrefixMode.HIGHEST, val colorizeNicknames: ColorizeNicknamesMode = ColorizeNicknamesMode.ALL_BUT_MINE, + val inputEnter: InputEnterMode = InputEnterMode.EMOJI, val colorizeMirc: Boolean = true, val useMonospace: Boolean = false, val showSeconds: Boolean = false, @@ -17,13 +18,33 @@ data class AppearanceSettings( enum class ColorizeNicknamesMode { ALL, ALL_BUT_MINE, - NONE + NONE; + + companion object { + private val map = values().associateBy { it.name } + fun of(name: String) = map[name] + } + } + + enum class InputEnterMode { + EMOJI, + SEND; + + companion object { + private val map = values().associateBy { it.name } + fun of(name: String) = map[name] + } } enum class ShowPrefixMode { ALL, HIGHEST, - NONE + NONE; + + companion object { + private val map = values().associateBy { it.name } + fun of(name: String) = map[name] + } } enum class Theme(@StyleRes val style: Int) { @@ -33,7 +54,12 @@ data class AppearanceSettings( SOLARIZED_LIGHT(R.style.Theme_ChatTheme_Solarized_Light), SOLARIZED_DARK(R.style.Theme_ChatTheme_Solarized_Dark), GRUVBOX_LIGHT(R.style.Theme_ChatTheme_Gruvbox_Light), - GRUVBOX_DARK(R.style.Theme_ChatTheme_Gruvbox_Dark) + GRUVBOX_DARK(R.style.Theme_ChatTheme_Gruvbox_Dark); + + companion object { + private val map = values().associateBy { it.name } + fun of(name: String) = map[name] + } } companion object { diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/settings/data/Settings.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/settings/data/Settings.kt index 3c820d012990c64e4fc3d254a8376a84878261b1..2e2e8857426bcac9aaa89a187a622bfe7753a8e7 100644 --- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/settings/data/Settings.kt +++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/settings/data/Settings.kt @@ -8,12 +8,12 @@ import de.kuschku.quasseldroid_ng.util.helper.sharedPreferences object Settings { fun appearance(context: Context) = context.sharedPreferences { AppearanceSettings( - theme = Theme.valueOf( + theme = Theme.of( getString( context.getString(R.string.preference_theme_key), - AppearanceSettings.DEFAULT.theme.name + null ) - ), + ) ?: AppearanceSettings.DEFAULT.theme, useMonospace = getBoolean( context.getString(R.string.preference_monospace_key), AppearanceSettings.DEFAULT.useMonospace @@ -26,18 +26,24 @@ object Settings { context.getString(R.string.preference_use_24h_clock_key), AppearanceSettings.DEFAULT.use24hClock ), - showPrefix = ShowPrefixMode.valueOf( + showPrefix = ShowPrefixMode.of( getString( context.getString(R.string.preference_show_prefix_key), - AppearanceSettings.DEFAULT.showPrefix.name + null ) - ), - colorizeNicknames = ColorizeNicknamesMode.valueOf( + ) ?: AppearanceSettings.DEFAULT.showPrefix, + colorizeNicknames = ColorizeNicknamesMode.of( getString( context.getString(R.string.preference_colorize_nicknames_key), - AppearanceSettings.DEFAULT.colorizeNicknames.name + null ) - ), + ) ?: AppearanceSettings.DEFAULT.colorizeNicknames, + inputEnter = InputEnterMode.of( + getString( + context.getString(R.string.preference_input_enter_key), + null + ) + ) ?: AppearanceSettings.DEFAULT.inputEnter, colorizeMirc = getBoolean( context.getString(R.string.preference_colorize_mirc_key), AppearanceSettings.DEFAULT.colorizeMirc diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/SpannedHelper.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/SpannedHelper.kt new file mode 100644 index 0000000000000000000000000000000000000000..496344b09d8feee695168b41e15d8654739b7194 --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/SpannedHelper.kt @@ -0,0 +1,129 @@ +package de.kuschku.quasseldroid_ng.util.helper + +private class DelimitedRangesSequence( + private val input: CharSequence, + private val startIndex: Int, + private val limit: Int, + private val getNextMatch: CharSequence.(Int) -> Pair<Int, Int>? +) : Sequence<IntRange> { + override fun iterator(): Iterator<IntRange> = object : Iterator<IntRange> { + var nextState: Int = -1 // -1 for unknown, 0 for done, 1 for continue + var currentStartIndex: Int = startIndex.coerceIn(0, input.length) + var nextSearchIndex: Int = currentStartIndex + var nextItem: IntRange? = null + var counter: Int = 0 + + private fun calcNext() { + if (nextSearchIndex < 0) { + nextState = 0 + nextItem = null + } else { + if (limit > 0 && ++counter >= limit || nextSearchIndex > input.length) { + nextItem = currentStartIndex..input.lastIndex + nextSearchIndex = -1 + } else { + val match = input.getNextMatch(nextSearchIndex) + if (match == null) { + nextItem = currentStartIndex..input.lastIndex + nextSearchIndex = -1 + } else { + val (index, length) = match + nextItem = currentStartIndex..index - 1 + currentStartIndex = index + length + nextSearchIndex = currentStartIndex + if (length == 0) 1 else 0 + } + } + nextState = 1 + } + } + + override fun next(): IntRange { + if (nextState == -1) + calcNext() + if (nextState == 0) + throw NoSuchElementException() + val result = nextItem as IntRange + // Clean next to avoid keeping reference on yielded instance + nextItem = null + nextState = -1 + return result + } + + override fun hasNext(): Boolean { + if (nextState == -1) + calcNext() + return nextState == 1 + } + } +} + +internal fun CharSequence.regionMatchesImpl(thisOffset: Int, other: CharSequence, otherOffset: Int, + length: Int, ignoreCase: Boolean): Boolean { + if ((otherOffset < 0) || (thisOffset < 0) || (thisOffset > this.length - length) + || (otherOffset > other.length - length)) { + return false + } + + for (index in 0 until length) { + if (!this[thisOffset + index].equals(other[otherOffset + index], ignoreCase)) + return false + } + return true +} + +private fun CharSequence.findAnyOf(strings: Collection<String>, startIndex: Int, + ignoreCase: Boolean, last: Boolean): Pair<Int, String>? { + if (!ignoreCase && strings.size == 1) { + val string = strings.single() + val index = if (!last) indexOf(string, startIndex) else lastIndexOf(string, startIndex) + return if (index < 0) null else index to string + } + + val indices = if (!last) startIndex.coerceAtLeast(0)..length else startIndex.coerceAtMost( + lastIndex + ) downTo 0 + + if (this is String) { + for (index in indices) { + val matchingString = strings.firstOrNull { + it.regionMatches( + 0, this, index, it.length, ignoreCase + ) + } + if (matchingString != null) + return index to matchingString + } + } else { + for (index in indices) { + val matchingString = strings.firstOrNull { + it.regionMatchesImpl( + 0, this, index, it.length, ignoreCase + ) + } + if (matchingString != null) + return index to matchingString + } + } + + return null +} + +private fun CharSequence.rangesDelimitedBy(delimiters: Array<out String>, startIndex: Int = 0, + ignoreCase: Boolean = false, + limit: Int = 0): Sequence<IntRange> { + require(limit >= 0, { "Limit must be non-negative, but was $limit." }) + val delimitersList = delimiters.asList() + + return DelimitedRangesSequence( + this, startIndex, limit, { startIndex -> + findAnyOf( + delimitersList, startIndex, ignoreCase = ignoreCase, last = false + )?.let { it.first to it.second.length } + }) +} + +fun CharSequence.splitToSequence(vararg delimiters: String, ignoreCase: Boolean = false, + limit: Int = 0): Sequence<CharSequence> = + rangesDelimitedBy(delimiters, ignoreCase = ignoreCase, limit = limit).map { subSequence(it) } + +fun CharSequence.lineSequence(): Sequence<CharSequence> = splitToSequence("\r\n", "\n", "\r") \ No newline at end of file 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 index 2b3fe8e07a9d6c5967cdf606115dc6ab2620ad68..be95a8de0562e1ccb087ffc890b6197b02221e89 100644 --- 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 @@ -251,6 +251,10 @@ class IrcFormatDeserializer(private val context: Context) { if (colorize) color.apply(plainText, plainText.length) color = null } + if (hexColor != null) { + if (colorize) hexColor.apply(plainText, plainText.length) + hexColor = null + } } else -> { // Just append it, if it’s not special diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/quassel/IrcUserUtils.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/quassel/IrcUserUtils.kt index 738f86ef330dd7c837b7051c9981189ec15c8b98..a259c1e11b054f4b132ed718bdb6b761478cbe09 100644 --- a/app/src/main/java/de/kuschku/quasseldroid_ng/util/quassel/IrcUserUtils.kt +++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/quassel/IrcUserUtils.kt @@ -18,20 +18,20 @@ object IrcUserUtils { fun user(hostmask: String): String { return hostmask.substring( - hostmask.lastIndex('!', hostmask.lastIndex('@')) ?: 0, + (hostmask.lastIndex('!', hostmask.lastIndex('@')) ?: -1) + 1, hostmask.lastIndex('@') ?: hostmask.length ) } fun host(hostmask: String): String { return hostmask.substring( - hostmask.lastIndex('@') ?: 0 + (hostmask.lastIndex('@') ?: -1) + 1 ) } fun mask(hostmask: String): String { return hostmask.substring( - hostmask.lastIndex('!', hostmask.lastIndex('@')) ?: 0 + (hostmask.lastIndex('!', hostmask.lastIndex('@')) ?: -1) + 1 ) } diff --git a/app/src/main/res/layout/layout_editor.xml b/app/src/main/res/layout/layout_editor.xml index a1e6accbbcfbab9cd8b565caaed009919627b1a2..f5ab7962bf76afd293312fca7561e42ce1841e1a 100644 --- a/app/src/main/res/layout/layout_editor.xml +++ b/app/src/main/res/layout/layout_editor.xml @@ -23,7 +23,7 @@ android:background="@android:color/transparent" android:gravity="center_vertical" android:hint="@string/label_placeholder" - android:imeOptions="actionSend|flagNoEnterAction|flagNoExtractUi" + android:imeOptions="flagNoExtractUi" android:inputType="textCapSentences|textAutoCorrect|textShortMessage" android:minHeight="?actionBarSize" android:paddingBottom="8dp" diff --git a/app/src/main/res/values/strings_preferences.xml b/app/src/main/res/values/strings_preferences.xml index 0ba6ff568e637deddf1a7ca1ddedd71115eda9e8..d68f77ebb1541711c16485162dfc18bccd6b48a9 100644 --- a/app/src/main/res/values/strings_preferences.xml +++ b/app/src/main/res/values/strings_preferences.xml @@ -76,6 +76,19 @@ <item>NONE</item> </string-array> + <string name="preference_input_enter_key" translatable="false">input_enter</string> + <string name="preference_input_enter_title">Enter key on keyboard</string> + <string name="preference_input_enter_entry_emoji">Emoji</string> + <string name="preference_input_enter_entry_send">Send</string> + <string-array name="preference_input_enter_entries"> + <item>@string/preference_input_enter_entry_emoji</item> + <item>@string/preference_input_enter_entry_send</item> + </string-array> + <string-array name="preference_input_enter_entryvalues"> + <item>EMOJI</item> + <item>SEND</item> + </string-array> + <string name="preference_hostmask_key" translatable="false">hostmask</string> <string name="preference_hostmask_title">Show Hostmask</string> <string name="preference_hostmask_summary">Display the full nick!ident@host in messages</string> diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 11c9426fe8063a73cec34283378d9d3c8ed4cac4..bffc6e13fdd970ba3a181413b4d78aa83c074e6b 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -37,6 +37,13 @@ android:key="@string/preference_colorize_nicknames_key" android:title="@string/preference_colorize_nicknames_title" /> + <DropDownPreference + android:defaultValue="EMOJI" + android:entries="@array/preference_input_enter_entries" + android:entryValues="@array/preference_input_enter_entryvalues" + android:key="@string/preference_input_enter_key" + android:title="@string/preference_input_enter_title" /> + <DropDownPreference android:defaultValue="HIGHEST" android:entries="@array/preference_show_prefix_entries"