Newer
Older
package org.futo.inputmethod.latin.uix
Aleksandras Kostarevas
committed
import android.app.Activity
import android.content.Context
Aleksandras Kostarevas
committed
import android.content.Intent
import android.os.Build
import android.view.View
import android.view.inputmethod.InlineSuggestionsResponse
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
Aleksandras Kostarevas
committed
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import org.futo.inputmethod.latin.LatinIME
import org.futo.inputmethod.latin.SuggestedWords
import org.futo.inputmethod.latin.common.Constants
import org.futo.inputmethod.latin.inputlogic.InputLogic
import org.futo.inputmethod.latin.suggestions.SuggestionStripView
import org.futo.inputmethod.latin.uix.actions.EmojiAction
Aleksandras Kostarevas
committed
import org.futo.inputmethod.latin.uix.settings.SettingsActivity
import org.futo.inputmethod.latin.uix.theme.ThemeOption
import org.futo.inputmethod.latin.uix.theme.UixThemeWrapper
Aleksandras Kostarevas
committed
import org.futo.inputmethod.latin.uix.voiceinput.downloader.DownloadActivity
import org.futo.inputmethod.updates.checkForUpdateAndSaveToPreferences
import org.futo.inputmethod.updates.retrieveSavedLastUpdateCheckResult
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
private class LatinIMEActionInputTransaction(
private val inputLogic: InputLogic,
shouldApplySpace: Boolean
): ActionInputTransaction {
private val isSpaceNecessary: Boolean
init {
val priorText = inputLogic.mConnection.getTextBeforeCursor(1, 0)
isSpaceNecessary = shouldApplySpace && !priorText.isNullOrEmpty() && !priorText.last().isWhitespace()
}
private fun transformText(text: String): String {
return if(isSpaceNecessary) { " $text" } else { text }
}
override fun updatePartial(text: String) {
inputLogic.mConnection.setComposingText(
transformText(text),
1
)
}
override fun commit(text: String) {
inputLogic.mConnection.commitText(
transformText(text),
1
)
}
override fun cancel() {
inputLogic.mConnection.finishComposingText()
}
}
class UixActionKeyboardManager(val uixManager: UixManager, val latinIME: LatinIME) : KeyboardManagerForAction {
override fun getContext(): Context {
return latinIME
}
override fun getLifecycleScope(): LifecycleCoroutineScope {
return latinIME.lifecycleScope
}
override fun triggerContentUpdate() {
uixManager.setContent()
}
override fun createInputTransaction(applySpaceIfNeeded: Boolean): ActionInputTransaction {
return LatinIMEActionInputTransaction(latinIME.inputLogic, applySpaceIfNeeded)
}
override fun typeText(v: String) {
latinIME.latinIMELegacy.onTextInput(v)
}
override fun backspace(amount: Int) {
latinIME.latinIMELegacy.onCodeInput(
Constants.CODE_DELETE,
Constants.NOT_A_COORDINATE,
Constants.NOT_A_COORDINATE, false)
}
override fun closeActionWindow() {
if(uixManager.currWindowActionWindow == null) return
uixManager.returnBackToMainKeyboardViewFromAction()
}
override fun triggerSystemVoiceInput() {
latinIME.latinIMELegacy.onCodeInput(
Constants.CODE_SHORTCUT,
Constants.SUGGESTION_STRIP_COORDINATE,
Constants.SUGGESTION_STRIP_COORDINATE,
false
);
}
override fun updateTheme(newTheme: ThemeOption) {
latinIME.updateTheme(newTheme)
}
override fun sendCodePointEvent(codePoint: Int) {
latinIME.latinIMELegacy.onCodeInput(codePoint,
Constants.NOT_A_COORDINATE,
Constants.NOT_A_COORDINATE, false)
}
override fun sendKeyEvent(keyCode: Int, metaState: Int) {
latinIME.inputLogic.sendDownUpKeyEvent(keyCode, metaState)
}
override fun cursorLeft(steps: Int, stepOverWords: Boolean, select: Boolean) {
latinIME.inputLogic.cursorLeft(steps, stepOverWords, select)
}
override fun cursorRight(steps: Int, stepOverWords: Boolean, select: Boolean) {
latinIME.inputLogic.cursorRight(steps, stepOverWords, select)
}
}
class UixManager(private val latinIME: LatinIME) {
private var shouldShowSuggestionStrip: Boolean = true
private var suggestedWords: SuggestedWords? = null
private var composeView: ComposeView? = null
private var currWindowAction: Action? = null
private var persistentStates: HashMap<Action, PersistentActionState?> = hashMapOf()
private var inlineSuggestions: List<MutableState<View?>> = listOf()
private val keyboardManagerForAction = UixActionKeyboardManager(this, latinIME)
private var mainKeyboardHidden = false
Aleksandras Kostarevas
committed
private var currentNotice: MutableState<ImportantNotice?> = mutableStateOf(null)
var currWindowActionWindow: ActionWindow? = null
val isMainKeyboardHidden get() = mainKeyboardHidden
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
private fun onActionActivated(action: Action) {
latinIME.inputLogic.finishInput()
if (action.windowImpl != null) {
enterActionWindowView(action)
} else if (action.simplePressImpl != null) {
action.simplePressImpl.invoke(keyboardManagerForAction, persistentStates[action])
} else {
throw IllegalStateException("An action must have either a window implementation or a simple press implementation")
}
}
@Composable
private fun MainKeyboardViewWithActionBar() {
Column {
// Don't show suggested words when it's not meant to be shown
val suggestedWordsOrNull = if(shouldShowSuggestionStrip) {
suggestedWords
} else {
null
}
ActionBar(
suggestedWordsOrNull,
latinIME.latinIMELegacy as SuggestionStripView.Listener,
inlineSuggestions = inlineSuggestions,
Aleksandras Kostarevas
committed
onActionActivated = { onActionActivated(it) },
importantNotice = currentNotice.value
)
}
}
private fun enterActionWindowView(action: Action) {
assert(action.windowImpl != null)
mainKeyboardHidden = true
currWindowAction = action
if (persistentStates[action] == null) {
persistentStates[action] = action.persistentState?.let { it(keyboardManagerForAction) }
}
currWindowActionWindow = action.windowImpl?.let { it(keyboardManagerForAction, persistentStates[action]) }
setContent()
}
fun returnBackToMainKeyboardViewFromAction() {
if(currWindowActionWindow == null) return
currWindowActionWindow!!.close()
currWindowAction = null
currWindowActionWindow = null
mainKeyboardHidden = false
latinIME.onKeyboardShown()
setContent()
}
private fun toggleExpandAction() {
mainKeyboardHidden = !mainKeyboardHidden
if(!mainKeyboardHidden) {
latinIME.onKeyboardShown()
}
setContent()
}
@Composable
private fun ActionViewWithHeader(windowImpl: ActionWindow) {
val heightDiv = if(mainKeyboardHidden) {
1
} else {
1.5
}
if(mainKeyboardHidden) {
ActionWindowBar(
onBack = { returnBackToMainKeyboardViewFromAction() },
canExpand = currWindowAction!!.canShowKeyboard,
onExpand = { toggleExpandAction() },
windowName = windowImpl.windowName()
)
}
Box(modifier = Modifier
.fillMaxWidth()
.height(with(LocalDensity.current) {
(latinIME.getInputViewHeight().toFloat() / heightDiv.toFloat()).toDp()
})
windowImpl.WindowContents(keyboardShown = !isMainKeyboardHidden)
}
if(!mainKeyboardHidden) {
val suggestedWordsOrNull = if (shouldShowSuggestionStrip) {
suggestedWords
} else {
null
}
CollapsibleSuggestionsBar(
onCollapse = { toggleExpandAction() },
onClose = { returnBackToMainKeyboardViewFromAction() },
words = suggestedWordsOrNull,
suggestionStripListener = latinIME.latinIMELegacy as SuggestionStripView.Listener,
inlineSuggestions = inlineSuggestions
)
}
}
}
fun setContent() {
composeView?.setContent {
UixThemeWrapper(latinIME.colorScheme) {
Column {
Spacer(modifier = Modifier.weight(1.0f))
Surface(modifier = Modifier.onSizeChanged {
latinIME.updateTouchableHeight(it.height)
}) {
Column {
when {
currWindowActionWindow != null -> ActionViewWithHeader(
currWindowActionWindow!!
)
else -> MainKeyboardViewWithActionBar()
}
latinIME.LegacyKeyboardView(hidden = isMainKeyboardHidden)
}
}
}
}
}
}
Aleksandras Kostarevas
committed
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
suspend fun showUpdateNoticeIfNeeded() {
val updateInfo = retrieveSavedLastUpdateCheckResult(latinIME)
if(updateInfo != null && updateInfo.isNewer()) {
currentNotice.value = object : ImportantNotice {
@Composable
override fun getText(): String {
return "Update available: ${updateInfo.nextVersionString}"
}
override fun onDismiss(context: Context) {
currentNotice.value = null
}
override fun onOpen(context: Context) {
currentNotice.value = null
val intent = Intent(context, SettingsActivity::class.java)
intent.putExtra("navDest", "update")
if(context !is Activity) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(intent)
}
}
}
}
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
fun createComposeView(): View {
if(composeView != null) {
composeView = null
//throw IllegalStateException("Attempted to create compose view, when one is already created!")
}
composeView = ComposeView(latinIME).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setParentCompositionContext(null)
latinIME.setOwners()
}
setContent()
return composeView!!
}
fun getComposeView(): View? {
return composeView
}
fun onColorSchemeChanged() {
setContent()
}
fun onInputFinishing() {
closeActionWindow()
}
fun cleanUpPersistentStates() {
println("Cleaning up persistent states")
for((key, value) in persistentStates.entries) {
if(currWindowAction != key) {
latinIME.lifecycleScope.launch { value?.cleanUp() }
}
}
}
fun closeActionWindow() {
if(currWindowActionWindow == null) return
returnBackToMainKeyboardViewFromAction()
}
fun updateVisibility(shouldShowSuggestionsStrip: Boolean, fullscreenMode: Boolean) {
this.shouldShowSuggestionStrip = shouldShowSuggestionsStrip
setContent()
}
fun setSuggestions(suggestedWords: SuggestedWords?, rtlSubtype: Boolean) {
this.suggestedWords = suggestedWords
setContent()
}
@RequiresApi(Build.VERSION_CODES.R)
fun onInlineSuggestionsResponse(response: InlineSuggestionsResponse): Boolean {
inlineSuggestions = response.inlineSuggestions.map {
latinIME.inflateInlineSuggestion(it)
}
setContent()
return true
}
fun openEmojiKeyboard() {
if(currWindowAction == null) {
onActionActivated(EmojiAction)
}
}