Revision control
++ // Copyright (C) 2022 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++
++ import QtQuick
++ import QtQuick.Controls
++ import QtQuick.Layouts
++ import QQEMLib 1.0
++
++ Item {
++ id: rootItem
++ property alias font: textArea.font
++ property alias text: textArea.text
++ property alias textDocument: textArea.textDocument
++ property alias lineColumn: lineNumbers
++ property alias textArea: textArea
++
++ // 0 = glsl, 1 = qml
++ property int contentType: 0
++ // 0 = QML, 1 = VERT, 2 = FRAG
++ property int editorIndex: 0
++
++ property int rowHeight: Math.ceil(fontMetrics.lineSpacing)
++ property int marginsTop: 0
++ property int marginsBottom: 0
++ property int marginsLeft: 4
++ property int lineCountWidth: 40
++ property bool showLineNumbers: true
++ property bool editable: true
++
++ // Scroll to show lineNumber
++ function scrollToLine(lineNumber) {
++ const lineCount = textArea.lineCount;
++ let pos = lineNumber / lineCount;
++ const padding = codeFlickable.height / 2;
++ const maxCY = codeFlickable.contentHeight - codeFlickable.height;
++ let cY = codeFlickable.contentHeight * pos - padding;
++ cY = Math.max(0, Math.min(maxCY, cY));
++ codeFlickable.contentY = cY;
++ }
++
++ clip: true
++ onWidthChanged: {
++ // This is required so texts are not clipped when resizing the
++ // split views or switching between design/code.
++ textArea.update();
++ }
++
++ Flickable {
++ id: lineNumberFlickable
++ height: parent.height
++ width: showLineNumbers ? lineNumbers.width + 2 * marginsLeft + 1 : 0
++ contentY: codeFlickable.contentY
++ interactive: false
++ visible: showLineNumbers
++ Column {
++ id: lineNumbers
++ anchors.left: parent.left
++ anchors.leftMargin: marginsLeft
++ anchors.topMargin: marginsTop
++ y: marginsTop
++ width: lineCountWidth
++ Repeater {
++ id: repeater
++ model: textArea.text != "" ? textArea.lineCount : 0
++ delegate: Label {
++ // UI combines QML error types
++ readonly property int errorType: (effectManager.effectError.type === 0 || effectManager.effectError.type === 3) ? 0 : effectManager.effectError.type
++ readonly property bool hasError: !editable && editorIndex == errorType && ((index + 1) === effectManager.effectError.line)
++ font: textArea.font
++ width: parent.width
++ horizontalAlignment: Text.AlignRight
++ verticalAlignment: Text.AlignVCenter
++ height: rowHeight
++ text: (index + 1)
++ color: hasError ? "#e04040" : mainView.foregroundColor1
++ }
++ }
++ }
++ Rectangle {
++ id: lineNumbersSeperator
++ y: 4
++ height: Math.max(lineNumbers.height, rootItem.height)
++ anchors.left: lineNumbers.right
++ anchors.leftMargin: marginsLeft
++ width: 1
++ color: mainView.foregroundColor1
++ }
++ }
++
++ Flickable {
++ id: codeFlickable
++ anchors.left: lineNumberFlickable.right
++ anchors.right: parent.right
++ height: parent.height
++ boundsBehavior: Flickable.StopAtBounds
++
++ ScrollBar.vertical: ScrollBar {
++ width: 15
++ active: true
++ }
++ ScrollBar.horizontal: ScrollBar {
++ width: 15
++ active: true
++ }
++
++ FontMetrics {
++ id: fontMetrics
++ font: textArea.font
++ }
++
++ SyntaxHighlighter {
++ id: syntaxHighlighter
++ document: (contentType === 0) ? textArea.textDocument : null
++ }
++
++ // Tries to keep rectangle r area inside the visible area
++ function ensureVisible(r) {
++ if (contentX >= r.x)
++ contentX = r.x;
++ else if (contentX + width <= r.x + r.width)
++ contentX = r.x + r.width - width;
++ if (contentY >= r.y)
++ contentY = r.y;
++ else if (contentY + height <= r.y + r.height)
++ contentY = r.y + r.height - height;
++ }
++
++ TextArea.flickable: TextArea {
++ id: textArea
++
++ property bool stringNotFound: false
++
++ function findAction() {
++ stringNotFound = false;
++ findBar.setFindString(selectedText);
++ }
++
++ // Find and select strings, starting from (optional) position
++ function findNext(findString, position) {
++ const fString = findString.toLowerCase();
++ let startSearchPos = cursorPosition;
++ if (position !== undefined)
++ startSearchPos = position;
++ let seachText = getText(startSearchPos, textArea.length);
++ seachText = seachText.toLowerCase();
++ const pos = seachText.indexOf(fString);
++ if (pos >= 0) {
++ const startPos = startSearchPos + pos;
++ const endPos = startPos + fString.length;
++ select(startPos, endPos);
++ stringNotFound = false;
++ } else if (startSearchPos !== 0) {
++ // If not found, try once for full text (so wrap around)
++ findNext(findString, 0);
++ } else {
++ stringNotFound = true;
++ }
++
++ ensureSelectionVisible();
++ }
++
++ function findPrev(findString, position) {
++ const fString = findString.toLowerCase();
++ let startSearchPos = cursorPosition;
++ if (position !== undefined)
++ startSearchPos = position;
++ let seachText = getText(0, startSearchPos - 1);
++ seachText = seachText.toLowerCase();
++ const pos = seachText.lastIndexOf(fString);
++ if (pos >= 0) {
++ const startPos = pos;
++ const endPos = startPos + fString.length;
++ select(startPos, endPos);
++ stringNotFound = false;
++ } else if (startSearchPos !== textArea.length) {
++ // If not found, try once for full text (so wrap around)
++ findPrev(findString, textArea.length);
++ } else {
++ stringNotFound = true;
++ }
++
++ ensureSelectionVisible();
++ }
++
++ // Keep selection in visible area manually, see QTBUG-105957
++ function ensureSelectionVisible() {
++ // Keep selection in visible area.
++ // First try top-left and then selectionEnd, this way view is as
++ // top & left as possible.
++ var p1 = Qt.rect(0, 0, 0, 0);
++ var p2 = positionToRectangle(textArea.selectionEnd);
++ p2 = addMarginToRect(p2);
++ codeFlickable.ensureVisible(p1);
++ codeFlickable.ensureVisible(p2);
++ codeFlickable.returnToBounds();
++ }
++
++ function addMarginToRect(rect) {
++ const margin = 100;
++ const maxWidth = codeFlickable.contentWidth - codeFlickable.width;
++ const maxHeight = codeFlickable.contentHeight - codeFlickable.height;
++ let marginRect = rect;
++ marginRect.x = Math.max(marginRect.x - margin, 0);
++ marginRect.y = Math.max(marginRect.y - margin, 0);
++ marginRect.width = Math.min(marginRect.width + (2 * margin), maxWidth);
++ marginRect.height = Math.min(marginRect.height + (2 * margin), maxHeight);
++ return marginRect;
++ }
++
++ textFormat: Qt.PlainText
++ focus: false
++ selectByMouse: true
++ leftPadding: rootItem.marginsLeft
++ rightPadding: rootItem.marginsLeft
++ topPadding: rootItem.marginsTop
++ bottomPadding: rootItem.marginsBottom
++ tabStopDistance: fontMetrics.averageCharacterWidth * 4;
++ readOnly: !rootItem.editable
++ // Override the default background
++ background: Item {}
++ font.family: codeFont ? codeFont.font.family : ""
++ font.pointSize: effectManager.settings.codeFontSize
++
++ Keys.onPressed:
++ (event)=> {
++ if (!editable)
++ return;
++
++ if ((event.modifiers & Qt.ControlModifier) && (event.key === Qt.Key_I)) {
++ effectManager.autoIndentCurrentCode(tabBarEditors.currentIndex, textArea.text);
++ event.accepted = true;
++ } else {
++ event.accepted = effectManager.processKey(tabBarEditors.currentIndex, event.key, event.modifiers, textArea);
++ }
++ }
++ MouseArea {
++ anchors.fill: parent
++ onPressed: (mouse)=> {
++ effectManager.codeHelper.setCodeCompletionVisible(false);
++ mouse.accepted = false;
++ }
++ }
++ }
++ }
++
++ ListView {
++ id: codeCompletionListView
++ // Position of cursor inside flickable
++ readonly property point cursorPos: Qt.point(textArea.cursorRectangle.x - codeFlickable.contentX,
++ textArea.cursorRectangle.y - codeFlickable.contentY)
++ // Position of popup, to keep it visible
++ readonly property real popupPosX: Math.min(cursorPos.x, rootItem.width - width);
++ readonly property real popupPosY: cursorPos.y + rowHeight
++ readonly property int maxItems: 10
++ // Show 1 .. maxItems elements in list, depending how much space there is below cursor
++ readonly property int showItems: Math.max(1, Math.min(maxItems, Math.floor((codeFlickable.height - cursorPos.y - 10) / rowHeight)))
++
++ width: 260
++ height: Math.min(rowHeight * showItems, rowHeight * count)
++ x: popupPosX
++ y: popupPosY
++ model: effectManager.codeHelper.codeCompletionModel
++ visible: effectManager.codeHelper.codeCompletionVisible
++ currentIndex: effectManager.codeHelper.codeCompletionModel.currentIndex
++ clip: true
++ delegate: Item {
++ width: codeCompletionListView.width
++ height: rowHeight
++ Rectangle {
++ anchors.fill: parent
++ color: mainView.highlightColor
++ opacity: 0.2
++ visible: codeCompletionListView.currentIndex === model.index
++ }
++ Rectangle {
++ height: parent.height / 2
++ width: height
++ anchors.left: parent.left
++ anchors.leftMargin: height / 2
++ anchors.verticalCenter: parent.verticalCenter
++ border.width: 1
++ border.color: mainView.foregroundColor1
++ color: {
++ let typeColor = "#ffffff";
++ if (model.type === 0)
++ typeColor = "#ff0000";
++ else if (model.type === 1)
++ typeColor = "#00ff00";
++ else if (model.type === 2)
++ typeColor = "#0000ff";
++ return typeColor;
++ }
++ }
++ Text {
++ anchors.verticalCenter: parent.verticalCenter
++ anchors.left: parent.left
++ anchors.leftMargin: rowHeight
++ anchors.right: parent.right
++ anchors.rightMargin: 10
++ elide: Text.ElideRight
++ text: model.name
++ font: textArea.font
++ color: mainView.foregroundColor2
++ }
++ MouseArea {
++ anchors.fill: parent
++ onClicked: {
++ effectManager.codeHelper.codeCompletionModel.currentIndex = model.index;
++ effectManager.codeHelper.applyCodeCompletion(textArea);
++ }
++ }
++ }
++ Rectangle {
++ anchors.fill: parent
++ z: -1
++ color: mainView.backgroundColor1
++ border.width: 1
++ border.color: mainView.foregroundColor1
++ }
++ }
++ }