diff --git a/CMakeLists.txt b/CMakeLists.txt index 44358bb0..ed33b5fd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -116,6 +116,8 @@ endif() add_library(libradiance ${libradiance_SOURCES}) add_executable(radiance WIN32 src/main.cpp + src/GlslDocument.cpp + src/GlslHighlighter.cpp src/QQuickVideoNodePreview.cpp src/QQuickPreviewAdapter.cpp) diff --git a/resources/qml/ConsoleWidget.qml b/resources/qml/ConsoleWidget.qml index 80b12243..7204aded 100644 --- a/resources/qml/ConsoleWidget.qml +++ b/resources/qml/ConsoleWidget.qml @@ -29,6 +29,21 @@ Item { popOut(); } + Connections { + target: graph.model + onGraphChanged: { + for (var i=0; ia {color: " + RadianceStyle.mainTextHighlightColor + ";}" + str Layout.maximumWidth: parent.width - 20 - } - } - MouseArea { - anchors.fill: parent - onClicked: { - graph.view.tileForVideoNode(videoNode).forceActiveFocus(); + onLinkActivated: { + graph.view.tileForVideoNode(videoNode).consoleLinkClicked(link); + } } } Rectangle { @@ -84,9 +112,7 @@ Item { } MouseArea { anchors.fill: parent - onClicked: { - listModel.remove(index) - } + onClicked: removeMe() } } } diff --git a/resources/qml/EffectNodeTile.qml b/resources/qml/EffectNodeTile.qml index e355c24d..6d7733cf 100644 --- a/resources/qml/EffectNodeTile.qml +++ b/resources/qml/EffectNodeTile.qml @@ -115,6 +115,42 @@ VideoNodeTile { */ } + Loader { + id: editorLoader + property int line; + property int col; + + function makeVisible() { + // Use show instead incase it isn't loaded yet + item.line = line; + item.col = col; + item.open(videoNode.file); + } + + Connections { + target: editorLoader.item + onSaved: { + if (editorLoader.item.file == videoNode.file) { + videoNode.reload(); + reloaded(); + } + } + } + + function show(line, col) { + editorLoader.line = line ? line : 0; + editorLoader.col = col ? col : 0; + if (status == Loader.Ready) { + makeVisible(); + } else if (source == "") { + editorLoader.source = "GlslEditor.qml" + } + } + onLoaded: { + makeVisible(); + } + } + Keys.onPressed: { if (event.modifiers == Qt.NoModifier) { if (event.key == Qt.Key_J) @@ -143,8 +179,11 @@ VideoNodeTile { slider.value = 0.9; else if (event.key == Qt.Key_0) slider.value = 1.0; - else if (event.key == Qt.Key_R) + else if (event.key == Qt.Key_R) { videoNode.reload(); + reloaded(); + } else if (event.key == Qt.Key_E) + editorLoader.show(); } else if (event.modifiers == Qt.ControlModifier) { if (event.key == Qt.Key_QuoteLeft) attachedParameter = -1; @@ -199,4 +238,13 @@ VideoNodeTile { intensity = i; } } + + function consoleLinkClicked(link) { + link = link.split(","); + if (link[0] == "editline") { + var line = link[1]; + var col = link[2]; + editorLoader.show(line, col); + } + } } diff --git a/resources/qml/GlslEditor.qml b/resources/qml/GlslEditor.qml new file mode 100644 index 00000000..071e1342 --- /dev/null +++ b/resources/qml/GlslEditor.qml @@ -0,0 +1,290 @@ +import QtQuick 2.7 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import QtQuick.Layouts 1.2 +import QtQuick.Window 2.1 +import QtQuick.Dialogs 1.2 +import radiance 1.0 +import "." + +ApplicationWindow { + id: window + title: "GLSL Editor" + property string file + property var afterSave + property int line + property int col + signal saved() + + menuBar: MenuBar { + Menu { + title: "&File" + MenuItem { + action: newAction + } + MenuItem { + action: openAction + } + MenuItem { + action: saveAction + } + MenuItem { + action: saveAsAction + } + MenuSeparator { } + MenuItem { + action: revertAction + } + MenuSeparator { } + MenuItem { + action: closeAction + } + } + } + + statusBar: StatusBar { + RowLayout { + Label { text: glslDocument.message } + } + } + + Action { + id: newAction + text: "&New..." + shortcut: "Ctrl+N" + onTriggered: { + tryClear(); + } + } + + Action { + id: openAction + text: "&Open..." + shortcut: "Ctrl+O" + onTriggered: { + showOpenDialog(); + } + } + + Action { + id: saveAction + text: "&Save" + shortcut: "Ctrl+S" + onTriggered: { + if (window.file) { + save(); + } else { + showSaveDialog(); + } + } + } + + Action { + id: saveAsAction + text: "Save &As..." + onTriggered: { + showSaveDialog(); + } + } + + Action { + id: revertAction + text: "Revert" + shortcut: "Ctrl+R" + onTriggered: { + revert(); + } + } + + Action { + id: closeAction + text: "E&xit" + shortcut: "Ctrl+Q" + onTriggered: { + tryClose() + } + } + + onClosing: { + closeAction.trigger(); + close.accepted = false; + } + + function urlToPath(url) { + var path = url.toString(); + // remove prefixed "file://" + path = path.replace(/^(file:\/{2})/,""); + return path; + } + + function showOpenDialog() { + openDialog.folder = glslDocument.loadDirectory(window.file); + openDialog.open(); + } + + function showSaveDialog() { + saveAsDialog.folder = glslDocument.saveDirectory(window.file); + saveAsDialog.open(); + } + + FileDialog { + id: saveAsDialog + title: "Save As" + selectExisting: false + nameFilters: [ "GLSL files (*.glsl)", "All files (*)" ] + onAccepted: { + var path = urlToPath(saveAsDialog.fileUrl); + if (path) { + window.file = glslDocument.contractLibraryPath(path); + save(); + } + afterSave = null; + } + onRejected: { + afterSave = null; + } + } + + FileDialog { + id: openDialog + title: "Open" + nameFilters: [ "GLSL files (*.glsl)", "All files (*)" ] + onAccepted: { + var path = urlToPath(openDialog.fileUrl); + if (path) { + window.file = glslDocument.contractLibraryPath(path); + load(); + } + } + onRejected: { + } + } + + MessageDialog { + id: saveChangesBeforeClose + title: "Save changes?" + icon: StandardIcon.Question + text: window.file ? "Save changes to \"" + window.file + "\" before closing?" : "Save changes before closing?" + standardButtons: StandardButton.Discard | StandardButton.Cancel | StandardButton.Save + onDiscard: { + window.close(); + } + onAccepted: { + afterSave = window.close; + saveAction.trigger(); + } + } + + MessageDialog { + id: saveChangesBeforeClear + title: "Save changes?" + icon: StandardIcon.Question + text: window.file ? "Save changes to \"" + window.file + "\" before clearing?" : "Save changes before clearing?" + standardButtons: StandardButton.Discard | StandardButton.Cancel | StandardButton.Save + onDiscard: { + window.clear(); + } + onAccepted: { + afterSave = window.clear; + saveAction.trigger(); + } + } + + TextArea { + id: textArea + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + textFormat: Qt.RichText + Component.onCompleted: forceActiveFocus() + font.family: "Monospace" + style: TextAreaStyle { + textColor: RadianceStyle.editorTextColor + backgroundColor: RadianceStyle.editorBackgroundColor + } + frameVisible: false + wrapMode: TextEdit.NoWrap + + Keys.onPressed: { + if (event.key == Qt.Key_Tab) { + textArea.remove(textArea.selectionStart, textArea.selectionEnd); + textArea.insert(textArea.cursorPosition, "    "); + event.accepted = true; + } + } + } + + GlslDocument { + id: glslDocument + document: textArea.textDocument + } + + GlslHighlighter { + document: textArea.textDocument + } + + function load() { + if (glslDocument.load(window.file)) { + setCursor(); + } + } + + function setCursor() { + var pos = glslDocument.cursorPositionAt(line, col); + textArea.cursorPosition = pos; + } + + function save() { + if (glslDocument.save(window.file)) { + window.saved(); + if (afterSave) { + afterSave(); + } + } + afterSave = null; + } + + function close() { + window.visible = false; + } + + function clear() { + window.file = "" + glslDocument.clear(); + } + + function open(file) { + if (window.visible && file == window.file) { + setCursor(); + } else { + window.file = file; + if (window.file) { + load(); + } + window.visible = true; + } + window.afterSave = null; + } + + function tryClear() { + if (glslDocument.modified) { + saveChangesBeforeClear.open(); + } else { + clear(); + } + } + + function tryClose() { + if (glslDocument.modified) { + saveChangesBeforeClose.open(); + } else { + close(); + } + } + + function revert() { + glslDocument.revert(window.file); + saved(); // Not actually but effectively + } +} diff --git a/resources/qml/RadianceStyle.qml b/resources/qml/RadianceStyle.qml index 9e7950db..bec9c824 100644 --- a/resources/qml/RadianceStyle.qml +++ b/resources/qml/RadianceStyle.qml @@ -21,6 +21,11 @@ QtObject { readonly property color tileLineColor: Qt.darker(white, 1.5) readonly property color tileLineHighlightColor: Qt.lighter(white, 1.5) + // Colors for the editor + readonly property color editorBackgroundColor: tileBackgroundColor + readonly property color editorTextColor: tileTextColor + readonly property color editorLineColor: tileLineColor + // Colors for UI elements readonly property color sliderKnobColor: gray readonly property color sliderTrackColor: black diff --git a/resources/qml/VideoNodeTile.qml b/resources/qml/VideoNodeTile.qml index c545308e..35f61b2c 100644 --- a/resources/qml/VideoNodeTile.qml +++ b/resources/qml/VideoNodeTile.qml @@ -25,6 +25,7 @@ BaseVideoNodeTile { minInputHeight: normalHeight blockWidth: normalWidth blockHeight: normalHeight + signal reloaded() z: activeFocus ? 1 : 0 diff --git a/src/EffectNode.cpp b/src/EffectNode.cpp index f2c2f099..ae22542c 100644 --- a/src/EffectNode.cpp +++ b/src/EffectNode.cpp @@ -406,7 +406,10 @@ void EffectNodeOpenGLWorker::initialize(QVector sourceCode) { } auto frag = headerString + "\n" + code.join("\n"); if(!program->addShaderFromSourceCode(QOpenGLShader::Fragment, frag)) { - emit error(QString("Could not compile fragment shader:\n") + program->log().trimmed()); + auto log = program->log().trimmed(); + QRegularExpression re("0:(\\d+)\\((\\d+)\\):"); + log.replace(re, "\\1(\\2):"); + emit error(QString("Could not compile fragment shader:\n") + log); p.setNodeState(VideoNode::Broken); return; } diff --git a/src/GlslDocument.cpp b/src/GlslDocument.cpp new file mode 100644 index 00000000..d1c92b12 --- /dev/null +++ b/src/GlslDocument.cpp @@ -0,0 +1,197 @@ +#include "GlslDocument.h" +#include "Paths.h" +#include +#include +#include +#include + +QQuickTextDocument *GlslDocument::document() { + return m_document; +} + +void GlslDocument::setDocument(QQuickTextDocument *document) { + if (document != m_document) { + if (m_document != nullptr) { + disconnect(m_document->textDocument(), NULL, this, NULL); + } + m_document = document; + if (m_document != nullptr) { + connect(m_document->textDocument(), &QTextDocument::modificationChanged, this, &GlslDocument::modifiedChanged); + connect(m_document->textDocument(), &QTextDocument::contentsChanged, this, &GlslDocument::onContentsChanged); + } + emit documentChanged(document); + } +} + +QString GlslDocument::message() { + return m_message; +} + +void GlslDocument::setMessage(QString message) { + if (message != m_message) { + m_message = message; + emit messageChanged(message); + } +} + +bool GlslDocument::load(QString filename) { + if (m_document == nullptr || m_document->textDocument() == nullptr) { + qWarning() << "Cannot call load() with no document set"; + return false; + } + if (filename.isEmpty()) { + qWarning() << "Cannot call load() with no filename set"; + return false; + } + filename = Paths::expandLibraryPath(filename); + + QFile f(filename); + if (!f.open(QFile::ReadOnly | QFile::Text)) { + setMessage(QString("Could not open \"%1\" for reading").arg(filename)); + return false; + } + QTextStream in(&f); + auto doc = m_document->textDocument(); + doc->setPlainText(in.readAll()); + doc->setModified(false); + setMessage(QString("Loaded \"%1\"").arg(filename)); + return true; +} + +bool GlslDocument::save(QString filename) { + if (m_document == nullptr || m_document->textDocument() == nullptr) { + qWarning() << "Cannot call save() with no document set"; + return false; + } + + if (filename.isEmpty()) { + qWarning() << "Cannot call save() with no filename set"; + return false; + } + filename = Paths::ensureUserLibrary(filename); + + QFile f(filename); + if (!f.open(QFile::WriteOnly | QFile::Text)) { + setMessage(QString("Could not open \"%1\" for writing").arg(filename)); + return false; + } + QTextStream out(&f); + auto doc = m_document->textDocument(); + out << doc->toPlainText(); + doc->setModified(false); + setMessage(QString("Saved \"%1\"").arg(filename)); + return true; +} + +void GlslDocument::clear() { + if (m_document == nullptr || m_document->textDocument() == nullptr) { + qWarning() << "Cannot call clear() with no document set"; + return; + } + auto doc = m_document->textDocument(); + doc->clear(); + doc->setModified(false); + setMessage(""); +} + +bool GlslDocument::revert(QString filename) { + if (m_document == nullptr || m_document->textDocument() == nullptr) { + qWarning() << "Cannot call revert() with no document set"; + return false; + } + if (filename.isEmpty()) { + qWarning() << "Cannot call revert() with no filename set"; + return false; + } + + if (QFileInfo(filename).isAbsolute()) { + setMessage(QString("\"%1\" is not a library path").arg(filename)); + return false; + } + auto systemPath = QDir::cleanPath(Paths::systemLibrary() + "/" + filename); + if (!QFileInfo(systemPath).exists()) { + setMessage(QString("\"%1\" not found in system library, nothing to revert to!").arg(filename)); + return false; + } + auto userPath = QDir::cleanPath(Paths::userLibrary() + "/" + filename); + if (!QFileInfo(userPath).exists()) { + setMessage(QString("\"%1\" not found in user library, no changes to revert!").arg(filename)); + return false; + } + if (QFile(userPath).remove()) { + if (load(filename)) { + setMessage(QString("Reverted to system version of \"%1\"").arg(filename)); + return true; + } + return false; + } + setMessage(QString("Could not remove \"%1\"").arg(userPath)); + return true; +} + +bool GlslDocument::modified() { + if (m_document == nullptr || m_document->textDocument() == nullptr) return false; + return m_document->textDocument()->isModified(); +} + +void GlslDocument::onContentsChanged() { + setMessage(""); +} + +QString GlslDocument::loadDirectory(QString filename) { + if (filename.isEmpty()) { + return Paths::userLibrary(); + } + auto p = Paths::expandLibraryPath(filename); + return QFileInfo(p).dir().absolutePath(); +} + +QString GlslDocument::saveDirectory(QString filename) { + if (filename.isEmpty()) { + return Paths::userLibrary(); + } + auto p = Paths::ensureUserLibrary(filename); + return QFileInfo(p).dir().absolutePath(); +} + +QString GlslDocument::contractLibraryPath(QString filename) { + auto p = Paths::contractLibraryPath(filename); + return p; +} + +int GlslDocument::cursorPositionAt(int line, int col) { + if (m_document == nullptr || m_document->textDocument() == nullptr) { + qWarning() << "Cannot call cursorPositionAt() with no document set"; + return false; + } + line--; // Zero-index lines + if (line < 0) return 0; + col--; // Zero-index cols + if (col < 0) col = 0; + + auto text = m_document->textDocument()->toPlainText(); + auto lines = text.split("\n"); + if (lines.count() <= line) { + return text.length(); // Return end of document + } + auto lineText = lines.at(line); + if (lineText.length() < col) col = lineText.length(); + + // Move over columns first + int position = 0; + for (int i=0; i= lineText.length()) break; + } + + // Now move down lines + for (int i=0; i + +class GlslDocument : public QObject { + Q_OBJECT + Q_PROPERTY(QQuickTextDocument *document READ document WRITE setDocument NOTIFY documentChanged) + Q_PROPERTY(QString message READ message WRITE setMessage NOTIFY messageChanged) + Q_PROPERTY(bool modified READ modified NOTIFY modifiedChanged) + +public slots: + bool load(QString filename); + bool save(QString filename); + void clear(); + QQuickTextDocument *document(); + void setDocument(QQuickTextDocument *document); + QString message(); + void setMessage(QString message); + bool modified(); + bool revert(QString filename); + QString loadDirectory(QString filename); + QString saveDirectory(QString filename); + QString contractLibraryPath(QString filename); + int cursorPositionAt(int line, int col); + +signals: + void documentChanged(QQuickTextDocument *document); + void messageChanged(QString message); + void modifiedChanged(bool modified); + +protected: + QQuickTextDocument *m_document{}; + QString m_message; + +protected slots: + void onContentsChanged(); +}; diff --git a/src/GlslHighlighter.cpp b/src/GlslHighlighter.cpp new file mode 100644 index 00000000..88e8aea5 --- /dev/null +++ b/src/GlslHighlighter.cpp @@ -0,0 +1,120 @@ +#include "GlslHighlighter.h" + +// This code largely based off of the Qt syntax highlighter example +// http://doc.qt.io/qt-5/qtwidgets-richtext-syntaxhighlighter-example.html + +GlslHighlighter::GlslHighlighter(QTextDocument *parent) + : QSyntaxHighlighter(parent) +{ + HighlightingRule rule; + + QTextCharFormat singleLineCommentFormat; + singleLineCommentFormat.setForeground(Qt::cyan); + rule.pattern = QRegularExpression("//[^\n]*"); + rule.format = singleLineCommentFormat; + highlightingRules.append(rule); + + multiLineCommentFormat.setForeground(Qt::cyan); + + QTextCharFormat numberFormat; + numberFormat.setForeground(Qt::magenta); + rule.pattern = QRegularExpression("[\\d\\.]+"); + rule.format = numberFormat; + highlightingRules.append(rule); + + QTextCharFormat quotationFormat; + quotationFormat.setForeground(Qt::magenta); + rule.pattern = QRegularExpression("\".*\""); + rule.format = quotationFormat; + highlightingRules.append(rule); + + QTextCharFormat preProcessorFormat; + preProcessorFormat.setForeground(QColor::fromRgb(100, 100, 255)); + rule.pattern = QRegularExpression("^\\s*#[A-Za-z0-9_]+\\s.*"); + rule.format = preProcessorFormat; + highlightingRules.append(rule); + + // Keywords + QTextCharFormat keywordFormat; + keywordFormat.setForeground(Qt::yellow); + QStringList keywords{ + "break", "continue", "do", "for", "while", + "if", "else", + "discard", "return", + }; + foreach (const QString &keyword, keywords) { + rule.pattern = QRegularExpression("\\b" + keyword + "\\b"); + rule.format = keywordFormat; + highlightingRules.append(rule); + } + + // Types + QTextCharFormat typeFormat; + typeFormat.setForeground(Qt::green); + QStringList types{ + "attribute", "const", "uniform", "varying", + "centroid", + "in", "out", "inout", + "float", "int", "void", "bool", "true", "false", + "invariant", + "mat2", "mat3", "mat4", + "mat2x2", "mat2x3", "mat2x4", + "mat3x2", "mat3x3", "mat3x4", + "mat4x2", "mat4x3", "mat4x4", + "vec2", "vec3", "vec4", "ivec2", "ivec3", "ivec4", "bvec2", "bvec3", "bvec4", + "sampler1D", "sampler2D", "sampler3D", "samplerCube", + "sampler1DShadow", "sampler2DShadow", + "struct" + }; + foreach (const QString &type, types) { + rule.pattern = QRegularExpression("\\b" + type + "\\b"); + rule.format = typeFormat; + highlightingRules.append(rule); + } + + commentStartExpression = QRegularExpression("/\\*"); + commentEndExpression = QRegularExpression("\\*/"); +} + +void GlslHighlighter::highlightBlock(const QString &text) { + foreach (const HighlightingRule &rule, highlightingRules) { + QRegularExpressionMatchIterator matchIterator = rule.pattern.globalMatch(text); + while (matchIterator.hasNext()) { + QRegularExpressionMatch match = matchIterator.next(); + setFormat(match.capturedStart(), match.capturedLength(), rule.format); + } + } + setCurrentBlockState(0); + + int startIndex = 0; + if (previousBlockState() != 1) + startIndex = text.indexOf(commentStartExpression); + + while (startIndex >= 0) { + QRegularExpressionMatch match = commentEndExpression.match(text, startIndex); + int endIndex = match.capturedStart(); + int commentLength = 0; + if (endIndex == -1) { + setCurrentBlockState(1); + commentLength = text.length() - startIndex; + } else { + commentLength = endIndex - startIndex + + match.capturedLength(); + } + setFormat(startIndex, commentLength, multiLineCommentFormat); + startIndex = text.indexOf(commentStartExpression, startIndex + commentLength); + } +} + +QQuickTextDocument *GlslHighlighter::qmlDocument() { + return m_document; +} + +void GlslHighlighter::setQmlDocument(QQuickTextDocument *document) { + if (document != m_document) { + m_document = document; + setDocument(m_document->textDocument()); + emit qmlDocumentChanged(document); + } +} + diff --git a/src/GlslHighlighter.h b/src/GlslHighlighter.h new file mode 100644 index 00000000..24bd744b --- /dev/null +++ b/src/GlslHighlighter.h @@ -0,0 +1,37 @@ +#pragma once +#include +#include +#include +#include + +class GlslHighlighter : public QSyntaxHighlighter +{ + Q_OBJECT + Q_PROPERTY(QQuickTextDocument *document READ qmlDocument WRITE setQmlDocument NOTIFY qmlDocumentChanged) + +public: + GlslHighlighter(QTextDocument *parent = nullptr); + +public slots: + QQuickTextDocument *qmlDocument(); + void setQmlDocument(QQuickTextDocument *document); + +signals: + void qmlDocumentChanged(QQuickTextDocument *document); + +protected: + QQuickTextDocument *m_document{}; + void highlightBlock(const QString &text) override; + +private: + struct HighlightingRule + { + QRegularExpression pattern; + QTextCharFormat format; + }; + QVector highlightingRules; + + QTextCharFormat multiLineCommentFormat; + QRegularExpression commentStartExpression; + QRegularExpression commentEndExpression; +}; diff --git a/src/main.cpp b/src/main.cpp index 30ccc52a..ff9290e8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -11,6 +11,8 @@ #include "BaseVideoNodeTile.h" #include "EffectNode.h" #include "FramebufferVideoNodeRender.h" +#include "GlslDocument.h" +#include "GlslHighlighter.h" #include "GraphicalDisplay.h" #include "Model.h" #include "OpenGLWorkerContext.h" @@ -53,6 +55,8 @@ runRadianceGui(QGuiApplication *app) { #endif qmlRegisterType("radiance", 1, 0, "VideoNodePreview"); + qmlRegisterType("radiance", 1, 0, "GlslDocument"); + qmlRegisterType("radiance", 1, 0, "GlslHighlighter"); #ifdef USE_RTMIDI qmlRegisterType("radiance", 1, 0, "MidiController");