From 8772ae4b6f4a28a9611b2cb873e7953f8b5bad7d Mon Sep 17 00:00:00 2001 From: Janne Koschinski <janne@kuschku.de> Date: Fri, 9 Mar 2018 17:26:00 +0100 Subject: [PATCH] Implement keyboard support for autocomplete --- .../quasseldroid_ng/ui/chat/ChatActivity.kt | 107 ++++++++++++++++-- .../quasseldroid_ng/ui/chat/InputEditor.kt | 20 ++++ .../ui/viewmodel/QuasselViewModel.kt | 10 +- .../util/helper/IntProgressionHelper.kt | 4 + 4 files changed, 124 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/IntProgressionHelper.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 4c21f6583..a43142266 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 @@ -110,10 +110,31 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc } } - private val lastWord = BehaviorSubject.createDefault("") + private val lastWord = BehaviorSubject.createDefault(Pair("", IntRange.EMPTY)) private val textWatcher = object : TextWatcher { override fun afterTextChanged(s: Editable?) { - lastWord.onNext(s?.lastWord(chatline.selectionStart, onlyBeforeCursor = true).toString()) + val previous = autocompletionState + val next = if (previous != null && s != null) { + val suffix = if (previous.range.start == 0) ": " else " " + val end = Math.min( + s.length, previous.range.start + previous.completion.name.length + suffix.length + ) + val sequence = s.subSequence(previous.range.start, end) + if (sequence == previous.completion.name + suffix) { + previous.originalWord to (previous.range.start until end) + } else { + autocompletionState = null + s.lastWordIndices(chatline.selectionStart, onlyBeforeCursor = true)?.let { indices -> + s.substring(indices) to indices + } + } + } else { + s?.lastWordIndices(chatline.selectionStart, onlyBeforeCursor = true)?.let { indices -> + s.substring(indices) to indices + } + } + + if (next != null) lastWord.onNext(next) } override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit @@ -177,11 +198,19 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc }.fold(0, Int::or) chatline.setOnKeyListener { _, keyCode, event -> - if (event.hasNoModifiers() && (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER)) { - send() - true - } else { - false + when (keyCode) { + KeyEvent.KEYCODE_ENTER, + KeyEvent.KEYCODE_NUMPAD_ENTER -> if (event.hasNoModifiers()) { + send() + true + } else { + false + } + KeyEvent.KEYCODE_TAB -> { + autoComplete(event.isShiftPressed) + true + } + else -> false } } @@ -243,6 +272,60 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc editorPanel.panelState = SlidingUpPanelLayout.PanelState.COLLAPSED } + data class AutoCompletionState( + val originalWord: String, + val range: IntRange, + val lastCompletion: AutoCompleteAdapter.AutoCompleteItem? = null, + val completion: AutoCompleteAdapter.AutoCompleteItem + ) + + private var autocompletionState: AutoCompletionState? = null + private fun autoComplete(reverse: Boolean = false) { + val originalWord = lastWord.value + + val previous = autocompletionState + if (!originalWord.second.isEmpty()) { + val autoCompletedWords = viewModel.autoCompleteData.value.orEmpty() + if (previous != null && lastWord.value == previous.originalWord to previous.range) { + val previousIndex = autoCompletedWords.indexOf(previous.completion) + val autoCompletedWord = if (previousIndex != -1) { + val change = if (reverse) -1 else +1 + val newIndex = (previousIndex + change + autoCompletedWords.size) % autoCompletedWords.size + + autoCompletedWords[newIndex] + } else { + autoCompletedWords.firstOrNull() + } + if (autoCompletedWord != null) { + val newState = AutoCompletionState( + previous.originalWord, + originalWord.second, + previous.completion, + autoCompletedWord + ) + inputEditor.autoComplete(newState) + autocompletionState = newState + } else { + autocompletionState = null + } + } else { + val autoCompletedWord = autoCompletedWords.firstOrNull() + if (autoCompletedWord != null) { + val newState = AutoCompletionState( + originalWord.first, + originalWord.second, + null, + autoCompletedWord + ) + inputEditor.autoComplete(newState) + autocompletionState = newState + } else { + autocompletionState = null + } + } + } + } + private fun send() { val text = chatline.text if (text.isNotBlank()) { @@ -300,7 +383,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc } override fun onOptionsItemSelected(item: MenuItem?) = when (item?.itemId) { - android.R.id.home -> { + android.R.id.home -> { drawerToggle.onOptionsItemSelected(item) } @@ -354,7 +437,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc } true } - R.id.clear -> { + R.id.clear -> { handler.post { viewModel.sessionManager { manager -> viewModel.getBuffer().let { buffer -> @@ -369,11 +452,11 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc } true } - R.id.settings -> { + R.id.settings -> { startActivity(Intent(applicationContext, SettingsActivity::class.java)) true } - R.id.disconnect -> { + R.id.disconnect -> { handler.post { sharedPreferences(Keys.Status.NAME, Context.MODE_PRIVATE) { editApply { @@ -387,7 +470,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc } true } - else -> super.onOptionsItemSelected(item) + else -> super.onOptionsItemSelected(item) } override fun onMenuItemClick(item: MenuItem?) = when (item?.itemId) { diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/InputEditor.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/InputEditor.kt index 6eed8d219..82aac52f6 100644 --- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/InputEditor.kt +++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/InputEditor.kt @@ -223,6 +223,26 @@ class InputEditor(private val editText: EditText) { } } + fun autoComplete(item: ChatActivity.AutoCompletionState) { + val suffix = if (item.range.start == 0) ": " else " " + val replacement = "${item.completion.name}$suffix" + val previousReplacement = item.lastCompletion?.let { "${item.lastCompletion.name}$suffix" } + + if (previousReplacement != null && + editText.text.length >= item.range.start + previousReplacement.length && + editText.text.substring( + item.range.start, item.range.start + previousReplacement.length + ) == previousReplacement) { + editText.text.replace( + item.range.start, item.range.start + previousReplacement.length, replacement + ) + editText.setSelection(item.range.start + replacement.length) + } else { + editText.text.replace(item.range.start, item.range.endInclusive + 1, replacement) + editText.setSelection(item.range.start + replacement.length) + } + } + fun share(text: CharSequence?) { editText.setText(text) editText.setSelection(editText.text.length) diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/viewmodel/QuasselViewModel.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/viewmodel/QuasselViewModel.kt index 3759193a6..faf2619f0 100644 --- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/viewmodel/QuasselViewModel.kt +++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/viewmodel/QuasselViewModel.kt @@ -185,18 +185,18 @@ class QuasselViewModel : ViewModel() { } } - val lastWord = MutableLiveData<Observable<String>>() + val lastWord = MutableLiveData<Observable<Pair<String, IntRange>>>() val autoCompleteData: LiveData<List<AutoCompleteAdapter.AutoCompleteItem>?> = session.zip( buffer, lastWord ).switchMapRx { (session, id, lastWordWrapper) -> lastWordWrapper .distinctUntilChanged() - .debounce(300, TimeUnit.MILLISECONDS) + .debounce(16, TimeUnit.MILLISECONDS) .switchMap { lastWord -> val bufferSyncer = session?.bufferSyncer val bufferInfo = bufferSyncer?.bufferInfo(id) - if (bufferSyncer != null && lastWord.length >= 3) { + if (bufferSyncer != null && lastWord.second.length >= 3) { bufferSyncer.liveBufferInfos().switchMap { infos -> if (bufferInfo?.type?.hasFlag( Buffer_Type.ChannelBuffer @@ -270,7 +270,7 @@ class QuasselViewModel : ViewModel() { .filter { it.name.trimStart(*ignoredStartingCharacters) .startsWith( - lastWord.trimStart(*ignoredStartingCharacters), + lastWord.first.trimStart(*ignoredStartingCharacters), ignoreCase = true ) }.sorted() @@ -491,4 +491,4 @@ class QuasselViewModel : ViewModel() { selectedBufferId.postValue(-1) collapsedNetworks.value = emptySet() } -} \ No newline at end of file +} diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/IntProgressionHelper.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/IntProgressionHelper.kt new file mode 100644 index 000000000..28ea37162 --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/IntProgressionHelper.kt @@ -0,0 +1,4 @@ +package de.kuschku.quasseldroid_ng.util.helper + +val IntProgression.length: Int + get() = this.last + 1 - this.first \ No newline at end of file -- GitLab