diff --git a/radiant/gtkdlgs.cpp b/radiant/gtkdlgs.cpp index a7730092..2fbceb6e 100644 --- a/radiant/gtkdlgs.cpp +++ b/radiant/gtkdlgs.cpp @@ -399,88 +399,6 @@ void DoAbout(){ dialog.exec(); } -// ============================================================================= - -class TextEditor -{ - QWidget *m_window = 0; - QPlainTextEdit *m_textView; // slave, text widget from the gtk editor - QPushButton *m_button; // save button - CopiedString m_filename; - - void construct(){ - m_window = new QWidget( MainFrame_getWindow(), Qt::Dialog | Qt::WindowMinimizeButtonHint | Qt::WindowMaximizeButtonHint | Qt::WindowCloseButtonHint ); - g_guiSettings.addWindow( m_window, "ShaderEditor/geometry" ); - - auto *vbox = new QVBoxLayout( m_window ); - vbox->setContentsMargins( 0, 0, 0, 0 ); - - m_textView = new QPlainTextEdit; - m_textView->setLineWrapMode( QPlainTextEdit::LineWrapMode::NoWrap ); - vbox->addWidget( m_textView ); - - m_button = new QPushButton( "Save" ); - m_button->setSizePolicy( QSizePolicy::Policy::Fixed, QSizePolicy::Policy::Fixed ); - vbox->addWidget( m_button, Qt::AlignmentFlag::AlignRight ); - - QObject::connect( m_textView->document(), &QTextDocument::modificationChanged, [this]( bool modified ){ - m_button->setEnabled( modified ); - - StringOutputStream str( 256 ); - str << ( modified? "*" : "" ) << m_filename; - m_window->setWindowTitle( str.c_str() ); - } ); - - QObject::connect( m_button, &QAbstractButton::clicked, [this](){ editor_save(); } ); - } - void editor_save(){ - FILE *f = fopen( m_filename.c_str(), "wb" ); //write in binary mode to preserve line feeds - - if ( f == nullptr ) { - globalErrorStream() << "Error saving file" << makeQuoted( m_filename ) << '\n'; - return; - } - - const auto str = m_textView->toPlainText().toLatin1(); - fwrite( str.constData(), 1, str.length(), f ); - fclose( f ); - - m_textView->document()->setModified( false ); - } -public: - void DoGtkTextEditor( const char* text, const char* shaderName, const char* filename, const bool editable ){ - if ( !m_window ) { - construct(); // build it the first time we need it - } - - m_filename = filename; - m_textView->setReadOnly( !editable ); - m_textView->setPlainText( text ); - - m_window->show(); - m_window->raise(); - m_window->activateWindow(); - - { // scroll to shader - const QRegularExpression::PatternOptions rxFlags = QRegularExpression::PatternOption::MultilineOption | - QRegularExpression::PatternOption::CaseInsensitiveOption; - const QRegularExpression rx( "^\\s*" + QRegularExpression::escape( shaderName ) + "(|:q3map)$", rxFlags ); - auto *doc = m_textView->document(); - - for( QTextCursor cursor( doc ); cursor = doc->find( rx ), !cursor.isNull(); ) - if( !doc->find( QRegularExpression( "^\\s*\\{", rxFlags ), cursor ).isNull() ){ - QTextCursor cur( cursor ); - cur.movePosition( QTextCursor::MoveOperation::NextBlock, QTextCursor::MoveMode::MoveAnchor, 99 ); - m_textView->setTextCursor( cur ); - m_textView->setTextCursor( cursor ); - break; - } - } - } -}; - -static TextEditor g_textEditor; - // ============================================================================= // Light Intensity dialog @@ -533,6 +451,1653 @@ void DoShaderInfoDlg( const char* name, const char* filename, const char* title } +// ============================================================================= +// Shader Editor + +/* + force dark background, bright foreground +?save font size + ctrl+d duplicate line, selection + find and replace +f3, shift+f3, ctrl+f +ctrl+s + move selected text block with alt+arrows + move line up/dn too + ctrl+x to cut whole line + ctrl+c to copy whole line +?paste these on new line: put cursor to start 1st + url to manual + hl bug: when \n} is deleted, then undone //was paste=state -1->hl -1 = unchanged = hl break + complete tex paths from radiant's VFS +separate shader path completion +?complete shader name from tex paths? + shader templates in completion; on { + color3f display + suggest common prefix, e.g. q3map_ for q3 input +?ctrl+bs del to _ + sort fix \d completion + animmap fix completion + map $lightmap etc + skyparms nearbox '-' completion is wanted + skyparms farbox '-' completion is wanted +sensible default num values on completion +num values description on completion //?in comment +?complete continuous num sequences at once +no next token completion in the middle, if line is complete +no completion on undo, paste? //atm on adding undo, not on removing +QStringLiteral optimization +QCompleter inactive entry in list // because is wrapAround() + check %p %t lengths in hl +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "stringio.h" +#include "plugin.h" +#include "ifilesystem.h" + +static const struct{ const char *name; const char *text; } c_shaderTemplates[] = { + { + "map", +R"( + { + map $lightmap + rgbGen identity + } + { + map textures/ + blendFunc GL_DST_COLOR GL_ZERO + rgbGen identity + } +} +)" + }, + { + "map-vertex", +R"( + surfaceparm nolightmap + { + map textures/ + rgbGen exactVertex + } +} +)" + }, + { + "mask", +R"( + cull none + { + map textures/ + alphaFunc GE128 + depthWrite + } + { + map $lightmap + rgbGen identity + depthFunc equal + } + { + map %s + blendFunc GL_DST_COLOR GL_ZERO + rgbGen identity + depthFunc equal + } +} +)" + }, + { + "mask-vertex", +R"( + surfaceparm nolightmap + cull none + { + map textures/ + alphaFunc GE128 + depthWrite + rgbGen exactVertex + } +} +)" + }, + { + "blend", +R"( + cull none + { + map textures/ + blendFunc GL_SRC_ALPHA GL_ONE_MINUS_SRC_ALPHA + } + { + map $lightmap + blendFunc GL_DST_COLOR GL_ZERO + rgbGen identity + } +} +)" + }, +}; + + +static const char c_pageGen[] = "general-directives.html#"; +static const char c_pageGlob[] = "q3map-global-directives.html#"; +static const char c_pageSurf[] = "q3map-surface-parameter-directives.html$"; +static const char c_pageQER[] = "quake-editor-radiant-directives.html#"; +static const char c_pageStage[] = "stage-directives.html#"; + +static const QColor c_colorComment( Qt::darkGray ); +static const QColor c_colorShaderName( 249, 174, 88 ); +static const QColor c_colorBrace( 248, 228, 0 ); +static const QColor c_colorBraceLv1( 248, 228, 0 ); +static const QColor c_colorBraceLv2( 236, 44, 215 ); +static const QColor c_colorNumbers( 172, 214, 167 ); +static const QColor c_colorColor3f( -1, -1, -1 ); // invalid color to pass info that it's color3f +static const QColor c_colorKeyLv1( 95, 141, 187 ); // build time keys +static const QColor c_colorKeyLv1E( 85, 111, 214 ); // engine runtime keys +static const QColor c_colorKeyLv2( 77, 172, 179 ); // stages +static const QColor c_colorValue( 196, 146, 188 ); +static const QColor c_colorPath( 178, 168, 96 ); + +const char c_float_regex[] = "[+-]?(?:[0-9]*[.])?[0-9]+"; // (?:) - non capturing group +const char c_int_regex[] = "[+-]?\\d+"; + +struct ShaderFormat{ + const char * const key; + const char * const page; + const QColor color{}; + std::vector values{}; + const QColor valuesColor = c_colorValue; +}; +/* Legend: +%s = one of $values, generic string, if $values is empty //latter possibly not supported in completer +%t = texture path +%p = generic path +%f = float +%с = float of color3f +%i = int +*/ +static const std::vector g_shaderGeneralFormats{ + { + "surfaceparm %s", c_pageSurf, c_colorKeyLv1, { + "alphashadow", + "antiportal", + "areaportal", + "botclip", + "clusterportal", + "detail", + "donotenter", + "dust", + "flesh", + "fog", + "hint", + "ladder", + "lava", + "lightfilter", + "lightgrid", + "metalsteps", + "monsterclip", + "nodamage", + "nodlight", + "nodraw", + "nodrop", + "noimpact", + "nomarks", + "nolightmap", + "nosteps", + "nonsolid", + "origin", + "playerclip", + "pointlight", + "skip", + "sky", + "slick", + "slime", + "structural", + "trans", + "trigger", + "water", + } + }, + { + "cull %s", c_pageGen, c_colorKeyLv1E, { + "none", + "disable", + "twosided", + "backsided", + "backside", + "back", //this last, so it doesn't take precendence, while matching longer version + } + }, + { + "noPicMip", c_pageGen, c_colorKeyLv1E + }, + { + "noMipMaps", c_pageGen, c_colorKeyLv1E + }, + { + "polygonOffset", c_pageGen, c_colorKeyLv1E + }, + { + "portal", c_pageGen, c_colorKeyLv1E + }, + { + "skyParms %t %i -", c_pageGen, c_colorKeyLv1E + }, + { + "skyParms %t - -", c_pageGen, c_colorKeyLv1E + }, + { + "skyParms - %i -", c_pageGen, c_colorKeyLv1E + }, + { + "skyParms - - -", c_pageGen, c_colorKeyLv1E + }, + { + "fogParms ( %c %c %c ) %i", c_pageGen, c_colorKeyLv1E + }, + { + "sort %s", c_pageGen, c_colorKeyLv1E, { + "portal", + "Sky", + "Opaque", + "Decal", + "SeeThrough", + "Banner", + "Underwater", + "Additive", + "Nearest", + } + }, + { + "sort %i", c_pageGen, c_colorKeyLv1E + }, + { + "deformVertexes wave %f %s %f %f %f %f", c_pageGen, c_colorKeyLv1E, { + "sin", + "triangle", + "square", + "sawtooth", + "inversesawtooth", + } + }, + { + "deformVertexes move %f %f %f %s %f %f %f %f", c_pageGen, c_colorKeyLv1E, { + "sin", + "triangle", + "square", + "sawtooth", + "inversesawtooth", + } + }, + { + "deformVertexes %s %f %f", c_pageGen, c_colorKeyLv1E, { + "normal", + } + }, + { + "deformVertexes %s %f %f %f", c_pageGen, c_colorKeyLv1E, { + "bulge", + } + }, + { + "deformVertexes %s", c_pageGen, c_colorKeyLv1E, { + "autosprite2", + "autosprite", //this last, so it doesn't take precendence, while matching longer version + } + }, + { + "qer_editorImage %t", "quake-editor-radiant-directives.html#editorImage", c_colorKeyLv1 + }, + { + "qer_trans %f", "quake-editor-radiant-directives.html#trans", c_colorKeyLv1 + }, + { + "qer_alphaFunc %s %f", "quake-editor-radiant-directives.html#alphaFunc", c_colorKeyLv1, { + "equal", + "greater", + "less", + "gequal", + "lequal", + } + }, + { + "light %p", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_alphaGen const %f", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_colorGen const ( %c %c %c )", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_alphaMod %s ( %f %f %f )", c_pageGlob, c_colorKeyLv1, { + "dotproduct", + "dotproduct2", + } + }, + { + "q3map_alphaMod %s ( %f %f %f %f %f )", c_pageGlob, c_colorKeyLv1, { + "dotproductScale", + "dotproduct2Scale", + } + }, + { + "q3map_alphaMod %s %f", c_pageGlob, c_colorKeyLv1, { + "scale", + "set", + } + }, + { + "q3map_alphaMod %s", c_pageGlob, c_colorKeyLv1, { + "volume", + } + }, + { + "q3map_colorMod %s ( %f %f %f )", c_pageGlob, c_colorKeyLv1, { + "dotproduct", + "dotproduct2", + } + }, + { + "q3map_colorMod %s ( %f %f %f %f %f )", c_pageGlob, c_colorKeyLv1, { + "dotproductScale", + "dotproduct2Scale", + } + }, + { + "q3map_colorMod %s ( %c %c %c )", c_pageGlob, c_colorKeyLv1, { + "scale", + "set", + } + }, + { + "q3map_colorMod %s", c_pageGlob, c_colorKeyLv1, { + "volume", + } + }, + { + "q3map_backShader %p", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_backSplash %f %f", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_baseShader %p", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_bounceScale %f", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_clipModel", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_cloneShader %p", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_deprecateShader %p", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_flare %p", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_flareShader %p", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_floodLight %c %c %c %f %f %f", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_fogDir %f %f %f", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_foliage %p %f %f %f %i", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_forceMeta", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_fur %i %f %f", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_globalTexture", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_indexed", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_invert", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_lightImage %t", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_lightmapAxis %s", c_pageGlob, c_colorKeyLv1, { + "x", + "y", + "z", + } + }, + { + "q3map_lightmapBrightness %f", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_lightmapFilterRadius %f %f", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_lightmapMergable", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_lightmapSampleOffset %f", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_lightmapSampleSize %i", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_lightmapSize %i %i", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_lightRGB %c %c %c", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_lightStyle %i", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_lightSubdivide %i", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_noClip", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_noDirty", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_noFast", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_noFog", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_nonPlanar", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_normalImage %t", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_noTJunc", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_noVertexLight", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_offset %f", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_remapShader %p", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_shadeAngle %f", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_skylight %f %i %f %f %i", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_skylight %f %i", c_pageGlob, c_colorKeyLv1 //this last, so it doesn't take precendence, while matching longer version + }, + { + "q3map_splotchFix", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_styleMarker2", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_styleMarker", c_pageGlob, c_colorKeyLv1 //this last, so it doesn't take precendence, while matching longer version + }, + { + "q3map_sun %c %c %c %f %f %f", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_sunExt %c %c %c %f %f %f %f %i", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_surfaceLight %f", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_surfaceModel %p %f %f %f %f %f %f %i", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_tcGen %s %f %f", c_pageGlob, c_colorKeyLv1, { + "vector", + "ivector", + } + }, + { + "q3map_tcMod %s %f", c_pageGlob, c_colorKeyLv1, { + "rotate", + } + }, + { + "q3map_tcMod %s %f %f", c_pageGlob, c_colorKeyLv1, { + "scale", + "translate", + "move", + "shift", + } + }, + { + "q3map_terrain", c_pageGlob, c_colorKeyLv1 + }, + { + "q3map_tessSize %f", c_pageGlob, c_colorKeyLv1 + }, + { + "tessSize %f", "q3map-global-directives.html#q3map_tessSize", c_colorKeyLv1 + }, + { + "q3map_vertexScale %f", c_pageGlob, c_colorKeyLv1 + }, +}; + +static const std::vector g_shaderStageFormats{ + { + "map %s", c_pageStage, c_colorKeyLv2, { + "$lightmap", // these do not work for highlighting, $ is special + "$whiteimage", // next rule highlights and this works for completion - acceptable + } + }, + { + "map %t", c_pageStage, c_colorKeyLv2 + }, + { + "clampMap %t", c_pageStage, c_colorKeyLv2 + }, + { + "videoMap %p", c_pageStage, c_colorKeyLv2 + }, + { + "animMap %f %t %t %t %t %t %t %t %t", c_pageStage, c_colorKeyLv2 + }, + { + "blendFunc %s", c_pageStage, c_colorKeyLv2, { + "add", + "filter", + "blend", + } + }, + { + "blendFunc %s %s", c_pageStage, c_colorKeyLv2, { + "GL_DST_COLOR", //fixme this only in src blend + "GL_SRC_COLOR", //fixme this only in dst blend + "GL_ONE_MINUS_DST_COLOR", //fixme this only in src blend + "GL_ONE_MINUS_SRC_COLOR", //fixme this only in dst blend + "GL_SRC_ALPHA", + "GL_ONE_MINUS_SRC_ALPHA", + "GL_ONE", //this last, so it doesn't take precendence, while matching longer version + "GL_ZERO", //this last, so it doesn't take precendence, while matching longer version + } + }, + { + "rgbGen %s", c_pageStage, c_colorKeyLv2, { + "identityLighting", + "identity", //this last, so it doesn't take precendence, while matching longer version + "vertex", + "oneMinusVertex", + "exactVertex", + "entity", + "oneMinusEntity", + "lightingDiffuse", + } + }, + { + "rgbGen wave %s %f %f %f %f", c_pageStage, c_colorKeyLv2, { + "sin", + "triangle", + "square", + "sawtooth", + "inversesawtooth", + "noise", + } + }, + { + "rgbGen const ( %c %c %c )", c_pageStage, c_colorKeyLv2 + }, + { + "alphaGen %s", c_pageStage, c_colorKeyLv2, { + "lightingSpecular", + "entity", + "oneMinusEntity", + "vertex", + "oneMinusVertex", + "portal", + } + }, + { + "alphaGen wave %s %f %f %f %f", c_pageStage, c_colorKeyLv2, { + "sin", + "triangle", + "square", + "sawtooth", + "inversesawtooth", + "noise", + } + }, + { + "alphaGen const %f", c_pageStage, c_colorKeyLv2 + }, + { + "tcGen %s", c_pageStage, c_colorKeyLv2, { + "base", + "lightmap", + "environment", + } + }, + { + "tcGen vector ( %f %f %f ) ( %f %f %f )", c_pageStage, c_colorKeyLv2 + }, + { + "tcMod rotate %f", c_pageStage, c_colorKeyLv2 + }, + { + "tcMod %s %f %f", c_pageStage, c_colorKeyLv2, { + "scale", + "scroll", + } + }, + { + "tcMod stretch %s %f %f %f %f", c_pageStage, c_colorKeyLv2, { + "sin", + "triangle", + "square", + "sawtooth", + "inversesawtooth", + "noise", + } + }, + { + "tcMod transform %f %f %f %f %f %f", c_pageStage, c_colorKeyLv2 + }, + { + "tcMod turb %f %f %f %f", c_pageStage, c_colorKeyLv2 + }, + { + "depthFunc %s", c_pageStage, c_colorKeyLv2, { + "equal", + "lequal", + } + }, + { + "depthWrite", c_pageStage, c_colorKeyLv2 + }, + { + "detail", c_pageStage, c_colorKeyLv2 + }, + { + "alphaFunc %s", c_pageStage, c_colorKeyLv2, { + "GT0", + "LT128", + "GE128", + } + }, +}; + +struct BlockData : public QTextBlockUserData +{ + const ShaderFormat *shaderFormat; + BlockData( const ShaderFormat *shaderFormat ) : shaderFormat( shaderFormat ){} +}; + +enum EShaderDepth +{ + eShaderDepth0 = 512, //shader names + eShaderDepth1 = 513, //general directives + eShaderDepth2 = 514, //stages +}; + + +class ShaderHighlighter : public QSyntaxHighlighter +{ +public: + ShaderHighlighter( QTextDocument *parent = 0 ); +protected: + void highlightBlock( const QString &text ) override; +private: + void depthSet( const std::int16_t depth ){ + std::int32_t state = currentBlockState(); + memcpy( &state, &depth, 2 ); + setCurrentBlockState( state ); + } +public: + static std::int16_t depth( const int state ){ + return state; + } +private: + bool stateIsComment( const int state ) const { + return !( state & c_comment_flag ); + } + void stateSetComment( const bool enabled ){ + setCurrentBlockState( enabled + ? ( currentBlockState() & ~c_comment_flag ) + : ( currentBlockState() | c_comment_flag ) ); + } + + struct Rule{ + QRegularExpression pattern; + std::vector colors; + const ShaderFormat& shaderFormat; + Rule( const ShaderFormat& shaderFormat ) : shaderFormat( shaderFormat ){} + }; + std::vector m_rulesGeneral; // general directives + std::vector m_rulesStage; // stage directives + + const int c_comment_flag = ( 1 << 16 ); + + QRegularExpression commentStartExpression{ QStringLiteral( "/\\*" ) }; + QRegularExpression commentEndExpression{ QStringLiteral( "\\*/" ) }; + QRegularExpression commentInlineExpression{ QStringLiteral( "//" ) }; +}; + +ShaderHighlighter::ShaderHighlighter( QTextDocument *parent ) + : QSyntaxHighlighter( parent ) +{ + //? may be alt style with \b match in the end + const auto construc_rules = []( std::vector& rules, const std::vector& formats ){ + for( const auto& format : formats ){ + Rule& rule = rules.emplace_back( format ); + QString pattern( "(\\s*" ); + rule.colors.push_back( format.color ); + for( const char *c = format.key; *c; ++c ){ + if( *c == ' ' ){ + pattern += "\\s+"; + } + else if( string_equal_prefix( c, "%s" ) ){ // string + ++c; + pattern += ")((?:"; // extra inner non capturing group, as space may be added + for( const auto value : format.values ){ + pattern += value; + pattern += '|'; + } + if( format.values.empty() ){ // no predefined list = generic string + pattern += "\\S+|"; + } + pattern.back() = ')'; // replace trailing | by non capturing group end + rule.colors.push_back( format.valuesColor ); + } + else if( string_equal_prefix( c, "%t" ) ){ // texture path + ++c; + pattern += ")("; + pattern += "\\S{1,63}"; + rule.colors.push_back( c_colorPath ); + if( string_equal_prefix_nocase( format.key, "animMap" ) ){ // special case... variable num of paths + pattern += "(?:\\s+\\S{1,63})+"; + break; + } + } + else if( string_equal_prefix( c, "%p" ) ){ // generic path + ++c; + pattern += ")("; + pattern += "\\S{1,63}"; + rule.colors.push_back( c_colorPath ); + } + else if( string_equal_prefix( c, "%f" ) ){ // float + ++c; + pattern += ")("; + pattern += c_float_regex; + rule.colors.push_back( c_colorNumbers ); + } + else if( string_equal_prefix( c, "%c %c %c" ) ){ // color3f + c += strlen( "%c %c %c" ) - 1; + pattern += ")("; + pattern.append( c_float_regex ).append( "\\s+" ).append( c_float_regex ).append( "\\s+" ).append( c_float_regex ); + rule.colors.push_back( c_colorColor3f ); + } + else if( string_equal_prefix( c, "%i" ) ){ // int + ++c; + pattern += ")("; + pattern += c_int_regex; + rule.colors.push_back( c_colorNumbers ); + } + else if( *c == '(' ){ + pattern += ")("; + pattern += "\\("; + rule.colors.push_back( c_colorBrace ); + } + else if( *c == ')' ){ + pattern += ")("; + pattern += "\\)"; + rule.colors.push_back( c_colorBrace ); + } + else{ + pattern += *c; + } + } + pattern += ')'; + rule.pattern = QRegularExpression( pattern, QRegularExpression::PatternOption::CaseInsensitiveOption ); + } + }; + + construc_rules( m_rulesGeneral, g_shaderGeneralFormats ); + construc_rules( m_rulesStage, g_shaderStageFormats ); +} + +void ShaderHighlighter::highlightBlock( const QString &text ) +{ + int start = 0; + stateSetComment( false ); + depthSet( depth( previousBlockState() ) == -1? eShaderDepth0 : depth( previousBlockState() ) ); + + if( auto *data = currentBlockUserData() ){ + static_cast( data )->shaderFormat = nullptr; + } + + const auto highlight_normal = [&]( const std::vector& rules, const QStringView str ){ + for( const auto& rule : rules ){ + const auto match = rule.pattern.match( str, start, QRegularExpression::MatchType::NormalMatch, + QRegularExpression::MatchOption::AnchoredMatchOption ); + if( match.hasMatch() ){ + for( int i = 1; i <= match.lastCapturedIndex(); ++i ){ + if( !rule.colors[i - 1].isValid() ){ // c_colorColor3f + Vector3 clr( 0 ); + string_parse_vector3( match.captured( i ).toLatin1().constData(), clr ); + if( const auto max = vector3_max_component( clr ); max > 0 ) // normalise color + clr /= max; + QTextCharFormat format; + format.setBackground( QColor::fromRgbF( clr[0], clr[1], clr[2] ) ); + format.setForeground( QColor::fromRgbF( 1 - clr[0], 1 - clr[1], 1 - clr[2] ) ); + setFormat( match.capturedStart( i ), match.capturedLength( i ), format ); + } + else + setFormat( match.capturedStart( i ), match.capturedLength( i ), rule.colors[i - 1] ); + } + + if( auto *data = currentBlockUserData() ) + static_cast( data )->shaderFormat = &rule.shaderFormat; + else + setCurrentBlockUserData( new BlockData( &rule.shaderFormat ) ); + + break; + } + } + }; + + const auto parse_normal = [&]( const QStringView str ){ + if( start < str.length() ){ + const auto d = depth( currentBlockState() ); + switch ( d ) + { + case eShaderDepth0: + setFormat( start, str.length(), c_colorShaderName ); + break; + case eShaderDepth1: + highlight_normal( m_rulesGeneral, str ); + break; + case eShaderDepth2: + highlight_normal( m_rulesStage, str ); + break; + default: + break; + } + } + }; + + const auto parse_blocks = [&]( const QStringView str ){ + while( start < str.length() ){ + const int matchOpen = str.indexOf( '{', start ); + const int matchClose = str.indexOf( '}', start ); + if( matchOpen >= 0 && ( matchClose < 0 || matchOpen < matchClose ) ){ + parse_normal( QStringView( str.cbegin(), matchOpen ) ); + const auto d = depth( currentBlockState() ) + 1; + depthSet( d ); + if( d == eShaderDepth1 ) + setFormat( matchOpen, 1, c_colorBraceLv1 ); + else if( d == eShaderDepth2 ) + setFormat( matchOpen, 1, c_colorBraceLv2 ); + start = matchOpen + 1; + } + else if( matchClose >= 0 && ( matchOpen < 0 || matchClose < matchOpen ) ){ + parse_normal( QStringView( str.cbegin(), matchClose ) ); + const auto d = depth( currentBlockState() ) - 1; + depthSet( d ); + if( d == eShaderDepth0 ) + setFormat( matchClose, 1, c_colorBraceLv1 ); + else if( d == eShaderDepth1 ) + setFormat( matchClose, 1, c_colorBraceLv2 ); + start = matchClose + 1; + } + else{ + parse_normal( QStringView( str.cbegin(), str.length() ) ); + start = str.length(); + } + } + }; + + const auto parse_block_comment = [&](){ + QRegularExpressionMatch match = commentEndExpression.match( text, start ); + if( !match.hasMatch() ){ // unclosed comment + stateSetComment( true ); + setFormat( start, text.length() - start, c_colorComment ); + start = text.length(); + } + else{ // closed + stateSetComment( false ); + setFormat( start, match.capturedEnd() - start, c_colorComment ); + start = match.capturedEnd(); + } + }; + + if( stateIsComment( previousBlockState() ) ){ // prev block is unclosed multiline comment + parse_block_comment(); + } + + while( start < text.length() ){ + const int matchBlock = commentStartExpression.match( text, start ).capturedStart(); + const int matchInline = commentInlineExpression.match( text, start ).capturedStart(); + if( matchBlock >= 0 && ( matchInline < 0 || matchBlock < matchInline ) ){ + parse_blocks( QStringView( text.constData(), matchBlock ) ); + start = matchBlock; + parse_block_comment(); + } + else if( matchInline >= 0 && ( matchBlock < 0 || matchInline < matchBlock ) ){ + parse_blocks( QStringView( text.constData(), matchInline ) ); + setFormat( matchInline, text.length() - matchInline, c_colorComment ); + start = text.length(); + } + else{ + parse_blocks( QStringView( text ) ); + start = text.length(); + } + } +} + +class QLineEdit_search : public QLineEdit +{ + QPlainTextEdit& m_textEdit; +public: + QLineEdit_search( QPlainTextEdit& textEdit ) : m_textEdit( textEdit ){ + QObject::connect( this, &QLineEdit::textEdited, [this]( const QString &text ){ + // when typing, we do not want jumping to next occurence on each letter input, set cursor to selection start + if( auto cursor = m_textEdit.textCursor(); cursor.hasSelection() ){ + cursor.setPosition( cursor.selectionStart() ); + m_textEdit.setTextCursor( cursor ); + } + search( text ); + } ); + } +protected: + bool event( QEvent *event ) override { + if( event->type() == QEvent::ShortcutOverride ){ + QKeyEvent *keyEvent = static_cast( event ); + // fix leaking keys + if( keyEvent->key() == Qt::Key_Enter + || keyEvent->key() == Qt::Key_Return + || keyEvent->key() == Qt::Key_Up + || keyEvent->key() == Qt::Key_Down + || keyEvent->key() == Qt::Key_Tab ) + event->accept(); + } + return QLineEdit::event( event ); + } + void keyPressEvent( QKeyEvent *event ) override { + if( !this->text().isEmpty() ){ + if( ( ( event->key() == Qt::Key_Return || event->key() == Qt::Key_Down ) && event->modifiers() == Qt::KeyboardModifier::NoModifier ) + || ( event->key() == Qt::Key_Enter && event->modifiers() == Qt::KeyboardModifier::KeypadModifier ) ) + search( this->text() ); + else if( ( ( event->key() == Qt::Key_Return || ( event->key() == Qt::Key_Enter && ( event->modifiers() & Qt::KeyboardModifier::KeypadModifier ) ) ) + && ( event->modifiers() & Qt::KeyboardModifier::ControlModifier || event->modifiers() & Qt::KeyboardModifier::ShiftModifier ) ) + || ( event->key() == Qt::Key_Up && event->modifiers() == Qt::KeyboardModifier::NoModifier ) ) + search( this->text(), true ); + } + QLineEdit::keyPressEvent( event ); + } +private: + void search( const QString &text, bool reverse = false, bool words = false, bool casesens = false ){ + QTextDocument::FindFlags flag; + if( reverse ) flag |= QTextDocument::FindBackward; + if( casesens ) flag |= QTextDocument::FindCaseSensitively; + if( words ) flag |= QTextDocument::FindWholeWords; + + QTextCursor cursor = m_textEdit.textCursor(); + QTextCursor cursorSaved = cursor; // save the cursor position + + if ( !m_textEdit.find( text, flag ) ){ + cursor.movePosition( reverse? QTextCursor::End : QTextCursor::Start ); //nothing is found: jump to start/end + m_textEdit.setTextCursor( cursor ); + if ( !m_textEdit.find( text, flag ) ){ + m_textEdit.setTextCursor( cursorSaved ); // word not found : we set the cursor back to its initial position + } + } + } +}; + + +class TexTree +{ +public: + struct Prefix{ const char *prefix; }; + struct Compare{ + using is_transparent = void; + + bool operator()( const TexTree& texTree, const TexTree& texTree2 ) const { + return string_less_nocase( texTree.m_name.c_str(), texTree2.m_name.c_str() ); + } + bool operator()( const TexTree& texTree, const char *name ) const { + return string_less_nocase( texTree.m_name.c_str(), name ); + } + bool operator()( const char *name, const TexTree& texTree ) const { + return string_less_nocase( name, texTree.m_name.c_str() ); + } + bool operator()( const TexTree& texTree, const Prefix prefix ) const { + return string_compare_nocase_n( texTree.m_name.c_str(), prefix.prefix, strlen( prefix.prefix ) ) < 0; + } + bool operator()( const Prefix prefix, const TexTree& texTree ) const { + return string_compare_nocase_n( texTree.m_name.c_str(), prefix.prefix, strlen( prefix.prefix ) ) > 0; + } + bool operator()( const TexTree& texTree, const StringRange range ) const { + return string_compare_nocase_n( texTree.m_name.c_str(), range.begin(), range.size() ) < 0; + } + bool operator()( const StringRange range, const TexTree& texTree ) const { + return string_compare_nocase_n( texTree.m_name.c_str(), range.begin(), range.size() ) > 0; + } + }; + + const CopiedString m_name; + TexTree() = default; + TexTree( const StringRange range ) : m_name( range ){ + } + TexTree( const char *name ) : m_name( name ){ + } + mutable std::set m_children; + void insert( const char* filepath ) const { + if( const char* slash = strchr( filepath, '/' ) ){ + m_children.emplace( StringRange( filepath, slash ) ).first->insert( slash + 1 ); + } + else{ + m_children.emplace( filepath ); + } + } + + std::pair + find( const char *filepath ) const { + if( const char* slash = strchr( filepath, '/' ) ){ + if( const auto it = m_children.find( StringRange( filepath, slash ) ); it != m_children.cend() ){ + return it->find( ++slash ); + } + else{ + return { m_children.cend(), m_children.cend() }; + } + } + else{ + return m_children.equal_range( Prefix{ filepath } ); + } + } + + bool isLeaf() const { + return m_children.empty(); + } +}; + + +class QPlainTextEdit_Shader : public QPlainTextEdit +{ + QCompleter *m_completer; + TexTree m_texTree; +public: + QPlainTextEdit_Shader(){ + m_completer = new QCompleter( this ); + m_completer->setWidget( this ); + m_completer->setCompletionMode( QCompleter::CompletionMode::UnfilteredPopupCompletion ); + QObject::connect( this, &QPlainTextEdit::textChanged, [this](){ autoComplete(); } ); + QObject::connect( m_completer, QOverload::of( &QCompleter::activated ), [this]( const QString& str ){ autoCompleteInsert( str ); } ); + + setLineWrapMode( QPlainTextEdit::LineWrapMode::NoWrap ); + new ShaderHighlighter( document() ); + + // force back/foreground colors to not be ruined by global theme + QPalette pal = palette(); + pal.setColor( QPalette::Base, QColor( 46, 52, 54 ) ); + pal.setColor( QPalette::Text, Qt::white ); + setPalette( pal ); + } +protected: + bool event( QEvent *event ) override { + if( event->type() == QEvent::ShortcutOverride ){ + QKeyEvent *keyEvent = static_cast( event ); + // fix leaking shortcuts + if( keyEvent->key() == Qt::Key_PageUp + || keyEvent->key() == Qt::Key_PageDown + || keyEvent->key() == Qt::Key_Up + || keyEvent->key() == Qt::Key_Down + || keyEvent->key() == Qt::Key_Escape // esc for completer + || keyEvent == QKeySequence::StandardKey::DeleteEndOfWord + || keyEvent == QKeySequence::StandardKey::DeleteStartOfWord ) + event->accept(); + // cut current line w/o selection + if( keyEvent == QKeySequence::StandardKey::Cut && !textCursor().hasSelection() ){ + auto cursor = textCursor(); + cursor.movePosition( QTextCursor::MoveOperation::StartOfBlock ); + if( !cursor.movePosition( QTextCursor::MoveOperation::NextBlock, QTextCursor::MoveMode::KeepAnchor ) ){ //no next line + cursor.movePosition( QTextCursor::MoveOperation::EndOfBlock ); + if( cursor.movePosition( QTextCursor::MoveOperation::PreviousBlock, QTextCursor::MoveMode::KeepAnchor ) ) //yes prev line + cursor.movePosition( QTextCursor::MoveOperation::EndOfBlock, QTextCursor::MoveMode::KeepAnchor ); + else + cursor.movePosition( QTextCursor::MoveOperation::StartOfBlock, QTextCursor::MoveMode::KeepAnchor ); //single line left + } + // while key is held, tight stream of clipboard copies causes crash (windows) + // thus let's only copy on single press, furthermore it's not too reasonable to do so otherwise + if( !keyEvent->isAutoRepeat() ){ + setTextCursor( cursor ); + this->cut(); + } + else{ + cursor.removeSelectedText(); + } + event->accept(); + return true; + } + // copy current line w/o selection + if( keyEvent == QKeySequence::StandardKey::Copy && !textCursor().hasSelection() && !keyEvent->isAutoRepeat() ){ + const auto cursorOriginal = textCursor(); + auto cursor( cursorOriginal ); + cursor.movePosition( QTextCursor::MoveOperation::StartOfBlock ); + cursor.movePosition( QTextCursor::MoveOperation::EndOfBlock, QTextCursor::MoveMode::KeepAnchor ); // helps when no next block + cursor.movePosition( QTextCursor::MoveOperation::NextBlock, QTextCursor::MoveMode::KeepAnchor ); + setTextCursor( cursor ); + this->copy(); + setTextCursor( cursorOriginal ); + event->accept(); + return true; + } + // move line down + if( keyEvent->modifiers() == Qt::KeyboardModifier::AltModifier && keyEvent->key() == Qt::Key_Down ){ + auto cursor = textCursor(); + if( !cursor.hasSelection() ){ + cursor.movePosition( QTextCursor::MoveOperation::StartOfBlock ); + cursor.movePosition( QTextCursor::MoveOperation::NextBlock, QTextCursor::MoveMode::KeepAnchor ); + } + + if( cursor.hasSelection() ){ + const int start = cursor.selectionStart(); + const int end = cursor.selectionEnd(); + cursor.setPosition( end ); + if( cursor.atBlockStart() || cursor.movePosition( QTextCursor::MoveOperation::NextBlock ) ){ // ensure there is next line + cursor.setPosition( start, QTextCursor::MoveMode::KeepAnchor ); + cursor.movePosition( QTextCursor::MoveOperation::StartOfBlock, QTextCursor::MoveMode::KeepAnchor ); + QString txt = cursor.selectedText(); + cursor.beginEditBlock(); + cursor.removeSelectedText(); + if( cursor.movePosition( QTextCursor::MoveOperation::NextBlock ) ){ + const int newStart = cursor.position(); + cursor.insertText( txt ); + cursor.setPosition( newStart ); + cursor.setPosition( newStart + txt.length(), QTextCursor::MoveMode::KeepAnchor ); + } + else{ + cursor.movePosition( QTextCursor::MoveOperation::EndOfBlock ); + const int newStart = cursor.position(); + txt.prepend( '\n' ); + txt.chop( 1 ); + cursor.insertText( txt ); + cursor.setPosition( newStart + 1 ); + cursor.setPosition( newStart + txt.length(), QTextCursor::MoveMode::KeepAnchor ); + } + cursor.endEditBlock(); + setTextCursor( cursor ); + } + } + + event->accept(); + return true; + } + // move line up + if( keyEvent->modifiers() == Qt::KeyboardModifier::AltModifier && keyEvent->key() == Qt::Key_Up ){ + auto cursor = textCursor(); + { + const int start = cursor.selectionStart(); + const int end = cursor.selectionEnd(); + cursor.setPosition( start ); + if( cursor.movePosition( QTextCursor::MoveOperation::PreviousBlock ) ){ // ensure there is prev line + cursor.movePosition( QTextCursor::MoveOperation::EndOfBlock ); // returns false for empty line... + cursor.setPosition( end, QTextCursor::MoveMode::KeepAnchor ); + if( !cursor.atBlockStart() || !textCursor().hasSelection() ) // select line to the end + cursor.movePosition( QTextCursor::MoveOperation::EndOfBlock, QTextCursor::MoveMode::KeepAnchor ); + else if( cursor.anchor() != end - 1 ) // remove trailing \n selection, unless it's the only \n + cursor.setPosition( end - 1, QTextCursor::MoveMode::KeepAnchor ); + + QString txt = cursor.selectedText(); + cursor.beginEditBlock(); + cursor.removeSelectedText(); + { + cursor.movePosition( QTextCursor::MoveOperation::StartOfBlock ); + const int newStart = cursor.position(); + txt.append( '\n' ); + txt.remove( 0, 1 ); + cursor.insertText( txt ); + cursor.setPosition( newStart ); + cursor.setPosition( newStart + txt.length(), QTextCursor::MoveMode::KeepAnchor ); + } + cursor.endEditBlock(); + setTextCursor( cursor ); + } + } + + event->accept(); + return true; + } + // duplicate + if( keyEvent->modifiers() == Qt::KeyboardModifier::ControlModifier && keyEvent->key() == Qt::Key_D ){ + auto cursor = textCursor(); + if( cursor.hasSelection() ){ + cursor.setPosition( cursor.selectionStart() ); + cursor.insertText( textCursor().selectedText() ); + } + else{ + cursor.movePosition( QTextCursor::MoveOperation::StartOfBlock ); + cursor.movePosition( QTextCursor::MoveOperation::EndOfBlock, QTextCursor::MoveMode::KeepAnchor ); + const QString txt = cursor.selectedText() + '\n'; + cursor.setPosition( cursor.selectionStart() ); + cursor.insertText( txt ); + } + + event->accept(); + return true; + } + } + return QPlainTextEdit::event( event ); + } + void keyPressEvent( QKeyEvent *e ) override { + if( m_completer->popup()->isVisible() ){ // The following keys are forwarded by the completer to the widget + if( e->key() == Qt::Key_Enter + || e->key() == Qt::Key_Return + || e->key() == Qt::Key_Escape + || e->key() == Qt::Key_Tab + || e->key() == Qt::Key_Backtab ){ + e->ignore(); + return; // let the completer do default behavior + } + } + QPlainTextEdit::keyPressEvent( e ); + } + void wheelEvent( QWheelEvent *e ) override { + // this is only allowed for read only state for some reason, we want for editable too + if( e->modifiers() & Qt::ControlModifier ){ + const float delta = e->angleDelta().y() / 120.f; + zoomInF( delta ); + return; + } + QPlainTextEdit::wheelEvent(e); + } +private: + void texTree_construct(){ + class LoadTexturesByTypeVisitor : public ImageModules::Visitor + { + const char* m_dirstring; + TexTree& m_texTree; + public: + void insert( const char *name ) const { + m_texTree.insert( StringOutputStream( 64 )( m_dirstring, PathExtensionless( name ) ) ); + } + typedef ConstMemberCaller1 InsertCaller; + LoadTexturesByTypeVisitor( const char* dirstring, TexTree& texTree ) : m_dirstring( dirstring ), m_texTree( texTree ){} + void visit( const char* minor, const _QERPlugImageTable& table ) const { + GlobalFileSystem().forEachFile( m_dirstring, minor, InsertCaller( *this ), 99 ); + } + }; + + Radiant_getImageModules().foreachModule( LoadTexturesByTypeVisitor( "textures/", m_texTree ) ); + Radiant_getImageModules().foreachModule( LoadTexturesByTypeVisitor( "models/", m_texTree ) ); + Radiant_getImageModules().foreachModule( LoadTexturesByTypeVisitor( "env/", m_texTree ) ); + } + auto texTree_find_completion( const char *path ){ + if( m_texTree.m_children.empty() ) + texTree_construct(); + + return m_texTree.find( path ); + } + void autoComplete(){ + QTextCursor cursor = textCursor(); + cursor.movePosition( QTextCursor::MoveOperation::StartOfLine, QTextCursor::MoveMode::KeepAnchor ); + const QString selectedText = cursor.selectedText(); + const auto line = selectedText.split( QRegularExpression( "\\s+" ), Qt::SplitBehaviorFlags::SkipEmptyParts ); + + const int depth = ShaderHighlighter::depth( cursor.block().userState() ); + + QStringList list; + const auto list_push = [&list]( const QString& string ){ + if( !list.contains( string, Qt::CaseSensitivity::CaseInsensitive ) ) + list.push_back( string ); + }; + + if( !line.isEmpty() && depth == eShaderDepth1 && line.back() == '{' ){ + for( const auto& shader : c_shaderTemplates ) + list.push_back( shader.name ); + } + else if( !line.isEmpty() && ( depth == eShaderDepth1 || depth == eShaderDepth2 ) ){ + const auto& shaderFormats = ( depth == eShaderDepth1 )? g_shaderGeneralFormats : g_shaderStageFormats; + for( const auto& format : shaderFormats ){ + const auto tokens = QString( format.key ).split( ' ', Qt::SplitBehaviorFlags::SkipEmptyParts ); + if( line.size() > tokens.size() ) // line too long, nothing to match + continue; + for( int i = 0; i < line.size(); ++i ){ + const auto& word = line[i]; + const auto& token = tokens[i]; + + const auto complete_tex_path = [&]( const char *path ){ + const auto range = texTree_find_completion( path ); + for( auto it = range.first; it != range.second; ++it ){ + QString str( it->m_name.c_str() ); + if( it->isLeaf() ){ + str += ".tga"; + if( i + 1 < tokens.size() ) // there is next token, add space + str += ' '; + } + else{ + str += '/'; + } + list_push( str ); + } + }; + + const auto push_next_token = [&](){ + ++i; // advance to the next token + const auto push_token = [&]( QString token ){ + if( i + 1 < tokens.size() ) // there is next token, add space + token.append( ' ' ); + if( !selectedText.endsWith( ' ' ) ) // no space after matched word, add one + token.prepend( ' ' ); + list_push( token ); + }; + if( i < tokens.size() ){ // token is available + if( tokens[i] == "%s" ){ + for( const auto value : format.values ){ + push_token( value ); + } + } + else if( tokens[i] == "%f" || tokens[i] == "%c" ){ + push_token( ".0" ); + } + else if( tokens[i] == "%i" ){ + push_token( "1" ); + } + else if( tokens[i] == "%t" ){ + complete_tex_path( "" ); + } + else if( tokens[i] == "%p" ){ + push_token( "textures/" ); // isn't textures/ every time, but mostly + } + else{ + push_token( tokens[i] ); + } + } + }; + + const auto values_contain = []( const std::vector values, const QString& string ){ + for( const auto value : values ) + if( string.compare( value, Qt::CaseSensitivity::CaseInsensitive ) == 0 ) + return true; + return false; + }; + + if( i == line.size() - 1 ){ // last word, partial match is okay + if( token == "%s" ){ + if( values_contain( format.values, word ) ){ // exact match, grab next token + push_next_token(); + } + else{ // partial match + if( !selectedText.back().isSpace() ){ + for( const auto v : format.values ){ + QString value( v ); + if( value.startsWith( word, Qt::CaseSensitivity::CaseInsensitive ) ){ + if( i + 1 < tokens.size() ) // there is next token, add space + value.append( ' ' ); + list_push( value ); + } + } + } + } + } + else if( token == "%f" || token == "%c" ){ + if( QRegularExpression( QRegularExpression::anchoredPattern( c_float_regex ) ).match( word ).hasMatch() ) + push_next_token(); + } + else if( token == "%i" ){ + if( QRegularExpression( QRegularExpression::anchoredPattern( c_int_regex ) ).match( word ).hasMatch() ) + push_next_token(); + } + else if( token == "%t" ){ //any string is fine + if( selectedText.back().isSpace() ) + push_next_token(); + else + complete_tex_path( word.toLatin1().constData() ); + } + else if( token == "%p" ){ //any string is fine + push_next_token(); + } + else if( token.compare( word, Qt::CaseSensitivity::CaseInsensitive ) == 0 ){ // exact match, grab next token + push_next_token(); + } + else if( token.startsWith( word, Qt::CaseSensitivity::CaseInsensitive ) ){ // partial match + if( !selectedText.back().isSpace() ){ + if( i + 1 < tokens.size() ) // there is next token, add space + list_push( token + ' ' ); + else + list_push( token ); + } + } + } + else{ // midway, want exact match + if( token == "%s" ){ + if( values_contain( format.values, word ) ) + continue; + } + else if( token == "%f" || token == "%c" ){ + if( QRegularExpression( QRegularExpression::anchoredPattern( c_float_regex ) ).match( word ).hasMatch() ) + continue; + } + else if( token == "%i" ){ + if( QRegularExpression( QRegularExpression::anchoredPattern( c_int_regex ) ).match( word ).hasMatch() ) + continue; + } + else if( token == "%t" || token == "%p" ){ + continue; //any string is fine + } + else if( token.compare( word, Qt::CaseSensitivity::CaseInsensitive ) == 0 ){ + continue; + } + break; // no match + } + } + } + } + if( !list.isEmpty() ){ + if( list.size() > 3 ){ // try to find long enough common prefix to reduce typing + int len = list[0].length(); + for( int i = 0; i < list.size() && len > 0; ++i ){ + len = std::min( len, list[i].length() ); + for( int j = 0; j < len; ++j ){ + if( list[0][j].toLower() != list[i][j].toLower() ){ + len = j; + break; + } + } + } + const int postSlashId = line.last().lastIndexOf( '/' ) + 1; // -1 + 1 when not found + if( len >= line.last().length() - postSlashId + 2 ){ // two or more chars may be completed, cool + QString prefix( list[0].left( len ) ); + list.clear(); + list.push_back( prefix ); + } + } + auto *model = new QStringListModel( list, m_completer ); + m_completer->setModel( model ); + m_completer->popup()->setCurrentIndex( m_completer->completionModel()->index( 0, 0 ) ); + QRect cr = cursorRect(); + cr.setWidth( m_completer->popup()->sizeHintForColumn( 0 ) + m_completer->popup()->verticalScrollBar()->sizeHint().width() ); + m_completer->complete( cr ); + } + else{ + m_completer->popup()->hide(); + } + } + void autoCompleteInsert( const QString& str ){ + QTextCursor cursor = textCursor(); + QTextCursor cu = cursor; + cu.movePosition( QTextCursor::MoveOperation::PreviousCharacter, QTextCursor::MoveMode::KeepAnchor ); + if( !str.startsWith( ' ' ) && !cu.selectedText().back().isSpace() ) // completing current token: overwrite it + cursor.movePosition( QTextCursor::MoveOperation::StartOfWord, QTextCursor::MoveMode::KeepAnchor ); + + for( const auto& shader : c_shaderTemplates ){ + if( shader.name == str ){ + cursor.insertText( shader.text ); + return; + } + } + cursor.insertText( str ); + } +}; + +class TextEditor +{ + QWidget *m_window = 0; + QPlainTextEdit *m_textView; // slave, text widget from the gtk editor + QPushButton *m_button; // save button + CopiedString m_filename; + + void construct(){ + m_window = new QWidget( MainFrame_getWindow(), Qt::Dialog | Qt::WindowMinimizeButtonHint | Qt::WindowMaximizeButtonHint | Qt::WindowCloseButtonHint ); + g_guiSettings.addWindow( m_window, "ShaderEditor/geometry" ); + + auto *vbox = new QVBoxLayout( m_window ); + vbox->setContentsMargins( 0, 0, 0, 0 ); + + m_textView = new QPlainTextEdit_Shader; + vbox->addWidget( m_textView ); + + auto *hbox = new QHBoxLayout; + vbox->addLayout( hbox ); + hbox->setContentsMargins( 0, 0, 0, 0 ); + + m_button = new QPushButton( "Save" ); + m_button->setSizePolicy( QSizePolicy::Policy::Fixed, QSizePolicy::Policy::Fixed ); + hbox->addWidget( m_button, Qt::AlignmentFlag::AlignRight ); + + QObject::connect( m_textView->document(), &QTextDocument::modificationChanged, [this]( bool modified ){ + m_button->setEnabled( modified ); + + StringOutputStream str( 256 ); + str << ( modified? "*" : "" ) << m_filename; + m_window->setWindowTitle( str.c_str() ); + } ); + + QObject::connect( m_button, &QAbstractButton::clicked, [this](){ editor_save(); } ); + + { + QLabel *label = new QLabel; + // label->setOpenExternalLinks( true ); + hbox->addWidget( label ); + QObject::connect( label, &QLabel::linkActivated, []( const QString& link ){ +#ifdef WIN32 + // win prohibits opening html with #fragment param for security reasons, so workaround + const QString filename = QString( SettingsPath_get() ) + "urlopener.html"; + if( QFile file( filename ); file.open( QIODevice::WriteOnly | QIODevice::Text ) ){ + QTextStream out( &file ); + out << "" + "" + ""; + file.close(); + QDesktopServices::openUrl( filename ); + } +#else + QDesktopServices::openUrl( link ); +#endif + } ); + + const auto cb = [this, label](){ + if( const auto *data = m_textView->textCursor().block().userData() ){ + if( const auto *shaderFormat = static_cast( data )->shaderFormat ){ + QString page( shaderFormat->page ); + if( page.back() == '#' ){ // no explicit id, id = 1st word + const QRegularExpression regex( "\\w+" ); + const QString id = regex.match( shaderFormat->key ).captured(); + page += id; + } + else if( page.back() == '$' ){ // no explicit id, id = one of values + page.back() = '#'; + const QString txt = m_textView->textCursor().block().text(); + for( const auto value : shaderFormat->values ){ + if( txt.contains( QRegularExpression( QString( "\\b" ) + value + "\\b" ) ) ){ + page += value; + break; + } + } + } + label->setText( QString( "" + + page + "" ); + return; + } + } + label->clear(); + }; + QObject::connect( m_textView, &QPlainTextEdit::cursorPositionChanged, cb ); + QObject::connect( m_textView, &QPlainTextEdit::textChanged, cb ); + } + + auto *search = new QLineEdit_search( *m_textView ); + hbox->addWidget( search ); + } + void editor_save(){ + FILE *f = fopen( m_filename.c_str(), "wb" ); //write in binary mode to preserve line feeds + + if ( f == nullptr ) { + globalErrorStream() << "Error saving file" << makeQuoted( m_filename ) << '\n'; + return; + } + + const auto str = m_textView->toPlainText().toLatin1(); + fwrite( str.constData(), 1, str.length(), f ); + fclose( f ); + + m_textView->document()->setModified( false ); + } +public: + void DoGtkTextEditor( const char* text, const char* shaderName, const char* filename, const bool editable ){ + if ( !m_window ) { + construct(); // build it the first time we need it + } + + m_filename = filename; + m_textView->setReadOnly( !editable ); + m_textView->setPlainText( text ); + + m_window->show(); + m_window->raise(); + m_window->activateWindow(); + + { // scroll to shader + const QRegularExpression::PatternOptions rxFlags = QRegularExpression::PatternOption::MultilineOption | + QRegularExpression::PatternOption::CaseInsensitiveOption; + const QRegularExpression rx( "^\\s*" + QRegularExpression::escape( shaderName ) + "(|:q3map)$", rxFlags ); + auto *doc = m_textView->document(); + + for( QTextCursor cursor( doc ); cursor = doc->find( rx ), !cursor.isNull(); ) + if( !doc->find( QRegularExpression( "^\\s*\\{", rxFlags ), cursor ).isNull() ){ + QTextCursor cur( cursor ); + cur.movePosition( QTextCursor::MoveOperation::NextBlock, QTextCursor::MoveMode::MoveAnchor, 99 ); + m_textView->setTextCursor( cur ); + m_textView->setTextCursor( cursor ); + break; + } + } + } +}; + +static TextEditor g_textEditor; CopiedString g_TextEditor_editorCommand;