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 +