Revision control
Copy as Markdown
// 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
}
}
}