/* Copyright (c) 2001, Loki software, inc. All rights reserved. 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 Loki software 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 REGENTS 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. */ // // Some small dialogs that don't need much // // Leonardo Zide (leo@lokigames.com) // #include "gtkdlgs.h" #include "debugging/debugging.h" #include "version.h" #include "aboutmsg.h" #include "igl.h" #include "iscenegraph.h" #include "iselection.h" #include #include #include #include #include #include #include #include #include #include "gtkutil/spinbox.h" #include "gtkutil/guisettings.h" #include #include #include "os/path.h" #include "math/aabb.h" #include "container/array.h" #include "generic/static.h" #include "stream/stringstream.h" #include "gtkutil/messagebox.h" #include "gtkutil/image.h" #include "gtkmisc.h" #include "brushmanip.h" #include "build.h" #include "qe3.h" #include "texwindow.h" #include "xywindow.h" #include "mainframe.h" #include "preferences.h" #include "url.h" #include "commandlib.h" #include "qerplugin.h" #include "os/file.h" // ============================================================================= // Project settings dialog class GameComboConfiguration { public: const char* basegame_dir; const char* basegame; const char* known_dir; const char* known; const char* custom; GameComboConfiguration() : basegame_dir( g_pGameDescription->getRequiredKeyValue( "basegame" ) ), basegame( g_pGameDescription->getRequiredKeyValue( "basegamename" ) ), known_dir( g_pGameDescription->getKeyValue( "knowngame" ) ), known( g_pGameDescription->getKeyValue( "knowngamename" ) ), custom( g_pGameDescription->getRequiredKeyValue( "unknowngamename" ) ){ } }; typedef LazyStatic LazyStaticGameComboConfiguration; inline GameComboConfiguration& globalGameComboConfiguration(){ return LazyStaticGameComboConfiguration::instance(); } struct gamecombo_t { gamecombo_t( int _game, const char* _fs_game, bool _sensitive ) : game( _game ), fs_game( _fs_game ), sensitive( _sensitive ) {} int game; const char* fs_game; bool sensitive; }; gamecombo_t gamecombo_for_dir( const char* dir ){ if ( path_equal( dir, globalGameComboConfiguration().basegame_dir ) ) { return gamecombo_t( 0, dir, false ); } else if ( path_equal( dir, globalGameComboConfiguration().known_dir ) ) { return gamecombo_t( 1, dir, false ); } else { return gamecombo_t( string_empty( globalGameComboConfiguration().known_dir ) ? 1 : 2, dir, true ); } } gamecombo_t gamecombo_for_gamename( const char* gamename ){ if ( string_empty( gamename ) || string_equal( gamename, globalGameComboConfiguration().basegame ) ) { return gamecombo_t( 0, globalGameComboConfiguration().basegame_dir, false ); } else if ( string_equal( gamename, globalGameComboConfiguration().known ) ) { return gamecombo_t( 1, globalGameComboConfiguration().known_dir, false ); } else { return gamecombo_t( string_empty( globalGameComboConfiguration().known_dir ) ? 1 : 2, "", true ); } } class MappingMode { public: bool do_mapping_mode; const char* sp_mapping_mode; const char* mp_mapping_mode; MappingMode() : do_mapping_mode( !string_empty( g_pGameDescription->getKeyValue( "show_gamemode" ) ) ), sp_mapping_mode( "Single Player mapping mode" ), mp_mapping_mode( "Multiplayer mapping mode" ){ } }; typedef LazyStatic LazyStaticMappingMode; inline MappingMode& globalMappingMode(){ return LazyStaticMappingMode::instance(); } struct GameCombo { QComboBox* game_select{}; QComboBox* fsgame_entry{}; }; static GameCombo s_gameCombo; void GameModeImport( int value ){ gamemode_set( value == 0? "sp" : "mp" ); } typedef FreeCaller1 GameModeImportCaller; void GameModeExport( const IntImportCallback& importer ){ const char *gamemode = gamemode_get(); importer( ( string_empty( gamemode ) || string_equal( gamemode, "sp" ) )? 0 : 1 ); } typedef FreeCaller1 GameModeExportCaller; void FSGameImport( int value ){ } typedef FreeCaller1 FSGameImportCaller; void FSGameExport( const IntImportCallback& importer ){ } typedef FreeCaller1 FSGameExportCaller; void GameImport( int value ){ const auto dir = s_gameCombo.fsgame_entry->currentText().toLatin1(); const char* new_gamename = dir.isEmpty() ? globalGameComboConfiguration().basegame_dir : dir.constData(); if ( !path_equal( new_gamename, gamename_get() ) ) { if ( ConfirmModified( "Edit Project Settings" ) ) { ScopeDisableScreenUpdates disableScreenUpdates( "Processing...", "Changing Game Name" ); EnginePath_Unrealise(); gamename_set( new_gamename ); EnginePath_Realise(); } } } typedef FreeCaller1 GameImportCaller; void GameExport( const IntImportCallback& importer ){ const gamecombo_t gamecombo = gamecombo_for_dir( gamename_get() ); s_gameCombo.game_select->setCurrentIndex( gamecombo.game ); s_gameCombo.fsgame_entry->setEditText( gamecombo.fs_game ); s_gameCombo.fsgame_entry->setEnabled( gamecombo.sensitive ); } typedef FreeCaller1 GameExportCaller; void Game_constructPreferences( PreferencesPage& page ){ { s_gameCombo.game_select = page.appendCombo( "Select mod", StringArrayRange(), IntImportCallback( GameImportCaller() ), IntExportCallback( GameExportCaller() ) ); s_gameCombo.game_select->addItem( globalGameComboConfiguration().basegame ); if ( !string_empty( globalGameComboConfiguration().known ) ) s_gameCombo.game_select->addItem( globalGameComboConfiguration().known ); s_gameCombo.game_select->addItem( globalGameComboConfiguration().custom ); } { s_gameCombo.fsgame_entry = page.appendCombo( "fs_game", StringArrayRange(), IntImportCallback( FSGameImportCaller() ), IntExportCallback( FSGameExportCaller() ) ); s_gameCombo.fsgame_entry->setEditable( true ); std::error_code err; // use func version with error handling, since other throws error on non-existing directory for( const auto& entry : std::filesystem::directory_iterator( EnginePath_get(), std::filesystem::directory_options::skip_permission_denied, err ) ) if( entry.is_directory() ) s_gameCombo.fsgame_entry->addItem( entry.path().filename().string().c_str() ); } QObject::connect( s_gameCombo.game_select, &QComboBox::currentTextChanged, []( const QString& text ){ const gamecombo_t gamecombo = gamecombo_for_gamename( text.toLatin1().constData() ); s_gameCombo.fsgame_entry->setEditText( gamecombo.fs_game ); s_gameCombo.fsgame_entry->setEnabled( gamecombo.sensitive ); } ); if( globalMappingMode().do_mapping_mode ){ page.appendCombo( "Mapping mode", (const char*[]){ globalMappingMode().sp_mapping_mode, globalMappingMode().mp_mapping_mode }, IntImportCallback( GameModeImportCaller() ), IntExportCallback( GameModeExportCaller() ) ); } } // ============================================================================= // Arbitrary Sides dialog void DoSides( EBrushPrefab type ){ QDialog dialog( MainFrame_getWindow(), Qt::Dialog | Qt::WindowCloseButtonHint ); dialog.setWindowTitle( "Arbitrary sides" ); auto spin = new SpinBox; auto check = new QCheckBox( "Truncate" ); { auto form = new QFormLayout( &dialog ); form->setSizeConstraint( QLayout::SizeConstraint::SetFixedSize ); QLabel* label = new SpinBoxLabel( "Sides:", spin ); form->addRow( label, spin ); form->addWidget( check ); check->hide(); { switch ( type ) { case EBrushPrefab::Prism : case EBrushPrefab::Cone : spin->setValue( 8 ); spin->setRange( 3, 1022 ); break; case EBrushPrefab::Sphere : spin->setValue( 8 ); spin->setRange( 3, 31 ); break; case EBrushPrefab::Rock : spin->setValue( 32 ); spin->setRange( 10, 1000 ); break; case EBrushPrefab::Icosahedron : spin->setValue( 1 ); spin->setRange( 0, 2 ); //possible with 3, but buggy check->show(); label->setText( "Subdivisions:" ); break; default: break; } } { auto buttons = new QDialogButtonBox( QDialogButtonBox::StandardButton::Ok | QDialogButtonBox::StandardButton::Cancel ); form->addWidget( buttons ); QObject::connect( buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept ); QObject::connect( buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject ); } } if ( dialog.exec() ) { const int sides = spin->value(); const bool option = check->isChecked(); Scene_BrushConstructPrefab( GlobalSceneGraph(), type, sides, option, TextureBrowser_GetSelectedShader() ); } } // ============================================================================= // About dialog (no program is complete without one) void DoAbout(){ QDialog dialog( MainFrame_getWindow(), Qt::Dialog | Qt::WindowCloseButtonHint ); dialog.setWindowTitle( "About NetRadiant" ); { auto vbox = new QVBoxLayout( &dialog ); { auto hbox = new QHBoxLayout; vbox->addLayout( hbox ); { auto label = new QLabel; label->setPixmap( new_local_image( "logo.png" ) ); hbox->addWidget( label ); } { auto label = new QLabel( "NetRadiant " RADIANT_VERSION "\n" __DATE__ "\n\n" RADIANT_ABOUTMSG "\n\n" "By alientrap.org\n\n" "This program is free software\n" "licensed under the GNU GPL.\n" ); hbox->addWidget( label ); } { auto buttons = new QDialogButtonBox( QDialogButtonBox::StandardButton::Ok, Qt::Orientation::Vertical ); QObject::connect( buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept ); hbox->addWidget( buttons ); { auto button = buttons->addButton( "Credits", QDialogButtonBox::ButtonRole::NoRole ); QObject::connect( button, &QPushButton::clicked, [](){ OpenURL( StringStream( AppPath_get(), "credits.html" ) ); } ); button->setEnabled( false ); } { auto button = buttons->addButton( "Changelog", QDialogButtonBox::ButtonRole::NoRole ); QObject::connect( button, &QPushButton::clicked, [](){ OpenURL( StringStream( AppPath_get(), "changelog.txt" ) ); } ); button->setEnabled( false ); } { auto button = buttons->addButton( "About Qt", QDialogButtonBox::ButtonRole::NoRole ); QObject::connect( button, &QPushButton::clicked, &QApplication::aboutQt ); } } } { { auto frame = new QGroupBox( "OpenGL Properties" ); vbox->addWidget( frame ); { auto form = new QFormLayout( frame ); form->addRow( "Vendor:", new QLabel( reinterpret_cast( gl().glGetString( GL_VENDOR ) ) ) ); form->addRow( "Version:", new QLabel( reinterpret_cast( gl().glGetString( GL_VERSION ) ) ) ); form->addRow( "Renderer:", new QLabel( reinterpret_cast( gl().glGetString( GL_RENDERER ) ) ) ); } } { auto frame = new QGroupBox( "OpenGL Extensions" ); vbox->addWidget( frame ); { auto textView = new QPlainTextEdit( reinterpret_cast( gl().glGetString( GL_EXTENSIONS ) ) ); textView->setReadOnly( true ); auto box = new QVBoxLayout( frame ); box->addWidget( textView ); } } } } dialog.exec(); } // ============================================================================= // Light Intensity dialog static bool g_dontDoLightIntensityDlg = false; bool DoLightIntensityDlg( int *intensity ){ if( g_dontDoLightIntensityDlg ) return true; QDialog dialog( MainFrame_getWindow(), Qt::Dialog | Qt::WindowCloseButtonHint ); dialog.setWindowTitle( "Light intensity" ); auto spin = new SpinBox( -99999, 99999, *intensity ); auto check = new QCheckBox( "Don't Show" ); QObject::connect( check, &QCheckBox::toggled, []( bool checked ){ g_dontDoLightIntensityDlg = checked; } ); { auto form = new QFormLayout( &dialog ); form->setSizeConstraint( QLayout::SizeConstraint::SetFixedSize ); form->addRow( new QLabel( "ESC for default, ENTER to validate" ) ); form->addRow( new SpinBoxLabel( "Intensity:", spin ), spin ); form->addWidget( check ); { auto buttons = new QDialogButtonBox( QDialogButtonBox::StandardButton::Ok | QDialogButtonBox::StandardButton::Cancel ); form->addWidget( buttons ); QObject::connect( buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept ); QObject::connect( buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject ); } } if ( dialog.exec() ) { *intensity = spin->value(); return true; } else return false; } void DoShaderInfoDlg( const char* name, const char* filename, const char* title ){ const auto text = StringStream( "  The selected shader
" "", name, "
" "  is located in file
" "", filename, "" ); qt_MessageBox( MainFrame_getWindow(), text, title ); } // ============================================================================= // Install dev files dialog #include #include #include #include void DoInstallDevFilesDlg( const char *enginePath ){ std::vector files; // relative source files paths const auto sourceBase = std::filesystem::path( g_pGameDescription->mGameToolsPath.c_str() ) / "install/"; const auto targetBase = std::filesystem::path( enginePath ) / basegame_get(); QString description; { std::error_code err; std::filesystem::recursive_directory_iterator dirIter( sourceBase, err ); if( err ){ globalErrorStream() << err.message().c_str() << ' ' << sourceBase.string().c_str() << '\n'; return; } for( const auto& dirEntry : dirIter ) { if( err ){ globalErrorStream() << err.message().c_str() << '\n'; break; } if( dirEntry.is_regular_file( err ) && !err ){ if( dirIter.depth() == 0 && dirEntry.path().filename() == ".description" ){ if( QFile f( QString::fromStdString( dirEntry.path().string() ) ); f.open( QIODevice::ReadOnly | QIODevice::Text ) ) description = QTextStream( &f ).readAll(); } else{ files.push_back( std::filesystem::relative( dirEntry.path(), sourceBase, err ) ); } } } } if( !files.empty() ){ QDialog dialog( nullptr, Qt::Window ); dialog.setWindowTitle( "Install Map Developer's Files" ); { auto *box = new QVBoxLayout( &dialog ); { auto *label = new QLabel( "Would you like to install following files recommended for fluent map development\nto " + QString::fromStdString( targetBase.string() ) + "?" ); label->setAlignment( Qt::AlignmentFlag::AlignHCenter ); box->addWidget( label ); } QListWidget *listWidget; { listWidget = new QListWidget; listWidget->setSelectionMode( QAbstractItemView::SelectionMode::NoSelection ); box->addWidget( listWidget, 0 ); for( const auto& file : files ){ listWidget->addItem( QString::fromStdString( file.string() ) ); } } if( !description.isEmpty() ){ box->addWidget( new QLabel( ".description" ) ); auto *text = new QPlainTextEdit( description ); text->setSizePolicy( QSizePolicy::Policy::MinimumExpanding, QSizePolicy::Policy::MinimumExpanding ); text->setLineWrapMode( QPlainTextEdit::LineWrapMode::NoWrap ); text->setReadOnly( true ); // set minimal size to fit text to avoid the need to resize window/scroll const auto rect = text->fontMetrics().boundingRect( QRect(), 0, description ); text->setMinimumSize( rect.width() + text->contentsMargins().left() + text->contentsMargins().right() + text->document()->documentMargin() * 2 + text->verticalScrollBar()->sizeHint().width(), rect.height() + text->contentsMargins().top() + text->contentsMargins().bottom() + text->document()->documentMargin() * 2 + text->horizontalScrollBar()->sizeHint().height() ); box->addWidget( text, 0 ); } const auto doCopy = [&](){ QMessageBox::StandardButton overwrite = QMessageBox::StandardButton::Yes; size_t copiedN = 0; for( size_t i = 0; i < files.size(); ++i ){ const auto source = sourceBase / files[i]; const auto target = targetBase / files[i]; std::error_code err; if( ( std::filesystem::exists( target, err ) || err ) && overwrite != QMessageBox::StandardButton::YesToAll ){ if( overwrite == QMessageBox::StandardButton::NoToAll ) continue; overwrite = (QMessageBox::StandardButton)QMessageBox( QMessageBox::Icon::Question, "File exists", QString( "File \"" ) + QString::fromStdString( target.string() ) + "\" exists.\nOverwrite it?", QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::YesToAll | QMessageBox::StandardButton::No | QMessageBox::StandardButton::NoToAll | QMessageBox::StandardButton::Abort, &dialog ).exec(); if( overwrite == QMessageBox::StandardButton::Abort ) break; if( overwrite == QMessageBox::StandardButton::NoToAll || overwrite == QMessageBox::StandardButton::No ) continue; } const auto copy_file = [&](){ if( std::filesystem::exists( target, err ) ){ if( !std::filesystem::remove( target, err ) ){ return false; } } else if( err ){ return false; } std::filesystem::create_directories( target.parent_path(), err ); if( err ) return false; // std::filesystem::copy_options::overwrite_existing is broken in libstdc++ on windows, thus using std::filesystem::remove return std::filesystem::copy_file( source, target, std::filesystem::copy_options::none, err ); }; retry: if( !copy_file() ){ const auto ret = (QMessageBox::StandardButton)QMessageBox( QMessageBox::Icon::Question, "Fail", "Failed to write \"" + QString::fromStdString( target.string() ) + "\"\n" + err.message().c_str(), QMessageBox::StandardButton::Retry | QMessageBox::StandardButton::Ignore | QMessageBox::StandardButton::Abort, &dialog ).exec(); if( ret == QMessageBox::StandardButton::Retry ) goto retry; if( ret == QMessageBox::StandardButton::Ignore ) continue; if( ret == QMessageBox::StandardButton::Abort ) break; } auto *item = listWidget->item( i ); item->setCheckState( Qt::CheckState::Checked ); listWidget->scrollToItem( item ); QCoreApplication::processEvents( QEventLoop::ProcessEventsFlag::ExcludeUserInputEvents ); ++copiedN; } if( copiedN == files.size() ) qt_MessageBox( &dialog, "All files have been copied.", "Great Success!" ); else if( copiedN != 0 ) qt_MessageBox( &dialog, StringStream<64>( copiedN, '/', files.size(), " files have been copied." ), "Moderate Success!" ); else qt_MessageBox( &dialog, "No files have been copied.", "Boo!" ); dialog.accept(); }; { auto *buttons = new QDialogButtonBox( QDialogButtonBox::StandardButton::Ok | QDialogButtonBox::StandardButton::Cancel ); box->addWidget( buttons ); QObject::connect( buttons, &QDialogButtonBox::accepted, doCopy ); QObject::connect( buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject ); } } dialog.exec(); } } // ============================================================================= // 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 display line numbers, exremely useful for error messages handling */ #include #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_colorForeground( Qt::white ); static const QColor c_colorBackground( 46, 52, 54 ); 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 %f ) ( %f %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", "shift", "offset", } }, { "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 ){ setPlaceholderText( QString::fromUtf8( u8"🔍" ) ); 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 LineNumberArea : public QWidget { QPlainTextEdit *m_textEdit; const Callback1 m_paintCallback; public: LineNumberArea( QPlainTextEdit *textEdit, const decltype( m_paintCallback )& paintCallback ) : QWidget( textEdit ), m_textEdit( textEdit ), m_paintCallback( paintCallback ){} QSize sizeHint() const override { return QSize( lineNumberAreaWidth(), 0 ); } int lineNumberAreaWidth() const { const int digits = 1 + std::log10( std::max( 1, m_textEdit->blockCount() ) ); return 3 + 10 + m_textEdit->fontMetrics().horizontalAdvance( QLatin1Char('9') ) * digits; } void updateLineNumberArea( const QRect &rect, int dy ){ if( dy ) scroll( 0, dy ); else update( 0, rect.y(), width(), rect.height() ); } protected: void paintEvent( QPaintEvent *event ) override { m_paintCallback( event ); } }; class QPlainTextEdit_Shader : public QPlainTextEdit { QCompleter *m_completer; TexTree m_texTree; LineNumberArea *m_lineNumberArea; 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() ); m_lineNumberArea = new LineNumberArea( this, MemberCaller1( *this ) ); QObject::connect( this, &QPlainTextEdit::blockCountChanged, [this]( int newBlockCount ){ updateLineNumberAreaWidth(); } ); QObject::connect( this, &QPlainTextEdit::updateRequest, [this]( const QRect &rect, int dy ){ m_lineNumberArea->updateLineNumberArea( rect, dy ); if( rect.contains( viewport()->rect() ) ) updateLineNumberAreaWidth(); } ); updateLineNumberAreaWidth(); // force back/foreground colors to not be ruined by global theme QPalette pal = palette(); pal.setColor( QPalette::Base, c_colorBackground ); pal.setColor( QPalette::Text, c_colorForeground ); setPalette( pal ); } void lineNumberAreaPaintEvent( QPaintEvent *event ){ QPainter painter( m_lineNumberArea ); painter.setFont( font() ); painter.setPen( Qt::darkGray ); painter.fillRect( event->rect(), c_colorBackground.lighter( 128 ) ); QTextBlock block = firstVisibleBlock(); int blockNumber = block.blockNumber(); int top = qRound( blockBoundingGeometry( block ).translated( contentOffset() ).top() ); int bottom = top + qRound( blockBoundingRect( block ).height() ); while( block.isValid() && top <= event->rect().bottom() ){ if( block.isVisible() && bottom >= event->rect().top() ){ painter.drawText( 0, top, m_lineNumberArea->width() - 10, fontMetrics().height(), Qt::AlignRight, QString::number( blockNumber + 1 ) ); } block = block.next(); top = bottom; bottom = top + qRound( blockBoundingRect( block ).height() ); ++blockNumber; } } 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); } void paintEvent( QPaintEvent* pEvent ) override { static QRect rect; static int block; int newblock = textCursor().blockNumber(); QRect newrect = cursorRect(); newrect.setLeft( -1 ); newrect.setRight( width() - 1 ); if( rect != newrect || block != newblock ){ QRegion region( newrect + QMargins( 0, 0, 0, 1 ) ); // expand invalidated area, bottom line appears drawn 1px lower // this may differ from static rect, if e.g. scrolled, thus reevaluate QRect oldr = cursorRect( QTextCursor( document()->findBlockByNumber( block ) ) ); oldr.setLeft( -1 ); oldr.setRight( width() - 1 ); region += oldr + QMargins( 0, 0, 0, 1 ); rect = newrect; block = newblock; viewport()->update( region ); } else{ // highlight current line QPainter painter( viewport() ); painter.setPen( c_colorBackground.lighter( 150 ) ); painter.drawRect( rect ); } QPlainTextEdit::paintEvent( pEvent ); } void resizeEvent( QResizeEvent *e ) override { QPlainTextEdit::resizeEvent( e ); const QRect cr = contentsRect(); m_lineNumberArea->setGeometry( QRect( cr.left(), cr.top(), m_lineNumberArea->lineNumberAreaWidth(), cr.height() ) ); } void updateLineNumberAreaWidth(){ setViewportMargins( m_lineNumberArea->lineNumberAreaWidth(), 0, 0, 0 ); } private: void texTree_construct(){ class LoadTexturesByTypeVisitor : public ImageModules::Visitor { const char* m_dirstring; TexTree& m_texTree; mutable StringOutputStream m_stringStream; public: void insert( const char *name ) const { m_texTree.insert( m_stringStream( m_dirstring, PathExtensionless( name ) ) ); } typedef ConstMemberCaller1 InsertCaller; LoadTexturesByTypeVisitor( const char* dirstring, TexTree& texTree ) : m_dirstring( dirstring ), m_texTree( texTree ), m_stringStream( 64 ) {} 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 : public QObject { 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" ); m_window->installEventFilter( this ); 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( 4, 0, 4, 4 ); 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 ); m_window->setWindowTitle( StringStream( ( modified? "*" : "" ), m_filename ).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( QUrl::fromUserInput( filename ) ); } #else QDesktopServices::openUrl( QUrl::fromUserInput( 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 ); } // returns true, if document modifications got saved or user decided to discard them bool ensure_saved(){ if( m_textView->document()->isModified() ) { const auto ret = qt_MessageBox( m_window, "Document has been modified.\nSave it?", "Save", EMessageBoxType::Question, EMessageBoxReturn::eIDYES | EMessageBoxReturn::eIDNO | EMessageBoxReturn::eIDCANCEL ); if( ret == EMessageBoxReturn::eIDYES ){ editor_save(); } if( ret == EMessageBoxReturn::eIDNO ){ // discard changes m_textView->clear(); // unset isModified flag this way to avoid messagebox on next opening } else if( ret == EMessageBoxReturn::eIDCANCEL ){ return false; } } return true; } 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 } if( !ensure_saved() ) return; 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; } } } protected: bool eventFilter( QObject *obj, QEvent *event ) override { if( event->type() == QEvent::Close ) { if( !ensure_saved() ){ // keep editor opened event->ignore(); return true; } } return QObject::eventFilter( obj, event ); // standard event processing } }; static TextEditor g_textEditor; CopiedString g_TextEditor_editorCommand; #include "ifilesystem.h" #include "iarchive.h" #include "idatastream.h" void DoShaderView( const char *shaderFileName, const char *shaderName, bool external_editor ){ const char* pathRoot = GlobalFileSystem().findFile( shaderFileName ); const bool pathEmpty = string_empty( pathRoot ); const bool pathIsDir = !pathEmpty && file_is_directory( pathRoot ); const auto pathFull = StringStream( pathRoot, ( pathIsDir? "" : "::" ), shaderFileName ); if( pathEmpty ){ globalErrorStream() << "Failed to load shader file " << shaderFileName << '\n'; } else if( external_editor && pathIsDir ){ if( g_TextEditor_editorCommand.empty() ){ #ifdef WIN32 ShellExecute( (HWND)MainFrame_getWindow()->effectiveWinId(), 0, pathFull.c_str(), 0, 0, SW_SHOWNORMAL ); #else globalWarningStream() << "Failed to open '" << pathFull << "'\nSet Shader Editor Command in preferences\n"; #endif } else{ auto command = StringStream( g_TextEditor_editorCommand, ' ', makeQuoted( pathFull ) ); globalOutputStream() << "Launching: " << command << '\n'; // note: linux does not return false if the command failed so it will assume success if ( !Q_Exec( 0, command.c_str(), 0, true, false ) ) globalErrorStream() << "Failed to execute " << command << '\n'; } } else if( ArchiveFile* file = GlobalFileSystem().openFile( shaderFileName ) ){ const std::size_t size = file->size(); char* text = ( char* )malloc( size + 1 ); file->getInputStream().read( ( InputStream::byte_type* )text, size ); text[size] = 0; file->release(); g_textEditor.DoGtkTextEditor( text, shaderName, pathFull, pathIsDir ); free( text ); } }