diff --git a/app/config/config.cpp b/app/config/config.cpp
index 336b1eeb49..7f59d391c9 100644
--- a/app/config/config.cpp
+++ b/app/config/config.cpp
@@ -164,6 +164,18 @@ void Config::SetDefaults()
SetEntryInternal(QStringLiteral("OfflinePixelFormat"), NodeValue::kInt, PixelFormat::F16);
SetEntryInternal(QStringLiteral("MarkerColor"), NodeValue::kInt, ColorCoding::kLime);
+
+ // Code editor
+ SetEntryInternal(QStringLiteral("EditorUseInternal"), NodeValue::kBoolean, true);
+
+ SetEntryInternal(QStringLiteral("EditorExternalCommand"), NodeValue::kText, QString());
+ SetEntryInternal(QStringLiteral("EditorExternalParams"), NodeValue::kText, QString());
+ SetEntryInternal(QStringLiteral("EditorExternalTempFolder"), NodeValue::kText,
+ QStandardPaths::standardLocations( QStandardPaths::TempLocation).at(0));
+ SetEntryInternal(QStringLiteral("EditorInternalFontSize"), NodeValue::kInt, 14);
+ SetEntryInternal(QStringLiteral("EditorInternalIndentSize"), NodeValue::kInt, 3);
+ SetEntryInternal(QStringLiteral("EditorInternalWindowWidth"), NodeValue::kInt, 800);
+ SetEntryInternal(QStringLiteral("EditorInternalWindowHeight"), NodeValue::kInt, 600);
}
void Config::Load()
diff --git a/app/dialog/CMakeLists.txt b/app/dialog/CMakeLists.txt
index f5c628f915..54c0c444fd 100644
--- a/app/dialog/CMakeLists.txt
+++ b/app/dialog/CMakeLists.txt
@@ -17,6 +17,7 @@
add_subdirectory(about)
add_subdirectory(actionsearch)
add_subdirectory(autorecovery)
+add_subdirectory(codeeditor)
add_subdirectory(color)
add_subdirectory(configbase)
add_subdirectory(diskcache)
diff --git a/app/dialog/codeeditor/CMakeLists.txt b/app/dialog/codeeditor/CMakeLists.txt
new file mode 100644
index 0000000000..cb2260979f
--- /dev/null
+++ b/app/dialog/codeeditor/CMakeLists.txt
@@ -0,0 +1,32 @@
+# Olive - Non-Linear Video Editor
+# Copyright (C) 2021 Olive Team
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+set(OLIVE_SOURCES
+ ${OLIVE_SOURCES}
+ dialog/codeeditor/editor.h
+ dialog/codeeditor/editor.cpp
+ dialog/codeeditor/glslhighlighter.h
+ dialog/codeeditor/glslhighlighter.cpp
+ dialog/codeeditor/codeeditordialog.h
+ dialog/codeeditor/codeeditordialog.cpp
+ dialog/codeeditor/externaleditorproxy.h
+ dialog/codeeditor/externaleditorproxy.cpp
+ dialog/codeeditor/searchtextbar.h
+ dialog/codeeditor/searchtextbar.cpp
+ dialog/codeeditor/messagehighlighter.h
+ dialog/codeeditor/messagehighlighter.cpp
+ PARENT_SCOPE
+)
diff --git a/app/dialog/codeeditor/codeeditordialog.cpp b/app/dialog/codeeditor/codeeditordialog.cpp
new file mode 100644
index 0000000000..f3570401b6
--- /dev/null
+++ b/app/dialog/codeeditor/codeeditordialog.cpp
@@ -0,0 +1,162 @@
+#include "codeeditordialog.h"
+#include "searchtextbar.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+
+namespace olive {
+
+CodeEditorDialog::CodeEditorDialog(const QString &start, QWidget* parent) :
+ QDialog(parent)
+{
+ QVBoxLayout* layout = new QVBoxLayout(this);
+
+ // Create text edit widget
+ text_edit_ = new CodeEditor( this);
+ text_edit_->document()->setPlainText(start);
+ text_edit_->gotoLineNumber(1);
+ layout->addWidget(text_edit_);
+
+ // Create buttons
+ QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+ layout->addWidget(buttons);
+ connect(buttons, &QDialogButtonBox::accepted, this, &CodeEditorDialog::accept);
+ connect(buttons, &QDialogButtonBox::rejected, this, &CodeEditorDialog::reject);
+
+ // this does not work on all platforms
+ setWindowFlag( Qt::WindowMaximizeButtonHint, true);
+
+ QMenuBar * menu_bar = new QMenuBar( text_edit_);
+ QMenu * edit_menu = new QMenu(tr("Edit"), text_edit_);
+ QMenu * edit_addSnippet_menu = new QMenu(tr("add input"), text_edit_);
+
+ edit_menu->addMenu( edit_addSnippet_menu);
+ menu_bar->addMenu( edit_menu);
+
+ QAction * add_texture_input = edit_addSnippet_menu->addAction(tr("texture"));
+ QAction * add_color_input = edit_addSnippet_menu->addAction(tr("color"));
+ QAction * add_float_input = edit_addSnippet_menu->addAction(tr("float"));
+ QAction * add_int_input = edit_addSnippet_menu->addAction(tr("integer"));
+ QAction * add_boolean_input = edit_addSnippet_menu->addAction(tr("boolean"));
+ QAction * add_selection_input = edit_addSnippet_menu->addAction(tr("selection"));
+ QAction * add_point_input = edit_addSnippet_menu->addAction(tr("point"));
+
+ connect( add_texture_input, &QAction::triggered, this, &CodeEditorDialog::OnActionAddInputTexture);
+ connect( add_color_input, &QAction::triggered, this, &CodeEditorDialog::OnActionAddInputColor);
+ connect( add_float_input, &QAction::triggered, this, &CodeEditorDialog::OnActionAddInputFloat);
+ connect( add_int_input, &QAction::triggered, this, &CodeEditorDialog::OnActionAddInputInt);
+ connect( add_boolean_input, &QAction::triggered, this, &CodeEditorDialog::OnActionAddInputBoolean);
+ connect( add_selection_input, &QAction::triggered, this, &CodeEditorDialog::OnActionAddInputSelection);
+ connect( add_point_input, &QAction::triggered, this, &CodeEditorDialog::OnActionAddInputPoint);
+
+ search_bar_ = new SearchTextBar( this);
+ search_bar_->hide();
+ layout->addWidget( search_bar_);
+
+ QAction * show_find_dialog = edit_menu->addAction(tr("find/replace"));
+ show_find_dialog->setShortcut( QKeySequence(Qt::CTRL | Qt::Key_F));
+
+ connect( show_find_dialog, &QAction::triggered, this, &CodeEditorDialog::OnFindRequest);
+
+ connect( search_bar_, &SearchTextBar::searchTextForward, text_edit_, &CodeEditor::onSearchForwardRequest);
+ connect( search_bar_, &SearchTextBar::searchTextBackward, text_edit_, &CodeEditor::onSearchBackwardRequest);
+ connect( search_bar_, &SearchTextBar::replaceText, text_edit_, &CodeEditor::onReplaceTextRequest);
+
+ connect( text_edit_, &CodeEditor::textNotFound, search_bar_, &SearchTextBar::onTextNotFound);
+
+ layout->setMenuBar( menu_bar);
+}
+
+void CodeEditorDialog::OnActionAddInputTexture()
+{
+ text_edit_->insertPlainText(
+ "//OVE name: my input\n"
+ "//OVE type: TEXTURE\n"
+ "//OVE flag: NOT_KEYFRAMABLE\n"
+ "//OVE description: \n"
+ "uniform sampler2D my_input;\n");
+}
+
+void CodeEditorDialog::OnActionAddInputColor()
+{
+ text_edit_->insertPlainText(
+ "//OVE name: my color\n"
+ "//OVE type: COLOR\n"
+ "//OVE default: RGBA(0.5, 0.5, 1, 1)\n"
+ "//OVE description: \n"
+ "uniform vec4 my_color;\n");
+}
+
+void CodeEditorDialog::OnActionAddInputFloat()
+{
+ text_edit_->insertPlainText(
+ "//OVE name: my float\n"
+ "//OVE type: FLOAT\n"
+ "//OVE flag: NOT_CONNECTABLE\n"
+ "//OVE min: 0.0\n"
+ "//OVE max: 3.0\n"
+ "//OVE default: 1.0\n"
+ "//OVE description: \n"
+ "uniform float my_float;\n");
+}
+
+void CodeEditorDialog::OnActionAddInputInt()
+{
+ text_edit_->insertPlainText(
+ "//OVE name: my int\n"
+ "//OVE type: INTEGER\n"
+ "//OVE flag: NOT_CONNECTABLE\n"
+ "//OVE min: -10\n"
+ "//OVE max: 10\n"
+ "//OVE default: 0\n"
+ "//OVE description:\n"
+ "uniform int my_int;\n");
+}
+
+void CodeEditorDialog::OnActionAddInputBoolean()
+{
+ text_edit_->insertPlainText(
+ "//OVE name: my boolean\n"
+ "//OVE type: BOOLEAN\n"
+ "//OVE flag: NOT_CONNECTABLE, NOT_KEYFRAMABLE\n"
+ "//OVE default: false\n"
+ "//OVE description: \n"
+ "uniform bool my_bool;\n");
+}
+
+void CodeEditorDialog::OnActionAddInputSelection()
+{
+ text_edit_->insertPlainText(
+ "//OVE name: my selection\n"
+ "//OVE type: SELECTION\n"
+ "//OVE values: \"FIRST CHOICE\", \"SECOND CHOICE\"\n"
+ "//OVE default: 0\n"
+ "//OVE description: \n"
+ "uniform int my_selection;\n");
+}
+
+void CodeEditorDialog::OnActionAddInputPoint()
+{
+ text_edit_->insertPlainText(
+ "//OVE name: my point\n"
+ "//OVE type: POINT\n"
+ "//OVE shape: SQUARE\n"
+ "//OVE default: (0.4, 0.2)\n"
+ "//OVE description: \n"
+ "uniform vec2 my_point;\n");
+}
+
+void CodeEditorDialog::OnFindRequest()
+{
+ QString text_to_find = text_edit_->textCursor().selection().toPlainText();
+ search_bar_->showBar( text_to_find);
+}
+
+
+} // namespace olive
+
diff --git a/app/dialog/codeeditor/codeeditordialog.h b/app/dialog/codeeditor/codeeditordialog.h
new file mode 100644
index 0000000000..dfaa0ba062
--- /dev/null
+++ b/app/dialog/codeeditor/codeeditordialog.h
@@ -0,0 +1,60 @@
+/***
+
+ Olive - Non-Linear Video Editor
+ Copyright (C) 2021 Olive Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+***/
+#ifndef CODEEDITORDIALOG_H
+#define CODEEDITORDIALOG_H
+
+#include
+#include "dialog/codeeditor/editor.h"
+
+namespace olive {
+
+class SearchTextBar;
+
+
+/// @brief Wrapper QDialog window for the code editor
+class CodeEditorDialog : public QDialog
+{
+ Q_OBJECT
+public:
+ CodeEditorDialog(const QString &start, QWidget* parent = nullptr);
+
+ QString text() const
+ {
+ return text_edit_->toPlainText();
+ }
+
+private:
+ CodeEditor* text_edit_;
+ SearchTextBar * search_bar_;
+
+private slots:
+ void OnActionAddInputTexture();
+ void OnActionAddInputColor();
+ void OnActionAddInputFloat();
+ void OnActionAddInputInt();
+ void OnActionAddInputBoolean();
+ void OnActionAddInputSelection();
+ void OnActionAddInputPoint();
+ void OnFindRequest();
+};
+
+} // namespace olive
+
+#endif // CODEEDITORDIALOG_H
diff --git a/app/dialog/codeeditor/editor.cpp b/app/dialog/codeeditor/editor.cpp
new file mode 100644
index 0000000000..a91b10753c
--- /dev/null
+++ b/app/dialog/codeeditor/editor.cpp
@@ -0,0 +1,522 @@
+/***
+
+ Olive - Non-Linear Video Editor
+ Copyright (C) 2021 Olive Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+***/
+
+#include "editor.h"
+#include "config/config.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "glslhighlighter.h"
+
+namespace {
+// matching bracket for each kind of parenthesis
+const QMap BRACKET_PAIR = QMap{{'(', ')'}, {'[', ']'}, {'{', '}'},
+ {')', '('}, {']', '['}, {'}', '{'}};
+
+const Qt::GlobalColor BRACKET_MATCH_COLOR = Qt::darkGreen;
+const Qt::GlobalColor BRACKET_NOT_MATCH_COLOR = Qt::darkRed;
+
+// used in parentheses match algorithm.
+const int BRACKET_LAST = -2;
+
+// forward declarations
+QString buildSearchExpression( const QStringView &str, QTextDocument::FindFlags & flags);
+}
+
+namespace olive {
+
+CodeEditor::CodeEditor(QWidget *parent) :
+ QPlainTextEdit(parent)
+{
+ m_lineNumberArea = new LineNumberArea(this);
+
+ connect( this, SIGNAL(blockCountChanged(int)), this, SLOT(updateLineNumberAreaWidth(int)));
+ connect( this, SIGNAL(updateRequest(QRect,int)), this, SLOT(updateLineNumberArea(QRect,int)));
+ connect( this, SIGNAL(cursorPositionChanged()), this, SLOT(onCursorPositionChanged()));
+
+ updateLineNumberAreaWidth(0);
+ highlightCurrentLine();
+
+ new GlslHighlighter( document());
+
+ setHorizontalScrollBarPolicy( Qt::ScrollBarAsNeeded);
+ setLineWrapMode( QPlainTextEdit::NoWrap);
+
+ setContextMenuPolicy( Qt::DefaultContextMenu);
+
+ setMinimumSize( 900, 600);
+
+ QFont font("Monospace");
+ font.setStyleHint(QFont::TypeWriter); // use monospaced font for all platform
+ QFontMetrics metrics(font);
+ bool valid;
+ int fontSize = Config::Current()["EditorInternalFontSize"].toInt( & valid);
+ fontSize = valid ? fontSize : 14;
+ font.setPointSize( fontSize);
+ int win_width = Config::Current()["EditorInternalWindowWidth"].toInt( & valid);
+ win_width = valid ? win_width : 800;
+ int win_height = Config::Current()["EditorInternalWindowHeight"].toInt( & valid);
+ win_height = valid ? win_height : 600;
+
+ setMinimumSize( win_width, win_height);
+
+ setFont( font);
+
+ indent_size_ = Config::Current()["EditorInternalIndentSize"].toInt( & valid);
+ indent_size_ = (valid && (indent_size_ > 0)) ? indent_size_ : 3;
+ setTabStopDistance( (qreal)(indent_size_) * metrics.horizontalAdvance(' '));
+
+ setStyleSheet("QPlainTextEdit { background-color: black }");
+}
+
+int CodeEditor::lineNumberAreaWidth()
+{
+ int digits = 1;
+ int max = qMax(1, blockCount());
+ while (max >= 10)
+ {
+ max /= 10;
+ ++digits;
+ }
+
+ int space = 3 + fontMetrics().horizontalAdvance(QLatin1Char('9')) * digits;
+
+ return space;
+}
+
+void CodeEditor::gotoLineNumber(int lineNumber)
+{
+ /* block number start from 0 where 'lineNumber' start from 1 */
+ QTextCursor cursor(document()->findBlockByLineNumber(lineNumber - 1));
+ setTextCursor(cursor);
+ onCursorPositionChanged();
+}
+
+void CodeEditor::onSearchBackwardRequest(const QString &text, QTextDocument::FindFlags flags)
+{
+ // try to find backword
+ bool found = findNext( text, flags | QTextDocument::FindBackward);
+
+ if ( ! found)
+ {
+ QTextCursor cursor = textCursor();
+ int currentPosition = cursor.position();
+
+ // search again from the end
+ cursor.movePosition( QTextCursor::End);
+ setTextCursor( cursor);
+
+ found = findNext( text, flags | QTextDocument::FindBackward);
+
+ if ( ! found)
+ {
+ // 'text' is not present. Go back where we were
+ cursor.setPosition( currentPosition );
+ setTextCursor( cursor);
+
+ emit textNotFound();
+ }
+ }
+}
+
+void CodeEditor::onSearchForwardRequest(const QString &text, QTextDocument::FindFlags flags)
+{
+ // try to find forward
+ bool found = findNext( text, flags);
+
+ if ( ! found)
+ {
+ QTextCursor cursor = textCursor();
+ int currentPosition = cursor.position();
+
+ // search again from the begin
+ cursor.movePosition( QTextCursor::Start);
+ setTextCursor( cursor);
+
+ found = findNext( text, flags);
+
+ if ( ! found)
+ {
+ // 'text' is not present. Go back where we were
+ cursor.setPosition( currentPosition );
+ setTextCursor( cursor);
+
+ emit textNotFound();
+ }
+ }
+}
+
+bool CodeEditor::findNext(const QString &text, QTextDocument::FindFlags flags)
+{
+ QString regExpText = buildSearchExpression( text, flags);
+ return find( QRegularExpression(regExpText), flags);
+}
+
+void CodeEditor::onReplaceTextRequest( const QString & src, const QString & dest,
+ QTextDocument::FindFlags flags, bool thenFind)
+{
+ // replace only if selection holds 'src' text
+ if ( src == textCursor().selection().toPlainText()) {
+ textCursor().insertText( dest);
+ }
+
+ if (thenFind == true) {
+ onSearchForwardRequest( src, flags);
+ }
+}
+
+void CodeEditor::updateLineNumberAreaWidth(int /* newBlockCount */)
+{
+ setViewportMargins(lineNumberAreaWidth(), 0, 0, 0);
+}
+
+
+void CodeEditor::updateLineNumberArea(const QRect &rect, int dy)
+{
+ if (dy)
+ {
+ m_lineNumberArea->scroll(0, dy);
+ }
+ else
+ {
+ m_lineNumberArea->update(0, rect.y(), m_lineNumberArea->width(), rect.height());
+ }
+
+ if (rect.contains(viewport()->rect()))
+ {
+ updateLineNumberAreaWidth(0);
+ }
+}
+
+
+void CodeEditor::resizeEvent(QResizeEvent *e)
+{
+ QPlainTextEdit::resizeEvent(e);
+
+ QRect cr = contentsRect();
+ m_lineNumberArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height()));
+}
+
+
+void CodeEditor::onCursorPositionChanged()
+{
+ extra_selections_.clear();
+
+ highlightCurrentLine();
+ matchParenthesis();
+
+ setExtraSelections(extra_selections_);
+}
+
+
+void CodeEditor::highlightCurrentLine()
+{
+ QTextEdit::ExtraSelection selection;
+
+ QColor lineColor = QColor(40,40,0);
+
+ selection.format.setBackground( lineColor);
+ selection.format.setProperty( QTextFormat::FullWidthSelection, true);
+ selection.cursor = textCursor();
+ selection.cursor.clearSelection();
+ extra_selections_.append(selection);
+}
+
+
+void CodeEditor::matchParenthesis()
+{
+ TextBlockData * parenthesis_data = static_cast(textCursor().block().userData());
+
+ if (parenthesis_data) {
+ const QVector & infos = parenthesis_data->parentheses();
+
+ int pos = textCursor().block().position();
+ for (int i = 0; i < infos.size(); ++i) {
+ const ParenthesisInfo & info = infos.at(i);
+
+ int cur_pos = textCursor().position() - textCursor().block().position();
+
+ if ((info.position == (cur_pos)) && (QString("([{").contains(info.character))) {
+ if (matchLeftParenthesis( info.character, textCursor().block(), i + 1, 0)) {
+ createParenthesisSelection(pos + info.position, BRACKET_MATCH_COLOR);
+ } else {
+ createParenthesisSelection(pos + info.position, BRACKET_NOT_MATCH_COLOR);
+ }
+ } else if ((info.position == (cur_pos - 1)) && (QString(")]}").contains(info.character))) {
+ if (matchRightParenthesis( info.character, textCursor().block(), i - 1, 0)) {
+ createParenthesisSelection(pos + info.position, BRACKET_MATCH_COLOR);
+ } else {
+ createParenthesisSelection(pos + info.position, BRACKET_NOT_MATCH_COLOR);
+ }
+ }
+ }
+ }
+}
+
+
+// search for a right (closing) bracket that matches a given left (opening) bracket.
+// The search is performed in the 'currentBlock' or recursively in next one.
+// 'numLeftParentheses' is the counter of additional left brackets found in previous calls.
+// Param 'i' (index) is used when the search does not start from the begin of the block: in this case,
+// we don't have to search brackets after the current cursor position.
+//
+bool CodeEditor::matchLeftParenthesis( char left, QTextBlock currentBlock, int i, int numLeftParentheses)
+{
+ TextBlockData * parenthesis_data = static_cast(currentBlock.userData());
+ const QVector & infos = parenthesis_data->parentheses();
+
+ bool found_matching = false;
+ int doc_pos = currentBlock.position();
+
+ for (; (i < infos.size()) && ( ! found_matching); ++i) {
+ const ParenthesisInfo & info = infos.at(i);
+
+ if (info.character == left) {
+ // found another opening bracket
+ ++numLeftParentheses;
+
+ } else if (info.character == BRACKET_PAIR.value(left)) {
+
+ if (numLeftParentheses == 0) {
+ //found matching closing bracket
+ createParenthesisSelection(doc_pos + info.position, BRACKET_MATCH_COLOR);
+ found_matching = true;
+ } else {
+ //found closing bracket, but not the correct one
+ --numLeftParentheses;
+ }
+ }
+ }
+
+ if (found_matching == false) {
+ currentBlock = currentBlock.next();
+ if (currentBlock.isValid()) {
+ found_matching = matchLeftParenthesis( left, currentBlock, 0, numLeftParentheses);
+ }
+ }
+
+ return found_matching;
+}
+
+// search for a left (opening) bracket that matches a given right (closing) bracket.
+// The search is performed in the 'currentBlock' or recursively in previous one.
+// 'numRightParentheses' is the counter of additional right brackets found in previous calls.
+// Param 'i' (index) is used when the search does not start from the end of the block: in this case,
+// we don't have to search brackets before the current cursor position. Value "BRACKET_LAST" means
+// to start from last item
+//
+bool CodeEditor::matchRightParenthesis(char right, QTextBlock currentBlock, int i, int numRightParentheses)
+{
+ TextBlockData * parenthesis_data = static_cast(currentBlock.userData());
+ const QVector & parentheses = parenthesis_data->parentheses();
+
+ bool found_matching = false;
+ int doc_pos = currentBlock.position();
+
+ if ((i == BRACKET_LAST) && (parentheses.size() > 0)){
+ // start from last item
+ i = parentheses.size() - 1;
+ }
+
+ for (; (i > -1) && (parentheses.size() > 0) && ( ! found_matching); --i) {
+ const ParenthesisInfo & info = parentheses.at(i);
+
+ if (info.character == right) {
+ // found another closing bracket
+ ++numRightParentheses;
+
+ } else if (info.character == BRACKET_PAIR.value(right)) {
+
+ if (numRightParentheses == 0) {
+ //found matching opening bracket
+ createParenthesisSelection(doc_pos + info.position, BRACKET_MATCH_COLOR);
+ found_matching = true;
+ } else {
+ // found opening bracket, but not the correct one
+ --numRightParentheses;
+ }
+ }
+ }
+
+ if (found_matching == false) {
+ currentBlock = currentBlock.previous();
+ if (currentBlock.isValid()) {
+ found_matching = matchRightParenthesis( right, currentBlock, BRACKET_LAST, numRightParentheses);
+ }
+ }
+
+ return found_matching;
+}
+
+
+void CodeEditor::createParenthesisSelection(int pos, Qt::GlobalColor color)
+{
+ QTextEdit::ExtraSelection selection;
+ QTextCharFormat format = selection.format;
+ format.setBackground(color);
+ selection.format = format;
+
+ QTextCursor cursor = textCursor();
+ cursor.setPosition(pos);
+ cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
+ selection.cursor = cursor;
+
+ extra_selections_.append(selection);
+}
+
+
+void CodeEditor::lineNumberAreaPaintEvent(QPaintEvent *event)
+{
+ QPainter painter(m_lineNumberArea);
+ painter.fillRect(event->rect(), Qt::lightGray);
+
+
+ QTextBlock block = firstVisibleBlock();
+ int blockNumber = block.blockNumber();
+ int top = (int) blockBoundingGeometry(block).translated(contentOffset()).top();
+ int bottom = top + (int) blockBoundingRect(block).height();
+
+ while (block.isValid() && top <= event->rect().bottom())
+ {
+ if (block.isVisible() && bottom >= event->rect().top())
+ {
+ QString number = QString::number(blockNumber + 1);
+ painter.setPen(Qt::black);
+ painter.drawText(0, top, m_lineNumberArea->width(), fontMetrics().height(),
+ Qt::AlignRight, number);
+ }
+
+ block = block.next();
+ top = bottom;
+ bottom = top + (int) blockBoundingRect(block).height();
+ ++blockNumber;
+ }
+}
+
+QSize LineNumberArea::sizeHint() const
+{
+ return QSize(code_editor_->lineNumberAreaWidth(), 0);
+}
+
+void LineNumberArea::paintEvent(QPaintEvent *event)
+{
+ code_editor_->lineNumberAreaPaintEvent(event);
+}
+
+void CodeEditor::keyPressEvent(QKeyEvent *event)
+{
+ // replace tab with spaces
+ if (event->key() == Qt::Key_Tab) {
+ int currentColumn = textCursor().positionInBlock();
+
+ // always add at least one space
+ do {
+ insertPlainText(" ");
+ currentColumn++;
+ } while( (currentColumn % indent_size_) != 0);
+ }
+ else if ((event->key() == Qt::Key_Return) || (event->key() == Qt::Key_Enter)) {
+ // we have not changed line yet. Count blanks of current line
+ int n_space = countTrailingSpaces( textCursor().block().position());
+
+ // indent if previous line ends with opening bracket
+ if (textCursor().block().text().trimmed().endsWith('{')) {
+ n_space += indent_size_;
+ }
+
+ // append new line
+ QPlainTextEdit::keyPressEvent( event);
+
+ // indent the new line with the same white spaces as previous line
+ insertPlainText( QString(' ').repeated(n_space));
+ }
+ else {
+ QPlainTextEdit::keyPressEvent( event);
+ }
+}
+
+// count the number of whitespaces at the begin of current line.
+// In case of TAB, a number between 1 and 'indent_size_' is added.
+// The returned value is always in spaces, not in tabs.
+int CodeEditor::countTrailingSpaces(int block_position)
+{
+ int n_spaces = 0;
+ QString text = toPlainText();
+ int text_size = text.length();
+ bool done = false;
+
+ Q_ASSERT( indent_size_ > 0);
+
+ while( done == false) {
+
+ // read one char until text finishes or a non blank is found
+ QChar c = (block_position < text_size) ? text.at(block_position) : '*';
+
+ switch (c.toLatin1()) {
+ case ' ':
+ n_spaces++;
+ break;
+ case '\t':
+ n_spaces =((n_spaces / indent_size_) * indent_size_) + indent_size_;
+ break;
+ default:
+ done = true;
+ }
+
+ block_position++;
+ }
+
+ return n_spaces;
+}
+
+
+
+} // namespace olive
+
+namespace {
+
+// This function is required to search for "whole word". The default QT function considers
+// underscore "_" as a word breaker, so if you searh for "hello" as whole word it will match
+// "hello_world".
+// For this reason, we always search for a regular expression and remove "FindWholeWords"
+// from 'flags', if present.
+QString buildSearchExpression( const QStringView &str, QTextDocument::FindFlags & flags)
+{
+ QString regExpString = QRegularExpression::escape(str);
+
+ if (flags & QTextDocument::FindWholeWords) {
+ regExpString.prepend("\\b");
+ regExpString.append("\\b");
+
+ flags &= (~QTextDocument::FindWholeWords);
+ }
+
+ return regExpString;
+}
+
+}
+
diff --git a/app/dialog/codeeditor/editor.h b/app/dialog/codeeditor/editor.h
new file mode 100644
index 0000000000..7b51917b63
--- /dev/null
+++ b/app/dialog/codeeditor/editor.h
@@ -0,0 +1,100 @@
+/***
+
+ Olive - Non-Linear Video Editor
+ Copyright (C) 2021 Olive Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+***/
+
+#ifndef CODEEDITOR_H
+#define CODEEDITOR_H
+
+#include
+#include
+#include
+
+namespace olive {
+
+class LineNumberArea;
+
+/// @brief A text editor for GLSL shaders
+class CodeEditor : public QPlainTextEdit
+{
+ Q_OBJECT
+
+public:
+ CodeEditor( QWidget *parent = nullptr);
+
+ void lineNumberAreaPaintEvent(QPaintEvent *event);
+ int lineNumberAreaWidth();
+
+ void gotoLineNumber(int lineNumber);
+
+public slots:
+ void onSearchBackwardRequest( const QString & text, QTextDocument::FindFlags flags);
+ void onSearchForwardRequest( const QString & text, QTextDocument::FindFlags flags);
+ void onReplaceTextRequest( const QString &src, const QString &dest,
+ QTextDocument::FindFlags flags, bool thenFind);
+
+signals:
+ // search operation failed
+ void textNotFound();
+
+protected:
+ void resizeEvent(QResizeEvent *event) override;
+ void keyPressEvent(QKeyEvent *event) override;
+
+private slots:
+ void updateLineNumberAreaWidth(int newBlockCount);
+ void onCursorPositionChanged();
+ bool matchLeftParenthesis(char left, QTextBlock currentBlock, int i, int numLeftParentheses);
+ bool matchRightParenthesis( char right, QTextBlock currentBlock, int i, int numRightParentheses);
+ void updateLineNumberArea(const QRect &, int);
+
+private:
+ void highlightCurrentLine();
+ void matchParenthesis();
+ int countTrailingSpaces( int block_position);
+ void createParenthesisSelection(int pos, Qt::GlobalColor color);
+ bool findNext(const QString &text, QTextDocument::FindFlags flags);
+
+private:
+ QWidget * m_lineNumberArea;
+ int indent_size_;
+ QList extra_selections_;
+};
+
+/// @brief helper widget to print line numbers
+class LineNumberArea : public QWidget
+{
+public:
+ LineNumberArea(CodeEditor *editor) : QWidget(editor) {
+ code_editor_ = editor;
+ }
+
+ QSize sizeHint() const override;
+
+protected:
+ void paintEvent(QPaintEvent *event) override;
+
+private:
+ CodeEditor *code_editor_;
+};
+
+
+} // namespace olive
+
+
+#endif // CODEEDITOR_H
diff --git a/app/dialog/codeeditor/externaleditorproxy.cpp b/app/dialog/codeeditor/externaleditorproxy.cpp
new file mode 100644
index 0000000000..1b3f5b11cf
--- /dev/null
+++ b/app/dialog/codeeditor/externaleditorproxy.cpp
@@ -0,0 +1,171 @@
+/***
+
+ Olive - Non-Linear Video Editor
+ Copyright (C) 2021 Olive Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+***/
+
+#include "externaleditorproxy.h"
+#include "config/config.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace {
+// files created in this session.
+// They can be deleted when application quits.
+QSet CreatedFiles;
+}
+
+namespace olive {
+
+ExternalEditorProxy::ExternalEditorProxy(QObject *parent) :
+ QObject(parent),
+ process_(nullptr),
+ watcher_( this)
+{
+ connect( & watcher_, & QFileSystemWatcher::fileChanged, this, & ExternalEditorProxy::onFileChanged);
+}
+
+ExternalEditorProxy::~ExternalEditorProxy()
+{
+ // Do not delete "file_path_" now, bacause it might be read from
+ // another instance of this class. This happens when the clip associated with
+ // this instance loses focus: when it gets focus back, the file file name is used.
+ //
+ // All files will be deleted when application quits.
+}
+
+void ExternalEditorProxy::Launch(const QString &start_text)
+{
+
+ // create file before watching it
+ QFile out(file_path_);
+ out.open( QIODevice::WriteOnly);
+
+ if (out.isOpen()) {
+ out.write( start_text.toLatin1());
+ out.close();
+
+ CreatedFiles.insert( file_path_);
+
+ // now the file exists. We can watch for changes
+ watcher_.addPath( file_path_);
+
+ } else {
+ QMessageBox::warning( nullptr, "Olive",
+ tr("Can't send data to external editor"));
+ }
+
+ if (process_ == nullptr) {
+ process_ = new QProcess(this);
+ connect( process_, static_cast(& QProcess::finished),
+ this, &ExternalEditorProxy::onProcessFinished);
+
+ QString cmd = Config::Current()["EditorExternalCommand"].toString();
+ QString params = Config::Current()["EditorExternalParams"].toString();
+ params.replace("%FILE", file_path_);
+ params.replace("%LINE", "1"); // not yet supported
+
+ process_->start( cmd, params.split(' ', Qt::SkipEmptyParts));
+ }
+}
+
+void ExternalEditorProxy::SetFilePath(const QString & path)
+{
+ file_path_ = path;
+
+ // at this point, "file_path_" may or may not exist.
+
+ // If it exists, align the shader to the content of the file
+ // as it is possible that user modified the file while the clip was
+ // not selected and so the shader did not update with file.
+ if (QFileInfo(file_path_).exists()) {
+ triggerSynchFromFile();
+ }
+
+ // The following instruction is effective when the file exists.
+ watcher_.addPath( file_path_);
+}
+
+void ExternalEditorProxy::Detach()
+{
+ watcher_.removePath( file_path_);
+}
+
+void ExternalEditorProxy::CleanGeneratedFiles()
+{
+ foreach (const QString & file, CreatedFiles) {
+ QFile::remove( file);
+ }
+}
+
+void ExternalEditorProxy::onFileChanged(const QString &path)
+{
+ // this should always be true
+ Q_ASSERT( path == file_path_);
+
+ QFile f(file_path_);
+ f.open( QIODevice::ReadOnly);
+
+ if (f.size() > 0)
+ {
+ if (f.isOpen()) {
+ QString content = QString::fromLatin1( f.readAll());
+ emit textChanged( content);
+ f.close();
+ } else {
+ QMessageBox::warning( nullptr, "Olive", tr("Can't update data from external editor"));
+ }
+ } else {
+ // on some platforms, when the file is saved, it is deleted before
+ // being re-written. In this case, there is a signal of file changed
+ // but it's size is zero. This must be discarded; a new message will arrive later.
+ f.close();
+ }
+}
+
+void ExternalEditorProxy::onProcessFinished(int /*exitCode*/, QProcess::ExitStatus /*exitStatus*/)
+{
+ // Process has saved file before exit or has unexpectedly finished.
+ // Do not emit the text changed signal
+
+ // Also, some editors fork their process and this slot is called when the editor is
+ // still running. For this reason, do not stop listening for modified file
+
+ process_->deleteLater();
+ process_ = nullptr;
+}
+
+void ExternalEditorProxy::triggerSynchFromFile()
+{
+ // run a 0 ms timer so that "synch" operation can be done
+ // immediately but after setting up param-view panel
+ synch_timer_.singleShot(0, this, & ExternalEditorProxy::synchFromFile);
+}
+
+void ExternalEditorProxy::synchFromFile()
+{
+ onFileChanged( file_path_);
+}
+
+
+} // namespace olive
diff --git a/app/dialog/codeeditor/externaleditorproxy.h b/app/dialog/codeeditor/externaleditorproxy.h
new file mode 100644
index 0000000000..b377cdcf42
--- /dev/null
+++ b/app/dialog/codeeditor/externaleditorproxy.h
@@ -0,0 +1,86 @@
+/***
+
+ Olive - Non-Linear Video Editor
+ Copyright (C) 2021 Olive Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+***/
+
+#ifndef EXTERNALEDITORPROXY_H
+#define EXTERNALEDITORPROXY_H
+
+
+#include
+#include
+#include
+
+
+namespace olive {
+
+
+/** @brief Interface to edit a shader code in external editor.
+ *
+ * This class generates a temporary file initialized with the contents
+ * passed in constructor. Then an external QProcess is launched and this
+ * class listens if generated file is changed externally.
+ * The class destructor deletes the temporary file.
+ */
+class ExternalEditorProxy : public QObject
+{
+ Q_OBJECT
+public:
+ explicit ExternalEditorProxy(QObject *parent = nullptr);
+
+ ~ExternalEditorProxy() override;
+
+ // launch an external process, if one is not already running.
+ // If a process is running, nothing is done.
+ void Launch( const QString & start_text);
+
+ // associate a file path to edit text with an external viewer.
+ // Olive will listen for changes in that file.
+ void SetFilePath(const QString & path);
+
+ // stop listening to changes of external files.
+ // Used when user switches from external to internal editor
+ void Detach();
+
+ // to be called when application quits in order to remove
+ // temporary files.
+ static void CleanGeneratedFiles();
+
+signals:
+ // emnitted when temporary file is saved
+ void textChanged( const QString & new_text);
+
+private:
+ void onFileChanged(const QString& path);
+ void onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus);
+ void triggerSynchFromFile();
+
+private slots:
+ void synchFromFile();
+
+private:
+ // QFileSystemWatcher watcher_;
+ QString file_path_;
+ QProcess *process_;
+ QFileSystemWatcher watcher_;
+ QTimer synch_timer_;
+};
+
+} // namespace olive
+
+#endif // EXTERNALEDITORPROXY_H
diff --git a/app/dialog/codeeditor/glslhighlighter.cpp b/app/dialog/codeeditor/glslhighlighter.cpp
new file mode 100644
index 0000000000..48ab400d32
--- /dev/null
+++ b/app/dialog/codeeditor/glslhighlighter.cpp
@@ -0,0 +1,247 @@
+/****************************************************************************
+**
+** Copyright (C) 2016 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "glslhighlighter.h"
+
+namespace olive {
+
+namespace {
+
+// flag to indicate to not store info about matching parentheses.
+// This is true for comments and string literals
+const int IS_NON_PARSABLE = 0x1000;
+
+const QString keywordPatterns[] = {
+ QStringLiteral("\\bclass\\b"), QStringLiteral("\\bconst\\b"), QStringLiteral("\\benum\\b"),
+ QStringLiteral("\\bbreak\\b"), QStringLiteral("\\bshort\\b"), QStringLiteral("\\bif\\b"),
+ QStringLiteral("\\bfor\\b"), QStringLiteral("\\bwhile\\b"), QStringLiteral("\\bstruct\\b"),
+ QStringLiteral("\\belse\\b"), QStringLiteral("\\breturn\\b"), QStringLiteral("\\btypedef\\b"),
+ QStringLiteral("\\bvolatile\\b"), QStringLiteral("\\bfrag_color\\b"), QStringLiteral("\\bswitch\\b"),
+ QStringLiteral("\\bcase\\b"), QStringLiteral("\\btrue\\b"), QStringLiteral("\\bfalse\\b"),
+ QStringLiteral("\\bstatic\\b"), QStringLiteral("\\bdefault\\b"), QStringLiteral("#define\\b"),
+ QStringLiteral("\\bove_texcoord\\b"), QStringLiteral("\\bin\\b"), QStringLiteral("\\bout\\b"),
+ QStringLiteral("\\bresolution_in\\b"), QStringLiteral("#version\\b")
+};
+
+const QString typePatterns[] = {
+ QStringLiteral("\\bchar\\b"),
+ QStringLiteral("\\bdouble\\b"), QStringLiteral("\\bfloat\\b"), QStringLiteral("\\blong\\b"),
+ QStringLiteral("\\bshort\\b"),QStringLiteral("\\bsigned\\b"), QStringLiteral("\\bunsigned\\b"),
+ QStringLiteral("\\bunion\\b"), QStringLiteral("\\bvoid\\b"),QStringLiteral("\\bbool\\b"),
+ QStringLiteral("\\buniform\\b"), QStringLiteral("\\bvec\\b"),
+ QStringLiteral("\\bvec2\\b"), QStringLiteral("\\bvec3\\b"), QStringLiteral("\\bvec4\\b"),
+ QStringLiteral("\\bint\\b"), QStringLiteral("\\bsampler2D\\b")
+};
+
+// a small set of built-in functions
+const QString functionPatterns[] = {
+ QStringLiteral("\\bmain\\b"), QStringLiteral("\\btexture2D\\b"), QStringLiteral("\\bfract\\b"),
+ QStringLiteral("\\babs\\b"), QStringLiteral("\\bdot\\b"), QStringLiteral("\\bmix\\b"),
+ QStringLiteral("\\bceil\\b"), QStringLiteral("\\bfloor\\b"), QStringLiteral("\\bclamp\\b"),
+ QStringLiteral("\\bsin\\b"), QStringLiteral("\\bcos\\b"), QStringLiteral("\\btan\\b"),
+ QStringLiteral("\\bexp(2)?\\b"), QStringLiteral("\\blog(2)?\\b"), QStringLiteral("\\b\\b"),
+ QStringLiteral("\\bmin\\b"), QStringLiteral("\\bmax\\b"), QStringLiteral("\\bpow\\b"),
+ QStringLiteral("\\bsqrt\\b"), QStringLiteral("\\bstep\\b"), QStringLiteral("\\bsmoothstep\\b"),
+ QStringLiteral("\\bdistance\\b")
+};
+
+const QString oliveMarkupPatterns[] = {
+ QStringLiteral("//OVE\\s+shader_name:[^\n]*"), QStringLiteral("//OVE\\s+shader_description:[^\n]*"),
+ QStringLiteral("//OVE\\s+shader_version:[^\n]*"), QStringLiteral("//OVE\\s+number_of_iterations:[^\n]*"),
+ QStringLiteral("//OVE\\s+main_input_name:[^\n]*"),
+ QStringLiteral("//OVE\\s+name:[^\n]*"), QStringLiteral("//OVE\\s+end\\b[^\n]*"),
+ QStringLiteral("//OVE\\s+type:[^\n]*"), QStringLiteral("//OVE\\s+flag:[^\n]*"),
+ QStringLiteral("//OVE\\s+values:[^\n]*"), QStringLiteral("//OVE\\s+description:[^\n]*"),
+ QStringLiteral("//OVE\\s+default:[^\n]*"), QStringLiteral("//OVE shape: [^\n]*"),
+ QStringLiteral("//OVE color: [^\n]*"), QStringLiteral("//OVE\\s+min:[^\n]*"),
+ QStringLiteral("//OVE\\s+max:[^\n]*")
+};
+
+} // namespace
+
+GlslHighlighter::GlslHighlighter(QTextDocument *parent)
+ : QSyntaxHighlighter(parent)
+{
+ HighlightingRule rule;
+
+ keyword_format_.setForeground(QColor(110,80,110));
+ keyword_format_.setFontWeight(QFont::Bold);
+
+ quotation_format_.setForeground(QColor(130,130,80));
+ quotation_format_.setProperty( IS_NON_PARSABLE, QVariant::fromValue(true));
+ rule.pattern = QRegularExpression(QStringLiteral("\".*\""));
+ rule.format = quotation_format_;
+ highlighting_rules_.append(rule);
+
+ for (const QString &pattern : keywordPatterns) {
+ rule.pattern = QRegularExpression(pattern);
+ rule.format = keyword_format_;
+ highlighting_rules_.append(rule);
+ }
+
+ type_format_.setForeground(QColor(70,110,160));
+
+ for (const QString &pattern : typePatterns) {
+ rule.pattern = QRegularExpression(pattern);
+ rule.format = type_format_;
+ highlighting_rules_.append(rule);
+ }
+
+ function_format_.setForeground(QColor(120,110,30));
+
+ for (const QString &pattern : functionPatterns) {
+ rule.pattern = QRegularExpression(pattern);
+ rule.format = function_format_;
+ highlighting_rules_.append(rule);
+ }
+
+ number_format_.setForeground(QColor(120,110,60));
+ rule.format = type_format_;
+ // decimal or float
+ rule.pattern = QRegularExpression("\\b\\d+\\.?(\\d+)?\\b");
+ highlighting_rules_.append(rule);
+ // hex
+ rule.pattern = QRegularExpression("\\b0[xX][0-9A-Fa-f]+\\b");
+ highlighting_rules_.append(rule);
+
+ comment_start_expression_ = QRegularExpression(QStringLiteral("/\\*"));
+ comment_end_expression_ = QRegularExpression(QStringLiteral("\\*/"));
+
+ single_line_comment_format_.setForeground(QColor(60,180,80));
+ single_line_comment_format_.setProperty( IS_NON_PARSABLE, QVariant::fromValue(true));
+ rule.pattern = QRegularExpression(QStringLiteral("//[^\n]*"));
+ rule.format = single_line_comment_format_;
+ highlighting_rules_.append(rule);
+
+ markup_line_comment_format_.setForeground(QColor(70,120,130));
+ markup_line_comment_format_.setProperty( IS_NON_PARSABLE, QVariant::fromValue(true));
+ for (const QString &pattern : oliveMarkupPatterns) {
+ rule.pattern = QRegularExpression(pattern);
+ rule.format = markup_line_comment_format_;
+ highlighting_rules_.append(rule);
+ }
+
+ multiline_comment_format_.setForeground(QColor(60,180,80));
+ multiline_comment_format_.setProperty( IS_NON_PARSABLE, QVariant::fromValue(true));
+}
+
+
+void GlslHighlighter::highlightBlock(const QString &text)
+{
+ highlightKeywords(text);
+ highlightMultilineComments(text);
+
+ storeParenthesisInfo(text);
+}
+
+void olive::GlslHighlighter::highlightKeywords(const QString &text)
+{
+ for (const HighlightingRule &rule : qAsConst(highlighting_rules_)) {
+ QRegularExpressionMatchIterator matchIterator = rule.pattern.globalMatch(text);
+ while (matchIterator.hasNext()) {
+ QRegularExpressionMatch match = matchIterator.next();
+ setFormat(match.capturedStart(), match.capturedLength(), rule.format);
+ }
+ }
+}
+
+
+void olive::GlslHighlighter::highlightMultilineComments(const QString &text)
+{
+ int start_index = 0;
+
+ setCurrentBlockState(0);
+
+ if (previousBlockState() != 1) {
+ start_index = text.indexOf(comment_start_expression_);
+ }
+
+ while (start_index >= 0) {
+ QRegularExpressionMatch match = comment_end_expression_.match(text, start_index);
+ int end_index = match.capturedStart();
+ int commentLength = 0;
+ if (end_index == -1) {
+ setCurrentBlockState(1);
+ commentLength = text.length() - start_index;
+ } else {
+ commentLength = end_index - start_index + match.capturedLength();
+ }
+ setFormat(start_index, commentLength, multiline_comment_format_);
+ start_index = text.indexOf(comment_start_expression_, start_index + commentLength);
+ }
+}
+
+
+void olive::GlslHighlighter::storeParenthesisInfo(const QString &text)
+{
+ static const QRegularExpression ParenthesisRegExp("[\\(\\)\\[\\]\\{\\}]{1}");
+
+ /* the ownership of this will be passed to QTextBlock */
+ TextBlockData *data = new TextBlockData;
+
+ QRegularExpressionMatchIterator i = ParenthesisRegExp.globalMatch(text);
+ while (i.hasNext()) {
+ QRegularExpressionMatch match = i.next();
+
+ // store parenthesis info only if not in comments or string literals
+ if (format(match.capturedStart(0)).hasProperty(IS_NON_PARSABLE) == false) {
+
+ ParenthesisInfo info;
+ info.character = match.captured().toLatin1().at(0);
+ info.position = match.capturedStart(0);
+ data->insert( info);
+ }
+ }
+
+ setCurrentBlockUserData(data);
+}
+
+} // namespace olive
+
diff --git a/app/dialog/codeeditor/glslhighlighter.h b/app/dialog/codeeditor/glslhighlighter.h
new file mode 100644
index 0000000000..9314f87071
--- /dev/null
+++ b/app/dialog/codeeditor/glslhighlighter.h
@@ -0,0 +1,134 @@
+/****************************************************************************
+**
+** Copyright (C) 2016 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef GLSLHIGHLIGHTER_H
+#define GLSLHIGHLIGHTER_H
+
+#include
+#include
+#include
+
+
+class QTextDocument;
+
+namespace olive {
+
+class GlslHighlighter : public QSyntaxHighlighter
+{
+ Q_OBJECT
+
+public:
+ GlslHighlighter(QTextDocument *parent = 0);
+
+protected:
+ void highlightBlock(const QString &text) override;
+
+private:
+ struct HighlightingRule
+ {
+ QRegularExpression pattern;
+ QTextCharFormat format;
+ };
+ QVector highlighting_rules_;
+
+ QRegularExpression comment_start_expression_;
+ QRegularExpression comment_end_expression_;
+
+ QTextCharFormat keyword_format_;
+ QTextCharFormat type_format_;
+ QTextCharFormat single_line_comment_format_;
+ QTextCharFormat markup_line_comment_format_;
+ QTextCharFormat multiline_comment_format_;
+ QTextCharFormat quotation_format_;
+ QTextCharFormat number_format_;
+ QTextCharFormat function_format_;
+
+private:
+ void highlightKeywords(const QString &text);
+ void highlightMultilineComments(const QString &text);
+ void storeParenthesisInfo(const QString &text);
+};
+
+
+struct ParenthesisInfo
+{
+ char character;
+ int position;
+};
+
+/* utility class to store brakets info */
+class TextBlockData : public QTextBlockUserData
+{
+public:
+ TextBlockData() {
+ }
+
+ const QVector & parentheses() {
+ return m_parentheses;
+ }
+
+ void insert(ParenthesisInfo & info) {
+ int i = 0;
+ while (i < m_parentheses.size() &&
+ info.position > m_parentheses.at(i).position)
+ {
+ ++i;
+ }
+
+ m_parentheses.insert(i, info);
+ }
+
+private:
+ QVector m_parentheses;
+};
+
+} // namespace olive
+
+#endif // GLSLHIGHLIGHTER_H
diff --git a/app/dialog/codeeditor/messagehighlighter.cpp b/app/dialog/codeeditor/messagehighlighter.cpp
new file mode 100644
index 0000000000..a02a7107d0
--- /dev/null
+++ b/app/dialog/codeeditor/messagehighlighter.cpp
@@ -0,0 +1,76 @@
+/***
+
+ Olive - Non-Linear Video Editor
+ Copyright (C) 2021 Olive Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+***/
+
+#include "messagehighlighter.h"
+
+namespace olive {
+
+MessageSyntaxHighlighter::MessageSyntaxHighlighter(QTextDocument *parent) :
+ QSyntaxHighlighter(parent)
+{
+ group_format_.setForeground(QColor(110,110,255));
+ group_format_.setFontWeight(QFont::Bold);
+
+ line_format_.setForeground(QColor(128,128,128));
+ line_format_.setFontItalic(true);
+
+ shader_error_format_.setForeground(QColor(255,70,70));
+ shader_warning_format_.setForeground(QColor(255,255,40));
+
+ group_regexp = QRegularExpression(QStringLiteral("\"[^\"]*\""));
+ line_regexp = QRegularExpression(QStringLiteral("line\\s\\d+:"));
+ shader_error_regexp = QRegularExpression(QStringLiteral("\\b(error|ERROR|Error)\\b"));
+ shader_warning_regexp = QRegularExpression(QStringLiteral("\\b(warning|WARNING|Warning)\\b"));
+}
+
+void MessageSyntaxHighlighter::highlightBlock(const QString &text)
+{
+ QRegularExpressionMatchIterator matchIterator = group_regexp.globalMatch(text);
+
+ while (matchIterator.hasNext()) {
+ QRegularExpressionMatch match = matchIterator.next();
+ setFormat(match.capturedStart(), match.capturedLength(), group_format_);
+ }
+
+ matchIterator = line_regexp.globalMatch(text);
+
+ while (matchIterator.hasNext()) {
+ QRegularExpressionMatch match = matchIterator.next();
+ setFormat(match.capturedStart(), match.capturedLength(), line_format_);
+ }
+
+ matchIterator = shader_error_regexp.globalMatch(text);
+
+ while (matchIterator.hasNext()) {
+ QRegularExpressionMatch match = matchIterator.next();
+ setFormat(match.capturedStart(1), match.capturedLength(1), shader_error_format_);
+ }
+
+ matchIterator = shader_warning_regexp.globalMatch(text);
+
+ while (matchIterator.hasNext()) {
+ QRegularExpressionMatch match = matchIterator.next();
+ setFormat(match.capturedStart(1), match.capturedLength(1), shader_warning_format_);
+ }
+}
+
+
+} // namespace olive
+
diff --git a/app/dialog/codeeditor/messagehighlighter.h b/app/dialog/codeeditor/messagehighlighter.h
new file mode 100644
index 0000000000..fb6e75cfa2
--- /dev/null
+++ b/app/dialog/codeeditor/messagehighlighter.h
@@ -0,0 +1,61 @@
+/***
+
+ Olive - Non-Linear Video Editor
+ Copyright (C) 2021 Olive Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+***/
+
+#ifndef MESSAGEHIGHLIGHTER_H
+#define MESSAGEHIGHLIGHTER_H
+
+#include
+#include
+#include
+
+class QTextDocument;
+
+namespace olive {
+
+#include
+#include
+
+class MessageSyntaxHighlighter : public QSyntaxHighlighter
+{
+ Q_OBJECT
+public:
+
+ MessageSyntaxHighlighter( QTextDocument * parent = nullptr);
+
+ // QSyntaxHighlighter interface
+protected:
+ void highlightBlock(const QString &text) override;
+
+private:
+ QTextCharFormat group_format_;
+ QTextCharFormat line_format_;
+ QTextCharFormat shader_error_format_;
+ QTextCharFormat shader_warning_format_;
+
+ QRegularExpression group_regexp;
+ QRegularExpression line_regexp;
+ QRegularExpression shader_error_regexp;
+ QRegularExpression shader_warning_regexp;
+};
+
+
+} // namespace olive
+
+#endif // MESSAGEHIGHLIGHTER_H
diff --git a/app/dialog/codeeditor/searchtextbar.cpp b/app/dialog/codeeditor/searchtextbar.cpp
new file mode 100644
index 0000000000..1093430cad
--- /dev/null
+++ b/app/dialog/codeeditor/searchtextbar.cpp
@@ -0,0 +1,211 @@
+/***
+
+ Olive - Non-Linear Video Editor
+ Copyright (C) 2021 Olive Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+***/
+
+#include "searchtextbar.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+
+namespace {
+// duration of visual feedback when text is not found
+const uint32_t TEXT_NOT_FOUND_FLASH_DURATION_ms = 600u;
+}
+
+namespace olive {
+
+SearchTextBar::SearchTextBar(QWidget *parent) :
+ QWidget(parent),
+ timer_( new QTimer(this))
+{
+ // build GUI
+ QGridLayout * main_layout = new QGridLayout();
+ setLayout( main_layout);
+ main_layout->setSpacing(2);
+
+ QLabel * find_label = new QLabel(tr("Find:"), this);
+ find_edit_ = new QLineEdit( this);
+ find_label->setAlignment( Qt::AlignLeft);
+ find_label->setBuddy( find_edit_);
+
+ case_sensitive_box_ = new QCheckBox( "Aa", this);
+ case_sensitive_box_->setToolTip(tr("case sensitive"));
+ whole_word_box_ = new QCheckBox( "[]", this);
+ whole_word_box_->setToolTip(tr("whole words only"));
+
+ QPushButton * find_forward_button = new QPushButton( ">>", this);
+ QPushButton * find_backword_button = new QPushButton( "<<", this);
+ QPushButton * hide_button = new QPushButton( "x", this);
+
+ find_edit_->setSizePolicy( QSizePolicy::Expanding , QSizePolicy::Preferred);
+ main_layout->addWidget( find_label, 0,0,1,1);
+ main_layout->addWidget( find_edit_, 0,1,1,1);
+ main_layout->addWidget( case_sensitive_box_, 0,2,1,1);
+ main_layout->addWidget( whole_word_box_, 0,3,1,1);
+ main_layout->addWidget( find_backword_button, 0,4,1,1);
+ main_layout->addWidget( find_forward_button, 0,5,1,1);
+ main_layout->addItem( new QSpacerItem(10,10, QSizePolicy::Minimum, QSizePolicy::Minimum), 0,6,1,1);
+ main_layout->addWidget( hide_button, 0,7,1,1);
+
+ QLabel * replace_label = new QLabel(tr("Replace:"), this);
+ replace_edit_ = new QLineEdit( this);
+ replace_label->setAlignment( Qt::AlignLeft);
+ replace_label->setBuddy( replace_edit_);
+
+ QPushButton * replace_button = new QPushButton(tr("replace"), this);
+ QPushButton * replace_and_find_button = new QPushButton(tr("replace and find"), this);
+
+ replace_edit_->setSizePolicy( QSizePolicy::Expanding , QSizePolicy::Preferred);
+ main_layout->addWidget( replace_label, 1,0,1,1);
+ main_layout->addWidget( replace_edit_, 1,1,1,1);
+ main_layout->addWidget( replace_button, 1,2,1,2);
+ main_layout->addWidget( replace_and_find_button, 1,4,1,2);
+
+ connect( find_backword_button, & QPushButton::clicked, this, & SearchTextBar::onPrevButtonClicked);
+ connect( find_forward_button, & QPushButton::clicked, this, & SearchTextBar::onNextButtonClicked);
+ connect( hide_button, & QPushButton::clicked, this, & SearchTextBar::onHideButtonClicked);
+ connect( replace_button, & QPushButton::clicked, this, & SearchTextBar::onReplaceButtonCLicked);
+ connect( replace_and_find_button, & QPushButton::clicked, this, & SearchTextBar::onReplaceAndFindButtonClicked);
+
+ find_edit_->setTabOrder( find_edit_, replace_edit_);
+
+ // visual feedback timer
+ connect( timer_, SIGNAL(timeout()), this, SLOT(onTimeoutExpired()));
+}
+
+SearchTextBar::~SearchTextBar()
+{
+}
+
+void SearchTextBar::activateBar()
+{
+ setVisible( true);
+ find_edit_->setFocus();
+}
+
+void SearchTextBar::onTextNotFound()
+{
+ // give a visual feedback that text can't be found
+ find_edit_->setStyleSheet("background-color: red;");
+ timer_->start( TEXT_NOT_FOUND_FLASH_DURATION_ms);
+}
+
+void SearchTextBar::showBar( const QString & search_text)
+{
+ show();
+ find_edit_->setFocus();
+ find_edit_->setText( search_text);
+}
+
+void SearchTextBar::onTimeoutExpired()
+{
+ // end of visual feedback
+ find_edit_->setStyleSheet("");
+ timer_->stop();
+}
+
+void SearchTextBar::keyPressEvent(QKeyEvent * event)
+{
+ // enter or return key search forward; use shift to search backward.
+ // Perform replace only if "replace" input has focus
+ if ((event->key() == Qt::Key_Enter) ||
+ (event->key() == Qt::Key_Return))
+ {
+ if (replace_edit_->hasFocus()) {
+ onReplaceAndFindButtonClicked();
+ }
+ else {
+
+ if (event->modifiers() & Qt::ShiftModifier)
+ {
+ onPrevButtonClicked();
+ }
+ else
+ {
+ onNextButtonClicked();
+ }
+ }
+
+ event->accept();
+ }
+ else if (event->key() == Qt::Key_Escape)
+ {
+ hide();
+ event->accept();
+ }
+ else {
+ QWidget::keyPressEvent( event);
+ }
+}
+
+
+void SearchTextBar::onHideButtonClicked()
+{
+ hide();
+}
+
+void SearchTextBar::onPrevButtonClicked()
+{
+ QTextDocument::FindFlags flags = getCurrentFlags();
+ emit searchTextBackward( find_edit_->text(), flags);
+}
+
+void SearchTextBar::onNextButtonClicked()
+{
+ QTextDocument::FindFlags flags = getCurrentFlags();
+ emit searchTextForward( find_edit_->text(), flags);
+}
+
+void SearchTextBar::onReplaceButtonCLicked()
+{
+ QTextDocument::FindFlags flags = getCurrentFlags();
+ emit replaceText( find_edit_->text(), replace_edit_->text(), flags, false);
+}
+
+void SearchTextBar::onReplaceAndFindButtonClicked()
+{
+ QTextDocument::FindFlags flags = getCurrentFlags();
+ emit replaceText( find_edit_->text(), replace_edit_->text(), flags, false);
+ emit searchTextForward( find_edit_->text(), flags);
+}
+
+QTextDocument::FindFlags SearchTextBar::getCurrentFlags()
+{
+ // default constructor sets to 0
+ QTextDocument::FindFlags flags;
+
+ if (case_sensitive_box_->isChecked()) {
+ flags |= QTextDocument::FindCaseSensitively;
+ }
+
+ if (whole_word_box_->isChecked()) {
+ flags |= QTextDocument::FindWholeWords;
+ }
+
+ return flags;
+}
+
+} // namespace olive
diff --git a/app/dialog/codeeditor/searchtextbar.h b/app/dialog/codeeditor/searchtextbar.h
new file mode 100644
index 0000000000..6674984dde
--- /dev/null
+++ b/app/dialog/codeeditor/searchtextbar.h
@@ -0,0 +1,80 @@
+/***
+
+ Olive - Non-Linear Video Editor
+ Copyright (C) 2021 Olive Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+***/
+
+#ifndef SEARCHTEXTBAR_H
+#define SEARCHTEXTBAR_H
+
+#include
+#include
+
+class QTimer;
+class QCheckBox;
+class QLineEdit;
+
+namespace olive {
+
+class SearchTextBar : public QWidget
+{
+ Q_OBJECT
+
+public:
+ explicit SearchTextBar(QWidget *parent = nullptr);
+ ~SearchTextBar() override;
+
+ /** make seach bar visible and set focus */
+ void activateBar();
+
+public slots:
+ void onTextNotFound();
+ void showBar(const QString &search_text);
+
+signals:
+ void searchTextForward( const QString & text, QTextDocument::FindFlags flgs);
+ void searchTextBackward( const QString & text, QTextDocument::FindFlags flgs);
+ // thenFind: when TRUE, replace and then find
+ void replaceText( const QString & src, const QString & dest,
+ QTextDocument::FindFlags flags, bool thenFind);
+
+private:
+ QTimer * timer_;
+ QCheckBox * case_sensitive_box_;
+ QCheckBox * whole_word_box_;
+ QLineEdit * find_edit_;
+ QLineEdit * replace_edit_;
+
+ // QWidget interface
+protected:
+ void keyPressEvent(QKeyEvent * event) override;
+
+private slots:
+ void onTimeoutExpired();
+ void onHideButtonClicked();
+ void onPrevButtonClicked();
+ void onNextButtonClicked();
+ void onReplaceButtonCLicked();
+ void onReplaceAndFindButtonClicked();
+
+private:
+ QTextDocument::FindFlags getCurrentFlags();
+};
+
+} // namespace olive
+
+#endif // SEARCHTEXTBAR_H
diff --git a/app/dialog/preferences/preferences.cpp b/app/dialog/preferences/preferences.cpp
index 5777e929d1..59d3935c88 100644
--- a/app/dialog/preferences/preferences.cpp
+++ b/app/dialog/preferences/preferences.cpp
@@ -32,6 +32,7 @@
#include "tabs/preferencesdisktab.h"
#include "tabs/preferencesaudiotab.h"
#include "tabs/preferenceskeyboardtab.h"
+#include "tabs/preferencesedittab.h"
#include "window/mainwindow/mainwindow.h"
namespace olive {
@@ -46,6 +47,7 @@ PreferencesDialog::PreferencesDialog(MainWindow *main_window) :
AddTab(new PreferencesBehaviorTab(), tr("Behavior"));
AddTab(new PreferencesDiskTab(), tr("Disk"));
AddTab(new PreferencesAudioTab(), tr("Audio"));
+ AddTab(new PreferencesEditTab(), tr("Text Editor"));
AddTab(new PreferencesKeyboardTab(main_window), tr("Keyboard"));
}
diff --git a/app/dialog/preferences/tabs/CMakeLists.txt b/app/dialog/preferences/tabs/CMakeLists.txt
index c26e22fa10..eeb6ac781a 100644
--- a/app/dialog/preferences/tabs/CMakeLists.txt
+++ b/app/dialog/preferences/tabs/CMakeLists.txt
@@ -28,5 +28,7 @@ set(OLIVE_SOURCES
dialog/preferences/tabs/preferencesaudiotab.cpp
dialog/preferences/tabs/preferenceskeyboardtab.h
dialog/preferences/tabs/preferenceskeyboardtab.cpp
+ dialog/preferences/tabs/preferencesedittab.h
+ dialog/preferences/tabs/preferencesedittab.cpp
PARENT_SCOPE
)
diff --git a/app/dialog/preferences/tabs/preferencesedittab.cpp b/app/dialog/preferences/tabs/preferencesedittab.cpp
new file mode 100644
index 0000000000..ea426d4901
--- /dev/null
+++ b/app/dialog/preferences/tabs/preferencesedittab.cpp
@@ -0,0 +1,173 @@
+/***
+
+ Olive - Non-Linear Video Editor
+ Copyright (C) 2022 Olive Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+***/
+
+#include "preferencesedittab.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+
+namespace olive {
+
+PreferencesEditTab::PreferencesEditTab()
+{
+ QVBoxLayout* outer_layout = new QVBoxLayout(this);
+
+ QGroupBox* selection_group = new QGroupBox(tr("Select editor"));
+ outer_layout->addWidget(selection_group);
+ QGroupBox * internal_editor_box = new QGroupBox(tr("Internal editor"));
+ outer_layout->addWidget(internal_editor_box);
+ QGroupBox * external_editor_box = new QGroupBox(tr("External editor"));
+ outer_layout->addWidget(external_editor_box);
+
+ QVBoxLayout* editor_slect_layout = new QVBoxLayout(selection_group);
+ use_internal_editor_ = new QRadioButton(tr("Use internal code editor"));
+ use_external_editor_ = new QRadioButton(tr("Use an external code editor"));
+
+ editor_slect_layout->addWidget(use_internal_editor_);
+ editor_slect_layout->addWidget(use_external_editor_);
+
+ connect( use_internal_editor_, & QRadioButton::clicked,
+ internal_editor_box, & QGroupBox::setEnabled);
+ connect( use_internal_editor_, & QRadioButton::clicked,
+ external_editor_box, & QGroupBox::setDisabled);
+ connect( use_external_editor_, & QRadioButton::clicked,
+ external_editor_box, & QGroupBox::setEnabled);
+ connect( use_external_editor_, & QRadioButton::clicked,
+ internal_editor_box, & QGroupBox::setDisabled);
+
+ //internal editor
+ QGridLayout * internal_editor_layout = new QGridLayout(internal_editor_box);
+ internal_editor_layout->addWidget( new QLabel(tr("Font size")), 0, 0);
+ font_size_ = new QSpinBox();
+ internal_editor_layout->addWidget( font_size_, 0, 1);
+ internal_editor_layout->addWidget( new QLabel(tr("Indent size")), 1, 0);
+ indent_size_ = new QSpinBox();
+ internal_editor_layout->addWidget( indent_size_, 1, 1);
+ internal_editor_layout->addWidget( new QLabel(tr("Window size (WxH)")), 2, 0);
+ window_width_ = new QSpinBox();
+ window_heigth_ = new QSpinBox();
+ internal_editor_layout->addWidget( window_width_, 2, 1);
+ internal_editor_layout->addWidget( window_heigth_, 2, 2);
+ internal_editor_layout->addItem( new QSpacerItem(1,1, QSizePolicy::Expanding), 0, 3);
+ internal_editor_layout->addItem( new QSpacerItem(1,1, QSizePolicy::Expanding), 1, 3);
+ internal_editor_layout->addItem( new QSpacerItem(1,1, QSizePolicy::Expanding), 2, 3);
+ outer_layout->addWidget(internal_editor_box);
+
+ window_heigth_->setMinimum(10);
+ window_heigth_->setMaximum(4096);
+ window_width_->setMinimum(10);
+ window_width_->setMaximum(4096);
+
+ //external editor
+ QVBoxLayout * external_editor_layout = new QVBoxLayout(external_editor_box);
+ external_editor_layout->addWidget(
+ new QLabel(tr("Command or full file path of external editor.\n"
+ "Use double quotes if executable path has spaces")) );
+ QHBoxLayout * editor_cmd_layout = new QHBoxLayout();
+ external_editor_layout->addLayout( editor_cmd_layout);
+ ext_command_ = new QLineEdit();
+ ext_select_button = new QPushButton("...", this);
+ editor_cmd_layout->addWidget( ext_command_);
+ editor_cmd_layout->addWidget( ext_select_button);
+
+ connect( ext_select_button, & QPushButton::clicked, this, & PreferencesEditTab::onCommandSelectDialogRequest);
+
+ ext_params_ = new QLineEdit();
+ external_editor_layout->addWidget(
+ new QLabel(tr("Parameters of external editor.\n"
+ "Use %FILE and %LINE for file path and line number")) );
+ external_editor_layout->addWidget( ext_params_);
+ outer_layout->addWidget(external_editor_box);
+
+ external_editor_layout->addWidget(
+ new QLabel(tr("Folder where temporary shaders are stored")) );
+ QHBoxLayout * temp_folder_layout = new QHBoxLayout();
+ external_editor_layout->addLayout( temp_folder_layout);
+ temp_folder_ = new QLineEdit();
+ temp_folder_button_ = new QPushButton("...", this);
+ temp_folder_layout->addWidget( temp_folder_);
+ temp_folder_layout->addWidget( temp_folder_button_);
+
+ connect( temp_folder_button_, & QPushButton::clicked, this, & PreferencesEditTab::onTempFolderDialogRequest);
+
+ outer_layout->addStretch();
+
+ bool use_internal = Config::Current()["EditorUseInternal"].toBool();
+ use_internal_editor_->setChecked( use_internal);
+ use_external_editor_->setChecked( ! use_internal);
+ internal_editor_box->setEnabled( use_internal);
+ external_editor_box->setEnabled( ! use_internal);
+ font_size_->setValue( Config::Current()["EditorInternalFontSize"].toInt());
+ indent_size_->setValue( Config::Current()["EditorInternalIndentSize"].toInt());
+ ext_command_->setText( Config::Current()["EditorExternalCommand"].toString());
+ ext_params_->setText( Config::Current()["EditorExternalParams"].toString());
+ temp_folder_->setText( Config::Current()["EditorExternalTempFolder"].toString());
+ window_heigth_->setValue( Config::Current()["EditorInternalWindowHeight"].toInt());
+ window_width_->setValue( Config::Current()["EditorInternalWindowWidth"].toInt());
+}
+
+bool PreferencesEditTab::Validate()
+{
+ return true;
+}
+
+void PreferencesEditTab::Accept(MultiUndoCommand *command)
+{
+ Q_UNUSED(command)
+
+ Config::Current()["EditorUseInternal"] = QVariant::fromValue( use_internal_editor_->isChecked());
+ Config::Current()["EditorExternalCommand"] = QVariant::fromValue(ext_command_->text());
+ Config::Current()["EditorExternalParams"] = QVariant::fromValue(ext_params_->text());
+ Config::Current()["EditorExternalTempFolder"] = QVariant::fromValue(temp_folder_->text());
+ Config::Current()["EditorInternalFontSize"] = QVariant::fromValue(font_size_->value());
+ Config::Current()["EditorInternalIndentSize"] = QVariant::fromValue(indent_size_->value());
+ Config::Current()["EditorInternalWindowHeight"] = QVariant::fromValue(window_heigth_->value());
+ Config::Current()["EditorInternalWindowWidth"] = QVariant::fromValue(window_width_->value());
+}
+
+void PreferencesEditTab::onCommandSelectDialogRequest()
+{
+ QString path = QFileDialog::getOpenFileName( this, tr("External editor"),
+ QStandardPaths::displayName( QStandardPaths::ApplicationsLocation));
+
+ if (path != QString()) {
+ ext_command_->setText( path);
+ }
+}
+
+void PreferencesEditTab::onTempFolderDialogRequest()
+{
+ QString path = QFileDialog::getExistingDirectory( this, tr("Temporary shaders folder"),
+ Config::Current()["EditorExternalTempFolder"].toString());
+
+ if (path != QString()) {
+ temp_folder_->setText( path);
+ }
+}
+
+} // olive
diff --git a/app/dialog/preferences/tabs/preferencesedittab.h b/app/dialog/preferences/tabs/preferencesedittab.h
new file mode 100644
index 0000000000..d301fd1328
--- /dev/null
+++ b/app/dialog/preferences/tabs/preferencesedittab.h
@@ -0,0 +1,71 @@
+/***
+
+ Olive - Non-Linear Video Editor
+ Copyright (C) 2022 Olive Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+***/
+
+#ifndef PREFERENCESEDITTAB_H
+#define PREFERENCESEDITTAB_H
+
+
+#include "dialog/configbase/configdialogbasetab.h"
+
+class QCheckBox;
+class QGroupBox;
+class QRadioButton;
+class QSpinBox;
+class QLineEdit;
+class QPushButton;
+
+
+namespace olive {
+
+class PreferencesEditTab : public ConfigDialogBaseTab
+{
+ Q_OBJECT
+public:
+ PreferencesEditTab();
+
+ virtual bool Validate() override;
+
+ virtual void Accept(MultiUndoCommand* command) override;
+
+private slots:
+ void onCommandSelectDialogRequest();
+ void onTempFolderDialogRequest();
+
+private:
+ QRadioButton * use_internal_editor_;
+ QRadioButton * use_external_editor_;
+
+ // for internal editor
+ QSpinBox * font_size_;
+ QSpinBox * indent_size_;
+ QSpinBox * window_width_;
+ QSpinBox * window_heigth_;
+
+ // for external editor
+ QLineEdit * ext_command_;
+ QPushButton * ext_select_button;
+ QLineEdit * ext_params_;
+ QPushButton * temp_folder_button_; // folder where temporary files are stored
+ QLineEdit * temp_folder_;
+};
+
+}
+
+#endif // PREFERENCESEDITTAB_H
diff --git a/app/dialog/text/text.h b/app/dialog/text/text.h
index e6542ceb44..33e8414d67 100644
--- a/app/dialog/text/text.h
+++ b/app/dialog/text/text.h
@@ -24,6 +24,7 @@
#include
#include
#include
+#include
#include "common/define.h"
#include "widget/slider/floatslider.h"
@@ -41,6 +42,11 @@ class TextDialog : public QDialog
return text_edit_->toPlainText();
}
+ void setSyntaxHighlight( QSyntaxHighlighter * highlighter) const
+ {
+ highlighter->setDocument(text_edit_->document());
+ }
+
private:
QPlainTextEdit* text_edit_;
diff --git a/app/node/block/transition/CMakeLists.txt b/app/node/block/transition/CMakeLists.txt
index bb1f316a46..9c36ed721a 100644
--- a/app/node/block/transition/CMakeLists.txt
+++ b/app/node/block/transition/CMakeLists.txt
@@ -16,6 +16,7 @@
add_subdirectory(crossdissolve)
add_subdirectory(diptocolor)
+add_subdirectory(shader)
set(OLIVE_SOURCES
${OLIVE_SOURCES}
diff --git a/app/node/block/transition/shader/CMakeLists.txt b/app/node/block/transition/shader/CMakeLists.txt
new file mode 100644
index 0000000000..62e233e94b
--- /dev/null
+++ b/app/node/block/transition/shader/CMakeLists.txt
@@ -0,0 +1,22 @@
+# Olive - Non-Linear Video Editor
+# Copyright (C) 2022 Olive Team
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+set(OLIVE_SOURCES
+ ${OLIVE_SOURCES}
+ node/block/transition/shader/shadertransition.h
+ node/block/transition/shader/shadertransition.cpp
+ PARENT_SCOPE
+)
diff --git a/app/node/block/transition/shader/shadertransition.cpp b/app/node/block/transition/shader/shadertransition.cpp
new file mode 100644
index 0000000000..e1d0e6f479
--- /dev/null
+++ b/app/node/block/transition/shader/shadertransition.cpp
@@ -0,0 +1,451 @@
+/***
+
+ Olive - Non-Linear Video Editor
+ Copyright (C) 2022 Olive Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+***/
+
+#include "shadertransition.h"
+
+#include
+
+#include "node/filter/shader/shaderinputsparser.h"
+
+
+namespace olive {
+
+namespace {
+const QString DEFAULT_NAME = "My Transition";
+const QString DEFAULT_VERSION = "0.1";
+const QString DEFAULT_DESCRIPTION = "a transition that's still to be implemented";
+}
+
+// a default shader transition
+const QString olive::ShaderTransition::FRAG_TEMPLATE = QString(
+"#version 150""\n"
+"//OVE shader_name: %1""\n"
+"//OVE shader_description: %2\n"
+"//OVE shader_version: %3\n\n"
+"//OVE end""\n"
+"\n"
+"uniform sampler2D out_block_in;""\n"
+"uniform sampler2D in_block_in;""\n"
+"uniform bool out_block_in_enabled;""\n"
+"uniform bool in_block_in_enabled;""\n"
+"\n"
+"uniform int curve_in;""\n"
+"\n"
+"uniform float ove_tprog_all;""\n"
+"\n"
+"in vec2 ove_texcoord;""\n"
+"out vec4 frag_color;""\n"
+"""\n"
+"void main(void) {""\n"
+" frag_color = texture(in_block_in, ove_texcoord*step( 1.0 - ove_tprog_all, ove_texcoord.xy)) +""\n"
+" texture(out_block_in, ove_texcoord*step( ove_tprog_all, ove_texcoord.xy));""\n"
+"}""\n").arg(DEFAULT_NAME, DEFAULT_DESCRIPTION, DEFAULT_VERSION);
+
+const QString olive::ShaderTransition::VERTEX_TEMPLATE = QString(
+ "#version 150\n"
+ "// No metadata will be parsed from this shader\n"
+ "// You can decalre 'uniform' inputs defined in fragment shader\n"
+ "uniform mat4 ove_mvpmat;\n"
+ "\n"
+ "in vec4 a_position;\n"
+ "in vec2 a_texcoord;\n"
+ "\n"
+ "out vec2 ove_texcoord;\n"
+ "\n"
+ "void main() {\n"
+ " gl_Position = ove_mvpmat * a_position;\n"
+ " ove_texcoord = a_texcoord;\n"
+ "}\n");
+
+
+#define super TransitionBlock
+
+const QString ShaderTransition::kFragShaderCode = QStringLiteral("frag_source");
+const QString ShaderTransition::kUseVertexCode = QStringLiteral("use_vertex");
+const QString ShaderTransition::kVertexShaderCode = QStringLiteral("vertex_source");
+const QString ShaderTransition::kOutputMessages = QStringLiteral("issues");
+
+ShaderTransition::ShaderTransition() :
+ invalidate_code_flag_(false),
+ shader_name_(DEFAULT_NAME),
+ shader_version_(DEFAULT_VERSION),
+ shader_description_(DEFAULT_DESCRIPTION),
+ number_of_iterations_(1),
+ use_vertex_shader(false)
+{
+ // Option to use vertex shader. When false, the default vertex shader is used
+ AddInput(kUseVertexCode, NodeValue::kBoolean, QVariant::fromValue(false),
+ InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable));
+ AddInput(kVertexShaderCode, NodeValue::kText, QVariant::fromValue(VERTEX_TEMPLATE),
+ InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable | kInputFlagHidden));
+ // Full code of fragment shader. Inputs to be exposed are defined within the shader code
+ // with mark-up comments.
+ AddInput(kFragShaderCode, NodeValue::kText, QVariant::fromValue(FRAG_TEMPLATE),
+ InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable));
+
+ // Output messages of shader parser
+ AddInput(kOutputMessages, NodeValue::kText, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable));
+
+ // mark this text input as code, so it will be edited with code editor
+ SetInputProperty( kFragShaderCode, QStringLiteral("text_type"), QString("shader_code_frag"));
+ SetInputProperty( kVertexShaderCode, QStringLiteral("text_type"), QString("shader_code_vert"));
+ // mark this text input as output messages
+ SetInputProperty( kOutputMessages, QStringLiteral("text_type"), QString("shader_issues"));
+}
+
+QString ShaderTransition::Name() const
+{
+ return tr("GLSL Transition");
+}
+
+QString ShaderTransition::id() const
+{
+ return QStringLiteral("org.olivevideoeditor.Olive.transitionshader");
+}
+
+QVector ShaderTransition::Category() const
+{
+ return {kCategoryTransition};
+}
+
+void ShaderTransition::InputValueChangedEvent(const QString &input, int element)
+{
+ Q_UNUSED(element)
+
+ if ((input == kFragShaderCode) || (input == kVertexShaderCode)) {
+ // for some reason, this function is called more than once for each input
+ // of each instance. Parse code only if it has changed
+ QString new_frag_code = GetStandardValue(kFragShaderCode).value();
+ QString new_vertex_code = GetStandardValue(kVertexShaderCode).value();
+
+ if ((frag_shader_code_ != new_frag_code) ||
+ (vertex_shader_code_ != new_vertex_code)) {
+
+ frag_shader_code_ = new_frag_code;
+ vertex_shader_code_ = new_vertex_code;
+ output_messages_.clear();
+
+ // the code of the shader has changed.
+ // Remove all inputs and re-parse the code
+ // to fix shader name and input parameters.
+ parseShaderMetadata();
+
+ // check for syntax
+ checkShaderSyntax();
+
+ SetStandardValue( kOutputMessages, output_messages_.isEmpty() ? tr("None") : output_messages_);
+
+ invalidate_code_flag_ = true;
+ }
+ } else if (input == kUseVertexCode) {
+ bool use_vertex_code = GetStandardValue(kUseVertexCode).toBool();
+ // hide or un-hide the widget for vertex shader
+ SetInputFlag( kVertexShaderCode, kInputFlagHidden, ! use_vertex_code);
+
+ // recalculate shader
+ invalidate_code_flag_ = true;
+ }
+}
+
+bool ShaderTransition::ShaderCodeInvalidateFlag() const
+{
+ bool invalid = invalidate_code_flag_;
+ invalidate_code_flag_ = false;
+ return invalid;
+}
+
+void ShaderTransition::getMetadata(QString &name, QString &description, QString &version) const
+{
+ name = shader_name_;
+ description = shader_description_;
+ version = shader_version_;
+}
+
+QString ShaderTransition::Description() const
+{
+ return tr("a transition made by a GLSL shader code");
+}
+
+void ShaderTransition::Retranslate()
+{
+ super::Retranslate();
+
+ // Retranslate only fixed inputs.
+ // Other inputs are read from the shader code
+ SetInputName( kUseVertexCode, tr("Use vertex shader"));
+ SetInputName( kVertexShaderCode, tr("Vertex Shader"));
+ SetInputName( kFragShaderCode, tr("Fragment Shader"));
+ SetInputName( kOutputMessages, tr("Issues"));
+}
+
+ShaderCode ShaderTransition::GetShaderCode(const Node::ShaderRequest &request) const
+{
+ Q_UNUSED(request);
+ const QString & vertex_code = (GetStandardValue(kUseVertexCode).toBool()) ? vertex_shader_code_ : QString();
+
+ return ShaderCode(frag_shader_code_, vertex_code);
+}
+
+
+void ShaderTransition::ShaderJobEvent(const NodeValueRow &value, ShaderJob *job) const
+{
+ // add user Inputs
+ for( const QString & name : user_input_list_)
+ {
+ job->Insert( name, value);
+ }
+
+ // resolution
+ TexturePtr p = value[QStringLiteral("in_block_in")].toTexture();
+
+ if (p) {
+ job->Insert(QStringLiteral("resolution_in"), NodeValue(NodeValue::kVec2, p->virtual_resolution(), this));
+ }
+
+ job->SetShaderID(GetLabel()); // TBD: label may not be enough to identify a shader univocally
+
+ if (number_of_iterations_ != 1) {
+ job->SetIterations( number_of_iterations_, kInBlockInput);
+ }
+}
+
+
+void ShaderTransition::checkShaderSyntax()
+{
+ bool ok;
+
+ QOpenGLShader frag_shader(QOpenGLShader::Fragment);
+ ok = compileShaderCode( frag_shader, frag_shader_code_);
+
+ if (ok == false)
+ {
+ output_messages_ += QString("Fragment Shader compilation errors:\n") + frag_shader.log();
+ }
+
+ bool use_vertex_code = GetStandardValue(kUseVertexCode).toBool();
+
+ if (use_vertex_code) {
+ QOpenGLShader vertex_shader(QOpenGLShader::Vertex);
+ ok = compileShaderCode( vertex_shader, vertex_shader_code_);
+
+ if (ok == false)
+ {
+ output_messages_ += QString("Vertex Shader compilation errors:\n") + vertex_shader.log();
+ }
+ }
+}
+
+bool ShaderTransition::compileShaderCode( QOpenGLShader & shader, const QString & souce_code)
+{
+ bool ok;
+
+ if (souce_code.startsWith("#version")) {
+
+ ok=shader.compileSourceCode( souce_code);
+ } else {
+
+ // NOTE: this replicates the preamble added in compilation (see openglrenderer.cpp for reference).
+ // This must be aligned if preamble changes
+ ok=shader.compileSourceCode( "#version 150\n\n"
+ "precision highp float;\n\n" + souce_code);
+ }
+
+ return ok;
+}
+
+
+void ShaderTransition::parseShaderMetadata()
+{
+ // only fragment shader has metadata
+ ShaderInputsParser parser(frag_shader_code_);
+
+ parser.Parse();
+
+ reportErrorList( parser);
+ updateInputList( parser);
+ updateGizmoList();
+
+ // update name, if defined in script; otherwise use a default.
+ QString label = (parser.ShaderName().isEmpty()) ? "unnamed" : parser.ShaderName();
+ SetLabel( label);
+ shader_name_ = label;
+ shader_description_ = parser.ShaderDescription();
+ shader_version_ = parser.ShaderVersion();
+ number_of_iterations_ = parser.NumberOfIterations();
+
+ metadataChanged( shader_name_, shader_description_, shader_version_);
+}
+
+void ShaderTransition::reportErrorList( const ShaderInputsParser & parser)
+{
+ const QList & errors = parser.ErrorList();
+
+ QString message;
+ if (errors.size() > 0) {
+ message = QString(tr("There are %1 metadata issues.\n").arg(errors.size()));
+ }
+
+ for (const ShaderInputsParser::Error & e : errors ) {
+ message.append(QString("\"%1\" line %2: %3\n").
+ arg( parser.ShaderName()).arg(e.line).arg(e.issue));
+ }
+
+ output_messages_ += message;
+}
+
+void ShaderTransition::updateInputList( const ShaderInputsParser & parser)
+{
+ const QList< ShaderInputsParser::InputParam> & input_list = parser.InputList();
+ QList< ShaderInputsParser::InputParam>::const_iterator it;
+
+ QStringList new_input_list;
+
+ for( it = input_list.begin(); it != input_list.end(); ++it) {
+
+ new_input_list.append( it->uniform_name);
+
+ if (HasInputWithID(it->uniform_name) == false) {
+ // this is a new input
+ AddInput( it->uniform_name, it->type, it->default_value, it->flags );
+
+ }
+ else {
+ // input already present. Check if some feature has changed
+ if (GetInputDataType( it->uniform_name) != it->type) {
+ SetInputDataType( it->uniform_name, it->type);
+ }
+
+ if (GetInputFlags( it->uniform_name).value() != it->flags.value()) {
+
+ SetInputFlag( it->uniform_name, kInputFlagArray, (it->flags & kInputFlagArray) ? true : false);
+ SetInputFlag(it->uniform_name, kInputFlagNotKeyframable, (it->flags & kInputFlagNotKeyframable) ? true : false);
+ SetInputFlag( it->uniform_name, kInputFlagNotConnectable, (it->flags & kInputFlagNotConnectable) ? true : false);
+ SetInputFlag( it->uniform_name, kInputFlagHidden, (it->flags & kInputFlagHidden) ? true : false);
+ SetInputFlag( it->uniform_name, kInputFlagIgnoreInvalidations, (it->flags & kInputFlagIgnoreInvalidations) ? true : false);
+ }
+ }
+
+ // set other features even if not changed
+ SetInputName( it->uniform_name, it->human_name);
+ if (it->min.isValid()) {
+ SetInputProperty( it->uniform_name, QStringLiteral("min"), it->min);
+ }
+ if (it->max.isValid()) {
+ SetInputProperty( it->uniform_name, QStringLiteral("max"), it->max);
+ }
+
+ if (it->type == NodeValue::kCombo) {
+ SetComboBoxStrings(it->uniform_name, it->values);
+ }
+
+ if (it->type == NodeValue::kVec2) {
+ handle_shape_table_.insert( it->uniform_name, it->pointShape);
+ handle_color_table_.insert( it->uniform_name, it->gizmoColor);
+ }
+ }
+
+
+ // compare 'new_input_list' and 'user_input_list_' to find deleted inputs.
+ checkDeletedInputs( new_input_list);
+
+ // update inputs
+ user_input_list_.clear();
+ user_input_list_ = new_input_list;
+
+ emit InputListChanged();
+}
+
+// this function assumes that 'updateInputList' has been called already.
+// Add a point gizmo for each 'kVec2' that does not already have one
+void ShaderTransition::updateGizmoList()
+{
+ for (QString & aInput : user_input_list_)
+ {
+ if (HasInputWithID(aInput)) {
+ if (GetInputDataType(aInput) == NodeValue::kVec2) {
+
+ // is point new?
+ if (handle_table_.contains(aInput) == false) {
+ PointGizmo * g = AddDraggableGizmo();
+ g->AddInput(NodeKeyframeTrackReference(NodeInput(this, aInput), 0));
+ g->AddInput(NodeKeyframeTrackReference(NodeInput(this, aInput), 1));
+ g->SetDragValueBehavior(PointGizmo::kAbsolute);
+ g->SetVisible( true);
+
+
+ handle_table_.insert( aInput, g);
+ }
+
+ PointGizmo * g = handle_table_[aInput];
+ g->SetShape( handle_shape_table_.value( aInput, PointGizmo::kSquare));
+ g->setProperty("color", QVariant::fromValue(handle_color_table_.value(aInput, Qt::white)));
+ }
+ }
+ }
+}
+
+void ShaderTransition::checkDeletedInputs(const QStringList & new_inputs)
+{
+ // search old inputs that are not present in new inputs
+ for( QString & input : user_input_list_) {
+ if (new_inputs.contains(input) == false) {
+ RemoveInput( input);
+
+ // remove gizmo, if any
+ if (handle_table_.contains(input)) {
+ RemoveGizmo( handle_table_[input]);
+ handle_table_.remove( input);
+ handle_shape_table_.remove( input);
+ handle_color_table_.remove( input);
+ }
+ }
+ }
+}
+
+void ShaderTransition::GizmoDragMove(double x, double y, const Qt::KeyboardModifiers & /*modifiers*/)
+{
+ DraggableGizmo *gizmo = static_cast(sender());
+
+ Q_ASSERT( resolution_.x() != 0.);
+ Q_ASSERT( resolution_.y() != 0.);
+
+ gizmo->GetDraggers()[0].Drag( x/resolution_.x());
+ gizmo->GetDraggers()[1].Drag( y/resolution_.y());
+}
+
+void ShaderTransition::UpdateGizmoPositions(const NodeValueRow &row, const NodeGlobals & globals)
+{
+ resolution_ = globals.vparams().resolution();
+
+ for (QString & aInput : user_input_list_)
+ {
+ if (HasInputWithID(aInput)) {
+ if (row[aInput].type() == NodeValue::kVec2) {
+ QVector2D pos_vec = row[aInput].value() * resolution_;
+ QPointF pos( pos_vec.x(), pos_vec.y());
+
+ handle_table_[aInput]->SetPoint( pos);
+ }
+ }
+ }
+}
+
+} // olive
+
diff --git a/app/node/block/transition/shader/shadertransition.h b/app/node/block/transition/shader/shadertransition.h
new file mode 100644
index 0000000000..92a2183eed
--- /dev/null
+++ b/app/node/block/transition/shader/shadertransition.h
@@ -0,0 +1,125 @@
+/***
+
+ Olive - Non-Linear Video Editor
+ Copyright (C) 2022 Olive Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+***/
+
+#ifndef SHADERTRANSITION_H
+#define SHADERTRANSITION_H
+
+#include "node/block/transition/transition.h"
+#include "node/gizmo/point.h"
+
+class QOpenGLShader;
+
+namespace olive {
+
+class ShaderInputsParser;
+
+// TBD There is a lot of code duplication between 'ShaderFilterNode' and 'ShaderTransition'
+// Common code may be factored in a separate class
+
+/** @brief
+ * A node that implements a transition with a GLSL script. The inputs of this node
+ * are defined in GLSL by markup comments
+ */
+class ShaderTransition : public TransitionBlock
+{
+ Q_OBJECT
+public:
+ ShaderTransition();
+
+ NODE_DEFAULT_FUNCTIONS(ShaderTransition)
+
+ virtual QString Name() const override;
+ virtual QString id() const override;
+ virtual QVector Category() const override;
+ virtual QString Description() const override;
+
+ virtual void Retranslate() override;
+
+ virtual void UpdateGizmoPositions(const NodeValueRow &row, const NodeGlobals &globals) override;
+
+ virtual ShaderCode GetShaderCode(const ShaderRequest &request) const override;
+ void InputValueChangedEvent(const QString &input, int element) override;
+
+ bool ShaderCodeInvalidateFlag() const override;
+
+ void getMetadata( QString & name, QString & description, QString & version) const;
+
+ static const QString kFragShaderCode;
+ static const QString kUseVertexCode;
+ static const QString kVertexShaderCode;
+ static const QString kOutputMessages;
+
+protected:
+ virtual void ShaderJobEvent(const NodeValueRow &value, ShaderJob *job) const override;
+
+signals:
+ void metadataChanged( const QString & name, const QString & description, const QString & version);
+
+private:
+ void parseShaderMetadata();
+ void checkShaderSyntax();
+ bool compileShaderCode( QOpenGLShader &shader, const QString &souce_code);
+ void reportErrorList( const ShaderInputsParser & parser);
+ void updateInputList( const ShaderInputsParser & parser);
+ void updateGizmoList();
+ void checkDeletedInputs( const QStringList & new_inputs);
+
+
+protected slots:
+ virtual void GizmoDragMove(double x, double y, const Qt::KeyboardModifiers &modifiers) override;
+
+private:
+ // set to true when shader code changes. Must be mutable because
+ // it is reset when it is read
+ mutable bool invalidate_code_flag_;
+
+ // source GLSL code
+ QString frag_shader_code_;
+ QString vertex_shader_code_;
+ // name of filter declared in metadata
+ QString shader_name_;
+ // version decalred in metadata
+ QString shader_version_;
+ // description in metadata
+ QString shader_description_;
+ // error in metadata or shader
+ QString output_messages_;
+ // number of times fragment shader is invoked
+ int number_of_iterations_;
+ // user defined inputs
+ QStringList user_input_list_;
+ // input of type vec2 to gizmo map
+ QMap handle_table_;
+ // shape for points in 'handle_table_'
+ QMap handle_shape_table_;
+ // color for points in 'handle_table_'
+ QMap handle_color_table_;
+ // when false, a default vertex shader is used
+ bool use_vertex_shader;
+
+ QVector2D resolution_;
+
+ static const QString FRAG_TEMPLATE;
+ static const QString VERTEX_TEMPLATE;
+};
+
+}
+
+#endif // SHADERTRANSITION_H
diff --git a/app/node/factory.cpp b/app/node/factory.cpp
index 1839d8f080..1a20fd77ac 100644
--- a/app/node/factory.cpp
+++ b/app/node/factory.cpp
@@ -29,6 +29,7 @@
#include "block/subtitle/subtitle.h"
#include "block/transition/crossdissolve/crossdissolvetransition.h"
#include "block/transition/diptocolor/diptocolortransition.h"
+#include "block/transition/shader/shadertransition.h"
#include "color/displaytransform/displaytransform.h"
#include "color/ociogradingtransformlinear/ociogradingtransformlinear.h"
#include "distort/cornerpin/cornerpindistortnode.h"
@@ -42,6 +43,7 @@
#include "distort/wave/wavedistortnode.h"
#include "effect/opacity/opacityeffect.h"
#include "filter/blur/blur.h"
+#include "filter/shader/shader.h"
#include "filter/dropshadow/dropshadowfilter.h"
#include "filter/mosaic/mosaicfilternode.h"
#include "filter/stroke/stroke.h"
@@ -237,6 +239,8 @@ Node *NodeFactory::CreateFromFactoryIndex(const NodeFactory::InternalID &id)
return new TimeInput();
case kBlurFilter:
return new BlurFilterNode();
+ case kShaderFilter:
+ return new ShaderFilterNode();
case kSolidGenerator:
return new SolidGenerator();
case kMerge:
@@ -253,6 +257,8 @@ Node *NodeFactory::CreateFromFactoryIndex(const NodeFactory::InternalID &id)
return new CrossDissolveTransition();
case kDipToColorTransition:
return new DipToColorTransition();
+ case kShaderTransition:
+ return new ShaderTransition();
case kMosaicFilter:
return new MosaicFilterNode();
case kCropDistort:
diff --git a/app/node/factory.h b/app/node/factory.h
index fbf3535514..64b339a725 100644
--- a/app/node/factory.h
+++ b/app/node/factory.h
@@ -45,6 +45,7 @@ class NodeFactory
kTime,
kTrigonometry,
kBlurFilter,
+ kShaderFilter,
kSolidGenerator,
kMerge,
kStrokeFilter,
@@ -53,6 +54,7 @@ class NodeFactory
kTextGeneratorV3,
kCrossDissolveTransition,
kDipToColorTransition,
+ kShaderTransition,
kMosaicFilter,
kCropDistort,
kProjectFootage,
diff --git a/app/node/filter/CMakeLists.txt b/app/node/filter/CMakeLists.txt
index ea93d93205..c927c5d872 100644
--- a/app/node/filter/CMakeLists.txt
+++ b/app/node/filter/CMakeLists.txt
@@ -15,6 +15,7 @@
# along with this program. If not, see .
add_subdirectory(blur)
+add_subdirectory(shader)
add_subdirectory(dropshadow)
add_subdirectory(mosaic)
add_subdirectory(stroke)
diff --git a/app/node/filter/shader/CMakeLists.txt b/app/node/filter/shader/CMakeLists.txt
new file mode 100644
index 0000000000..58d3078222
--- /dev/null
+++ b/app/node/filter/shader/CMakeLists.txt
@@ -0,0 +1,24 @@
+# Olive - Non-Linear Video Editor
+# Copyright (C) 2021 Olive Team
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+set(OLIVE_SOURCES
+ ${OLIVE_SOURCES}
+ node/filter/shader/shader.h
+ node/filter/shader/shader.cpp
+ node/filter/shader/shaderinputsparser.h
+ node/filter/shader/shaderinputsparser.cpp
+ PARENT_SCOPE
+)
diff --git a/app/node/filter/shader/shader.cpp b/app/node/filter/shader/shader.cpp
new file mode 100644
index 0000000000..fabca1807a
--- /dev/null
+++ b/app/node/filter/shader/shader.cpp
@@ -0,0 +1,454 @@
+/***
+
+ Olive - Non-Linear Video Editor
+ Copyright (C) 2022 Olive Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+***/
+
+#include "shader.h"
+
+#include
+
+#include "shaderinputsparser.h"
+
+
+namespace olive {
+
+#define super Node
+
+#define DEFAULT_FILTER_NAME "My Filter"
+
+
+// a default shader filter
+const QString ShaderFilterNode::FRAG_TEMPLATE = QString(
+ "#version 150""\n"
+ "//OVE shader_name: %1\n"
+ "//OVE shader_description: a filter that's still to be implemented\n"
+ "//OVE shader_version: 0.1\n\n"
+ "//OVE main_input_name: Input\n"
+ "uniform sampler2D tex_in;\n\n"
+ "//OVE end\n\n\n"
+ "// pixel coordinates in range [0..1]x[0..1]\n"
+ "in vec2 ove_texcoord;\n"
+ "// output color\n"
+ "out vec4 frag_color;\n\n"
+ "void main(void) {\n"
+ " vec4 textureColor = texture(tex_in, ove_texcoord);\n"
+ " frag_color= textureColor.brga;\n"
+ "}\n").arg(DEFAULT_FILTER_NAME);
+
+const QString ShaderFilterNode::VERTEX_TEMPLATE = QString(
+ "#version 150\n"
+ "// No metadata will be parsed from this shader\n"
+ "// You can decalre 'uniform' inputs defined in fragment shader\n"
+ "uniform mat4 ove_mvpmat;\n"
+ "\n"
+ "in vec4 a_position;\n"
+ "in vec2 a_texcoord;\n"
+ "\n"
+ "out vec2 ove_texcoord;\n"
+ "\n"
+ "void main() {\n"
+ " gl_Position = ove_mvpmat * a_position;\n"
+ " ove_texcoord = a_texcoord;\n"
+ "}\n");
+
+const QString ShaderFilterNode::kTextureInput = QStringLiteral("tex_in");
+const QString ShaderFilterNode::kFragShaderCode = QStringLiteral("frag_source");
+const QString ShaderFilterNode::kUseVertexCode = QStringLiteral("use_vertex");
+const QString ShaderFilterNode::kVertexShaderCode = QStringLiteral("vertex_source");
+const QString ShaderFilterNode::kOutputMessages = QStringLiteral("issues");
+
+
+ShaderFilterNode::ShaderFilterNode():
+ invalidate_code_flag_(false),
+ main_input_name_("Input"),
+ number_of_iterations_(1),
+ use_vertex_shader(false)
+{
+ // Option to use vertex shader. When false, the default vertex shader is used
+ AddInput(kUseVertexCode, NodeValue::kBoolean, QVariant::fromValue(false),
+ InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable));
+ AddInput(kVertexShaderCode, NodeValue::kText, QVariant::fromValue(VERTEX_TEMPLATE),
+ InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable));
+ // Full code of fragment shader. Inputs to be exposed are defined within the shader code
+ // with mark-up comments.
+ AddInput(kFragShaderCode, NodeValue::kText, QVariant::fromValue(FRAG_TEMPLATE),
+ InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable));
+
+ // Output messages of shader parser
+ AddInput(kOutputMessages, NodeValue::kText, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable));
+
+ // mark this text input as code, so it will be edited with code editor
+ SetInputProperty( kFragShaderCode, QStringLiteral("text_type"), QString("shader_code_frag"));
+ SetInputProperty( kVertexShaderCode, QStringLiteral("text_type"), QString("shader_code_vert"));
+ // mark this text input as output messages
+ SetInputProperty( kOutputMessages, QStringLiteral("text_type"), QString("shader_issues"));
+
+ // A default texture. Must be connected even for shaders that do not require a texture.
+ AddInput(kTextureInput, NodeValue::kTexture, InputFlags(kInputFlagNotKeyframable));
+ SetEffectInput(kTextureInput);
+
+ SetFlag(kVideoEffect);
+}
+
+Node *ShaderFilterNode::copy() const
+{
+ ShaderFilterNode * new_node = new ShaderFilterNode();
+
+ // copy all inputs not created in constructor
+ CopyInputs( this, new_node, false);
+
+ return new_node;
+}
+
+QString ShaderFilterNode::Name() const
+{
+ return QString("Shader");
+}
+
+QString ShaderFilterNode::id() const
+{
+ return QStringLiteral("org.olivevideoeditor.Olive.shader");
+}
+
+QVector ShaderFilterNode::Category() const
+{
+ return {kCategoryFilter};
+}
+
+
+void ShaderFilterNode::InputValueChangedEvent(const QString &input, int element)
+{
+ Q_UNUSED(element)
+
+ if ((input == kFragShaderCode) || (input == kVertexShaderCode)) {
+ // for some reason, this function is called more than once for each input
+ // of each instance. Parse code only if it has changed
+ QString new_frag_code = GetStandardValue(kFragShaderCode).value();
+ QString new_vertex_code = GetStandardValue(kVertexShaderCode).value();
+
+ if ((frag_shader_code_ != new_frag_code) ||
+ (vertex_shader_code_ != new_vertex_code)) {
+
+ frag_shader_code_ = new_frag_code;
+ vertex_shader_code_ = new_vertex_code;
+ output_messages_.clear();
+
+ // the code of the shader has changed.
+ // Remove all inputs and re-parse the code
+ // to fix shader name and input parameters.
+ parseShaderMetadata();
+
+ // check for syntax
+ checkShaderSyntax();
+
+ SetStandardValue( kOutputMessages, output_messages_.isEmpty() ? tr("None") : output_messages_);
+
+ invalidate_code_flag_ = true;
+ }
+ } else if (input == kUseVertexCode) {
+ bool use_vertex_code = GetStandardValue(kUseVertexCode).toBool();
+ // hide or un-hide the widget for vertex shader
+ SetInputFlag( kVertexShaderCode, kInputFlagHidden, ! use_vertex_code);
+
+ // recalculate shader
+ invalidate_code_flag_ = true;
+ }
+}
+
+bool ShaderFilterNode::ShaderCodeInvalidateFlag() const
+{
+ bool invalid = invalidate_code_flag_;
+ invalidate_code_flag_ = false;
+ return invalid;
+}
+
+void ShaderFilterNode::getMetadata(QString &name, QString &description, QString &version) const
+{
+ name = shader_name_;
+ description = shader_description_;
+ version = shader_version_;
+}
+
+
+QString ShaderFilterNode::Description() const
+{
+ return tr("a filter made by a GLSL shader code");
+}
+
+void ShaderFilterNode::Retranslate()
+{
+ super::Retranslate();
+
+ // Retranslate only fixed inputs.
+ // Other inputs are read from the shader code
+ SetInputName( kUseVertexCode, tr("Use vertex shader"));
+ SetInputName( kVertexShaderCode, tr("Vertex Shader"));
+ SetInputName( kFragShaderCode, tr("Fragment Shader"));
+ SetInputName( kOutputMessages, tr("Issues"));
+}
+
+ShaderCode ShaderFilterNode::GetShaderCode(const Node::ShaderRequest &request) const
+{
+ Q_UNUSED(request);
+ const QString & vertex_code = (GetStandardValue(kUseVertexCode).toBool()) ? vertex_shader_code_ : QString();
+
+ return ShaderCode(frag_shader_code_, vertex_code);
+}
+
+
+void ShaderFilterNode::Value(const NodeValueRow &value, const NodeGlobals &globals, NodeValueTable *table) const
+{
+ if (TexturePtr tex = value[kTextureInput].toTexture()) {
+ ShaderJob job(value);
+ job.SetShaderID(GetLabel()); // TBD: label may not be enough to identify a shader univocally
+ job.Insert(QStringLiteral("resolution_in"), NodeValue(NodeValue::kVec2, tex->virtual_resolution(), this));
+
+ if (number_of_iterations_ != 1) {
+ job.SetIterations( number_of_iterations_, main_input_name_);
+ }
+
+ table->Push(NodeValue::kTexture, tex->toJob(job), this);
+ }
+}
+
+
+void ShaderFilterNode::checkShaderSyntax()
+{
+ bool ok;
+
+ QOpenGLShader frag_shader(QOpenGLShader::Fragment);
+ ok = compileShaderCode( frag_shader, frag_shader_code_);
+
+ if (ok == false)
+ {
+ output_messages_ += QString("Fragment Shader compilation errors:\n") + frag_shader.log();
+ }
+
+ bool use_vertex_code = GetStandardValue(kUseVertexCode).toBool();
+
+ if (use_vertex_code) {
+ QOpenGLShader vertex_shader(QOpenGLShader::Vertex);
+ ok = compileShaderCode( vertex_shader, vertex_shader_code_);
+
+ if (ok == false)
+ {
+ output_messages_ += QString("Vertex Shader compilation errors:\n") + vertex_shader.log();
+ }
+ }
+}
+
+bool ShaderFilterNode::compileShaderCode( QOpenGLShader & shader, const QString & souce_code)
+{
+ bool ok;
+
+ if (souce_code.startsWith("#version")) {
+
+ ok=shader.compileSourceCode( souce_code);
+ } else {
+
+ // NOTE: this replicates the preamble added in compilation (see openglrenderer.cpp for reference).
+ // This must be aligned if preamble changes
+ ok=shader.compileSourceCode( "#version 150\n\n"
+ "precision highp float;\n\n" + souce_code);
+ }
+
+ return ok;
+}
+
+void ShaderFilterNode::parseShaderMetadata()
+{
+ // only fragment shader has metadata
+ ShaderInputsParser parser(frag_shader_code_);
+
+ parser.Parse();
+
+ reportErrorList( parser);
+ updateInputList( parser);
+ updateGizmoList();
+
+ // update name, if defined in script; otherwise use a default.
+ QString label = (parser.ShaderName().isEmpty()) ? "unnamed" : parser.ShaderName();
+ SetLabel( label);
+ shader_name_ = label;
+ shader_description_ = parser.ShaderDescription();
+ shader_version_ = parser.ShaderVersion();
+ number_of_iterations_ = parser.NumberOfIterations();
+
+ metadataChanged( shader_name_, shader_description_, shader_version_);
+}
+
+void ShaderFilterNode::reportErrorList( const ShaderInputsParser & parser)
+{
+ const QList & errors = parser.ErrorList();
+
+ QString message;
+ if (errors.size() > 0) {
+ message = QString(tr("There are %1 metadata issues.\n").arg(errors.size()));
+ }
+
+ for (const ShaderInputsParser::Error & e : errors ) {
+ message.append(QString("\"%1\" line %2: %3\n").
+ arg( parser.ShaderName()).arg(e.line).arg(e.issue));
+ }
+
+ output_messages_ += message;
+}
+
+void ShaderFilterNode::updateInputList( const ShaderInputsParser & parser)
+{
+ const QList< ShaderInputsParser::InputParam> & input_list = parser.InputList();
+ QList< ShaderInputsParser::InputParam>::const_iterator it;
+
+ QStringList new_input_list;
+
+ for( it = input_list.begin(); it != input_list.end(); ++it) {
+
+ new_input_list.append( it->uniform_name);
+
+ if (HasInputWithID(it->uniform_name) == false) {
+ // this is a new input
+ AddInput( it->uniform_name, it->type, it->default_value, it->flags );
+
+ }
+ else {
+ // input already present. Check if some feature has changed
+ if (GetInputDataType( it->uniform_name) != it->type) {
+ SetInputDataType( it->uniform_name, it->type);
+ }
+
+ if (GetInputFlags( it->uniform_name).value() != it->flags.value()) {
+
+ SetInputFlag( it->uniform_name, kInputFlagArray, (it->flags & kInputFlagArray) ? true : false);
+ SetInputFlag(it->uniform_name, kInputFlagNotKeyframable, (it->flags & kInputFlagNotKeyframable) ? true : false);
+ SetInputFlag( it->uniform_name, kInputFlagNotConnectable, (it->flags & kInputFlagNotConnectable) ? true : false);
+ SetInputFlag( it->uniform_name, kInputFlagHidden, (it->flags & kInputFlagHidden) ? true : false);
+ SetInputFlag( it->uniform_name, kInputFlagIgnoreInvalidations, (it->flags & kInputFlagIgnoreInvalidations) ? true : false);
+ }
+ }
+
+ // set other features even if not changed
+ SetInputName( it->uniform_name, it->human_name);
+ if (it->min.isValid()) {
+ SetInputProperty( it->uniform_name, QStringLiteral("min"), it->min);
+ }
+ if (it->max.isValid()) {
+ SetInputProperty( it->uniform_name, QStringLiteral("max"), it->max);
+ }
+
+ if (it->type == NodeValue::kCombo) {
+ SetComboBoxStrings(it->uniform_name, it->values);
+ }
+
+ if (it->type == NodeValue::kVec2) {
+ handle_shape_table_.insert( it->uniform_name, it->pointShape);
+ handle_color_table_.insert( it->uniform_name, it->gizmoColor);
+ }
+ }
+
+ // main input
+ main_input_name_ = (parser.MainInputName().isEmpty()) ? "Input" : parser.MainInputName();
+ SetInputName( kTextureInput, main_input_name_);
+
+ // compare 'new_input_list' and 'user_input_list_' to find deleted inputs.
+ checkDeletedInputs( new_input_list);
+
+ // update inputs
+ user_input_list_.clear();
+ user_input_list_ = new_input_list;
+
+ emit InputListChanged();
+}
+
+// this function assumes that 'updateInputList' has been called already.
+// Add a point gizmo for each 'kVec2' that does not already have one
+void ShaderFilterNode::updateGizmoList()
+{
+ for (QString & aInput : user_input_list_)
+ {
+ if (HasInputWithID(aInput)) {
+ if (GetInputDataType(aInput) == NodeValue::kVec2) {
+
+ // is point new?
+ if (handle_table_.contains(aInput) == false) {
+ PointGizmo * g = AddDraggableGizmo();
+ g->AddInput(NodeKeyframeTrackReference(NodeInput(this, aInput), 0));
+ g->AddInput(NodeKeyframeTrackReference(NodeInput(this, aInput), 1));
+ g->SetDragValueBehavior(PointGizmo::kAbsolute);
+ g->SetVisible( true);
+
+
+ handle_table_.insert( aInput, g);
+ }
+
+ PointGizmo * g = handle_table_[aInput];
+ g->SetShape( handle_shape_table_.value( aInput, PointGizmo::kSquare));
+ g->setProperty("color", QVariant::fromValue(handle_color_table_.value(aInput, Qt::white)));
+ }
+ }
+ }
+}
+
+void ShaderFilterNode::checkDeletedInputs(const QStringList & new_inputs)
+{
+ // search old inputs that are not present in new inputs
+ for( QString & input : user_input_list_) {
+ if (new_inputs.contains(input) == false) {
+ RemoveInput( input);
+
+ // remove gizmo, if any
+ if (handle_table_.contains(input)) {
+ RemoveGizmo( handle_table_[input]);
+ handle_table_.remove( input);
+ handle_shape_table_.remove( input);
+ handle_color_table_.remove( input);
+ }
+ }
+ }
+}
+
+void ShaderFilterNode::GizmoDragMove(double x, double y, const Qt::KeyboardModifiers & /*modifiers*/)
+{
+ DraggableGizmo *gizmo = static_cast(sender());
+
+ Q_ASSERT( resolution_.x() != 0.);
+ Q_ASSERT( resolution_.y() != 0.);
+
+ gizmo->GetDraggers()[0].Drag( x/resolution_.x());
+ gizmo->GetDraggers()[1].Drag( y/resolution_.y());
+}
+
+void ShaderFilterNode::UpdateGizmoPositions(const NodeValueRow &row, const NodeGlobals & globals)
+{
+ resolution_ = globals.vparams().resolution();
+
+ for (QString & aInput : user_input_list_)
+ {
+ if (HasInputWithID(aInput)) {
+ if (row[aInput].type() == NodeValue::kVec2) {
+ QVector2D pos_vec = row[aInput].value() * resolution_;
+ QPointF pos( pos_vec.x(), pos_vec.y());
+
+ handle_table_[aInput]->SetPoint( pos);
+ }
+ }
+ }
+}
+
+
+} // namespace olive
+
diff --git a/app/node/filter/shader/shader.h b/app/node/filter/shader/shader.h
new file mode 100644
index 0000000000..93c817bb29
--- /dev/null
+++ b/app/node/filter/shader/shader.h
@@ -0,0 +1,127 @@
+/***
+
+ Olive - Non-Linear Video Editor
+ Copyright (C) 2022 Olive Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+***/
+
+#ifndef SHADERFILTERNODE_H
+#define SHADERFILTERNODE_H
+
+#include "node/node.h"
+#include "node/gizmo/point.h"
+
+class QOpenGLShader;
+
+
+namespace olive {
+
+class ShaderInputsParser;
+
+
+/** @brief
+ * A node that implements a filter with a GLSL script. The inputs of this node
+ * are defined in GLSL by markup comments
+ */
+class ShaderFilterNode : public Node
+{
+ Q_OBJECT
+public:
+ ShaderFilterNode();
+
+ NODE_DEFAULT_DESTRUCTOR(ShaderFilterNode)
+
+ virtual Node* copy() const override;
+
+ virtual QString Name() const override;
+ virtual QString id() const override;
+ virtual QVector Category() const override;
+ virtual QString Description() const override;
+
+ virtual void Retranslate() override;
+
+ virtual void UpdateGizmoPositions(const NodeValueRow &row, const NodeGlobals &globals) override;
+
+ virtual ShaderCode GetShaderCode(const ShaderRequest &request) const override;
+ virtual void Value(const NodeValueRow& value, const NodeGlobals &globals, NodeValueTable *table) const override;
+ void InputValueChangedEvent(const QString &input, int element) override;
+
+ bool ShaderCodeInvalidateFlag() const override;
+
+ void getMetadata( QString & name, QString & description, QString & version) const;
+
+ static const QString kTextureInput;
+ static const QString kFragShaderCode;
+ static const QString kUseVertexCode;
+ static const QString kVertexShaderCode;
+ static const QString kOutputMessages;
+
+signals:
+ void metadataChanged( const QString & name, const QString & description, const QString & version);
+
+private:
+ void parseShaderMetadata();
+ void checkShaderSyntax();
+ bool compileShaderCode( QOpenGLShader &shader, const QString &souce_code);
+ void reportErrorList( const ShaderInputsParser & parser);
+ void updateInputList( const ShaderInputsParser & parser);
+ void updateGizmoList();
+ void checkDeletedInputs( const QStringList & new_inputs);
+
+
+protected slots:
+ virtual void GizmoDragMove(double x, double y, const Qt::KeyboardModifiers &modifiers) override;
+
+private:
+ // set to true when shader code changes. Must be mutable because
+ // it is reset when it is read
+ mutable bool invalidate_code_flag_;
+
+ // source GLSL code
+ QString frag_shader_code_;
+ QString vertex_shader_code_;
+ // name of filter declared in metadata
+ QString shader_name_;
+ // version decalred in metadata
+ QString shader_version_;
+ // description in metadata
+ QString shader_description_;
+ // error in metadata or shader
+ QString output_messages_;
+ // name of default texture input
+ QString main_input_name_;
+ // number of times fragment shader is invoked
+ int number_of_iterations_;
+ // user defined inputs
+ QStringList user_input_list_;
+ // input of type vec2 to gizmo map
+ QMap handle_table_;
+ // shape for points in 'handle_table_'
+ QMap handle_shape_table_;
+ // color for points in 'handle_table_'
+ QMap handle_color_table_;
+ // when false, a default vertex shader is used
+ bool use_vertex_shader;
+
+ QVector2D resolution_;
+
+ static const QString FRAG_TEMPLATE;
+ static const QString VERTEX_TEMPLATE;
+};
+
+}
+
+#endif // SHADERFILTERNODE_H
diff --git a/app/node/filter/shader/shaderinputsparser.cpp b/app/node/filter/shader/shaderinputsparser.cpp
new file mode 100644
index 0000000000..f54e004b13
--- /dev/null
+++ b/app/node/filter/shader/shaderinputsparser.cpp
@@ -0,0 +1,656 @@
+/***
+
+ Olive - Non-Linear Video Editor
+ Copyright (C) 2021 Olive Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+***/
+
+#include "shaderinputsparser.h"
+
+#include
+#include
+#include
+#include
+#include
+
+
+
+namespace olive {
+
+namespace {
+
+// This kind of comments are parsed for input parameters
+const QString OLIVE_MARKED_COMMENT = QString::fromLatin1("//OVE");
+// definition of a uniform variable
+const QString UNIFORM_TAG = QString::fromLatin1("uniform");
+
+// per Shader metadata
+
+// title of the shader. This will become part of node name
+QRegularExpression SHADER_NAME_REGEX("^\\s*//OVE\\s*shader_name:\\s*(?.*)\\s*$");
+// description. So far not used by application
+QRegularExpression SHADER_DESCRIPTION_REGEX("^\\s*//OVE\\s*shader_description:\\s*(?.*)\\s*$");
+// Version string. So far not used by application
+QRegularExpression SHADER_VERSION_REGEX("^\\s*//OVE\\s*shader_version:\\s*(?.*)\\s*$");
+// human name for default input
+QRegularExpression SHADER_MAIN_INPUT_NAME_REGEX("^\\s*//OVE\\s*main_input_name:\\s*(?.*)\\s*$");
+
+// number of times the fragment shader is invoked
+QRegularExpression NUM_OF_ITERATIONS_REGEX("^\\s*//OVE\\s*number_of_iterations:\\s*(?.*)\\s*$");
+
+// per Input metadata
+
+// human name of the input
+QRegularExpression INPUT_NAME_REGEX("^\\s*//OVE\\s*name:\\s*(?.*)\\s*$");
+// name of uniform variable
+QRegularExpression INPUT_UNIFORM_REGEX("^\\s*uniform\\s+(?[A-Za-z0-9_]+)\\s+(?[A-Za-z0-9_]+)\\s*;");
+// kind of input
+QRegularExpression INPUT_TYPE_REGEX("^\\s*//OVE\\s*type:\\s*(?.*)\\s*$");
+// input flags
+QRegularExpression INPUT_FLAG_REGEX("^\\s*//OVE\\s*flag:\\s*(?.*)\\s*$");
+// minimum value
+QRegularExpression INPUT_MIN_REGEX("^\\s*//OVE\\s*min:\\s*(?.*)\\s*$");
+// maximum value
+QRegularExpression INPUT_MAX_REGEX("^\\s*//OVE\\s*max:\\s*(?.*)\\s*$");
+// default value
+QRegularExpression INPUT_DEFAULT_REGEX("^\\s*//OVE\\s*default:\\s*(?.*)\\s*$");
+// list of values for a selection combo
+QRegularExpression INPUT_VALUES_REGEX("^\\s*//OVE\\s*values:\\s*(?.*)\\s*$");
+// shape used to draw a point
+QRegularExpression INPUT_SHAPE_REGEX("^\\s*//OVE\\s*shape:\\s*(?.*)\\s*$");
+// color used to draw gizmo
+QRegularExpression INPUT_COLOR_REGEX("^\\s*//OVE\\s*color:\\s*(?.*)\\s*$");
+// description. So far not used by application
+QRegularExpression INPUT_DESCRIPTION_REGEX("^\\s*//OVE\\s*description:\\s*(?.*)\\s*$");
+
+// when this is found, parsing is stopped
+QRegularExpression STOP_PARSE_REGEX("^\\s*//OVE\\s*end\\s*$");
+
+// lookup table for kind of input
+const QMap INPUT_TYPE_TABLE{{"TEXTURE", NodeValue::kTexture},
+ {"COLOR", NodeValue::kColor},
+ {"FLOAT", NodeValue::kFloat},
+ {"INTEGER", NodeValue::kInt},
+ {"BOOLEAN", NodeValue::kBoolean},
+ {"SELECTION", NodeValue::kCombo},
+ {"POINT", NodeValue::kVec2}
+ };
+
+// table with default minimum and maximum values for a node type.
+// This is needed to avoid that 0 is used as minimum and maximum if user does not specify one
+const QMap MINIMUM_TABLE{{NodeValue::kFloat, QVariant( std::numeric_limits::lowest())},
+ {NodeValue::kInt, QVariant( std::numeric_limits::lowest())}
+ };
+
+const QMap MAXIMUM_TABLE{{NodeValue::kFloat, QVariant( std::numeric_limits::max())},
+ {NodeValue::kInt, QVariant( std::numeric_limits::max())}
+ };
+
+} // namespace
+
+
+ShaderInputsParser::ShaderInputsParser( const QString & shader_code) :
+ shader_code_(shader_code),
+ line_number_(1),
+ number_of_iterations_(1)
+{
+ clearCurrentInput();
+
+ // build the table that matches an OVE markup comment with its parse function
+ INPUT_PARAM_PARSE_TABLE.insert( & SHADER_NAME_REGEX, & ShaderInputsParser::parseShaderName);
+ INPUT_PARAM_PARSE_TABLE.insert( & SHADER_DESCRIPTION_REGEX, & ShaderInputsParser::parseShaderDescription);
+ INPUT_PARAM_PARSE_TABLE.insert( & SHADER_VERSION_REGEX, & ShaderInputsParser::parseShaderVersion);
+ INPUT_PARAM_PARSE_TABLE.insert( & SHADER_MAIN_INPUT_NAME_REGEX, & ShaderInputsParser::parseMainInputName);
+ INPUT_PARAM_PARSE_TABLE.insert( & NUM_OF_ITERATIONS_REGEX, & ShaderInputsParser::parseNumberOfIterations);
+
+ INPUT_PARAM_PARSE_TABLE.insert( & INPUT_NAME_REGEX, & ShaderInputsParser::parseInputName);
+ INPUT_PARAM_PARSE_TABLE.insert( & INPUT_UNIFORM_REGEX, & ShaderInputsParser::parseInputUniform);
+ INPUT_PARAM_PARSE_TABLE.insert( & INPUT_TYPE_REGEX, & ShaderInputsParser::parseInputType);
+ INPUT_PARAM_PARSE_TABLE.insert( & INPUT_FLAG_REGEX, & ShaderInputsParser::parseInputFlags);
+ INPUT_PARAM_PARSE_TABLE.insert( & INPUT_VALUES_REGEX, & ShaderInputsParser::parseInputValueList);
+ INPUT_PARAM_PARSE_TABLE.insert( & INPUT_SHAPE_REGEX, & ShaderInputsParser::parseInputPointShape);
+ INPUT_PARAM_PARSE_TABLE.insert( & INPUT_COLOR_REGEX, & ShaderInputsParser::parseInputGizmoColor);
+ INPUT_PARAM_PARSE_TABLE.insert( & INPUT_MIN_REGEX, & ShaderInputsParser::parseInputMin);
+ INPUT_PARAM_PARSE_TABLE.insert( & INPUT_MAX_REGEX, & ShaderInputsParser::parseInputMax);
+ INPUT_PARAM_PARSE_TABLE.insert( & INPUT_DEFAULT_REGEX, & ShaderInputsParser::parseInputDefault);
+ INPUT_PARAM_PARSE_TABLE.insert( & INPUT_DESCRIPTION_REGEX, & ShaderInputsParser::parseInputDescription);
+
+ INPUT_PARAM_PARSE_TABLE.insert( & STOP_PARSE_REGEX, & ShaderInputsParser::stopParse);
+}
+
+void ShaderInputsParser::Parse()
+{
+ int fragment_size = shader_code_.length();
+ int cursor = 0;
+ QString line;
+ InputParseState state = PARSING;
+
+ static const QRegularExpression LINE_BREAK = QRegularExpression("[\\n\\r]+");
+
+ input_list_.clear();
+ error_list_.clear();
+ line_number_ = 1;
+
+ shader_name_ = QString();
+ shader_description_ = QString();
+ shader_version_ = QString();
+ number_of_iterations_ = 1;
+
+ // parse line by line
+ while (state != SHADER_COMPLETE) {
+
+ int line_end_index = shader_code_.indexOf( LINE_BREAK, cursor);
+
+ if (line_end_index == -1) {
+ // last line of file
+ line = QString( shader_code_).mid( cursor, fragment_size - cursor);
+ cursor = fragment_size;
+ }
+ else {
+ line = QString( shader_code_).mid( cursor, line_end_index - cursor);
+ cursor = line_end_index;
+ }
+
+ line = line.trimmed();
+
+ // check if this line must be parsed
+ if ( (line.startsWith(QString(OLIVE_MARKED_COMMENT))) ||
+ (line.startsWith(QString(UNIFORM_TAG))) ) {
+
+ state = parseSingleLine( line);
+
+ if (state == INPUT_COMPLETE) {
+ input_list_.append( currentInput_);
+ clearCurrentInput();
+ }
+ }
+
+ if (state != SHADER_COMPLETE) {
+
+ // go to the begin of next line.
+ // Different platforms have different end of line.
+ while ((cursor < fragment_size) &&
+ (shader_code_.at(cursor).isSpace()) ) {
+
+ if (shader_code_.at(cursor) == '\n') {
+ line_number_++;
+ }
+ ++cursor;
+ }
+
+ // check for end of fragment
+ if (cursor >= fragment_size) {
+ state = SHADER_COMPLETE;
+ }
+ }
+ }
+}
+
+
+void ShaderInputsParser::clearCurrentInput()
+{
+ currentInput_.human_name.clear();
+ currentInput_.uniform_name.clear();
+ currentInput_.description.clear();
+ currentInput_.type_string.clear();
+ currentInput_.type = NodeValue::kNone;
+ currentInput_.flags = InputFlags(kInputFlagNormal);
+ currentInput_.values.clear();
+ currentInput_.pointShape = PointGizmo::kSquare;
+ currentInput_.gizmoColor = Qt::white;
+ currentInput_.min = QVariant();
+ currentInput_.max = QVariant();
+ currentInput_.default_value = QVariant();
+}
+
+
+ShaderInputsParser::InputParseState ShaderInputsParser::parseSingleLine( const QString & line)
+{
+ QRegularExpressionMatch match;
+ InputParseState new_state = PARSING;
+
+ QMap::iterator it;
+
+ // iterate over all regular expressions for lines
+ for (it = INPUT_PARAM_PARSE_TABLE.begin(); it != INPUT_PARAM_PARSE_TABLE.end(); ++it) {
+
+ match = it.key()->match( line);
+
+ if (match.hasMatch()) {
+ LineParseFunction parseFunction = it.value();
+ new_state = (this->*parseFunction)( match);
+
+ // stop at the first match
+ break;
+ }
+ }
+
+ return new_state;
+}
+
+ShaderInputsParser::InputParseState
+ShaderInputsParser::parseShaderName(const QRegularExpressionMatch & match)
+{
+ shader_name_ = match.captured("name");
+ return PARSING;
+}
+
+ShaderInputsParser::InputParseState
+ShaderInputsParser::parseShaderDescription(const QRegularExpressionMatch & match)
+{
+ if (shader_description_.size() > 0) {
+ shader_description_.append(QStringLiteral("\n"));
+ }
+ shader_description_ += match.captured("description");
+ return PARSING;
+}
+
+ShaderInputsParser::InputParseState
+ShaderInputsParser::parseShaderVersion(const QRegularExpressionMatch & match)
+{
+ shader_version_ = match.captured("version");
+ return PARSING;
+}
+
+ShaderInputsParser::InputParseState
+ShaderInputsParser::parseMainInputName(const QRegularExpressionMatch & match)
+{
+ main_input_name_ = match.captured("name");
+ return PARSING;
+}
+
+ShaderInputsParser::InputParseState ShaderInputsParser::parseNumberOfIterations(const QRegularExpressionMatch & match)
+{
+ bool ok = false;
+ int num_iter = match.captured("num").toInt( & ok);
+
+ if (ok && (num_iter >= 1)) {
+ number_of_iterations_ = num_iter;
+ } else {
+ number_of_iterations_ = 1;
+ reportError(QObject::tr("Number of iterations must be a number greater or equal to 1"));
+ }
+
+ return PARSING;
+}
+
+ShaderInputsParser::InputParseState
+ShaderInputsParser::parseInputName(const QRegularExpressionMatch & match)
+{
+ currentInput_.human_name = match.captured("name");
+ return PARSING;
+}
+
+ShaderInputsParser::InputParseState
+ShaderInputsParser::parseInputUniform(const QRegularExpressionMatch & match)
+{
+ ShaderInputsParser::InputParseState state = PARSING;
+
+ // not all uniforms are exposed as inputs
+ if (currentInput_.type != NodeValue::kNone) {
+ currentInput_.uniform_name = match.captured("name");
+ checkConsistentType( currentInput_.type, match.captured("type"));
+ state = INPUT_COMPLETE;
+ }
+
+ return state;
+}
+
+ShaderInputsParser::InputParseState
+ShaderInputsParser::parseInputType(const QRegularExpressionMatch & match)
+{
+ currentInput_.type_string = match.captured("type");
+ currentInput_.type = INPUT_TYPE_TABLE.value( currentInput_.type_string, NodeValue::kNone);
+
+ if (currentInput_.type == NodeValue::kNone) {
+ reportError(QObject::tr("type %1 is invalid").arg(currentInput_.type_string));
+ }
+
+ // preset 'min' and 'max', in case they are not defined
+ currentInput_.min = MINIMUM_TABLE.value( currentInput_.type, QVariant());
+ currentInput_.max = MAXIMUM_TABLE.value( currentInput_.type, QVariant());
+
+ return PARSING;
+}
+
+
+ShaderInputsParser::InputParseState
+ShaderInputsParser::parseInputFlags(const QRegularExpressionMatch & line_match)
+{
+ // more flags can be present in one line
+ static const QRegularExpression FLAG_REGEX("(ARRAY|NOT_CONNECTABLE|NOT_KEYFRAMABLE|HIDDEN|MAIN_INPUT)");
+
+ static const QMap FLAG_TABLE{{"ARRAY", kInputFlagArray},
+ {"NOT_CONNECTABLE", kInputFlagNotConnectable},
+ {"NOT_KEYFRAMABLE", kInputFlagNotKeyframable},
+ {"HIDDEN", kInputFlagHidden}
+ };
+
+
+ QString flags_line = line_match.captured("flags");
+ QRegularExpressionMatchIterator flag_match = FLAG_REGEX.globalMatch( flags_line);
+
+ while (flag_match.hasNext()) {
+ QRegularExpressionMatch flag = flag_match.next();
+ QString flag_str = flag.captured(1);
+
+ currentInput_.flags |= InputFlags(FLAG_TABLE.value( flag_str, kInputFlagNormal));
+ }
+
+ return PARSING;
+}
+
+
+
+ShaderInputsParser::InputParseState
+ShaderInputsParser::parseInputValueList(const QRegularExpressionMatch & line_match)
+{
+ // entries are enclosed in double quotes
+ static const QRegularExpression COMBO_ENTRY_REGEX("\"([^\"]+)\"");
+
+ QString values_line = line_match.captured("values");
+ QRegularExpressionMatchIterator value_match = COMBO_ENTRY_REGEX.globalMatch( values_line);
+
+ while (value_match.hasNext()) {
+ QRegularExpressionMatch value = value_match.next();
+ QString value_str = value.captured(1);
+
+ currentInput_.values << value_str;
+ }
+
+ return PARSING;
+}
+
+ShaderInputsParser::InputParseState ShaderInputsParser::parseInputPointShape(const QRegularExpressionMatch & line_match)
+{
+ // lookup table for point shape
+ static const QMap< QString, PointGizmo::Shape> INPUT_POINT_SHAPE_TABLE{
+ {"SQUARE", PointGizmo::kSquare},
+ {"CIRCLE", PointGizmo::kCircle},
+ {"ANCHOR", PointGizmo::kAnchorPoint}
+ };
+
+ if (line_match.hasMatch()) {
+ QString shape_str = line_match.captured("shape");
+
+ if (INPUT_POINT_SHAPE_TABLE.contains(shape_str)) {
+ currentInput_.pointShape = INPUT_POINT_SHAPE_TABLE[shape_str];
+ }
+ else {
+ currentInput_.pointShape = PointGizmo::kSquare;
+ reportError(QObject::tr("'%1' is not a valid point shape.").
+ arg(shape_str));
+ }
+ }
+
+ return PARSING;
+}
+
+ShaderInputsParser::InputParseState ShaderInputsParser::parseInputGizmoColor(const QRegularExpressionMatch & line_match)
+{
+ if (currentInput_.type == NodeValue::kVec2) {
+ QString color_string = line_match.captured("color").trimmed();
+ Color ove_color = parseColor( color_string).value();
+ currentInput_.gizmoColor = QColor( int(ove_color.red()*255.0),
+ int(ove_color.green()*255.0),
+ int(ove_color.blue()*255.0), 255);
+ }
+ else {
+ reportError( QObject::tr("Attribute 'color' can be used for type POINT, not for %1").
+ arg(currentInput_.type_string));
+ }
+
+ return PARSING;
+}
+
+ShaderInputsParser::InputParseState
+ShaderInputsParser::parseInputMin(const QRegularExpressionMatch & match)
+{
+ bool ok = false;
+ QString min_str = match.captured("min");
+
+ if (currentInput_.type == NodeValue::kFloat) {
+ currentInput_.min = min_str.toFloat( &ok);
+ }
+ else if (currentInput_.type == NodeValue::kInt) {
+ currentInput_.min = min_str.toInt( &ok);
+ }
+ else {
+ // this type does not support "min" property
+ }
+
+ if (ok == false) {
+ reportError(QObject::tr("%1 is not valid as minimum for type %2").
+ arg(min_str, currentInput_.type_string));
+ }
+
+ return PARSING;
+}
+
+ShaderInputsParser::InputParseState
+ShaderInputsParser::parseInputMax(const QRegularExpressionMatch & match)
+{
+ bool ok = false;
+ QString max_str = match.captured("max");
+
+ if (currentInput_.type == NodeValue::kFloat) {
+ currentInput_.max = max_str.toFloat( &ok);
+ }
+ else if (currentInput_.type == NodeValue::kInt) {
+ currentInput_.max = max_str.toInt( &ok);
+ }
+ else {
+ // this type does not support "max" property
+ }
+
+ if (ok == false) {
+ reportError(QObject::tr("%1 is not valid as maximum for type %2").
+ arg(max_str, currentInput_.type_string));
+ }
+
+ return PARSING;
+}
+
+ShaderInputsParser::InputParseState
+ShaderInputsParser::parseInputDefault(const QRegularExpressionMatch & match)
+{
+ QString default_string = match.captured("default").trimmed();
+ bool ok = false;
+ float float_value;
+ int int_value;
+
+ switch (currentInput_.type)
+ {
+ case NodeValue::kFloat:
+ float_value = default_string.toFloat( &ok);
+
+ if (ok)
+ {
+ currentInput_.default_value = QVariant::fromValue(float_value);
+ }
+ else
+ {
+ reportError(QObject::tr("%1 is not a valid float").
+ arg(default_string));
+ }
+ break;
+
+ case NodeValue::kInt:
+ int_value = default_string.toInt( &ok);
+
+ if (ok)
+ {
+ currentInput_.default_value = QVariant::fromValue(int_value);
+ }
+ else
+ {
+ reportError(QObject::tr("%1 is not a valid integer").arg(default_string));
+ }
+ break;
+
+ case NodeValue::kBoolean:
+ if (default_string == QString::fromLatin1("false")) {
+ currentInput_.default_value = QVariant::fromValue(false);
+ }
+ else if (default_string == QString::fromLatin1("true")) {
+ currentInput_.default_value = QVariant::fromValue(true);
+ }
+ else {
+ reportError(QObject::tr("value must be 'true' or 'false'"));
+ }
+ break;
+
+ case NodeValue::kColor:
+ currentInput_.default_value = parseColor( default_string);
+ break;
+
+ case NodeValue::kCombo:
+ int_value = default_string.toInt( &ok);
+
+ if (ok)
+ {
+ currentInput_.default_value = QVariant::fromValue(int_value);
+ }
+ else
+ {
+ reportError(QObject::tr("Default for SELECTION must be a number from 0 to number of entries minus 1."));
+ }
+ break;
+
+ case NodeValue::kVec2:
+ currentInput_.default_value = parsePoint( default_string);
+ break;
+
+ default:
+ case NodeValue::kTexture:
+ case NodeValue::kNone:
+ reportError(QObject::tr("type %1 does not support a default value").arg(currentInput_.type_string));
+ break;
+ }
+
+ return PARSING;
+}
+
+ShaderInputsParser::InputParseState
+ShaderInputsParser::parseInputDescription(const QRegularExpressionMatch & match)
+{
+ currentInput_.description.append( match.captured("description"));
+ return PARSING;
+}
+
+ShaderInputsParser::InputParseState
+ShaderInputsParser::stopParse(const QRegularExpressionMatch & /*match*/)
+{
+ return SHADER_COMPLETE;
+}
+
+// Assume 'line' has format RGBA( r,g,b,a), where 'r,g,b,a' are in range 0.0 .. 1.0
+// and stand for 'red', 'green', 'blue', 'alpha'
+QVariant ShaderInputsParser::parseColor(const QString& line)
+{
+ static const QRegularExpression
+ COLOR_REGEX("RGBA\\(\\s*(?[\\d\\.]+)\\s*,\\s*(?[\\d\\.]+)\\s*,\\s*(?[\\d\\.]+)\\s*,\\s*(?[\\d\\.]+)\\s*\\)");
+
+ QVariant color;
+ QRegularExpressionMatch match = COLOR_REGEX.match(line);
+ bool valid = false;
+
+ if (match.hasMatch()) {
+ bool ok_r, ok_g, ok_b, ok_a;
+ float r,g,b,a;
+
+ r = match.captured("r").toFloat( & ok_r);
+ g = match.captured("g").toFloat( & ok_g);
+ b = match.captured("b").toFloat( & ok_b);
+ a = match.captured("a").toFloat( & ok_a);
+
+ // check ranges
+ ok_r &= ((r>=0.0f) && (r <= 1.0f));
+ ok_g &= ((g>=0.0f) && (g <= 1.0f));
+ ok_b &= ((b>=0.0f) && (b <= 1.0f));
+ ok_a &= ((a>=0.0f) && (a <= 1.0f));
+
+ if (ok_r && ok_g && ok_b && ok_a) {
+ color = QVariant::fromValue( Color(r,g,b,a));
+ valid = true;
+ }
+ }
+
+ if (valid == false) {
+ reportError(QObject::tr("Color must be in format 'RGBA(r,g,b,a)' "
+ "where r,g,b,a are in range (0,1)"));
+ }
+
+ return color;
+}
+
+QVariant ShaderInputsParser::parsePoint(const QString &line)
+{
+ static const QRegularExpression
+ POINT_REGEX("\\s*\\(\\s*(?[\\d\\.]+)\\s*,\\s*(?[\\d\\.]+)\\s*\\)");
+
+ QVariant point;
+ QRegularExpressionMatch match = POINT_REGEX.match(line);
+
+ if (match.hasMatch()) {
+ float x,y;
+ bool ok_x, ok_y;
+
+ x = match.captured("x").toFloat( & ok_x);
+ y = match.captured("y").toFloat( & ok_y);
+
+ if (ok_x && ok_y) {
+ point = QVariant::fromValue({x,y});
+ }
+ }
+
+ if (point == QVariant()) {
+ reportError(QObject::tr("Point must be in format '(x,y)' where x and y are float values"));
+ }
+
+ return point;
+}
+
+void ShaderInputsParser::checkConsistentType(const NodeValue::Type &metadata_type, const QString &uniform_type)
+{
+ static const QMap UNIFORM_STRING_MAP{{"TEXTURE", "sampler2D"},
+ {"COLOR", "vec4"},
+ {"FLOAT", "float"},
+ {"INTEGER", "int"},
+ {"BOOLEAN", "bool"},
+ {"SELECTION", "int"},
+ {"POINT", "vec2"}
+ };
+
+ QString meta_type_string = INPUT_TYPE_TABLE.key( metadata_type, "-");
+ QString uniform_type_expected = UNIFORM_STRING_MAP.value( meta_type_string, "-");
+
+ if (uniform_type_expected != uniform_type) {
+ reportError(QObject::tr("metadata type %1 requires uniform type %2 and not %3").
+ arg( meta_type_string, uniform_type_expected, uniform_type));
+ }
+}
+
+void ShaderInputsParser::reportError(const QString& error)
+{
+ Error new_err;
+ new_err.line = line_number_;
+ new_err.issue = error;
+
+ error_list_.append( new_err);
+}
+
+} // namespace olive
diff --git a/app/node/filter/shader/shaderinputsparser.h b/app/node/filter/shader/shaderinputsparser.h
new file mode 100644
index 0000000000..df1e43e0bd
--- /dev/null
+++ b/app/node/filter/shader/shaderinputsparser.h
@@ -0,0 +1,176 @@
+/***
+
+ Olive - Non-Linear Video Editor
+ Copyright (C) 2021 Olive Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+***/
+
+#ifndef SHADERINPUTSPARSER_H
+#define SHADERINPUTSPARSER_H
+
+#include
+#include
+#include
+
+#include "node/value.h"
+#include "node/param.h"
+#include "node/gizmo/point.h"
+
+namespace olive {
+
+/** @brief This class parses a GLSL shader code to find 'uniform' and
+ * marked comments for inputs to be exposed to the GUI.
+ * The output is a list of inputs with parameters and a list of parsing errors.
+ */
+class ShaderInputsParser
+{
+public:
+
+ struct InputParam
+ {
+ // name that appares in GUI
+ QString human_name;
+ // name of uniform in code
+ QString uniform_name;
+ // so far, not used
+ QString description;
+ // string for input type
+ QString type_string;
+
+ NodeValue::Type type;
+ InputFlags flags;
+ // list of entries for a selection combo
+ QStringList values;
+ // only applicable for type kVec2. How the point is drawn
+ PointGizmo::Shape pointShape;
+ // color used to draw a point gizmo
+ QColor gizmoColor;
+
+ QVariant min;
+ QVariant max;
+ QVariant default_value;
+ };
+
+ struct Error {
+ int line;
+ QString issue;
+ };
+
+
+ // The constructor accepts the full code of the shader
+ ShaderInputsParser( const QString & shader_code);
+
+ ~ShaderInputsParser() {
+ input_list_.clear();
+ }
+
+ // main function that extracts inputs from shader code
+ void Parse();
+
+ // valid after call to 'Parse'
+ const QList< InputParam> & InputList() const {
+ return input_list_;
+ }
+
+ // valid after call to 'Parse'
+ const QString & ShaderName() const {
+ return shader_name_;
+ }
+
+ // shader description
+ const QString & ShaderDescription() const {
+ return shader_description_;
+ }
+
+ // shader version
+ const QString & ShaderVersion() const {
+ return shader_version_;
+ }
+
+ // name of default input
+ const QString & MainInputName() const {
+ return main_input_name_;
+ }
+
+ int NumberOfIterations() const {
+ return number_of_iterations_;
+ }
+
+ // list of issue found in parsing inputs
+ const QList & ErrorList() const {
+ return error_list_;
+ }
+
+private:
+ // value returned after parsing one line
+ enum InputParseState {
+ // adding parameters for the current input
+ PARSING = 0,
+ // a 'uniform' has been found. An input is complete
+ INPUT_COMPLETE,
+ // final tag found. Parsing will be stopped
+ SHADER_COMPLETE
+ };
+
+ void clearCurrentInput();
+ InputParseState parseSingleLine( const QString & line);
+
+ // pointer to a function that parse a line that matches an //OVE comment
+ typedef InputParseState (ShaderInputsParser::* LineParseFunction)( const QRegularExpressionMatch & match);
+
+ InputParseState parseShaderName( const QRegularExpressionMatch &);
+ InputParseState parseShaderDescription( const QRegularExpressionMatch &);
+ InputParseState parseShaderVersion( const QRegularExpressionMatch &);
+ InputParseState parseMainInputName( const QRegularExpressionMatch &);
+ InputParseState parseNumberOfIterations( const QRegularExpressionMatch &);
+ InputParseState parseInputName( const QRegularExpressionMatch &);
+ InputParseState parseInputUniform( const QRegularExpressionMatch &);
+ InputParseState parseInputType( const QRegularExpressionMatch &);
+ InputParseState parseInputFlags( const QRegularExpressionMatch &);
+ InputParseState parseInputValueList( const QRegularExpressionMatch &);
+ InputParseState parseInputPointShape( const QRegularExpressionMatch &);
+ InputParseState parseInputGizmoColor( const QRegularExpressionMatch &);
+ InputParseState parseInputMin( const QRegularExpressionMatch &);
+ InputParseState parseInputMax( const QRegularExpressionMatch &);
+ InputParseState parseInputDefault( const QRegularExpressionMatch &);
+ InputParseState parseInputDescription( const QRegularExpressionMatch &);
+ InputParseState stopParse( const QRegularExpressionMatch &);
+
+ QVariant parseColor( const QString & line);
+ QVariant parsePoint( const QString & line);
+ void checkConsistentType( const NodeValue::Type & metadata_type, const QString & uniform_type);
+ void reportError( const QString & error);
+
+private:
+ const QString & shader_code_;
+ QList< InputParam> input_list_;
+ QList error_list_;
+ int line_number_;
+
+ // features of the input that's currently being parsed
+ InputParam currentInput_;
+
+ QString shader_name_;
+ QString shader_description_;
+ QString shader_version_;
+ QString main_input_name_;
+ int number_of_iterations_;
+ QMap< const QRegularExpression *, LineParseFunction> INPUT_PARAM_PARSE_TABLE;
+};
+
+}
+
+#endif // SHADERINPUTSPARSER_H
diff --git a/app/node/gizmo/point.cpp b/app/node/gizmo/point.cpp
index 213948ba1d..669e21e7e4 100644
--- a/app/node/gizmo/point.cpp
+++ b/app/node/gizmo/point.cpp
@@ -44,12 +44,17 @@ PointGizmo::PointGizmo(QObject *parent) :
void PointGizmo::Draw(QPainter *p) const
{
QRectF rect = GetDrawingRect(p->transform(), GetStandardRadius());
+ QVariant color = this->property("color");
if (shape_ != kAnchorPoint) {
p->setPen(QPen(Qt::black, 0));
p->setBrush(Qt::white);
}
+ if (color.isValid()) {
+ p->setBrush(color.value());
+ }
+
switch (shape_) {
case kSquare:
p->drawRect(rect);
diff --git a/app/node/node.h b/app/node/node.h
index 6058122dee..2bac55fe20 100644
--- a/app/node/node.h
+++ b/app/node/node.h
@@ -773,6 +773,13 @@ class Node : public QObject
*/
virtual ShaderCode GetShaderCode(const ShaderRequest &request) const;
+ /**
+ * @brief Set to true when shader code that was already compiled must be recompiled.
+ */
+ virtual bool ShaderCodeInvalidateFlag() const {
+ return false;
+ }
+
/**
* @brief If Value() pushes a ShaderJob, this is the function that will process them.
*/
@@ -1080,6 +1087,10 @@ class Node : public QObject
return AddDraggableGizmo(refs, behavior);
}
+ void RemoveGizmo( NodeGizmo* gizmo) {
+ gizmos_.removeAll( gizmo);
+ }
+
protected slots:
virtual void GizmoDragStart(const olive::NodeValueRow &row, double x, double y, const olive::core::rational &time){}
@@ -1143,6 +1154,10 @@ protected slots:
void InputFlagsChanged(const QString &input, const InputFlags &flags);
+ // emitted after one or more inputs have been added or removed.
+ // Only applicable for nodes that change input list dynamically.
+ void InputListChanged();
+
private:
struct Input {
NodeValue::Type type;
diff --git a/app/render/renderprocessor.cpp b/app/render/renderprocessor.cpp
index 05a79b8c86..7536d1e03f 100644
--- a/app/render/renderprocessor.cpp
+++ b/app/render/renderprocessor.cpp
@@ -448,7 +448,11 @@ void RenderProcessor::ProcessShader(TexturePtr destination, const Node *node, co
QMutexLocker locker(shader_cache_->mutex());
- QVariant shader = shader_cache_->value(full_shader_id);
+ QVariant shader = QVariant();
+
+ if (node->ShaderCodeInvalidateFlag() == false) {
+ shader = shader_cache_->value(full_shader_id);
+ }
if (shader.isNull()) {
// Since we have shader code, compile it now
diff --git a/app/widget/nodeparamview/CMakeLists.txt b/app/widget/nodeparamview/CMakeLists.txt
index 1b80edebc2..2496d261c1 100644
--- a/app/widget/nodeparamview/CMakeLists.txt
+++ b/app/widget/nodeparamview/CMakeLists.txt
@@ -36,6 +36,8 @@ set(OLIVE_SOURCES
widget/nodeparamview/nodeparamviewkeyframecontrol.h
widget/nodeparamview/nodeparamviewtextedit.cpp
widget/nodeparamview/nodeparamviewtextedit.h
+ widget/nodeparamview/nodeparamviewshader.cpp
+ widget/nodeparamview/nodeparamviewshader.h
widget/nodeparamview/nodeparamviewwidgetbridge.cpp
widget/nodeparamview/nodeparamviewwidgetbridge.h
PARENT_SCOPE
diff --git a/app/widget/nodeparamview/nodeparamview.cpp b/app/widget/nodeparamview/nodeparamview.cpp
index a4aada805e..ba20fd3264 100644
--- a/app/widget/nodeparamview/nodeparamview.cpp
+++ b/app/widget/nodeparamview/nodeparamview.cpp
@@ -709,6 +709,8 @@ void NodeParamView::AddNode(Node *n, Node *ctx, NodeParamViewContext *context)
connect(item, &NodeParamViewItem::Clicked, this, &NodeParamView::ItemClicked);
connect(item, &NodeParamViewItem::RequestEditTextInViewer, this, &NodeParamView::RequestEditTextInViewer);
+ connect( n, & Node::InputFlagsChanged, item, & NodeParamViewItem::OnInputFlagsChanged );
+
item->SetContext(ctx);
item->SetTimeTarget(GetConnectedNode());
item->SetTimebase(timebase());
@@ -726,8 +728,10 @@ void NodeParamView::AddNode(Node *n, Node *ctx, NodeParamViewContext *context)
connect(item, &NodeParamViewItem::ExpandedChanged, this, &NodeParamView::QueueKeyframePositionUpdate);
connect(item, &NodeParamViewItem::Moved, this, &NodeParamView::QueueKeyframePositionUpdate);
connect(item, &NodeParamViewItem::InputArraySizeChanged, this, &NodeParamView::InputArraySizeChanged);
+ connect(item, &NodeParamViewItem::InputsChanged, this, &NodeParamView::UpdateElementY);
item->SetKeyframeConnections(keyframe_view_->AddKeyframesOfNode(n));
+ item->SetKeyframeView( keyframe_view_);
}
}
diff --git a/app/widget/nodeparamview/nodeparamviewitem.cpp b/app/widget/nodeparamview/nodeparamviewitem.cpp
index 76e22da891..d024b97116 100644
--- a/app/widget/nodeparamview/nodeparamviewitem.cpp
+++ b/app/widget/nodeparamview/nodeparamviewitem.cpp
@@ -61,6 +61,11 @@ NodeParamViewItem::NodeParamViewItem(Node *node, NodeParamViewCheckBoxBehavior c
connect(node_, &Node::LabelChanged, this, &NodeParamViewItem::Retranslate);
connect(node_, &Node::InputArraySizeChanged, this, &NodeParamViewItem::InputArraySizeChanged);
+ // for dynamically changed inputs
+ connect(node_, &Node::InputListChanged, this, &NodeParamViewItem::OnInputListChanged);
+ connect(node_, &Node::InputAdded, this, &NodeParamViewItem::OnInputAdded);
+ connect(node_, &Node::InputRemoved, this, &NodeParamViewItem::OnInputRemoved);
+
// FIXME: Implemented to pick up when an input is set to hidden or not - DEFINITELY not a fast
// way of doing this, but "fine" for now.
connect(node_, &Node::InputFlagsChanged, this, &NodeParamViewItem::RecreateBody);
@@ -102,6 +107,61 @@ void NodeParamViewItem::RecreateBody()
SetBody(body_);
}
+
+void NodeParamViewItem::OnInputFlagsChanged(const QString &input, const InputFlags &flags)
+{
+ if (flags & kInputFlagNotKeyframable) {
+
+ // Delete all keyframes
+ MultiUndoCommand* command = new MultiUndoCommand();
+ NodeInput node_input = NodeInput( node_,input);
+
+ foreach (const NodeKeyframeTrack& track, node_->GetKeyframeTracks(node_input)) {
+ for (int i=track.size()-1;i>=0;i--) {
+ command->add_child(new NodeParamRemoveKeyframeCommand(track.at(i)));
+ }
+ }
+
+ Core::instance()->undo_stack()->push(command,
+ QString("Remove keyframes for input %1").arg(input));
+ }
+}
+
+void NodeParamViewItem::OnInputListChanged()
+{
+ RecreateBody();
+
+ // allow to add keyframes immediately, without changing context
+ body_->SetTimeTarget(time_target_);
+
+ emit InputsChanged();
+}
+
+void NodeParamViewItem::OnInputAdded(const QString &id)
+{
+ KeyframeView::InputConnections input_conn = keyframe_view_->AddKeyframesOfInput( node_, id);
+ keyframe_connections_.insert( id, input_conn);
+}
+
+void NodeParamViewItem::OnInputRemoved( const QString & id)
+{
+ // remove keyframes of the input, if any.
+ // It is required to find all keyframe view input connections of the input.
+ KeyframeView::InputConnections con = keyframe_connections_.value( id);
+
+ QVectorIterator elem_it( con);
+
+ while (elem_it.hasNext()) {
+ KeyframeView::ElementConnections elem_con = elem_it.next();
+
+ QVectorIterator keyframe_it(elem_con);
+
+ while (keyframe_it.hasNext()) {
+ keyframe_view_->RemoveKeyframesOfTrack( keyframe_it.next());
+ }
+ }
+}
+
int NodeParamViewItem::GetElementY(const NodeInput &c) const
{
if (IsExpanded()) {
@@ -384,6 +444,12 @@ void NodeParamViewItemBody::PlaceWidgetsFromBridge(QGridLayout* layout, NodePara
colspan = 1;
}
+ // some non-keyframeable widgets can use all space to the right
+ if (w->property("is_exapandable").toBool())
+ {
+ colspan = -1;
+ }
+
layout->addWidget(w, row, col, 1, colspan);
}
}
diff --git a/app/widget/nodeparamview/nodeparamviewitem.h b/app/widget/nodeparamview/nodeparamviewitem.h
index 56149064c3..56d4fc7bab 100644
--- a/app/widget/nodeparamview/nodeparamviewitem.h
+++ b/app/widget/nodeparamview/nodeparamviewitem.h
@@ -211,6 +211,10 @@ class NodeParamViewItem : public NodeParamViewItemBase
keyframe_connections_ = c;
}
+ void SetKeyframeView( KeyframeView * kfv) {
+ keyframe_view_ = kfv;
+ }
+
signals:
void RequestSelectNode(Node *node);
@@ -218,13 +222,23 @@ class NodeParamViewItem : public NodeParamViewItemBase
void InputCheckedChanged(const NodeInput &input, bool e);
+ void InputsChanged( void);
+
void RequestEditTextInViewer();
void InputArraySizeChanged(const QString &input, int old_size, int new_size);
+public slots:
+ void OnInputFlagsChanged(const QString &input, const InputFlags &flags);
+
protected slots:
virtual void Retranslate() override;
+private slots:
+ void OnInputListChanged();
+ void OnInputAdded(const QString &id);
+ void OnInputRemoved(const QString &id);
+
private:
NodeParamViewItemBody* body_;
@@ -239,6 +253,7 @@ protected slots:
rational timebase_;
KeyframeView::NodeConnections keyframe_connections_;
+ KeyframeView * keyframe_view_;
private slots:
void RecreateBody();
diff --git a/app/widget/nodeparamview/nodeparamviewshader.cpp b/app/widget/nodeparamview/nodeparamviewshader.cpp
new file mode 100644
index 0000000000..da54c2ae90
--- /dev/null
+++ b/app/widget/nodeparamview/nodeparamviewshader.cpp
@@ -0,0 +1,154 @@
+/***
+
+ Olive - Non-Linear Video Editor
+ Copyright (C) 2023 Olive Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+***/
+
+#include
+#include
+#include
+#include
+
+#include "nodeparamviewshader.h"
+#include "node/filter/shader/shader.h"
+#include "node/block/transition/shader/shadertransition.h"
+#include "dialog/codeeditor/externaleditorproxy.h"
+#include "dialog/codeeditor/codeeditordialog.h"
+#include "config/config.h"
+
+
+namespace olive {
+
+const QString DEFAULT_DESCRIPTION = QObject::tr("No description available");
+
+NodeParamViewShader::NodeParamViewShader(const QPlainTextEdit &content,
+ QObject *parent) :
+ QObject(parent),
+ full_shader_(content),
+ is_vertex_(false)
+{
+ summary_ = new QTextEdit();
+ summary_->setReadOnly( true);
+ summary_->setVisible( false);
+
+ ext_editor_proxy_ = new ExternalEditorProxy( this);
+
+ connect( ext_editor_proxy_, & ExternalEditorProxy::textChanged,
+ this, & NodeParamViewShader::OnTextChangedExternally);
+}
+
+
+void NodeParamViewShader::attachOwnerNode(const Node *owner, bool is_vertex)
+{
+ summary_->setVisible( true);
+ is_vertex_ = is_vertex;
+
+ // create a file whose name is unique for the node this instance belongs to.
+ QString base_dir = Config::Current()["EditorExternalTempFolder"].toString();
+
+ if (base_dir.isEmpty() || ( ! QDir(base_dir).exists())) {
+ // 'QStandardPaths::TempLocation' is guaranteed not to be empty
+ base_dir = QStandardPaths::standardLocations( QStandardPaths::TempLocation).at(0);
+ }
+
+ QString file_path = QString("%1%2%3.%4").arg(base_dir).
+ arg(QString(QDir::separator())).arg((uint64_t)owner).arg(is_vertex ? "vert" : "frag");
+
+ ext_editor_proxy_->SetFilePath(file_path);
+
+ /* list of nodes that use this view: */
+
+ const ShaderFilterNode * owner_filter = dynamic_cast(owner);
+ if (owner_filter != nullptr) {
+ owner_filter->getMetadata( name_, description_, version_);
+ // initialize summary text ...
+ onMetadataChanged( name_, description_, version_);
+
+ // ... and track changes
+ connect( owner_filter, & ShaderFilterNode::metadataChanged,
+ this, & NodeParamViewShader::onMetadataChanged);
+ }
+
+ const ShaderTransition * owner_transition = dynamic_cast(owner);
+ if (owner_transition != nullptr) {
+ owner_transition->getMetadata( name_, description_, version_);
+ // initialize summary text ...
+ onMetadataChanged( name_, description_, version_);
+
+ // ... and track changes
+ connect( owner_transition, & ShaderTransition::metadataChanged,
+ this, & NodeParamViewShader::onMetadataChanged);
+ }
+}
+
+void NodeParamViewShader::onMetadataChanged(const QString &name,
+ const QString &description,
+ const QString &version)
+{
+ name_ = name;
+ description_ = description;
+ version_ = version;
+
+ summary_->setHtml( displayedText());
+
+}
+
+QString NodeParamViewShader::displayedText() const
+{
+ QString version = version_.isEmpty() ? QString() : QString("(%1)").arg(version_);
+ const QString & description = description_.isEmpty() ? DEFAULT_DESCRIPTION : description_;
+
+ // We don't display the full shader, but only some metadata
+ if (is_vertex_) {
+ return QString("%1 (vertex)"
+ " %2"
+ "%3
").
+ arg( name_, version, description);
+ } else {
+
+ return QString("%1"
+ " %2"
+ "%3
").
+ arg( name_, version, description);
+ }
+}
+
+void NodeParamViewShader::launchCodeEditor(QString & text)
+{
+ if (Config::Current()["EditorUseInternal"].toBool()) {
+
+ // internal editor
+ CodeEditorDialog d(full_shader_.toPlainText(), nullptr);
+
+ // in case external editor was previously opened but
+ // then user switched to internal editor.
+ ext_editor_proxy_->Detach();
+
+ if (d.exec() == QDialog::Accepted) {
+ text = d.text();
+ }
+ }
+ else {
+
+ // external editor
+ ext_editor_proxy_->Launch( full_shader_.toPlainText());
+ }
+}
+
+}
+
+
diff --git a/app/widget/nodeparamview/nodeparamviewshader.h b/app/widget/nodeparamview/nodeparamviewshader.h
new file mode 100644
index 0000000000..8310a6fd8e
--- /dev/null
+++ b/app/widget/nodeparamview/nodeparamviewshader.h
@@ -0,0 +1,81 @@
+/***
+
+ Olive - Non-Linear Video Editor
+ Copyright (C) 2023 Olive Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+***/
+
+#ifndef NODEPARAMVIEWSHADER_H
+#define NODEPARAMVIEWSHADER_H
+
+#include
+#include
+#include
+
+class QPlainTextEdit;
+
+namespace olive {
+
+class ExternalEditorProxy;
+class Node;
+
+
+// This is a facility for text-type param view that is used
+// as shader code entry (fragment or vertex).
+// Instead of showing the full code, a summary is shown
+class NodeParamViewShader : public QObject
+{
+ Q_OBJECT
+public:
+ NodeParamViewShader( const QPlainTextEdit & content, QObject * parent=nullptr);
+
+ QWidget* widget() {
+ return summary_;
+ }
+
+ // bind this viewer to a Node that handles one GLSL script,
+ // so changes in the script will update this widget.
+ // 'is_vertex' is false for Fragment shader, true for vertex shader
+ void attachOwnerNode(const Node * owner, bool is_vertex);
+
+ // start editing shader in internal or external editor
+ void launchCodeEditor(QString & text);
+
+signals:
+ void OnTextChangedExternally( const QString & text);
+
+private slots:
+ // invoked when the owner Node has parsed a shader script
+ void onMetadataChanged( const QString & name, const QString & description, const QString & version);
+
+private:
+ QString displayedText() const;
+
+private:
+ const QPlainTextEdit & full_shader_;
+ QTextEdit* summary_;
+ ExternalEditorProxy* ext_editor_proxy_;
+
+ QString name_;
+ QString description_;
+ QString version_;
+
+ bool is_vertex_;
+};
+
+}
+
+#endif // NODEPARAMVIEWSHADER_H
diff --git a/app/widget/nodeparamview/nodeparamviewtextedit.cpp b/app/widget/nodeparamview/nodeparamviewtextedit.cpp
index 95c533bd52..d7d7abf106 100644
--- a/app/widget/nodeparamview/nodeparamviewtextedit.cpp
+++ b/app/widget/nodeparamview/nodeparamviewtextedit.cpp
@@ -22,13 +22,18 @@
#include
-#include "dialog/text/text.h"
+#include "dialog/codeeditor/messagehighlighter.h"
#include "ui/icons/icons.h"
+#include "nodeparamviewshader.h"
+
namespace olive {
NodeParamViewTextEdit::NodeParamViewTextEdit(QWidget *parent) :
- QWidget(parent)
+ QWidget(parent),
+ code_editor_flag_(false),
+ code_issues_flag_(false),
+ text_dlg_(nullptr)
{
QHBoxLayout* layout = new QHBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
@@ -38,6 +43,11 @@ NodeParamViewTextEdit::NodeParamViewTextEdit(QWidget *parent) :
connect(line_edit_, &QPlainTextEdit::textChanged, this, &NodeParamViewTextEdit::InnerWidgetTextChanged);
layout->addWidget(line_edit_);
+ shader_edit_ = new NodeParamViewShader( *line_edit_, this);
+ layout->addWidget( shader_edit_->widget());
+ connect( shader_edit_, & NodeParamViewShader::OnTextChangedExternally,
+ this, & NodeParamViewTextEdit::OnTextChangedExternally);
+
edit_btn_ = new QPushButton();
edit_btn_->setIcon(icon::ToolEdit);
edit_btn_->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Expanding);
@@ -52,6 +62,17 @@ NodeParamViewTextEdit::NodeParamViewTextEdit(QWidget *parent) :
SetEditInViewerOnlyMode(false);
}
+NodeParamViewTextEdit::~NodeParamViewTextEdit()
+{
+ // This prevents a crash if text changes externally while
+ // the dialog is open. For example this happens when code issues dialog
+ // is open and shader code changes externally.
+ if (text_dlg_ != nullptr) {
+ text_dlg_->reject();
+ }
+}
+
+
void NodeParamViewTextEdit::SetEditInViewerOnlyMode(bool on)
{
line_edit_->setVisible(!on);
@@ -59,20 +80,70 @@ void NodeParamViewTextEdit::SetEditInViewerOnlyMode(bool on)
edit_in_viewer_btn_->setVisible(on);
}
+
void NodeParamViewTextEdit::ShowTextDialog()
{
- TextDialog d(this->text(), this);
- if (d.exec() == QDialog::Accepted) {
- QString s = d.text();
+ QString text;
+
+ if (code_editor_flag_) {
+
+ shader_edit_->launchCodeEditor(text);
+ } else {
+ text_dlg_ = new TextDialog(this->text(), nullptr);
+
+ if (code_issues_flag_) {
+ text_dlg_->setSyntaxHighlight( new MessageSyntaxHighlighter());
+ }
+
+ if (text_dlg_->exec() == QDialog::Accepted) {
+ text= text_dlg_->text();
+ }
+
+ delete text_dlg_;
+ text_dlg_ = nullptr;
+ }
- line_edit_->setPlainText(s);
- emit textEdited(s);
+ if (text != QString()) {
+ setText(text);
+ emit textEdited( text);
}
}
+
void NodeParamViewTextEdit::InnerWidgetTextChanged()
{
emit textEdited(this->text());
}
+void NodeParamViewTextEdit::OnTextChangedExternally(const QString & content)
+{
+ line_edit_->setPlainText( content);
+ emit textEdited( content);
+}
+
+void NodeParamViewTextEdit::setShaderCodeEditorFlag( const Node * owner, bool is_vertex)
+{
+ code_editor_flag_ = true;
+
+ // no need to draw the stopwatch to keyframe this input
+ setProperty("is_exapandable", QVariant::fromValue(true));
+
+ // as this is a shader code editor, it is edited by a
+ // separate dialog or external editor
+ line_edit_->setReadOnly( true);
+ line_edit_->setVisible(false);
+ shader_edit_->attachOwnerNode( owner, is_vertex);
+}
+
+void NodeParamViewTextEdit::setShaderIssuesFlag()
+{
+ code_issues_flag_ = true;
+ line_edit_->setReadOnly( true);
+
+ // no need to draw the stopwatch to keyframe this input
+ setProperty("is_exapandable", QVariant::fromValue(true));
+
+ new MessageSyntaxHighlighter( line_edit_->document());
+}
+
}
diff --git a/app/widget/nodeparamview/nodeparamviewtextedit.h b/app/widget/nodeparamview/nodeparamviewtextedit.h
index 2c2577566b..f110478499 100644
--- a/app/widget/nodeparamview/nodeparamviewtextedit.h
+++ b/app/widget/nodeparamview/nodeparamviewtextedit.h
@@ -22,24 +22,39 @@
#define NODEPARAMVIEWTEXTEDIT_H
#include
+#include
#include
#include
#include "common/define.h"
+#include "dialog/text/text.h"
+
namespace olive {
+class Node;
+class NodeParamViewShader;
+
+
class NodeParamViewTextEdit : public QWidget
{
Q_OBJECT
public:
NodeParamViewTextEdit(QWidget* parent = nullptr);
+ ~NodeParamViewTextEdit();
QString text() const
{
return line_edit_->toPlainText();
}
+ // let this instance perform as a code editor.
+ // This input will be edited with a built-in or external code editor.
+ // 'is_vertex' is false for Fragment shader, true for vertex shader
+ void setShaderCodeEditorFlag(const Node *owner, bool is_vertex);
+ // set flag to view text as code issues
+ void setShaderIssuesFlag();
+
void SetEditInViewerOnlyMode(bool on);
public slots:
@@ -71,7 +86,13 @@ public slots:
private:
QPlainTextEdit* line_edit_;
+ bool code_editor_flag_;
+ bool code_issues_flag_;
+ NodeParamViewShader * shader_edit_;
+ TextDialog * text_dlg_;
+
+private:
QPushButton* edit_btn_;
QPushButton *edit_in_viewer_btn_;
@@ -81,8 +102,11 @@ private slots:
void InnerWidgetTextChanged();
+ void OnTextChangedExternally(const QString & content);
+
};
}
+
#endif // NODEPARAMVIEWTEXTEDIT_H
diff --git a/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp b/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp
index 585f2526e6..def54b867e 100644
--- a/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp
+++ b/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp
@@ -138,6 +138,21 @@ void NodeParamViewWidgetBridge::CreateWidgets()
{
NodeParamViewTextEdit* line_edit = new NodeParamViewTextEdit(parent);
widgets_.append(line_edit);
+
+ // check for special type of text
+ QString text_type = GetInnerInput().GetProperty(QStringLiteral("text_type")).toString();
+ if (text_type == "shader_code_frag") {
+ const Node * owner = GetInnerInput().node();
+ line_edit->setShaderCodeEditorFlag( owner, false);
+ }
+ if (text_type == "shader_code_vert") {
+ const Node * owner = GetInnerInput().node();
+ line_edit->setShaderCodeEditorFlag( owner, true);
+ }
+ else if (text_type == "shader_issues") {
+ line_edit->setShaderIssuesFlag();
+ }
+
connect(line_edit, &NodeParamViewTextEdit::textEdited, this, &NodeParamViewWidgetBridge::WidgetCallback);
connect(line_edit, &NodeParamViewTextEdit::RequestEditInViewer, this, &NodeParamViewWidgetBridge::RequestEditTextInViewer);
break;
diff --git a/app/widget/nodeview/nodeviewitem.cpp b/app/widget/nodeview/nodeviewitem.cpp
index 5e1aefe42b..77640fcce1 100644
--- a/app/widget/nodeview/nodeviewitem.cpp
+++ b/app/widget/nodeview/nodeviewitem.cpp
@@ -66,6 +66,8 @@ NodeViewItem::NodeViewItem(Node *node, const QString &input, int element, Node *
connect(node_, &Node::LabelChanged, this, &NodeViewItem::NodeAppearanceChanged);
connect(node_, &Node::ColorChanged, this, &NodeViewItem::NodeAppearanceChanged);
+ // a change in input flags (NOT_CONNECTABLE, HIDDEN) may require to show or hide an input of this node
+ connect(node_, &Node::InputListChanged, this, &NodeViewItem::RepopulateInputs);
if (IsOutputItem()) {
connect(node_, &Node::InputAdded, this, &NodeViewItem::RepopulateInputs);
diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp
index 7b9927344c..14013eb001 100644
--- a/app/window/mainwindow/mainwindow.cpp
+++ b/app/window/mainwindow/mainwindow.cpp
@@ -33,6 +33,7 @@
#include "mainmenu.h"
#include "mainstatusbar.h"
#include "timeline/timelineundoworkarea.h"
+#include "dialog/codeeditor/externaleditorproxy.h"
namespace olive {
@@ -450,6 +451,8 @@ void MainWindow::closeEvent(QCloseEvent *e)
return;
}
+ ExternalEditorProxy::CleanGeneratedFiles();
+
scope_panel_->SetViewerPanel(nullptr);
PanelManager::instance()->DeleteAllPanels();
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 3dbbd2ccd4..44c0550b98 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -1,5 +1,5 @@
# Olive - Non-Linear Video Editor
-# Copyright (C) 2022 Olive Team
+# Copyright (C) 2023 Olive Team
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -72,3 +72,4 @@ endfunction()
add_subdirectory(compositing)
add_subdirectory(general)
add_subdirectory(timeline)
+add_subdirectory(shader)
diff --git a/tests/shader/CMakeLists.txt b/tests/shader/CMakeLists.txt
new file mode 100644
index 0000000000..9214b568fb
--- /dev/null
+++ b/tests/shader/CMakeLists.txt
@@ -0,0 +1,17 @@
+# Olive - Non-Linear Video Editor
+# Copyright (C) 2023 Olive Team
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+olive_add_test(Filter shader-tests shader-tests.cpp)
diff --git a/tests/shader/shader-tests.cpp b/tests/shader/shader-tests.cpp
new file mode 100644
index 0000000000..bf24608354
--- /dev/null
+++ b/tests/shader/shader-tests.cpp
@@ -0,0 +1,538 @@
+/***
+
+ Olive - Non-Linear Video Editor
+ Copyright (C) 2022 Olive Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+***/
+
+#include "testutil.h"
+
+#include "node/filter/shader/shaderinputsparser.h"
+
+// shortcut for std::string
+#define STR(x) QString(x).toStdString()
+
+//#include
+
+namespace olive {
+
+// Script header information.
+OLIVE_ADD_TEST(HeaderTest)
+{
+ QString code(
+ "#version 150""\n"
+ "//OVE shader_name: slide""\n"
+ "//OVE shader_description: slide transition""\n"
+ "//OVE shader_version: 0.1""\n"
+ "//OVE number_of_iterations: 2""\n"
+ );
+
+ ShaderInputsParser parser(code);
+ parser.Parse();
+
+ OLIVE_ASSERT_EQUAL( parser.ErrorList().size(), 0);
+ OLIVE_ASSERT_EQUAL( STR(parser.ShaderName()), STR("slide"));
+ OLIVE_ASSERT_EQUAL( STR(parser.ShaderDescription()), STR("slide transition"));
+ OLIVE_ASSERT_EQUAL( STR(parser.ShaderVersion()), STR("0.1"));
+ OLIVE_ASSERT_EQUAL( parser.NumberOfIterations(), 2);
+
+ OLIVE_TEST_END;
+}
+
+// Script description over multiple lines
+OLIVE_ADD_TEST(DescriptionTest)
+{
+ QString code(
+ "#version 150""\n"
+ "//OVE shader_name: slide""\n"
+ "//OVE shader_description: slide transition ""\n"
+ "//OVE shader_description: that works fine. ""\n"
+ "//OVE shader_version: 0.1""\n"
+ );
+
+ ShaderInputsParser parser(code);
+ parser.Parse();
+
+ OLIVE_ASSERT_EQUAL( parser.ErrorList().size(), 0);
+ OLIVE_ASSERT_EQUAL( STR(parser.ShaderName()), STR("slide"));
+ OLIVE_ASSERT_EQUAL( STR(parser.ShaderDescription()), STR("slide transition\nthat works fine."));
+ OLIVE_ASSERT_EQUAL( STR(parser.ShaderVersion()), STR("0.1"));
+
+ OLIVE_TEST_END;
+}
+
+
+// The main input does not count in the inputs list
+OLIVE_ADD_TEST(MainInputTest)
+{
+ QString code(
+ "//OVE main_input_name: dummy""\n"
+ "uniform sampler2D dummy;""\n"
+ );
+
+ ShaderInputsParser parser(code);
+ parser.Parse();
+
+ OLIVE_ASSERT_EQUAL( parser.ErrorList().size(), 0);
+ OLIVE_ASSERT_EQUAL( parser.InputList().size(), 0);
+
+ OLIVE_TEST_END;
+}
+
+// An input of kind Texture
+OLIVE_ADD_TEST(InputTextureTest)
+{
+ QString code(
+ "//OVE name: Base image""\n"
+ "//OVE type: TEXTURE""\n"
+ "//OVE flag: NOT_KEYFRAMABLE""\n"
+ "//OVE description: the base image""\n"
+ "uniform sampler2D tex_base;""\n"
+ );
+
+ ShaderInputsParser parser(code);
+ parser.Parse();
+
+ OLIVE_ASSERT_EQUAL( parser.ErrorList().size(), 0);
+ OLIVE_ASSERT_EQUAL( parser.InputList().size(), 1);
+
+ const ShaderInputsParser::InputParam & param = parser.InputList().at(0);
+
+ OLIVE_ASSERT_EQUAL( STR(param.human_name), STR("Base image"));
+ OLIVE_ASSERT_EQUAL( STR(param.description), STR("the base image"));
+ OLIVE_ASSERT_EQUAL( param.flags, InputFlags(kInputFlagNotKeyframable));
+ OLIVE_ASSERT_EQUAL( STR(param.uniform_name), STR("tex_base"));
+ OLIVE_ASSERT_EQUAL( STR(param.type_string), STR("TEXTURE"));
+ OLIVE_ASSERT_EQUAL( param.type, NodeValue::kTexture);
+
+ OLIVE_TEST_END;
+}
+
+// An input of kind Float
+OLIVE_ADD_TEST(InputFloatTest)
+{
+ QString code(
+ "//OVE name: Tolerance %%""\n"
+ "//OVE type: FLOAT""\n"
+ "//OVE min: 1.0""\n"
+ "//OVE default: 5.5""\n"
+ "//OVE max: 100""\n"
+ "//OVE description: the tolerance of filter""\n"
+ "uniform float toler_in;""\n"
+ );
+
+ ShaderInputsParser parser(code);
+ parser.Parse();
+
+ OLIVE_ASSERT_EQUAL( parser.ErrorList().size(), 0);
+ OLIVE_ASSERT_EQUAL( parser.InputList().size(), 1);
+
+ const ShaderInputsParser::InputParam & param = parser.InputList().at(0);
+
+ OLIVE_ASSERT_EQUAL( STR(param.human_name), STR("Tolerance %%"));
+ OLIVE_ASSERT_EQUAL( STR(param.description), STR("the tolerance of filter"));
+ OLIVE_ASSERT_EQUAL( param.flags, InputFlags(0));
+ OLIVE_ASSERT_EQUAL( STR(param.uniform_name), STR("toler_in"));
+ OLIVE_ASSERT_EQUAL( STR(param.type_string), STR("FLOAT"));
+ OLIVE_ASSERT( param.type == NodeValue::kFloat);
+ OLIVE_ASSERT_EQUAL( param.min.toDouble(), 1.);
+ OLIVE_ASSERT_EQUAL( param.max.toDouble(), 100.);
+ OLIVE_ASSERT_EQUAL( param.default_value.toDouble(), 5.5);
+
+ OLIVE_TEST_END;
+}
+
+// An input of kind Float with 'min' and 'max' not specified
+OLIVE_ADD_TEST(InputFloatTestBoundary)
+{
+ QString code(
+ "//OVE name: Tolerance""\n"
+ "//OVE type: FLOAT""\n"
+ "uniform float toler_in;""\n"
+ "//OVE name: Number of bars""\n"
+ "//OVE type: INTEGER""\n"
+ "uniform int bars_in;""\n"
+ );
+
+ ShaderInputsParser parser(code);
+ parser.Parse();
+
+ OLIVE_ASSERT_EQUAL( parser.ErrorList().size(), 0);
+ OLIVE_ASSERT_EQUAL( parser.InputList().size(), 2);
+
+ const ShaderInputsParser::InputParam & param1 = parser.InputList().at(0);
+
+ OLIVE_ASSERT_EQUAL( STR(param1.human_name), STR("Tolerance"));
+ OLIVE_ASSERT( param1.min.toDouble() < -1e30);
+ OLIVE_ASSERT( param1.max.toDouble() > 1e30);
+
+ const ShaderInputsParser::InputParam & param2 = parser.InputList().at(1);
+
+ OLIVE_ASSERT_EQUAL( STR(param2.human_name), STR("Number of bars"));
+ OLIVE_ASSERT( param2.min.toInt() < -1000000000);
+ OLIVE_ASSERT( param2.max.toInt() > 1000000000);
+
+ OLIVE_TEST_END;
+}
+
+bool operator == (const Color & rhs, const Color & lhs)
+{
+ bool r_ok = (abs(rhs.red() - lhs.red()) < 0.01) ;
+ bool g_ok = (abs(rhs.green() - lhs.green()) < 0.01) ;
+ bool b_ok = (abs(rhs.blue() - lhs.blue()) < 0.01) ;
+ bool a_ok = (abs(rhs.alpha() - lhs.alpha()) < 0.01);
+
+ return r_ok && g_ok && b_ok && a_ok;
+}
+
+// An input of kind Color
+OLIVE_ADD_TEST(InputColorTest)
+{
+ QString code(
+ "//OVE name: Final tone""\n"
+ "//OVE type: COLOR""\n"
+ "//OVE default: RGBA( 0.5,0.4 ,0.1, 1)""\n"
+ "//OVE description: color applied to the grayscale""\n"
+ "uniform vec4 tone_in;""\n"
+ );
+
+ ShaderInputsParser parser(code);
+ parser.Parse();
+
+ OLIVE_ASSERT_EQUAL( parser.ErrorList().size(), 0);
+ OLIVE_ASSERT_EQUAL( parser.InputList().size(), 1);
+
+ const ShaderInputsParser::InputParam & param = parser.InputList().at(0);
+
+ OLIVE_ASSERT_EQUAL( STR(param.human_name), STR("Final tone"));
+ OLIVE_ASSERT_EQUAL( STR(param.description), STR("color applied to the grayscale"));
+ OLIVE_ASSERT_EQUAL( STR(param.uniform_name), STR("tone_in"));
+ OLIVE_ASSERT_EQUAL( STR(param.type_string), STR("COLOR"));
+ OLIVE_ASSERT_EQUAL( param.type, NodeValue::kColor);
+ OLIVE_ASSERT( param.default_value.value() == Color(0.5f, 0.4f, 0.1f, 1.0f));
+
+ OLIVE_TEST_END;
+}
+
+// An input of kind Color with components outside [0..1]
+OLIVE_ADD_TEST(ColorOutOfRangeTest)
+{
+ QString code(
+ "//OVE name: Final tone""\n"
+ "//OVE type: COLOR""\n"
+ "//OVE default: RGBA( 2, 0, 0, 1)""\n" // <-- 2 is outisde range
+ "//OVE description: color applied to the grayscale""\n"
+ "uniform vec4 tone_in;""\n"
+ );
+
+ ShaderInputsParser parser(code);
+ parser.Parse();
+
+ OLIVE_ASSERT_EQUAL( parser.ErrorList().size(), 1);
+ OLIVE_ASSERT_EQUAL( parser.InputList().size(), 1); // input is still present
+
+ OLIVE_ASSERT_EQUAL( STR(parser.ErrorList().at(0).issue), STR("Color must be in format 'RGBA(r,g,b,a)' "
+ "where r,g,b,a are in range (0,1)"));
+ OLIVE_ASSERT_EQUAL( parser.ErrorList().at(0).line, 3);
+
+ OLIVE_TEST_END;
+}
+
+// An input of kind Boolean
+OLIVE_ADD_TEST(InputBooleanTest)
+{
+ QString code(
+ "//OVE name: bypass effect""\n"
+ "//OVE type: BOOLEAN""\n"
+ "//OVE flag: NOT_CONNECTABLE""\n"
+ "//OVE default: false""\n"
+ "//OVE description: when True, the input is passed to output as is.""\n"
+ "uniform bool disable_in;""\n"
+ );
+
+ ShaderInputsParser parser(code);
+ parser.Parse();
+
+ OLIVE_ASSERT_EQUAL( parser.ErrorList().size(), 0);
+ OLIVE_ASSERT_EQUAL( parser.InputList().size(), 1);
+
+ const ShaderInputsParser::InputParam & param = parser.InputList().at(0);
+
+ OLIVE_ASSERT_EQUAL( STR(param.human_name), STR("bypass effect"));
+ OLIVE_ASSERT_EQUAL( STR(param.description), STR("when True, the input is passed to output as is."));
+ OLIVE_ASSERT_EQUAL( param.flags, InputFlags(kInputFlagNotConnectable));
+ OLIVE_ASSERT_EQUAL( STR(param.uniform_name), STR("disable_in"));
+ OLIVE_ASSERT_EQUAL( STR(param.type_string), STR("BOOLEAN"));
+ OLIVE_ASSERT_EQUAL( param.type, NodeValue::kBoolean);
+ OLIVE_ASSERT_EQUAL( param.default_value.toBool(), false);
+
+ OLIVE_TEST_END;
+}
+
+// An input of kind Selection
+OLIVE_ADD_TEST(InputSelectionTest)
+{
+ QString code(
+ "//OVE name: mode""\n"
+ "//OVE type: SELECTION""\n"
+ "//OVE values: \"ADD\", \"AVERAGE\", \"COLOR BURN\"""\n"
+ "//OVE values: \"COLOR DODGE\" \"DARKEN\" \"DIFFERENCE\"""\n"
+ "//OVE values: \"EXCLUSION\" \"GLOW\"""\n"
+ "//OVE default: 3""\n"
+ "//OVE description: the blend mode""\n"
+ "uniform int mode_in; ""\n"
+ );
+
+ ShaderInputsParser parser(code);
+ parser.Parse();
+
+ OLIVE_ASSERT_EQUAL( parser.ErrorList().size(), 0);
+ OLIVE_ASSERT_EQUAL( parser.InputList().size(), 1);
+
+ const ShaderInputsParser::InputParam & param = parser.InputList().at(0);
+
+ OLIVE_ASSERT_EQUAL( STR(param.human_name), STR("mode"));
+ OLIVE_ASSERT_EQUAL( STR(param.description), STR("the blend mode"));
+ OLIVE_ASSERT_EQUAL( STR(param.uniform_name), STR("mode_in"));
+ OLIVE_ASSERT_EQUAL( STR(param.type_string), STR("SELECTION"));
+ OLIVE_ASSERT_EQUAL( param.type, NodeValue::kCombo);
+ OLIVE_ASSERT_EQUAL( param.default_value.toInt(), 3);
+
+ OLIVE_TEST_END;
+}
+
+// An input of kind Point
+OLIVE_ADD_TEST(InputPointTest)
+{
+ QString code(
+ "//OVE name: From""\n"
+ "//OVE type: POINT""\n"
+ "//OVE default: (0.4,0.2)""\n"
+ "//OVE color: RGBA( 0.5,0.5 ,0.0, 1)""\n"
+ "//OVE description: gradient start point""\n"
+ "uniform vec2 from_in;""\n"
+ );
+
+ ShaderInputsParser parser(code);
+ parser.Parse();
+
+ OLIVE_ASSERT_EQUAL( parser.ErrorList().size(), 0);
+ OLIVE_ASSERT_EQUAL( parser.InputList().size(), 1);
+
+ const ShaderInputsParser::InputParam & param = parser.InputList().at(0);
+
+ OLIVE_ASSERT_EQUAL( STR(param.human_name), STR("From"));
+ OLIVE_ASSERT_EQUAL( STR(param.description), STR("gradient start point"));
+ OLIVE_ASSERT_EQUAL( param.flags, InputFlags(0));
+ OLIVE_ASSERT_EQUAL( STR(param.uniform_name), STR("from_in"));
+ OLIVE_ASSERT_EQUAL( STR(param.type_string), STR("POINT"));
+ OLIVE_ASSERT_EQUAL( param.type, NodeValue::kVec2);
+ OLIVE_ASSERT( param.gizmoColor == QColor(127,127,0));
+
+ OLIVE_TEST_END;
+}
+
+// An input with multiple flags (with different separators)
+OLIVE_ADD_TEST(MultipleFlagsTest)
+{
+ QString code(
+ "//OVE name: From""\n"
+ "//OVE type: POINT""\n"
+ "//OVE default: (0.4,0.2)""\n"
+ "//OVE flag: NOT_KEYFRAMABLE, NOT_CONNECTABLE HIDDEN""\n"
+ "//OVE description: gradient start point""\n"
+ "uniform vec2 from_in;""\n"
+ );
+
+ ShaderInputsParser parser(code);
+ parser.Parse();
+
+ OLIVE_ASSERT_EQUAL( parser.ErrorList().size(), 0);
+ OLIVE_ASSERT_EQUAL( parser.InputList().size(), 1);
+
+ const ShaderInputsParser::InputParam & param = parser.InputList().at(0);
+
+ OLIVE_ASSERT_EQUAL( STR(param.human_name), STR("From"));
+ OLIVE_ASSERT_EQUAL( param.flags, InputFlags(kInputFlagNotKeyframable | kInputFlagNotConnectable | kInputFlagHidden));
+
+ OLIVE_TEST_END;
+}
+
+
+// input type typo error. Check that errors are detected
+OLIVE_ADD_TEST(InputTypoTest)
+{
+ QString code(
+ "//OVE name: Tolerance %%""\n"
+ "//OVE type: FLOA""\n" // <-- typo: "FLOA" for "FLOAT"
+ "//OVE min: 1.0""\n"
+ "//OVE default: 5.5""\n"
+ "//OVE max: 100""\n"
+ "//OVE description: the tolerance of filter""\n"
+ "uniform float toler_in;""\n"
+ );
+
+ ShaderInputsParser parser(code);
+ parser.Parse();
+
+ OLIVE_ASSERT_EQUAL( parser.ErrorList().size(), 4);
+
+ OLIVE_ASSERT_EQUAL( STR(parser.ErrorList().at(0).issue),
+ STR("type FLOA is invalid") );
+ OLIVE_ASSERT_EQUAL( parser.ErrorList().at(0).line, 2);
+
+ OLIVE_ASSERT_EQUAL( STR(parser.ErrorList().at(1).issue),
+ STR("1.0 is not valid as minimum for type FLOA") );
+ OLIVE_ASSERT_EQUAL( parser.ErrorList().at(1).line, 3);
+
+ OLIVE_ASSERT_EQUAL( STR(parser.ErrorList().at(2).issue),
+ STR("type FLOA does not support a default value") );
+ OLIVE_ASSERT_EQUAL( parser.ErrorList().at(2).line, 4);
+
+ OLIVE_ASSERT_EQUAL( STR(parser.ErrorList().at(3).issue),
+ STR("100 is not valid as maximum for type FLOA") );
+ OLIVE_ASSERT_EQUAL( parser.ErrorList().at(3).line, 5);
+
+ OLIVE_TEST_END;
+}
+
+
+// an input defined after "//OVE end"
+// The input is not counted
+OLIVE_ADD_TEST(InputAfterEndTest)
+{
+ QString code(
+ "//OVE end""\n" // <- end
+ "//OVE name: Tolerance %%""\n"
+ "//OVE type: FLOAT""\n"
+ "//OVE description: the tolerance of filter""\n"
+ "uniform float toler_in;""\n"
+ );
+
+ ShaderInputsParser parser(code);
+ parser.Parse();
+
+ OLIVE_ASSERT_EQUAL( parser.ErrorList().size(), 0);
+ OLIVE_ASSERT_EQUAL( parser.InputList().size(), 0);
+
+ OLIVE_TEST_END;
+}
+
+
+// presence of multiple inputs
+OLIVE_ADD_TEST(InputManyTest)
+{
+ QString code(
+ "//OVE name: Base image""\n"
+ "//OVE type: TEXTURE""\n"
+ "//OVE flag: NOT_KEYFRAMABLE""\n"
+ "//OVE description: the base image""\n"
+ "uniform sampler2D tex_base;""\n"
+ "\n"
+ "//OVE name: Tolerance %%""\n"
+ "//OVE type: FLOAT""\n"
+ "//OVE description: the tolerance of filter""\n"
+ "uniform float toler_in;""\n"
+ "\n"
+ "//OVE name: base color""\n"
+ "//OVE type: COLOR""\n"
+ "//OVE description: the final color""\n"
+ "uniform vec4 tone_in;""\n"
+ "\n"
+ );
+
+ ShaderInputsParser parser(code);
+ parser.Parse();
+
+ OLIVE_ASSERT_EQUAL( parser.ErrorList().size(), 0);
+ OLIVE_ASSERT_EQUAL( parser.InputList().size(), 3);
+
+ OLIVE_TEST_END;
+}
+
+// An input whose type is not consistent with the uniform type
+OLIVE_ADD_TEST(InconsistentTypeTest_1)
+{
+ QString code(
+ "//OVE name: Base image""\n"
+ "//OVE type: TEXTURE""\n"
+ "//OVE description: the base image""\n"
+ "uniform float tex_base;""\n" // <-- 'float' vs 'TEXTURE'
+ );
+
+ ShaderInputsParser parser(code);
+ parser.Parse();
+
+ OLIVE_ASSERT_EQUAL( parser.ErrorList().size(), 1);
+ OLIVE_ASSERT_EQUAL( parser.InputList().size(), 1); // input is still present
+
+ OLIVE_ASSERT_EQUAL( parser.ErrorList().at(0).line, 4);
+ OLIVE_ASSERT_EQUAL( STR(parser.ErrorList().at(0).issue),
+ STR("metadata type TEXTURE requires uniform type sampler2D and not float"));
+
+
+ OLIVE_TEST_END;
+}
+
+// An input whose type is not consistent with the uniform type
+OLIVE_ADD_TEST(InconsistentTypeTest_2)
+{
+ QString code(
+ "//OVE name: Gain""\n"
+ "//OVE type: FLOAT""\n"
+ "//OVE description: the base image""\n"
+ "uniform vec2 gain_in;""\n" // <-- 'vec2' vs 'FLOAT'
+ );
+
+ ShaderInputsParser parser(code);
+ parser.Parse();
+
+ OLIVE_ASSERT_EQUAL( parser.ErrorList().size(), 1);
+ OLIVE_ASSERT_EQUAL( parser.InputList().size(), 1); // input is still present
+
+ OLIVE_ASSERT_EQUAL( parser.ErrorList().at(0).line, 4);
+ OLIVE_ASSERT_EQUAL( STR(parser.ErrorList().at(0).issue),
+ STR("metadata type FLOAT requires uniform type float and not vec2"));
+
+
+ OLIVE_TEST_END;
+}
+
+// Illegal number of iterations
+OLIVE_ADD_TEST(WrongNumOfIterations)
+{
+ QString code(
+ "#version 150""\n"
+ "//OVE shader_name: slide""\n"
+ "//OVE shader_description: slide transition""\n"
+ "//OVE shader_version: 0.1""\n"
+ "//OVE number_of_iterations: two""\n"
+ );
+
+ ShaderInputsParser parser(code);
+ parser.Parse();
+
+ OLIVE_ASSERT_EQUAL( parser.ErrorList().size(), 1);
+ OLIVE_ASSERT_EQUAL( parser.ErrorList().at(0).line, 5);
+ OLIVE_ASSERT_EQUAL( STR(parser.ErrorList().at(0).issue),
+ STR("Number of iterations must be a number greater or equal to 1"));
+
+ OLIVE_TEST_END;
+}
+
+
+} // olive
+