From cf7c05f79eed05b85677cf9ec6bee6c8b0efc0fd Mon Sep 17 00:00:00 2001 From: Janne Koschinski <janne@kuschku.de> Date: Wed, 28 Feb 2018 16:33:35 +0100 Subject: [PATCH] Added enter key option, improved hostmask option --- .../quasseldroid_ng/ui/chat/ChatActivity.kt | 29 +++- .../chat/messages/QuasselMessageRenderer.kt | 22 ++- .../ui/settings/data/AppearanceSettings.kt | 32 ++++- .../ui/settings/data/Settings.kt | 24 ++-- .../util/helper/SpannedHelper.kt | 129 ++++++++++++++++++ .../util/irc/format/IrcFormatDeserializer.kt | 4 + .../util/quassel/IrcUserUtils.kt | 6 +- app/src/main/res/layout/layout_editor.xml | 2 +- .../main/res/values/strings_preferences.xml | 13 ++ app/src/main/res/xml/preferences.xml | 7 + 10 files changed, 239 insertions(+), 29 deletions(-) create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/SpannedHelper.kt 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 beb5782ac..df74b7053 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 993162c30..8d141bc8e 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 f3f53c065..0419da8de 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 3c820d012..2e2e88574 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 000000000..496344b09 --- /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 2b3fe8e07..be95a8de0 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 738f86ef3..a259c1e11 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 a1e6accbb..f5ab7962b 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 0ba6ff568..d68f77ebb 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 11c9426fe..bffc6e13f 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" -- GitLab