// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only

import QtQuick
// Deliberately imported after QtQuick to avoid missing restoreMode property in Binding. Fix in Qt 6.
import QtQml
import QtQuick.Layouts
import QtQuick.Window
import QtQuick.VirtualKeyboard
import QtQuick.VirtualKeyboard.Styles
import QtQuick.VirtualKeyboard.Settings
import QtQuick.VirtualKeyboard.Plugins
import Qt.labs.folderlistmodel

Item {
    id: keyboard
    objectName: "keyboard"

    property alias style: styleLoader.item
    property alias wordCandidateView: wordCandidateView
    property alias shadowInputControl: shadowInputControl
    property alias alternativeKeys: alternativeKeys
    property alias characterPreview: characterPreview
    property alias wordCandidateContextMenu: wordCandidateContextMenu
    property alias fullScreenModeSelectionControl: fullScreenModeSelectionControl
    property alias naviationHighlight: naviationHighlight
    property alias keyboardInputArea: keyboardInputArea
    property Item activeKey: null
    property TouchPoint activeTouchPoint
    property int localeIndex: -1
    property var availableLocaleIndices: []
    property var availableCustomLocaleIndices: []
    property string locale: localeIndex >= 0 && localeIndex < layoutsModel.count ? layoutsModel.get(localeIndex, "fileName") : ""
    property string inputLocale
    property int defaultLocaleIndex: -1
    readonly property bool latinOnly: InputContext.inputMethodHints & (Qt.ImhLatinOnly | Qt.ImhEmailCharactersOnly | Qt.ImhUrlCharactersOnly)
    readonly property bool preferNumbers: InputContext.inputMethodHints & Qt.ImhPreferNumbers
    readonly property bool dialableCharactersOnly: InputContext.inputMethodHints & Qt.ImhDialableCharactersOnly
    readonly property bool formattedNumbersOnly: InputContext.inputMethodHints & Qt.ImhFormattedNumbersOnly
    readonly property bool digitsOnly: InputContext.inputMethodHints & Qt.ImhDigitsOnly
    property string layout
    property string layoutType: {
        if (keyboard.handwritingMode) return "handwriting"
        if (keyboard.dialableCharactersOnly) return "dialpad"
        if (keyboard.formattedNumbersOnly) return "numbers"
        if (keyboard.digitsOnly) return "digits"
        if (keyboard.symbolMode) return "symbols"
        return "main"
    }
    property bool active: Qt.inputMethod.visible
    property bool handwritingMode
    property bool fullScreenHandwritingMode
    property bool symbolMode
    property bool fullScreenMode: VirtualKeyboardSettings.fullScreenMode
    property var defaultInputMethod: initDefaultInputMethod()
    property var plainInputMethod: PlainInputMethod {}
    property var customInputMethod: null
    property var customInputMethodSharedLayouts: []
    property int defaultInputMode: InputEngine.InputMode.Latin
    property bool inputMethodNeedsReset: true
    property bool inputModeNeedsReset: true
    property bool navigationModeActive: false
    readonly property bool languagePopupListActive: languagePopupList.enabled
    property alias soundEffect: soundEffect
    property alias keyboardLayoutLoader: keyboardLayoutLoader
    property real screenHeight: parent.parent ? parent.parent.height : Screen.height
    property bool noAnimations
    property int pressAndHoldDelay: 500

    function initDefaultInputMethod() {
        try {
            return Qt.createQmlObject('import QtQuick; import QtQuick.VirtualKeyboard.Plugins; DefaultInputMethod {}', keyboard, "defaultInputMethod")
        } catch (e) { }
        return plainInputMethod
    }

    Component.onCompleted: InputContext.priv.registerInputPanel(parent)

    width: keyboardBackground.width
    height: keyboardBackground.height
    onActiveChanged: {
        hideLanguagePopup()
        if (active && symbolMode && !preferNumbers)
            symbolMode = false
        keyboardInputArea.reset()
        wordCandidateViewAutoHideTimer.stop()
    }
    onActiveKeyChanged: {
        if (InputContext.inputEngine.activeKey !== Qt.Key_unknown)
            InputContext.inputEngine.virtualKeyCancel()
    }
    Connections {
        target: VirtualKeyboardSettings
        function onLocaleChanged() {
            updateDefaultLocale()
            localeIndex = defaultLocaleIndex
        }
        function onActiveLocalesChanged() {
            updateDefaultLocale()
            if (!isValidLocale(localeIndex) || VirtualKeyboardSettings.locale)
                localeIndex = defaultLocaleIndex
        }
        function onDefaultInputMethodDisabledChanged() {
            updateInputMethod()
        }
    }
    onAvailableLocaleIndicesChanged: hideLanguagePopup()
    onAvailableCustomLocaleIndicesChanged: hideLanguagePopup()
    onLocaleChanged: {
        hideLanguagePopup()
        inputMethodNeedsReset = true
        inputModeNeedsReset = true
        updateLayout()
    }
    onInputLocaleChanged: {
        if (Qt.locale(inputLocale).name !== "C")
            InputContext.priv.locale = inputLocale
    }
    onLayoutChanged: hideLanguagePopup()
    onLayoutTypeChanged: {
        updateAvailableLocaleIndices()
        updateLayout()
    }
    onLatinOnlyChanged: inputModeNeedsReset = true
    onPreferNumbersChanged: {
        keyboard.symbolMode = !keyboard.handwritingMode && preferNumbers
        inputModeNeedsReset = true
    }
    onDialableCharactersOnlyChanged: inputModeNeedsReset = true
    onFormattedNumbersOnlyChanged: inputModeNeedsReset = true
    onDigitsOnlyChanged: inputModeNeedsReset = true
    onHandwritingModeChanged: if (!keyboard.handwritingMode) keyboard.fullScreenHandwritingMode = false
    onFullScreenHandwritingModeChanged: if (keyboard.fullScreenHandwritingMode) keyboard.handwritingMode = true
    onLanguagePopupListActiveChanged: {
        if (languagePopupListActive && navigationModeActive)
            keyboardInputArea.initialKey = null
    }

    Connections {
        target: InputContext
        function onInputMethodHintsChanged() {
            if (InputContext.priv.focus)
                updateInputMethod()
        }
    }
    Connections {
        target: InputContext.priv
        function onInputItemChanged() {
            keyboard.hideLanguagePopup()
            if (active && symbolMode && !preferNumbers)
                symbolMode = false
        }
        function onFocusChanged() {
            if (InputContext.priv.focus)
                updateInputMethod()
        }
        function onNavigationKeyPressed(key, isAutoRepeat) {
            var initialKey
            var direction = wordCandidateView.effectiveLayoutDirection == Qt.LeftToRight ? 1 : -1
            switch (key) {
            case Qt.Key_Left:
                if (keyboard.navigationModeActive && !keyboardInputArea.initialKey) {
                    if (languagePopupListActive) {
                        hideLanguagePopup()
                        keyboardInputArea.setActiveKey(null)
                        keyboardInputArea.navigateToNextKey(0, 0, false)
                        break
                    }
                    if (alternativeKeys.active) {
                        if (alternativeKeys.listView.currentIndex > 0) {
                            alternativeKeys.listView.decrementCurrentIndex()
                        } else {
                            alternativeKeys.close()
                            keyboardInputArea.setActiveKey(null)
                            keyboardInputArea.navigateToNextKey(0, 0, false)
                        }
                        break
                    }
                    if (functionPopupList.active) {
                        if (functionPopupList.listView.currentIndex > 0) {
                            functionPopupList.listView.decrementCurrentIndex()
                        } else {
                            functionPopupList.close()
                            keyboardInputArea.setActiveKey(null)
                            keyboardInputArea.navigateToNextKey(0, 0, false)
                        }
                        break
                    }
                    if (wordCandidateContextMenu.active) {
                        hideWordCandidateContextMenu()
                        break
                    }
                    if (wordCandidateView.count) {
                        if (wordCandidateView.effectiveLayoutDirection == Qt.LeftToRight &&
                                wordCandidateView.currentIndex > 0) {
                            wordCandidateView.decrementCurrentIndex()
                        } else if (wordCandidateView.effectiveLayoutDirection == Qt.RightToLeft &&
                                   wordCandidateView.currentIndex + 1 < wordCandidateView.count) {
                            wordCandidateView.incrementCurrentIndex()
                        } else {
                            keyboardInputArea.navigateToNextKey(0, 0, false)
                            initialKey = keyboardInputArea.initialKey
                            while (keyboardInputArea.navigateToNextKey(0, 1 * direction, false))
                                initialKey = keyboardInputArea.initialKey
                            while (keyboardInputArea.navigateToNextKey(1, 0, false))
                                initialKey = keyboardInputArea.initialKey
                            keyboardInputArea.initialKey = initialKey
                            keyboardInputArea.navigateToNextKey(0, 0, false)
                        }
                        break
                    }
                }
                initialKey = keyboardInputArea.initialKey
                if (!keyboardInputArea.navigateToNextKey(-1 * direction, 0, false)) {
                    keyboardInputArea.initialKey = initialKey
                    if (!keyboardInputArea.navigateToNextKey(0, -1 * direction, false)) {
                        if (wordCandidateView.count) {
                            wordCandidateView.currentIndex =
                                    wordCandidateView.effectiveLayoutDirection == Qt.LeftToRight ?
                                        (wordCandidateView.count - 1) : 0
                            break
                        }
                        keyboardInputArea.initialKey = initialKey
                        keyboardInputArea.navigateToNextKey(0, -1 * direction, true)
                    }
                    keyboardInputArea.navigateToNextKey(-1 * direction, 0, true)
                }
                break
            case Qt.Key_Up:
                if (languagePopupListActive) {
                    if (languagePopupList.currentIndex > 0) {
                        languagePopupList.decrementCurrentIndex()
                    } else if (languagePopupList.keyNavigationWraps) {
                        languagePopupList.currentIndex = languagePopupList.count - 1
                    } else {
                        hideLanguagePopup()
                        keyboardInputArea.setActiveKey(null)
                        keyboardInputArea.navigateToNextKey(0, 0, false)
                    }
                } else if (alternativeKeys.active) {
                    alternativeKeys.close()
                    keyboardInputArea.setActiveKey(null)
                    keyboardInputArea.navigateToNextKey(0, 0, false)
                } else if (functionPopupList.active) {
                    functionPopupList.close()
                    keyboardInputArea.setActiveKey(null)
                    keyboardInputArea.navigateToNextKey(0, 0, false)
                } else if (wordCandidateContextMenu.active) {
                    if (wordCandidateContextMenuList.currentIndex > 0) {
                        wordCandidateContextMenuList.decrementCurrentIndex()
                    } else if (wordCandidateContextMenuList.keyNavigationWraps && wordCandidateContextMenuList.count > 1) {
                        wordCandidateContextMenuList.currentIndex = wordCandidateContextMenuList.count - 1
                    } else {
                        hideWordCandidateContextMenu()
                    }
                } else if (keyboard.navigationModeActive && !keyboardInputArea.initialKey && wordCandidateView.count) {
                    keyboardInputArea.navigateToNextKey(0, 0, false)
                    initialKey = keyboardInputArea.initialKey
                    if (!keyboardInputArea.navigateToNextKey(0, -1, false)) {
                        keyboardInputArea.initialKey = initialKey
                        keyboardInputArea.navigateToNextKey(0, -1, true)
                    } else {
                        keyboardInputArea.navigateToNextKey(0, 1, false)
                    }
                } else if (!keyboardInputArea.navigateToNextKey(0, -1, !keyboard.navigationModeActive || !keyboardInputArea.initialKey || wordCandidateView.count == 0)) {
                    if (wordCandidateView.currentIndex === -1)
                        wordCandidateView.incrementCurrentIndex()
                }
                break
            case Qt.Key_Right:
                if (keyboard.navigationModeActive && !keyboardInputArea.initialKey) {
                    if (languagePopupListActive) {
                        hideLanguagePopup()
                        keyboardInputArea.setActiveKey(null)
                        keyboardInputArea.navigateToNextKey(0, 0, false)
                        break
                    }
                    if (alternativeKeys.active) {
                        if (alternativeKeys.listView.currentIndex + 1 < alternativeKeys.listView.count) {
                            alternativeKeys.listView.incrementCurrentIndex()
                        } else {
                            alternativeKeys.close()
                            keyboardInputArea.setActiveKey(null)
                            keyboardInputArea.navigateToNextKey(0, 0, false)
                        }
                        break
                    }
                    if (functionPopupList.active) {
                        if (functionPopupList.listView.currentIndex + 1 < functionPopupList.listView.count) {
                            functionPopupList.listView.incrementCurrentIndex()
                        } else {
                            functionPopupList.close()
                            keyboardInputArea.setActiveKey(null)
                            keyboardInputArea.navigateToNextKey(0, 0, false)
                        }
                        break
                    }
                    if (wordCandidateContextMenu.active) {
                        hideWordCandidateContextMenu()
                        break
                    }
                    if (wordCandidateView.count) {
                        if (wordCandidateView.effectiveLayoutDirection == Qt.LeftToRight &&
                                wordCandidateView.currentIndex + 1 < wordCandidateView.count) {
                            wordCandidateView.incrementCurrentIndex()
                        } else if (wordCandidateView.effectiveLayoutDirection == Qt.RightToLeft &&
                                   wordCandidateView.currentIndex > 0) {
                            wordCandidateView.decrementCurrentIndex()
                        } else {
                            keyboardInputArea.navigateToNextKey(0, 0, false)
                            initialKey = keyboardInputArea.initialKey
                            while (keyboardInputArea.navigateToNextKey(0, -1 * direction, false))
                                initialKey = keyboardInputArea.initialKey;
                            while (keyboardInputArea.navigateToNextKey(-1, 0, false))
                                initialKey = keyboardInputArea.initialKey;
                            keyboardInputArea.initialKey = initialKey
                            keyboardInputArea.navigateToNextKey(0, 0, false)
                        }
                        break
                    }
                }
                initialKey = keyboardInputArea.initialKey
                if (!keyboardInputArea.navigateToNextKey(1 * direction, 0, false)) {
                    keyboardInputArea.initialKey = initialKey
                    if (!keyboardInputArea.navigateToNextKey(0, 1 * direction, false)) {
                        if (wordCandidateView.count) {
                            wordCandidateView.currentIndex =
                                    wordCandidateView.effectiveLayoutDirection == Qt.LeftToRight ?
                                        0 : (wordCandidateView.count - 1)
                            break
                        }
                        keyboardInputArea.initialKey = initialKey
                        keyboardInputArea.navigateToNextKey(0, 1 * direction, true)
                    }
                    keyboardInputArea.navigateToNextKey(1 * direction, 0, true)
                }
                break
            case Qt.Key_Down:
                if (languagePopupListActive) {
                    if (languagePopupList.currentIndex + 1 < languagePopupList.count) {
                        languagePopupList.incrementCurrentIndex()
                    } else if (languagePopupList.keyNavigationWraps) {
                        languagePopupList.currentIndex = 0
                    } else {
                        hideLanguagePopup()
                        keyboardInputArea.setActiveKey(null)
                        keyboardInputArea.navigateToNextKey(0, 0, false)
                    }
                } else if (alternativeKeys.active) {
                    alternativeKeys.close()
                    keyboardInputArea.setActiveKey(null)
                    keyboardInputArea.navigateToNextKey(0, 0, false)
                } else if (functionPopupList.active) {
                    functionPopupList.close()
                    keyboardInputArea.setActiveKey(null)
                    keyboardInputArea.navigateToNextKey(0, 0, false)
                } else if (wordCandidateContextMenu.active) {
                    if (wordCandidateContextMenuList.currentIndex + 1 < wordCandidateContextMenuList.count) {
                        wordCandidateContextMenuList.incrementCurrentIndex()
                    } else if (wordCandidateContextMenuList.keyNavigationWraps && wordCandidateContextMenuList.count > 1) {
                        wordCandidateContextMenuList.currentIndex = 0
                    } else {
                        hideWordCandidateContextMenu()
                        keyboardInputArea.setActiveKey(null)
                        keyboardInputArea.navigateToNextKey(0, 0, false)
                    }
                } else if (keyboard.navigationModeActive && !keyboardInputArea.initialKey && wordCandidateView.count) {
                    keyboardInputArea.navigateToNextKey(0, 0, false)
                    initialKey = keyboardInputArea.initialKey
                    if (!keyboardInputArea.navigateToNextKey(0, 1, false)) {
                        keyboardInputArea.initialKey = initialKey
                        keyboardInputArea.navigateToNextKey(0, 1, true)
                    } else {
                        keyboardInputArea.navigateToNextKey(0, -1, false)
                    }
                } else if (!keyboardInputArea.navigateToNextKey(0, 1, !keyboard.navigationModeActive || !keyboardInputArea.initialKey || wordCandidateView.count == 0)) {
                    if (wordCandidateView.currentIndex === -1)
                        wordCandidateView.incrementCurrentIndex()
                }
                break
            case Qt.Key_Return:
                if (!keyboard.navigationModeActive)
                    break
                if (languagePopupListActive) {
                    if (!isAutoRepeat) {
                        languagePopupList.model.selectItem(languagePopupList.currentIndex)
                        keyboardInputArea.reset()
                        keyboardInputArea.navigateToNextKey(0, 0, false)
                    }
                } else if (keyboardInputArea.initialKey) {
                    if (!isAutoRepeat) {
                        pressAndHoldTimer.restart()
                        keyboardInputArea.setActiveKey(keyboardInputArea.initialKey)
                        keyboardInputArea.press(keyboardInputArea.initialKey, true)
                    }
                } else if (!wordCandidateContextMenu.active && wordCandidateView.count > 0) {
                    if (!isAutoRepeat) {
                        pressAndHoldTimer.restart()
                    }
                }
                break
            default:
                break
            }
        }
        function onNavigationKeyReleased(key, isAutoRepeat) {
            switch (key) {
            case Qt.Key_Return:
                if (!keyboard.navigationModeActive) {
                    if (languagePopupListActive)
                        languagePopupList.model.selectItem(languagePopupList.currentIndex)
                    break
                }
                if (isAutoRepeat)
                    break
                if (!languagePopupListActive && !alternativeKeys.active && !functionPopupList.active && !wordCandidateContextMenu.active && keyboard.activeKey) {
                    keyboardInputArea.release(keyboard.activeKey)
                    pressAndHoldTimer.stop()
                    alternativeKeys.close()
                    functionPopupList.close()
                    keyboardInputArea.setActiveKey(null)
                    if (!languagePopupListActive && keyboardInputArea.navigationCursor !== Qt.point(-1, -1))
                        keyboardInputArea.navigateToNextKey(0, 0, false)
                } else if (wordCandidateContextMenu.active) {
                    if (!wordCandidateContextMenu.openedByNavigationKeyLongPress) {
                        wordCandidateContextMenu.selectCurrentItem()
                        keyboardInputArea.navigateToNextKey(0, 0, false)
                    } else {
                        wordCandidateContextMenu.openedByNavigationKeyLongPress = false
                    }
                } else if (alternativeKeys.active) {
                    if (!alternativeKeys.openedByNavigationKeyLongPress) {
                        alternativeKeys.clicked()
                        alternativeKeys.close()
                        keyboardInputArea.navigateToNextKey(0, 0, false)
                        keyboardInputArea.reset()
                    } else {
                        alternativeKeys.openedByNavigationKeyLongPress = false
                    }
                } else if (functionPopupList.active) {
                    if (!functionPopupList.openedByNavigationKeyLongPress) {
                        functionPopupList.clicked()
                        functionPopupList.close()
                        keyboardInputArea.navigateToNextKey(0, 0, false)
                        keyboardInputArea.reset()
                    } else {
                        functionPopupList.openedByNavigationKeyLongPress = false
                    }
                } else if (!wordCandidateContextMenu.active && wordCandidateView.count > 0) {
                    wordCandidateView.model.selectItem(wordCandidateView.currentIndex)
                    if (!InputContext.preeditText.length)
                        keyboardInputArea.navigateToNextKey(0, 1, true)
                }
                break
            default:
                break
            }
        }
    }
    Connections {
        target: InputContext.inputEngine
        function onVirtualKeyClicked(key, text, modifiers, isAutoRepeat) {
            if (isAutoRepeat && keyboard.activeKey)
                soundEffect.play(keyboard.activeKey.soundEffect)
            if (key !== Qt.Key_unknown && keyboardInputArea.dragSymbolMode) {
                keyboardInputArea.dragSymbolMode = false
                keyboard.symbolMode = false
            } else if (key === Qt.Key_Space) {
                var surroundingText = InputContext.surroundingText.trim()
                if (InputContext.priv.shiftHandler.sentenceEndingCharacters.indexOf(surroundingText.charAt(surroundingText.length-1)) >= 0)
                    keyboard.symbolMode = false
            }
        }
    }
    FolderListModel {
        id: layoutsModel
        nameFilters: ["$"]
        folder: VirtualKeyboardSettings.layoutPath
    }
    Connections {
        target: layoutsModel
        function onCountChanged() {
            updateDefaultLocale()
            localeIndex = defaultLocaleIndex
        }
    }
    AlternativeKeys {
        id: alternativeKeys
        objectName: "alternativeKeys"
        // Add some extra margin for decoration
        property real horizontalMargin: style.alternateKeysListItemWidth
        property real verticalMargin: style.alternateKeysListItemHeight
        property rect previewRect: Qt.rect(keyboard.x + alternativeKeys.listView.x - horizontalMargin,
                                           keyboard.y + alternativeKeys.listView.y - verticalMargin,
                                           alternativeKeys.listView.width + horizontalMargin * 2,
                                           alternativeKeys.listView.height + verticalMargin * 2)
        property bool openedByNavigationKeyLongPress
        onVisibleChanged: {
            if (visible)
                InputContext.priv.previewRectangle = Qt.binding(function() {return previewRect})
            else
                openedByNavigationKeyLongPress = false
            InputContext.priv.previewVisible = visible
        }
    }
    FunctionPopupList {
        id: functionPopupList
        property bool openedByNavigationKeyLongPress
    }
    Timer {
        id: pressAndHoldTimer
        interval: keyboard.pressAndHoldDelay
        onTriggered: {
            if (keyboard.activeKey && keyboard.activeKey === keyboardInputArea.initialKey) {
                var origin = keyboard.mapFromItem(activeKey, activeKey.width / 2, 0)
                if (keyboard.activeKey.smallText === "\u2699" &&
                        functionPopupList.open(keyboard.activeKey, origin.x, origin.y)) {
                    InputContext.inputEngine.virtualKeyCancel()
                    keyboardInputArea.initialKey = null
                    functionPopupList.openedByNavigationKeyLongPress = keyboard.navigationModeActive
                } else if (alternativeKeys.open(keyboard.activeKey, origin.x, origin.y)) {
                    InputContext.inputEngine.virtualKeyCancel()
                    keyboardInputArea.initialKey = null
                    alternativeKeys.openedByNavigationKeyLongPress = keyboard.navigationModeActive
                } else if (keyboard.activeKey.key === Qt.Key_Context1 && !keyboard.symbolMode) {
                    InputContext.inputEngine.virtualKeyCancel()
                    keyboardInputArea.dragSymbolMode = true
                    keyboard.symbolMode = true
                    keyboardInputArea.initialKey = null
                    if (keyboardInputArea.navigationCursor !== Qt.point(-1, -1))
                        keyboardInputArea.navigateToNextKey(0, 0, false)
                }
            } else if (keyboardInputArea.dragSymbolMode &&
                       keyboard.activeKey &&
                       keyboard.activeKey.functionKey &&
                       !keyboard.activeKey.repeat) {
                InputContext.inputEngine.virtualKeyCancel()
                keyboardInputArea.click(keyboard.activeKey)
                keyboardInputArea.initialKey = null
                if (keyboardInputArea.navigationCursor !== Qt.point(-1, -1))
                    keyboardInputArea.navigateToNextKey(0, 0, false)
            } else if (!wordCandidateContextMenu.active && keyboard.navigationModeActive) {
                wordCandidateContextMenu.show(wordCandidateView.currentIndex)
                wordCandidateContextMenu.openedByNavigationKeyLongPress = keyboard.navigationModeActive
            }
        }
    }
    Timer {
        id: releaseInaccuracyTimer
        interval: 500
        onTriggered: {
            if (keyboardInputArea.pressed && activeTouchPoint && !alternativeKeys.active && !keyboardInputArea.dragSymbolMode && !functionPopupList.active) {
                var key = keyboardInputArea.keyOnPoint(activeTouchPoint.x, activeTouchPoint.y)
                if (key !== keyboard.activeKey) {
                    InputContext.inputEngine.virtualKeyCancel()
                    keyboardInputArea.setActiveKey(key)
                    keyboardInputArea.press(key, false)
                }
            }
        }
    }
    CharacterPreviewBubble {
        id: characterPreview
        objectName: "characterPreviewBubble"
        active: keyboardInputArea.pressed && !alternativeKeys.active && !functionPopupList.active
        property rect previewRect: Qt.rect(keyboard.x + characterPreview.x,
                                           keyboard.y + characterPreview.y,
                                           characterPreview.width,
                                           characterPreview.height)
    }
    Binding {
        target: InputContext.priv
        property: "previewRectangle"
        value: characterPreview.previewRect
        when: characterPreview.visible
        restoreMode: Binding.RestoreBinding
    }
    Binding {
        target: InputContext.priv
        property: "previewRectangle"
        value: languagePopupList.previewRect
        when: languagePopupListActive
        restoreMode: Binding.RestoreBinding
    }
    Binding {
        target: InputContext.priv
        property: "previewVisible"
        value: characterPreview.visible || languagePopupListActive
        restoreMode: Binding.RestoreBinding
    }
    Loader {
        id: styleLoader
        source: VirtualKeyboardSettings.style
        Binding {
            target: styleLoader.item
            property: "keyboardHeight"
            value: keyboardInnerContainer.height
            restoreMode: Binding.RestoreBinding
        }
    }
    Loader {
        id: naviationHighlight
        objectName: "naviationHighlight"
        property var highlightItem: {
            if (keyboard.navigationModeActive) {
                if (languagePopupListActive) {
                    return languagePopupList.highlightItem
                } else if (keyboardInputArea.initialKey) {
                    return keyboardInputArea.initialKey
                } else if (alternativeKeys.listView.count > 0) {
                    return alternativeKeys.listView.highlightItem
                } else if (functionPopupList.listView.count > 0) {
                    return functionPopupList.listView.highlightItem
                } else if (wordCandidateContextMenu.active) {
                    return wordCandidateContextMenuList.highlightItem
                } else if (wordCandidateView.count > 0) {
                    return wordCandidateView.highlightItem
                }
            }
            return keyboard
        }
        // Note: without "highlightItem.x - highlightItem.x" the binding does not work for alternativeKeys
        property var highlightItemOffset: highlightItem ? keyboard.mapFromItem(highlightItem, highlightItem.x - highlightItem.x, highlightItem.y - highlightItem.y) : ({x:0, y:0})
        property int moveDuration: !keyboard.noAnimations ? 200 : 0
        property int resizeDuration: !keyboard.noAnimations ? 200 : 0
        z: 2
        x: highlightItemOffset.x
        y: highlightItemOffset.y
        width: highlightItem ? highlightItem.width : 0
        height: highlightItem ? highlightItem.height : 0
        visible: keyboard.navigationModeActive && highlightItem !== null && highlightItem !== keyboard
        sourceComponent: keyboard.style.navigationHighlight
        Behavior on x {
            NumberAnimation { id: xAnimation; duration: naviationHighlight.moveDuration; easing.type: Easing.OutCubic }
        }
        Behavior on y {
            NumberAnimation { id: yAnimation; duration: naviationHighlight.moveDuration; easing.type: Easing.OutCubic }
        }
        Behavior on width {
            NumberAnimation { id: widthAnimation; duration: naviationHighlight.resizeDuration; easing.type: Easing.OutCubic }
        }
        Behavior on height {
            NumberAnimation { id: heightAnimation; duration: naviationHighlight.resizeDuration; easing.type: Easing.OutCubic }
        }
    }

    ShadowInputControl {
        id: shadowInputControl
        objectName: "shadowInputControl"
        z: -3
        anchors.left: parent.left
        anchors.right: parent.right
        anchors.bottom: wordCandidateView.top
        height: keyboard.screenHeight -
                keyboard.height -
                wordCandidateView.height
        visible: fullScreenMode && (shadowInputControlVisibleTimer.running || InputContext.animating)

        Connections {
            target: keyboard
            function onActiveChanged() {
                if (keyboard.active)
                    shadowInputControlVisibleTimer.start()
                else
                    shadowInputControlVisibleTimer.stop()
            }
        }

        Timer {
            id: shadowInputControlVisibleTimer
            interval: 2147483647
            repeat: true
        }

        MouseArea {
            onPressed: keyboard.hideLanguagePopup()
            anchors.fill: parent
            enabled: languagePopupList.enabled
        }
    }

    SelectionControl {
        id: fullScreenModeSelectionControl
        objectName: "fullScreenModeSelectionControl"
        inputContext: InputContext.priv.shadow
        anchors.top: shadowInputControl.top
        anchors.left: shadowInputControl.left
        enabled: keyboard.enabled && fullScreenMode
    }

    ListView {
        id: wordCandidateView
        objectName: "wordCandidateView"
        clip: true
        z: -2
        property bool empty: true
        readonly property bool visibleCondition: (((!wordCandidateView.empty || wordCandidateViewAutoHideTimer.running) &&
                                                   InputContext.inputEngine.wordCandidateListVisibleHint) || VirtualKeyboardSettings.wordCandidateList.alwaysVisible) &&
                                                 keyboard.active
        readonly property real visibleYOffset: -height
        readonly property real currentYOffset: visibleCondition ? visibleYOffset : 0
        height: style ? style.selectionListHeight : 0
        anchors.left: parent.left
        anchors.right: parent.right
        spacing: 0
        orientation: ListView.Horizontal
        snapMode: ListView.SnapToItem
        delegate: style.selectionListDelegate
        highlight: style.selectionListHighlight ? style.selectionListHighlight : defaultHighlight
        highlightMoveDuration: 0
        highlightResizeDuration: 0
        add: !keyboard.noAnimations ? style.selectionListAdd : null
        remove: !keyboard.noAnimations ? style.selectionListRemove : null
        keyNavigationWraps: true
        model: InputContext.inputEngine.wordCandidateListModel
        onCurrentItemChanged: if (currentItem) soundEffect.register(currentItem.soundEffect)
        Connections {
            target: wordCandidateView.model ? wordCandidateView.model : null
            function onActiveItemChanged(index) { wordCandidateView.currentIndex = index }
            function onItemSelected() { if (wordCandidateView.currentItem) soundEffect.play(wordCandidateView.currentItem.soundEffect) }
            function onCountChanged() {
                var empty = wordCandidateView.model.count === 0
                if (empty)
                    wordCandidateViewAutoHideTimer.restart()
                else
                    wordCandidateViewAutoHideTimer.stop()
                wordCandidateView.empty = empty
                keyboard.hideWordCandidateContextMenu()
            }
        }
        Connections {
            target: InputContext.priv
            function onInputItemChanged() { wordCandidateViewAutoHideTimer.stop() }
        }
        Connections {
            target: InputContext.inputEngine
            function onWordCandidateListVisibleHintChanged() { wordCandidateViewAutoHideTimer.stop() }
        }
        Timer {
            id: wordCandidateViewAutoHideTimer
            interval: VirtualKeyboardSettings.wordCandidateList.autoHideDelay
        }
        Loader {
            sourceComponent: style.selectionListBackground
            anchors.fill: parent
            z: -1
        }
        Component {
            id: defaultHighlight
            Item {}
        }
        states: [
            State {
                name: "visible"
                when: wordCandidateView.visibleCondition
                PropertyChanges {
                    target: wordCandidateView
                    y: wordCandidateView.visibleYOffset
                }
            },
            State {
                name: "alwaysVisible"
                when: keyboard.fullScreenMode || VirtualKeyboardSettings.wordCandidateList.alwaysVisible
                PropertyChanges {
                    target: wordCandidateView
                    y: wordCandidateView.visibleYOffset
                }
            }
        ]
        transitions: Transition {
            id: wordCandidateViewTransition
            from: ""
            to: "visible"
            enabled: !InputContext.animating && !keyboard.noAnimations
            reversible: true
            ParallelAnimation {
                NumberAnimation {
                    properties: "y"
                    duration: 250
                    easing.type: Easing.InOutQuad
                }
            }
        }

        function longPressItem(index) {
            return keyboard.showWordCandidateContextMenu(index)
        }
    }

    Item {
        id: soundEffect
        property var __sounds: ({})
        property bool available: false

        signal playingChanged(url source, bool playing)

        Connections {
            target: VirtualKeyboardSettings
            function onStyleNameChanged() {
                soundEffect.__sounds = {}
                soundEffect.available = false
            }
        }

        function play(sound) {
            if (enabled && sound != Qt.resolvedUrl("")) {
                var soundId = Qt.md5(sound)
                var multiSoundEffect = __sounds[soundId]
                if (!multiSoundEffect)
                    multiSoundEffect = register(sound)
                if (multiSoundEffect)
                    multiSoundEffect.play()
            }
        }

        function register(sound) {
            var multiSoundEffect = null
            if (enabled && sound != Qt.resolvedUrl("")) {
                var soundId = Qt.md5(sound)
                multiSoundEffect = __sounds[soundId]
                if (!multiSoundEffect) {
                    multiSoundEffect = Qt.createQmlObject('import QtQuick; import QtQuick.VirtualKeyboard; MultiSoundEffect {}', soundEffect)
                    if (multiSoundEffect) {
                        multiSoundEffect.playingChanged.connect(soundEffect.playingChanged)
                        multiSoundEffect.source = sound
                        __sounds[soundId] = multiSoundEffect
                        available = true
                    }
                }
            }
            return multiSoundEffect
        }
    }

    Loader {
        id: keyboardBackground
        z: -1
        anchors.left: parent.left
        anchors.right: parent.right
        anchors.bottom: parent.bottom
        height: keyboardInnerContainer.height
        sourceComponent: style.keyboardBackground

        Item {
            id: keyboardInnerContainer
            z: 1
            width: Math.round(keyboardBackground.width)
            height: style ? Math.round(style.keyboardDesignHeight * width / style.keyboardDesignWidth) : 0
            anchors.horizontalCenter: parent.horizontalCenter
            LayoutMirroring.enabled: false
            LayoutMirroring.childrenInherit: true

            KeyboardObserver {
                id: keyboardObserver

                function scanLayout() {
                    if (keyboardLayoutLoader.item == null)
                        return null

                    return keyboardLayoutLoader.item.scanLayout()
                }
            }

            Component.onCompleted: InputContext.priv.setKeyboardObserver(keyboardObserver)

            onWidthChanged: notifyLayoutChanged()
            onHeightChanged: notifyLayoutChanged()

            Loader {
                id: keyboardLayoutLoader
                objectName: "keyboardLayoutLoader"

                anchors.fill: parent
                anchors.leftMargin: Math.round(style.keyboardRelativeLeftMargin * parent.width)
                anchors.rightMargin: Math.round(style.keyboardRelativeRightMargin * parent.width)
                anchors.topMargin: Math.round(style.keyboardRelativeTopMargin * parent.height)
                anchors.bottomMargin: Math.round(style.keyboardRelativeBottomMargin * parent.height)

                Binding {
                    target: keyboardLayoutLoader
                    property: "source"
                    value: keyboard.layout
                    when: keyboard.width > 0 && keyboard.layout.length > 0
                    restoreMode: Binding.RestoreNone
                }

                onItemChanged: {
                    if (!item)
                        return

                    // Reset input mode if the new layout wants to override it
                    if (item.inputMode !== -1)
                        inputModeNeedsReset = true

                    if (!InputContext.inputEngine.inputMethod)
                        updateInputMethod()

                    notifyLayoutChanged()
                }

                MultiPointTouchArea {
                    id: keyboardInputArea
                    objectName: "keyboardInputArea"

                    property Item initialKey: null
                    property bool dragSymbolMode
                    property real releaseMargin: initialKey !== null ? Math.min(initialKey.width / 3, initialKey.height / 3) : 0
                    property point navigationCursor: Qt.point(-1, -1)

                    anchors.fill: keyboardLayoutLoader

                    Connections {
                        target: keyboardLayoutLoader
                        function onLoaded() {
                            if (keyboard.navigationModeActive &&
                                    keyboardInputArea.navigationCursor !== Qt.point(-1, -1))
                                keyboard.navigationModeActive = keyboardInputArea.navigateToNextKey(0, 0, false)
                        }
                    }
                    Connections {
                        target: keyboard
                        function onNavigationModeActiveChanged() {
                            if (!keyboard.navigationModeActive) {
                                keyboardInputArea.navigationCursor = Qt.point(-1, -1)
                                keyboardInputArea.reset()
                            }
                        }
                    }

                    function press(key, isRealPress) {
                        if (key && key.enabled) {
                            if (!key.noKeyEvent)
                                InputContext.inputEngine.virtualKeyPress(key.key, key.uppercased ? key.text.toUpperCase() : key.text, key.uppercased ? Qt.ShiftModifier : 0, key.repeat && !dragSymbolMode)
                            if (isRealPress)
                                soundEffect.play(key.soundEffect)
                        }
                    }
                    function release(key) {
                        if (key && key.enabled) {
                            if (!key.noKeyEvent)
                                InputContext.inputEngine.virtualKeyRelease(key.key, key.uppercased ? key.text.toUpperCase() : key.text, key.uppercased ? Qt.ShiftModifier : 0)
                            key.clicked()
                        }
                    }
                    function click(key) {
                        if (key && key.enabled) {
                            if (!key.noKeyEvent)
                                InputContext.inputEngine.virtualKeyClick(key.key, InputContext.uppercase ? key.text.toUpperCase() : key.text, InputContext.uppercase ? Qt.ShiftModifier : 0)
                            key.clicked()
                        }
                    }
                    function setActiveKey(activeKey) {
                        if (keyboard.activeKey === activeKey)
                            return
                        if (keyboard.activeKey) {
                            if (keyboard.activeKey.keyType === QtVirtualKeyboard.KeyType.FlickKey)
                                keyboard.activeKey.onKeyChanged.disconnect(onFlickKeyKeyChanged)
                            keyboard.activeKey.active = false
                        }
                        keyboard.activeKey = activeKey
                        if (keyboard.activeKey) {
                            keyboard.activeKey.active = true
                        }
                    }
                    function keyOnPoint(px, py) {
                        var parentItem = keyboardLayoutLoader
                        var child = parentItem.childAt(px, py)
                        while (child !== null) {
                            var position = parentItem.mapToItem(child, px, py)
                            px = position.x; py = position.y
                            parentItem = child
                            child = parentItem.childAt(px, py)
                            if (child && child.key !== undefined)
                                return child
                        }
                        return null
                    }
                    function hitInitialKey(x, y, margin) {
                        if (!initialKey)
                            return false
                        var position = initialKey.mapFromItem(keyboardInputArea, x, y)
                        return (position.x > -margin
                                && position.y > -margin
                                && position.x < initialKey.width + margin
                                && position.y < initialKey.height + margin)
                    }
                    function containsPoint(touchPoints, point) {
                        if (!point)
                            return false
                        for (var i in touchPoints)
                            if (touchPoints[i].pointId == point.pointId)
                                return true
                        return false
                    }
                    function releaseActiveKey() {
                        if (alternativeKeys.active) {
                            alternativeKeys.clicked()
                        } else if (functionPopupList.active) {
                            functionPopupList.clicked()
                        } else if (keyboard.activeKey) {
                            release(keyboard.activeKey)
                        }
                        reset()
                    }
                    function reset() {
                        releaseInaccuracyTimer.stop()
                        pressAndHoldTimer.stop()
                        setActiveKey(null)
                        activeTouchPoint = null
                        alternativeKeys.close()
                        functionPopupList.close()
                        if (dragSymbolMode) {
                            keyboard.symbolMode = false
                            dragSymbolMode = false
                        }
                    }
                    function nextKeyInNavigation(dX, dY, wrapEnabled) {
                        var nextKey = null, x, y, itemOffset
                        if (dX !== 0 || dY !== 0) {
                            var offsetX, offsetY
                            for (offsetX = dX, offsetY = dY;
                                 Math.abs(offsetX) < width && Math.abs(offsetY) < height;
                                 offsetX += dX, offsetY += dY) {
                                x = navigationCursor.x + offsetX
                                if (x < 0) {
                                    if (!wrapEnabled)
                                        break
                                    x += width
                                } else if (x >= width) {
                                    if (!wrapEnabled)
                                        break
                                    x -= width
                                }
                                y = navigationCursor.y + offsetY
                                if (y < 0) {
                                    if (!wrapEnabled)
                                        break
                                    y += height
                                } else if (y >= height) {
                                    if (!wrapEnabled)
                                        break
                                    y -= height
                                }
                                nextKey = keyOnPoint(x, y)
                                if (nextKey) {
                                    // Check if key is visible. Only the visible keys have keyPanelDelegate set.
                                    if (nextKey != initialKey && nextKey.hasOwnProperty("keyPanelDelegate") && nextKey.keyPanelDelegate)
                                        break
                                    // Jump over the item to reduce the number of iterations in this loop
                                    itemOffset = mapToItem(nextKey, x, y)
                                    if (dX > 0)
                                        offsetX += nextKey.width - itemOffset.x
                                    else if (dX < 0)
                                        offsetX -= itemOffset.x
                                    else if (dY > 0)
                                        offsetY += nextKey.height - itemOffset.y
                                    else if (dY < 0)
                                        offsetY -= itemOffset.y
                                }
                                nextKey = null
                            }
                        } else {
                            nextKey = keyOnPoint(navigationCursor.x, navigationCursor.y)
                        }
                        if (nextKey) {
                            itemOffset = mapFromItem(nextKey, nextKey.width / 2, nextKey.height / 2)
                            if (dX) {
                                x = itemOffset.x
                            } else if (dY) {
                                y = itemOffset.y
                            } else {
                                x = itemOffset.x
                                y = itemOffset.y
                            }
                            navigationCursor = Qt.point(x, y)
                        }
                        return nextKey
                    }
                    function navigateToNextKey(dX, dY, wrapEnabled) {
                        // Resolve initial landing point of the navigation cursor
                        if (!keyboard.navigationModeActive || keyboard.navigationCursor === Qt.point(-1, -1)) {
                            if (dX > 0)
                                navigationCursor = Qt.point(0, height / 2)
                            else if (dX < 0)
                                navigationCursor = Qt.point(width, height / 2)
                            else if (dY > 0)
                                navigationCursor = Qt.point(width / 2, 0)
                            else if (dY < 0)
                                navigationCursor = Qt.point(width / 2, height)
                            else
                                navigationCursor = Qt.point(width / 2, height / 2)
                            keyboard.navigationModeActive = true
                        }
                        if (dX && dY) {
                            initialKey = nextKeyInNavigation(dX, 0, wrapEnabled)
                            if (initialKey || wrapEnabled)
                                initialKey = nextKeyInNavigation(0, dY, wrapEnabled)
                        } else {
                            initialKey = nextKeyInNavigation(dX, dY, wrapEnabled)
                        }
                        return initialKey !== null
                    }

                    function onFlickKeyKeyChanged() {
                        InputContext.inputEngine.virtualKeyCancel()
                        press(activeKey, false)
                    }

                    onPressed: (touchPoints) => {
                        keyboard.navigationModeActive = false

                        // Immediately release any pending key that the user might be
                        // holding (and about to release) when a second key is pressed.
                        if (activeTouchPoint)
                            releaseActiveKey();

                        for (var i in touchPoints) {
                            // Release any key pressed by a previous iteration of the loop.
                            if (containsPoint(touchPoints, activeTouchPoint))
                                releaseActiveKey();

                            initialKey = keyOnPoint(touchPoints[i].x, touchPoints[i].y)
                            if (!initialKey)
                                continue
                            activeTouchPoint = touchPoints[i]
                            if (initialKey.keyType === QtVirtualKeyboard.KeyType.FlickKey) {
                                initialKey.press(activeTouchPoint.x, activeTouchPoint.y)
                                initialKey.onKeyChanged.connect(onFlickKeyKeyChanged)
                            } else {
                                releaseInaccuracyTimer.start()
                                pressAndHoldTimer.start()
                            }
                            setActiveKey(initialKey)
                            press(initialKey, true)
                        }
                    }
                    onUpdated: (touchPoints) => {
                        if (!containsPoint(touchPoints, activeTouchPoint))
                            return

                        if (alternativeKeys.active) {
                            alternativeKeys.move(mapToItem(alternativeKeys, activeTouchPoint.x, 0).x)
                        } else if (functionPopupList.active) {
                            functionPopupList.move(mapToItem(functionPopupList, activeTouchPoint.x, activeTouchPoint.y))
                        } else if (activeKey && activeKey.keyType === QtVirtualKeyboard.KeyType.FlickKey) {
                            activeKey.update(activeTouchPoint.x, activeTouchPoint.y)
                        } else {
                            var key = null
                            if (releaseInaccuracyTimer.running) {
                                if (hitInitialKey(activeTouchPoint.x, activeTouchPoint.y, releaseMargin)) {
                                    key = initialKey
                                } else if (initialKey) {
                                    releaseInaccuracyTimer.stop()
                                    initialKey = null
                                }
                            }
                            if (key === null) {
                                key = keyOnPoint(activeTouchPoint.x, activeTouchPoint.y)
                            }
                            if (key !== keyboard.activeKey) {
                                InputContext.inputEngine.virtualKeyCancel()
                                setActiveKey(key)
                                press(key, false)
                                if (dragSymbolMode) {
                                    if (key && key.functionKey && key.key !== Qt.Key_Context1)
                                        pressAndHoldTimer.restart()
                                    else
                                        pressAndHoldTimer.stop()
                                }
                            }
                        }
                    }
                    onReleased: (touchPoints) => {
                        if (containsPoint(touchPoints, activeTouchPoint)) {
                            if (dragSymbolMode) {
                                var key = keyOnPoint(activeTouchPoint.x, activeTouchPoint.y)
                                if (key && key.key === Qt.Key_Context1) {
                                    dragSymbolMode = false
                                    InputContext.inputEngine.virtualKeyCancel()
                                    reset()
                                    return
                                }
                            }
                            releaseActiveKey();
                        }
                    }
                    onCanceled: (touchPoints) => {
                        if (containsPoint(touchPoints, activeTouchPoint))
                            reset()
                    }
                }
            }
        }
    }

    Item {
        id: languagePopup
        z: 1
        anchors.fill: parent
        LayoutMirroring.enabled: false
        LayoutMirroring.childrenInherit: true

        MouseArea {
            onPressed: keyboard.hideLanguagePopup()
            anchors.fill: parent
            enabled: languagePopupList.enabled
        }

        PopupList {
            id: languagePopupList
            objectName: "languagePopupList"
            z: 2
            anchors.left: parent.left
            anchors.top: parent.top
            enabled: false
            model: languageListModel
            delegate: keyboard.style ? keyboard.style.languageListDelegate : null
            highlight: keyboard.style ? keyboard.style.languageListHighlight : defaultHighlight
            add: keyboard.style && !keyboard.noAnimations ? keyboard.style.languageListAdd : null
            remove: keyboard.style && !keyboard.noAnimations ? keyboard.style.languageListRemove : null
            property rect previewRect: Qt.rect(keyboard.x + languagePopupList.x,
                                               keyboard.y + languagePopupList.y,
                                               languagePopupList.width,
                                               languagePopupList.height)
        }

        Loader {
            sourceComponent: keyboard.style.languageListBackground
            anchors.fill: languagePopupList
            z: -1
            visible: languagePopupList.visible
        }

        ListModel {
            id: languageListModel

            function selectItem(index) {
                languagePopupList.currentIndex = index
                keyboard.soundEffect.play(languagePopupList.currentItem.soundEffect)
                changeLanguageTimer.newLocaleIndex = languageListModel.get(index).localeIndex
                changeLanguageTimer.start()
            }
        }

        Timer {
            id: changeLanguageTimer
            interval: 1
            property int newLocaleIndex
            onTriggered: {
                if (languagePopupListActive) {
                    hideLanguagePopup()
                    start()
                } else {
                    localeIndex = newLocaleIndex
                }
            }
        }

        function show(locales, parentItem, customLayoutsOnly) {
            if (!languagePopupList.enabled) {
                languageListModel.clear()
                for (var i = 0; i < locales.length; i++) {
                    languageListModel.append({localeName: locales[i].name, displayName: locales[i].locale.nativeLanguageName, localeIndex: locales[i].index})
                    if (locales[i].index === keyboard.localeIndex)
                        languagePopupList.currentIndex = i
                }
                languagePopupList.positionViewAtIndex(languagePopupList.currentIndex, ListView.Center)
                if (parentItem) {
                    languagePopupList.anchors.leftMargin = Qt.binding(function() {
                        const newLeftMargin = Math.round(keyboard.mapFromItem(parentItem, (parentItem.width - languagePopupList.width) / 2, 0).x)
                        return Math.min(Math.max(0, newLeftMargin), keyboard.width - languagePopupList.width)
                    })
                    languagePopupList.anchors.topMargin = Qt.binding(function() {return Math.round(keyboard.mapFromItem(parentItem, 0, -languagePopupList.height).y)})
                } else {
                    languagePopupList.anchors.leftMargin = Qt.binding(function() {return Math.round((keyboard.width - languagePopupList.width) / 2)})
                    languagePopupList.anchors.topMargin = Qt.binding(function() {return Math.round((keyboard.height - languagePopupList.height) / 2)})
                }
            }
            languagePopupList.enabled = true
        }

        function hide() {
            if (languagePopupList.enabled) {
                languagePopupList.enabled = false
                languagePopupList.anchors.leftMargin = undefined
                languagePopupList.anchors.topMargin = undefined
                languageListModel.clear()
            }
        }
    }

    function showLanguagePopup(parentItem, customLayoutsOnly) {
        var locales = keyboard.listLocales(customLayoutsOnly, parent.externalLanguageSwitchEnabled)
        if (parent.externalLanguageSwitchEnabled) {
            var currentIndex = 0
            for (var i = 0; i < locales.length; i++) {
                if (locales[i] === keyboard.locale) {
                    currentIndex = i
                    break
                }
            }
            parent.externalLanguageSwitch(locales, currentIndex)
            return
        }
        languagePopup.show(locales, parentItem, customLayoutsOnly)
    }

    function hideLanguagePopup() {
        languagePopup.hide()
    }

    MouseArea {
        anchors.left: parent.left
        anchors.right: parent.right
        anchors.bottom: parent.bottom
        height: keyboard.screenHeight
        onPressed: keyboard.hideWordCandidateContextMenu()
        enabled: wordCandidateContextMenuList.enabled
    }

    Item {
        id: wordCandidateContextMenu
        objectName: "wordCandidateContextMenu"
        z: 1
        anchors.fill: parent
        LayoutMirroring.enabled: false
        LayoutMirroring.childrenInherit: true
        property int previousWordCandidateIndex: -1
        readonly property bool active: wordCandidateContextMenuList.visible
        property bool openedByNavigationKeyLongPress

        PopupList {
            id: wordCandidateContextMenuList
            objectName: "wordCandidateContextMenuList"
            z: 2
            anchors.left: parent.left
            anchors.top: parent.top
            enabled: false
            model: wordCandidateContextMenuListModel
            property rect previewRect: Qt.rect(keyboard.x + wordCandidateContextMenuList.x,
                                               keyboard.y + wordCandidateContextMenuList.y,
                                               wordCandidateContextMenuList.width,
                                               wordCandidateContextMenuList.height)
        }

        ListModel {
            id: wordCandidateContextMenuListModel

            function selectItem(index) {
                wordCandidateContextMenu.previousWordCandidateIndex = -1
                wordCandidateContextMenuList.currentIndex = index
                keyboard.soundEffect.play(wordCandidateContextMenuList.currentItem.soundEffect)
                switch (get(index).action) {
                case "remove":
                    wordCandidateView.model.removeItem(wordCandidateView.currentIndex)
                    break
                }
                keyboard.hideWordCandidateContextMenu()
            }
        }

        function show(wordCandidateIndex) {
            if (wordCandidateContextMenu.enabled)
                wordCandidateContextMenu.hide()

            wordCandidateContextMenuListModel.clear()

            var canRemoveSuggestion = wordCandidateView.model.dataAt(wordCandidateIndex, SelectionListModel.Role.CanRemoveSuggestion)
            if (canRemoveSuggestion) {
                var dictionaryType = wordCandidateView.model.dataAt(wordCandidateIndex, SelectionListModel.Role.Dictionary)
                var removeItemText;
                switch (dictionaryType) {
                case SelectionListModel.DictionaryType.User:
                    //~ VirtualKeyboard Context menu for word suggestion if it can be removed from the user dictionary.
                    removeItemText = qsTr("Remove from dictionary")
                    break
                case SelectionListModel.DictionaryType.Default:
                    // Fallthrough
                default:
                    //~ VirtualKeyboard Context menu for word suggestion if it can be removed from the default dictionary.
                    removeItemText = qsTr("Block word")
                    break
                }
                wordCandidateContextMenuListModel.append({action: "remove", display: removeItemText, wordCompletionLength: 0})
            }

            if (wordCandidateContextMenuListModel.count === 0)
                return

            previousWordCandidateIndex = wordCandidateView.currentIndex
            wordCandidateView.currentIndex = wordCandidateIndex

            wordCandidateContextMenuList.anchors.leftMargin = Qt.binding(function() {
                if (!wordCandidateView.currentItem)
                    return 0
                var leftBorder = Math.round(wordCandidateView.mapFromItem(wordCandidateView.currentItem, (wordCandidateView.currentItem.width - wordCandidateContextMenuList.width) / 2, 0).x)
                var rightBorder = Math.round(wordCandidateContextMenuList.parent.width - wordCandidateContextMenuList.width)
                return Math.max(0, Math.min(leftBorder, rightBorder))
            })

            wordCandidateContextMenuList.enabled = true
        }

        function hide() {
            if (wordCandidateContextMenuList.enabled) {
                if (previousWordCandidateIndex !== -1) {
                    wordCandidateView.currentIndex = previousWordCandidateIndex
                    previousWordCandidateIndex = -1
                }
                wordCandidateContextMenuList.enabled = false
                wordCandidateContextMenuList.anchors.leftMargin = undefined
                wordCandidateContextMenuListModel.clear()
            }
            openedByNavigationKeyLongPress = false
        }

        function selectCurrentItem() {
            if (active && wordCandidateContextMenuList.currentIndex !== -1)
                wordCandidateContextMenuListModel.selectItem(wordCandidateContextMenuList.currentIndex)
        }
    }

    function showWordCandidateContextMenu(wordCandidateIndex) {
        wordCandidateContextMenu.show(wordCandidateIndex)
    }

    function hideWordCandidateContextMenu() {
        wordCandidateContextMenu.hide()
    }

    function updateInputMethod() {
        if (!keyboardLayoutLoader.item)
            return
        if (!InputContext.priv.focus)
            return

        // Reset the custom input method if it is not included in the list of shared layouts
        if (customInputMethod && !inputMethodNeedsReset && customInputMethodSharedLayouts.indexOf(layoutType) === -1)
            inputMethodNeedsReset = true

        var customInputMethodToDestroy = null
        if (inputMethodNeedsReset) {
            if (customInputMethod) {
                // Postpones the destruction of the custom input method after creating a new one
                // and after assigning it to the input engine. This allows the input method to clear
                // its state before destroying.
                customInputMethodToDestroy = customInputMethod
                customInputMethod = null
            }
            customInputMethodSharedLayouts = []
            inputMethodNeedsReset = false
        }

        var inputMethod = null
        var inputMode = InputContext.inputEngine.inputMode

        // Use input method from keyboard layout
        if (keyboardLayoutLoader.item.inputMethod) {
            inputMethod = keyboardLayoutLoader.item.inputMethod
        } else if (!customInputMethod) {
            try {
                customInputMethod = keyboardLayoutLoader.item.createInputMethod()
                if (customInputMethod) {
                    // Pull the list of shared layouts from the keyboard layout
                    if (keyboardLayoutLoader.item.sharedLayouts)
                        customInputMethodSharedLayouts = customInputMethodSharedLayouts.concat(keyboardLayoutLoader.item.sharedLayouts)

                    // Make sure the current layout is included in the list
                    if (customInputMethodSharedLayouts.indexOf(layoutType) === -1)
                        customInputMethodSharedLayouts.push(layoutType)

                    // Reset input mode, since inputEngine.inputModes is updated
                    inputModeNeedsReset = true
                }
            } catch (e) {
                console.error(e.message)
            }
        }
        if (!inputMethod) {
            if (customInputMethod) {
                inputMethod = customInputMethod
            } else if (!VirtualKeyboardSettings.defaultInputMethodDisabled) {
                inputMethod = defaultInputMethod
            } else {
                inputMethod = plainInputMethod
            }
        }

        var inputMethodChanged = InputContext.inputEngine.inputMethod !== inputMethod
        if (inputMethodChanged) {
            InputContext.inputEngine.inputMethod = inputMethod
        }

        if (InputContext.inputEngine.inputMethod) {
            var inputModes = InputContext.inputEngine.inputModes
            if (inputModes.length > 0) {
                // Reset to default input mode if the input locale has changed
                if (inputModeNeedsReset) {
                    inputMode = inputModes[0]

                    // Check the current layout for input mode override
                    if (keyboardLayoutLoader.item.inputMode !== -1)
                        inputMode = keyboardLayoutLoader.item.inputMode

                    // Update input mode automatically in handwriting mode
                    if (keyboard.handwritingMode) {
                        if (keyboard.dialableCharactersOnly && inputModes.indexOf(InputEngine.InputMode.Dialable) !== -1)
                            inputMode = InputEngine.InputMode.Dialable
                        else if ((keyboard.formattedNumbersOnly || keyboard.digitsOnly) && inputModes.indexOf(InputEngine.InputMode.Numeric) !== -1)
                            inputMode = InputEngine.InputMode.Numeric
                        else if (keyboardLayoutLoader.item.inputMode === -1)
                            inputMode = inputModes[0]
                    }

                    // Check the input method hints for input mode overrides
                    if (latinOnly)
                        inputMode = InputEngine.InputMode.Latin
                    if (preferNumbers)
                        inputMode = InputEngine.InputMode.Numeric
                }

                // Make sure the input mode is supported by the current input method
                if (inputModes.indexOf(inputMode) === -1)
                    inputMode = inputModes[0]

                if (InputContext.inputEngine.inputMode !== inputMode || inputMethodChanged || inputModeNeedsReset) {
                    InputContext.priv.setKeyboardObserver(keyboardObserver)
                    InputContext.inputEngine.inputMode = inputMode
                }

                inputModeNeedsReset = false
            }
        }

        if (customInputMethodToDestroy !== null)
            customInputMethodToDestroy.destroy()

        // Clear the toggle shift timer
        InputContext.priv.shiftHandler.clearToggleShiftTimer()
    }

    function updateLayout() {
        var newLayout
        newLayout = findLayout(locale, layoutType)
        if (!newLayout.length) {
            newLayout = findLayout(locale, "main")
        }
        layout = newLayout
        inputLocale = locale
        updateInputMethod()
    }

    function updateDefaultLocale() {
        updateAvailableLocaleIndices()
        if (layoutsModel.count > 0) {
            var defaultLocales = []
            if (isValidLocale(VirtualKeyboardSettings.locale))
                defaultLocales.push(VirtualKeyboardSettings.locale)
            if (isValidLocale(InputContext.locale))
                defaultLocales.push(InputContext.locale)
            if (VirtualKeyboardSettings.activeLocales.length > 0 && isValidLocale(VirtualKeyboardSettings.activeLocales[0]))
                defaultLocales.push(VirtualKeyboardSettings.activeLocales[0])
            if (VirtualKeyboardSettings.availableLocales.indexOf("en_GB") !== -1)
                defaultLocales.push("en_GB")
            if (availableLocaleIndices.length > 0)
                defaultLocales.push(layoutsModel.get(availableLocaleIndices[0], "fileName"))
            var newDefaultLocaleIndex = -1
            for (var i = 0; i < defaultLocales.length; i++) {
                newDefaultLocaleIndex = findLocale(defaultLocales[i], -1)
                if (availableLocaleIndices.indexOf(newDefaultLocaleIndex) !== -1)
                    break;
                newDefaultLocaleIndex = -1
            }
            defaultLocaleIndex = newDefaultLocaleIndex
        } else {
            defaultLocaleIndex = -1
        }
    }

    function filterLocaleIndices(filterCb) {
        var localeIndices = []
        for (var i = 0; i < layoutsModel.count; i++) {
            if (localeIndices.indexOf(i) === -1) {
                var localeName = layoutsModel.get(i, "fileName")
                if (filterCb(localeName) && findLayout(localeName, "main"))
                    localeIndices.push(i)
            }
        }
        return localeIndices
    }

    function updateAvailableLocaleIndices() {
        // Update list of all available locales
        var fallbackIndex = findFallbackIndex()
        var newIndices = filterLocaleIndices(function(localeName) {
            return isValidLocale(localeName)
        })

        // Handle case where the VirtualKeyboardSettings.activeLocales contains no valid entries
        // Fetch all locales by ignoring active locales setting
        var ignoreActiveLocales = newIndices.length === 0
        if (ignoreActiveLocales) {
            newIndices = filterLocaleIndices(function(localeName) {
                return isValidLocale(localeName, ignoreActiveLocales)
            })
        }

        // Fetch matching locale names
        var newAvailableLocales = []
        for (var i = 0; i < newIndices.length; i++) {
            newAvailableLocales.push(layoutsModel.get(newIndices[i], "fileName"))
        }

        newAvailableLocales.sort()

        var sortOrder = !ignoreActiveLocales && VirtualKeyboardSettings.activeLocales.length > 0 ?
                    VirtualKeyboardSettings.activeLocales :
                    newAvailableLocales

        newIndices.sort(function(localeIndexA, localeIndexB) {
            var localeNameA = layoutsModel.get(localeIndexA, "fileName")
            var localeNameB = layoutsModel.get(localeIndexB, "fileName")
            var sortIndexA = sortOrder.indexOf(localeNameA)
            var sortIndexB = sortOrder.indexOf(localeNameB)
            return sortIndexA - sortIndexB
        })

        availableLocaleIndices = newIndices
        InputContext.priv.updateAvailableLocales(newAvailableLocales)

        // Update list of custom locale indices
        newIndices = []
        for (i = 0; i < availableLocaleIndices.length; i++) {
            if (availableLocaleIndices[i] === localeIndex ||
                    layoutExists(layoutsModel.get(availableLocaleIndices[i], "fileName"), layoutType))
                newIndices.push(availableLocaleIndices[i])
        }
        availableCustomLocaleIndices = newIndices
    }

    function listLocales(customLayoutsOnly, localeNameOnly) {
        var locales = []
        var localeIndices = customLayoutsOnly ? availableCustomLocaleIndices : availableLocaleIndices
        for (var i = 0; i < localeIndices.length; i++) {
            var layoutFolder = layoutsModel.get(localeIndices[i], "fileName")
            if (localeNameOnly)
                locales.push(layoutFolder)
            else
                locales.push({locale:Qt.locale(layoutFolder), index:localeIndices[i], name:layoutFolder})
        }
        return locales
    }

    function nextLocaleIndex(customLayoutsOnly) {
        var newLocaleIndex = localeIndex
        var localeIndices = customLayoutsOnly ? availableCustomLocaleIndices : availableLocaleIndices
        var i = localeIndices.indexOf(localeIndex)
        if (i !== -1) {
            i = (i + 1) % localeIndices.length
            newLocaleIndex = localeIndices[i]
        }
        return newLocaleIndex
    }

    function changeInputLanguage(customLayoutsOnly) {
        var newLocaleIndex = nextLocaleIndex(customLayoutsOnly)
        if (newLocaleIndex !== -1 && newLocaleIndex !== localeIndex)
            localeIndex = newLocaleIndex
    }

    function canChangeInputLanguage(customLayoutsOnly) {
        if (customLayoutsOnly)
            return availableCustomLocaleIndices.length > 1
        return availableLocaleIndices.length > 1
    }

    function findLocale(localeName, defaultValue) {
        var languageCode = localeName.substring(0, 3) // Including the '_' delimiter
        var languageMatch = -1
        for (var i = 0; i < layoutsModel.count; i++) {
            if (!layoutsModel.isFolder(i))
                continue
            var layoutFolder = layoutsModel.get(i, "fileName")
            if (layoutFolder === localeName)
                return i
            if (languageMatch == -1 && layoutFolder.substring(0, 3) === languageCode)
                languageMatch = i
        }
        return (languageMatch != -1) ? languageMatch : defaultValue
    }

    function findFallbackIndex() {
        for (var i = 0; i < layoutsModel.count; i++) {
            var layoutFolder = layoutsModel.get(i, "fileName")
            if (layoutFolder === "fallback")
                return i
        }
        return -1
    }

    function isValidLocale(localeNameOrIndex, ignoreActiveLocales) {
        var localeName
        if (typeof localeNameOrIndex == "number") {
            if (localeNameOrIndex < 0 || localeNameOrIndex >= layoutsModel.count)
                return false
            localeName = layoutsModel.get(localeNameOrIndex, "fileName")
        } else {
            localeName = localeNameOrIndex
        }

        if (!localeName)
            return false

        if (localeName === "fallback")
            return false

        if (Qt.locale(localeName).name === "C")
            return false

        if (ignoreActiveLocales !== true &&
                VirtualKeyboardSettings.activeLocales.length > 0 &&
                VirtualKeyboardSettings.activeLocales.indexOf(localeName) === -1)
            return false

        return true
    }

    function getLayoutFile(localeName, layoutType) {
        if (localeName === "" || layoutType === "")
            return ""
        return layoutsModel.folder + "/" + localeName + "/" + layoutType + ".qml"
    }

    function getFallbackFile(localeName, layoutType) {
        if (localeName === "" || layoutType === "")
            return ""
        return layoutsModel.folder + "/" + localeName + "/" + layoutType + ".fallback"
    }

    function layoutExists(localeName, layoutType) {
        var result = InputContext.priv.fileExists(getLayoutFile(localeName, layoutType))
        if (!result && layoutType === "handwriting")
            result = InputContext.priv.fileExists(getFallbackFile(localeName, layoutType))
        return result
    }

    function findLayout(localeName, layoutType) {
        var layoutFile = getLayoutFile(localeName, layoutType)
        if (InputContext.priv.fileExists(layoutFile))
            return layoutFile
        var fallbackFile = getFallbackFile(localeName, layoutType)
        if (InputContext.priv.fileExists(fallbackFile)) {
            layoutFile = getLayoutFile("fallback", layoutType)
            if (InputContext.priv.fileExists(layoutFile))
                return layoutFile
        }
        return ""
    }

    function isHandwritingAvailable() {
        if (VirtualKeyboardSettings.handwritingModeDisabled)
            return false
        return VirtualKeyboardFeatures.Handwriting && layoutExists(locale, "handwriting")
    }

    function setHandwritingMode(enabled, resetInputMode) {
        if (VirtualKeyboardSettings.handwritingModeDisabled)
            return
        if (enabled && resetInputMode)
            inputModeNeedsReset = true
        handwritingMode = enabled
    }

    function notifyLayoutChanged() {
        Qt.callLater(function() {
            if (keyboardLayoutLoader.item != null) keyboardObserver.layoutChanged()
        })
    }

    function doKeyboardFunction(keyboardFunction) {
        if (!isKeyboardFunctionAvailable(keyboardFunction))
            return
        switch (keyboardFunction) {
        case QtVirtualKeyboard.KeyboardFunction.HideInputPanel:
            InputContext.priv.hideInputPanel()
            break
        case QtVirtualKeyboard.KeyboardFunction.ChangeLanguage:
            if (style.languagePopupListEnabled) {
                if (!languagePopupListActive) {
                    showLanguagePopup(activeKey, false)
                } else {
                    hideLanguagePopup()
                }
            } else {
                const customLayoutsOnly = arguments.length == 2 && arguments[1]
                changeInputLanguage(customLayoutsOnly)
            }
            break
        case QtVirtualKeyboard.KeyboardFunction.ToggleHandwritingMode:
            setHandwritingMode(!handwritingMode)
            break
        default:
            console.warn("Unknown keyboard function '%1'".arg(keyboardFunction))
            break
        }
    }

    function isKeyboardFunctionAvailable(keyboardFunction) {
        switch (keyboardFunction) {
        case QtVirtualKeyboard.KeyboardFunction.HideInputPanel:
            return true
        case QtVirtualKeyboard.KeyboardFunction.ChangeLanguage:
            const customLayoutsOnly = arguments.length == 2 && arguments[1]
            return canChangeInputLanguage(customLayoutsOnly)
        case QtVirtualKeyboard.KeyboardFunction.ToggleHandwritingMode:
            return isHandwritingAvailable()
        default:
            return false
        }
    }

    function isFunctionPopupListAvailable() {
        const allFunctionKeys = QtVirtualKeyboard.KeyboardFunctionKeys.Hide |
                              QtVirtualKeyboard.KeyboardFunctionKeys.Language
        return (VirtualKeyboardSettings.visibleFunctionKeys & allFunctionKeys) !== allFunctionKeys ||
                isHandwritingAvailable()
    }
}
