diff --git a/Makefile b/Makefile index f3d2fe79..8719e9c4 100644 --- a/Makefile +++ b/Makefile @@ -882,6 +882,7 @@ $(INSTALLDIR)/radiant.$(EXE): \ radiant/gtkmisc.o \ radiant/help.o \ radiant/image.o \ + radiant/layerswindow.o \ radiant/mainframe.o \ radiant/main.o \ radiant/map.o \ diff --git a/docs/changelog-custom.txt b/docs/changelog-custom.txt index 42ac2b5b..527373c7 100644 --- a/docs/changelog-custom.txt +++ b/docs/changelog-custom.txt @@ -369,6 +369,9 @@ menus... * Modify->Repeat: saved transforms feedback and individual resets * Modify->Repeat->Reset Transforms (alt + r) * Entity->Reload Entity Definitions + * View->Layers Browser (L) + * Edit->Expand Selection To Layers (ctrl + q) + * shift + q: move-to LayersMenu misc... * removed note to save before going to preferences, project settings @@ -1133,6 +1136,7 @@ misc... * build menu.customize: drag and drop items; ctrl + drag = copy * build menu.customize: remove command line item when set empty * build menu.customize.build variables: are editable + * layers system (stored in comments of .map file) diff --git a/include/iscenegraph.h b/include/iscenegraph.h index 55ea2409..b33cc8b6 100644 --- a/include/iscenegraph.h +++ b/include/iscenegraph.h @@ -55,6 +55,8 @@ const TypeId NODETYPEID_NONE = NODETYPEID_MAX; const TypeId INSTANCETYPEID_MAX = 64; const TypeId INSTANCETYPEID_NONE = INSTANCETYPEID_MAX; +class Layer; + namespace scene { /// \brief A unique key to an instance of a node in the scene-graph. @@ -88,6 +90,8 @@ public: virtual void insert_root( Node& root ) = 0; /// \brief Clears the root-node of the graph. virtual void erase_root() = 0; + /// \brief Returns current layer or 0 when no root is inserted. + virtual Layer* currentLayer() = 0; /// \brief Traverses all nodes in the graph depth-first, starting from the root node. virtual void traverse( const Walker& walker ) = 0; /// \brief Traverses all nodes in the graph depth-first, starting from 'start'. diff --git a/include/iscriplib.h b/include/iscriplib.h index 7ac6b6b8..23f6a7d2 100644 --- a/include/iscriplib.h +++ b/include/iscriplib.h @@ -63,6 +63,7 @@ struct _QERScripLibTable STRING_CONSTANT( Name, "scriptlib" ); Tokeniser& ( *m_pfnNewScriptTokeniser )( TextInputStream & istream ); + Tokeniser& ( *m_pfnNewMapTokeniser )( TextInputStream & istream ); Tokeniser& ( *m_pfnNewSimpleTokeniser )( TextInputStream & istream ); TokenWriter& ( *m_pfnNewSimpleTokenWriter )( TextOutputStream & ostream ); }; diff --git a/libs/layers.h b/libs/layers.h new file mode 100644 index 00000000..78d8198b --- /dev/null +++ b/libs/layers.h @@ -0,0 +1,86 @@ +/* + Copyright (C) 2001-2006, William Joseph. + All Rights Reserved. + + This file is part of GtkRadiant. + + GtkRadiant is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + GtkRadiant is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with GtkRadiant; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#pragma once + +#include "scenelib.h" +#include "string/string.h" +#include + + +#define LAYERIDX0 0 +#define LAYERIDXPARENT -1 + +template +concept LayerFunctor = std::invocable && std::is_void_v>; + +class Layer +{ +public: + CopiedString m_name; + std::list m_children; + Layer *m_parent; // must be valid besides 0 in the root 'Layers' container + int m_color[ 3 ]{}; // 0 0 0 = default gui color + int m_parentIndex = LAYERIDXPARENT; // parent index during map loading // -1 parent = root + int m_ownIndex = LAYERIDXPARENT; // own index for map writing and operator== set via update_ownIndices() + Layer( const char *name, Layer *parent ) : m_name( name ), m_parent( parent ){ + } + Layer( Layer&& ) noexcept = default; // no copy + bool operator==( const Layer& other ) const { + return m_name == other.m_name + && m_parent->m_ownIndex == const_cast( other.m_parent )->m_ownIndex + && !memcmp( m_color, other.m_color, sizeof( m_color ) ) + && m_children == other.m_children; + } +private: + void forEach( std::list& layers, LayerFunctor auto&& functor ){ + for( Layer& layer : layers ){ + functor( layer ); + forEach( layer.m_children, functor ); + } + } +public: + void forEach( LayerFunctor auto&& functor ){ + forEach( m_children, functor ); + } + + using iterator = decltype( m_children )::iterator; +}; + +class Layers : public Layer +{ +public: + STRING_CONSTANT( Name, "Layers" ); + Layer *m_currentLayer; + Layers() : Layer( "", nullptr ){ + m_children.emplace_back( "0", this ); // add default layer + m_currentLayer = &m_children.front(); + } + Layers( Layers&& ) noexcept = default; // no copy + + void update_ownIndices(){ + forEach( [i = 0]( Layer& layer ) mutable { layer.m_ownIndex = i++; } ); + } +}; + +inline Layers* Node_getLayers( scene::Node& node ){ + return NodeTypeCast::cast( node ); +} diff --git a/libs/maplib.h b/libs/maplib.h index 32cd5503..45352c64 100644 --- a/libs/maplib.h +++ b/libs/maplib.h @@ -23,6 +23,7 @@ #include "nameable.h" #include "mapfile.h" +#include "layers.h" #include "traverselib.h" #include "transformlib.h" @@ -136,6 +137,7 @@ class MapRoot : public scene::Node::Symbiot, public scene::Instantiable, public NodeContainedCast::install( m_casts ); NodeContainedCast::install( m_casts ); NodeContainedCast::install( m_casts ); + NodeContainedCast::install( m_casts ); } NodeTypeCastTable& get(){ return m_casts; @@ -149,6 +151,7 @@ class MapRoot : public scene::Node::Symbiot, public scene::Instantiable, public typedef SelectableInstance Instance; NameableString m_name; UndoFileChangeTracker m_changeTracker; + Layers m_layers; public: typedef LazyStatic StaticTypeCasts; @@ -164,8 +167,11 @@ public: MapFile& get( NullType){ return m_changeTracker; } + Layers& get( NullType){ + return m_layers; + } - MapRoot( const char* name ) : m_node( this, this, StaticTypeCasts::instance().get() ), m_name( name ){ + MapRoot( const char* name ) : m_node( this, this, StaticTypeCasts::instance().get(), nullptr ), m_name( name ){ m_node.m_isRoot = true; m_traverse.attach( this ); @@ -174,6 +180,7 @@ public: } ~MapRoot(){ } + MapRoot( MapRoot&& ) noexcept = default; // no copy: Layers use m_parent pointer void release(){ GlobalUndoSystem().trackerDetach( m_changeTracker ); @@ -203,9 +210,9 @@ public: m_instances.erase( child ); } - scene::Node& clone() const { - return ( new MapRoot( *this ) )->node(); - } + // scene::Node& clone() const { + // return ( new MapRoot( *this ) )->node(); + // } scene::Instance* create( const scene::Path& path, scene::Instance* parent ){ return new Instance( path, parent ); diff --git a/libs/scenelib.h b/libs/scenelib.h index 17cb8b5c..1145eb28 100644 --- a/libs/scenelib.h +++ b/libs/scenelib.h @@ -146,6 +146,8 @@ class NodeIdentityCast : { }; +class Layer; + namespace scene { class Node @@ -156,6 +158,7 @@ public: eHidden = 1 << 0, eFiltered = 1 << 1, eExcluded = 1 << 2, + eLayerHidden = 1 << 3, }; class Symbiot @@ -175,18 +178,20 @@ private: public: bool m_isRoot; + Layer *m_layer; // nullptr for group entity node; they are not finely manageable and affect children visibility bool isRoot(){ return m_isRoot; } - Node( Symbiot* symbiot, void* node, NodeTypeCastTable& casts ) : + Node( Symbiot* symbiot, void* node, NodeTypeCastTable& casts, Layer *layer ) : m_state( eVisible ), m_refcount( 0 ), m_symbiot( symbiot ), m_node( node ), m_casts( casts ), - m_isRoot( false ){ + m_isRoot( false ), + m_layer( layer ){ } ~Node(){ } @@ -238,7 +243,7 @@ class NullNode : public Node::Symbiot NodeTypeCastTable m_casts; Node m_node; public: - NullNode() : m_node( this, 0, m_casts ){ + NullNode() : m_node( this, 0, m_casts, nullptr ){ } void release(){ delete this; @@ -921,6 +926,47 @@ inline bool Entity_isSelected( scene::Instance& entity ){ } +template +bool Traversable_all_of_children( scene::Traversable* traversable, const Check&& check ){ + class Check_all : public scene::Traversable::Walker + { + const Check m_check; + public: + mutable bool m_all = true; // true for empty container + Check_all( Check check ) : m_check( check ){ + } + bool pre( scene::Node& node ) const override { + if( m_all && !m_check( node ) ) + m_all = false; + return m_all; + } + } check_all( check ); + + traversable->traverse( check_all ); + return check_all.m_all; +} + +template +bool Traversable_any_of_children( scene::Traversable* traversable, const Check&& check ){ + class Check_all : public scene::Traversable::Walker + { + const Check m_check; + public: + mutable bool m_any = false; // false for empty container + Check_all( Check check ) : m_check( check ){ + } + bool pre( scene::Node& node ) const override { + if( !m_any && m_check( node ) ) + m_any = true; + return !m_any; + } + } check_all( check ); + + traversable->traverse( check_all ); + return check_all.m_any; +} + + class InstanceCounter { diff --git a/libs/script/scripttokeniser.h b/libs/script/scripttokeniser.h index 4b885c9e..b01cee77 100644 --- a/libs/script/scripttokeniser.h +++ b/libs/script/scripttokeniser.h @@ -53,7 +53,10 @@ class ScriptTokeniser final : public Tokeniser bool m_unget; bool m_emit; - bool m_special; + const bool m_special; + const bool m_specialComments; + const char m_specialCommentSig[4] = "@$&"; + const char *m_specialCommentRead; CharType charType( const char c ){ switch ( c ) @@ -144,7 +147,8 @@ class ScriptTokeniser final : public Tokeniser m_emit = true; // emit token break; case eCharSolidus: -#if 0 //SPoG: ignore comments in the middle of tokens. +#define MID_TOKEN_COMMENTS 0 +#if MID_TOKEN_COMMENTS //SPoG: ignore comments in the middle of tokens. push( Tokenise( &ScriptTokeniser::tokeniseSolidus ) ); break; #endif @@ -200,7 +204,12 @@ class ScriptTokeniser final : public Tokeniser break; case eCharSolidus: pop(); - push( Tokenise( &ScriptTokeniser::tokeniseComment ) ); + if( m_specialComments ){ + push( Tokenise( &ScriptTokeniser::tokeniseCommentSpecialSig ) ); + m_specialCommentRead = m_specialCommentSig; + } + else + push( Tokenise( &ScriptTokeniser::tokeniseComment ) ); break; // dont emit single slash case eCharStar: pop(); @@ -214,13 +223,25 @@ class ScriptTokeniser final : public Tokeniser bool tokeniseComment( char c ){ if ( c == '\n' ) { pop(); +#if MID_TOKEN_COMMENTS if ( state() == Tokenise( &ScriptTokeniser::tokeniseToken ) ) { pop(); m_emit = true; // emit token immediately preceding comment } +#endif } return true; } + bool tokeniseCommentSpecialSig( char c ){ // test for '//@$&' signature + if( *m_specialCommentRead++ == c ){ + if( *m_specialCommentRead == '\0' ) // match, do tokeniseDefault + pop(); // note no MID_TOKEN_COMMENTS support + return true; + } + pop(); + push( Tokenise( &ScriptTokeniser::tokeniseComment ) ); + return tokeniseComment( c ); + } bool tokeniseBlockComment( char c ){ if ( c == '*' ) { pop(); @@ -233,10 +254,12 @@ class ScriptTokeniser final : public Tokeniser { case '/': pop(); +#if MID_TOKEN_COMMENTS if ( state() == Tokenise( &ScriptTokeniser::tokeniseToken ) ) { pop(); m_emit = true; // emit token immediately preceding comment } +#endif break; // dont emit comment case '*': break; // no state change @@ -303,7 +326,7 @@ class ScriptTokeniser final : public Tokeniser } public: - ScriptTokeniser( TextInputStream& istream, bool special ) : + ScriptTokeniser( TextInputStream& istream, bool special, bool specialComments ) : m_state( m_stack ), m_istream( istream ), m_scriptline( 1 ), @@ -311,7 +334,8 @@ public: m_crossline( false ), m_unget( false ), m_emit( false ), - m_special( special ){ + m_special( special ), + m_specialComments( specialComments ){ m_stack[0] = Tokenise( &ScriptTokeniser::tokeniseDefault ); m_eof = !m_istream.readChar( m_current ); m_token[MAXTOKEN - 1] = '\0'; @@ -347,9 +371,13 @@ public: inline Tokeniser& NewScriptTokeniser( TextInputStream& istream ){ - return *( new ScriptTokeniser( istream, true ) ); + return *( new ScriptTokeniser( istream, true, false ) ); +} + +inline Tokeniser& NewMapTokeniser( TextInputStream& istream ){ + return *( new ScriptTokeniser( istream, false, true ) ); } inline Tokeniser& NewSimpleTokeniser( TextInputStream& istream ){ - return *( new ScriptTokeniser( istream, false ) ); + return *( new ScriptTokeniser( istream, false, false ) ); } diff --git a/libs/stringio.h b/libs/stringio.h index 7e110024..b26c5e0a 100644 --- a/libs/stringio.h +++ b/libs/stringio.h @@ -287,6 +287,21 @@ inline bool Tokeniser_nextTokenIsDigit( Tokeniser& tokeniser ){ return std::isdigit( c ) != 0; } +inline bool Tokeniser_inlineTokenAvailable( Tokeniser& tokeniser ){ + const size_t line = tokeniser.getLine(); + if ( tokeniser.getToken() ) { + tokeniser.ungetToken(); + return line == tokeniser.getLine(); + } + return false; +} + +inline void Tokeniser_skipToNextLine( Tokeniser& tokeniser ){ + const size_t line = tokeniser.getLine(); + while ( tokeniser.getToken() && line == tokeniser.getLine() ) {} + tokeniser.ungetToken(); +} + diff --git a/plugins/assmodel/model.cpp b/plugins/assmodel/model.cpp index b249077d..c74b6559 100644 --- a/plugins/assmodel/model.cpp +++ b/plugins/assmodel/model.cpp @@ -634,9 +634,9 @@ class PicoModelNode : public scene::Node::Symbiot, public scene::Instantiable public: typedef LazyStatic StaticTypeCasts; - PicoModelNode() : m_node( this, this, StaticTypeCasts::instance().get() ){ + PicoModelNode() : m_node( this, this, StaticTypeCasts::instance().get(), nullptr ){ } - PicoModelNode( const AssScene scene ) : m_node( this, this, StaticTypeCasts::instance().get() ), m_picomodel( scene ){ + PicoModelNode( const AssScene scene ) : m_node( this, this, StaticTypeCasts::instance().get(), nullptr ), m_picomodel( scene ){ } void release(){ diff --git a/plugins/entity/doom3group.cpp b/plugins/entity/doom3group.cpp index bc75c95a..4677ec38 100644 --- a/plugins/entity/doom3group.cpp +++ b/plugins/entity/doom3group.cpp @@ -657,7 +657,7 @@ public: } Doom3GroupNode( EntityClass* eclass ) : - m_node( this, this, StaticTypeCasts::instance().get() ), + m_node( this, this, StaticTypeCasts::instance().get(), nullptr ), m_contained( eclass, m_node, InstanceSet::TransformChangedCaller( m_instances ), InstanceSet::BoundsChangedCaller( m_instances ), InstanceSetEvaluateTransform::Caller( m_instances ) ){ construct(); } @@ -666,7 +666,7 @@ public: scene::Instantiable( other ), scene::Cloneable( other ), scene::Traversable::Observer( other ), - m_node( this, this, StaticTypeCasts::instance().get() ), + m_node( this, this, StaticTypeCasts::instance().get(), nullptr ), m_contained( other.m_contained, m_node, InstanceSet::TransformChangedCaller( m_instances ), InstanceSet::BoundsChangedCaller( m_instances ), InstanceSetEvaluateTransform::Caller( m_instances ) ){ construct(); } diff --git a/plugins/entity/eclassmodel.cpp b/plugins/entity/eclassmodel.cpp index 00dc17c5..dcf697ee 100644 --- a/plugins/entity/eclassmodel.cpp +++ b/plugins/entity/eclassmodel.cpp @@ -437,7 +437,7 @@ public: } EclassModelNode( EntityClass* eclass ) : - m_node( this, this, StaticTypeCasts::instance().get() ), + m_node( this, this, StaticTypeCasts::instance().get(), GlobalSceneGraph().currentLayer() ), m_contained( eclass, m_node, InstanceSet::TransformChangedCaller( m_instances ), InstanceSetEvaluateTransform::Caller( m_instances ) ){ construct(); } @@ -446,7 +446,7 @@ public: scene::Instantiable( other ), scene::Cloneable( other ), scene::Traversable::Observer( other ), - m_node( this, this, StaticTypeCasts::instance().get() ), + m_node( this, this, StaticTypeCasts::instance().get(), other.m_node.m_layer ), m_contained( other.m_contained, m_node, InstanceSet::TransformChangedCaller( m_instances ), InstanceSetEvaluateTransform::Caller( m_instances ) ){ construct(); } diff --git a/plugins/entity/generic.cpp b/plugins/entity/generic.cpp index ee9df385..83a1ccbc 100644 --- a/plugins/entity/generic.cpp +++ b/plugins/entity/generic.cpp @@ -388,14 +388,14 @@ public: } GenericEntityNode( EntityClass* eclass ) : - m_node( this, this, StaticTypeCasts::instance().get() ), + m_node( this, this, StaticTypeCasts::instance().get(), GlobalSceneGraph().currentLayer() ), m_contained( eclass, m_node, InstanceSet::TransformChangedCaller( m_instances ), InstanceSetEvaluateTransform::Caller( m_instances ) ){ } GenericEntityNode( const GenericEntityNode& other ) : scene::Node::Symbiot( other ), scene::Instantiable( other ), scene::Cloneable( other ), - m_node( this, this, StaticTypeCasts::instance().get() ), + m_node( this, this, StaticTypeCasts::instance().get(), other.m_node.m_layer ), m_contained( other.m_contained, m_node, InstanceSet::TransformChangedCaller( m_instances ), InstanceSetEvaluateTransform::Caller( m_instances ) ){ } void release(){ diff --git a/plugins/entity/group.cpp b/plugins/entity/group.cpp index fe20fd44..0e5aff4f 100644 --- a/plugins/entity/group.cpp +++ b/plugins/entity/group.cpp @@ -445,7 +445,7 @@ public: } GroupNode( EntityClass* eclass ) : - m_node( this, this, StaticTypeCasts::instance().get() ), + m_node( this, this, StaticTypeCasts::instance().get(), nullptr ), m_contained( eclass, m_node, InstanceSet::TransformChangedCaller( m_instances ), InstanceSetEvaluateTransform::Caller( m_instances ) ){ construct(); } @@ -454,7 +454,7 @@ public: scene::Instantiable( other ), scene::Cloneable( other ), scene::Traversable::Observer( other ), - m_node( this, this, StaticTypeCasts::instance().get() ), + m_node( this, this, StaticTypeCasts::instance().get(), nullptr ), m_contained( other.m_contained, m_node, InstanceSet::TransformChangedCaller( m_instances ), InstanceSetEvaluateTransform::Caller( m_instances ) ){ construct(); } diff --git a/plugins/entity/light.cpp b/plugins/entity/light.cpp index ba534f60..f7d94a25 100644 --- a/plugins/entity/light.cpp +++ b/plugins/entity/light.cpp @@ -2044,7 +2044,7 @@ public: } LightNode( EntityClass* eclass ) : - m_node( this, this, StaticTypeCasts::instance().get() ), + m_node( this, this, StaticTypeCasts::instance().get(), GlobalSceneGraph().currentLayer() ), m_contained( eclass, m_node, InstanceSet::TransformChangedCaller( m_instances ), InstanceSet::BoundsChangedCaller( m_instances ), InstanceSetEvaluateTransform::Caller( m_instances ) ){ construct(); } @@ -2053,7 +2053,7 @@ public: scene::Instantiable( other ), scene::Cloneable( other ), scene::Traversable::Observer( other ), - m_node( this, this, StaticTypeCasts::instance().get() ), + m_node( this, this, StaticTypeCasts::instance().get(), other.m_node.m_layer ), m_contained( other.m_contained, m_node, InstanceSet::TransformChangedCaller( m_instances ), InstanceSet::BoundsChangedCaller( m_instances ), InstanceSetEvaluateTransform::Caller( m_instances ) ){ construct(); } diff --git a/plugins/entity/miscmodel.cpp b/plugins/entity/miscmodel.cpp index a17f95c3..610bc108 100644 --- a/plugins/entity/miscmodel.cpp +++ b/plugins/entity/miscmodel.cpp @@ -491,7 +491,7 @@ public: } MiscModelNode( EntityClass* eclass ) : - m_node( this, this, StaticTypeCasts::instance().get() ), + m_node( this, this, StaticTypeCasts::instance().get(), GlobalSceneGraph().currentLayer() ), m_contained( eclass, m_node, InstanceSet::TransformChangedCaller( m_instances ), InstanceSetEvaluateTransform::Caller( m_instances ) ){ construct(); } @@ -500,7 +500,7 @@ public: scene::Instantiable( other ), scene::Cloneable( other ), scene::Traversable::Observer( other ), - m_node( this, this, StaticTypeCasts::instance().get() ), + m_node( this, this, StaticTypeCasts::instance().get(), other.m_node.m_layer ), m_contained( other.m_contained, m_node, InstanceSet::TransformChangedCaller( m_instances ), InstanceSetEvaluateTransform::Caller( m_instances ) ){ construct(); } diff --git a/plugins/mapq3/parse.cpp b/plugins/mapq3/parse.cpp index 82f43bf0..37bd04e9 100644 --- a/plugins/mapq3/parse.cpp +++ b/plugins/mapq3/parse.cpp @@ -32,6 +32,98 @@ #include "string/string.h" #include "stringio.h" #include "eclasslib.h" +#include "layers.h" + +class LayersParser +{ + Layers& m_layers; + std::vector m_layersVec; // vector to access layers by index + int m_currentLayer = LAYERIDX0; +public: + LayersParser( scene::Node& root ) : m_layers( *Node_getLayers( root ) ){ + } + void construct_tree(){ + { + auto& list = m_layers.m_children; + + if( list.size() > 1 ) // something was parsed, remove default inserted layer + list.pop_front(); + + m_layersVec.reserve( list.size() ); + for( auto it = list.begin(); it != list.end(); ++it ) + m_layersVec.push_back( it ); + // check safety // note possible circular reference is not checked + if( std::ranges::find( list, LAYERIDXPARENT, &Layer::m_parentIndex ) == list.cend() ){ + globalErrorStream() << "No layers in the root\n"; + list.front().m_parentIndex = LAYERIDXPARENT; + } + } + for( auto it : m_layersVec ){ + // check safety + if( it->m_parentIndex < LAYERIDXPARENT || it->m_parentIndex >= static_cast( m_layersVec.size() ) ){ + globalErrorStream() << it->m_parentIndex << " parent layer index out of bounds\n"; + it->m_parentIndex = LAYERIDXPARENT; + } + if( it->m_parentIndex > LAYERIDXPARENT && m_layersVec[ it->m_parentIndex ] == it ){ + globalErrorStream() << it->m_parentIndex << " parent layer index self reference\n"; + it->m_parentIndex = LAYERIDXPARENT; + } + // link & reference parent + Layer& parent = it->m_parentIndex == LAYERIDXPARENT? m_layers : *m_layersVec[ it->m_parentIndex ]; + parent.m_children.splice( parent.m_children.cend(), m_layers.m_children, it ); + it->m_parent = &parent; + } + + m_layers.m_currentLayer = &m_layers.m_children.front(); + } + bool read_layers( Tokeniser& tokeniser ){ + for (;; ) + { + tokeniser.nextLine(); + const char *token; + if( !( token = tokeniser.getToken() ) ) + return false; + if( !string_equal( token, "layerdef" ) ){ + tokeniser.ungetToken(); + return true; + } + if( !( token = tokeniser.getToken() ) ){ + Tokeniser_unexpectedError( tokeniser, token, "#layer-name" ); + return false; + } + auto& layer = m_layers.m_children.emplace_back( token, nullptr ); // initially construct plain list + + if( !Tokeniser_getInteger( tokeniser, layer.m_parentIndex ) + || !Tokeniser_getInteger( tokeniser, layer.m_color[ 0 ] ) + || !Tokeniser_getInteger( tokeniser, layer.m_color[ 1 ] ) + || !Tokeniser_getInteger( tokeniser, layer.m_color[ 2 ] ) ) + return false; + + Tokeniser_skipToNextLine( tokeniser ); // skip possible format extension + } + } + bool read_layer( Tokeniser& tokeniser ){ + const char *token = tokeniser.getToken(); + if ( token == nullptr ) + return false; + if( !string_equal( token, "layer" ) ){ + tokeniser.ungetToken(); + return true; + } + if( !Tokeniser_getInteger( tokeniser, m_currentLayer ) ){ + return false; + } + else if( m_currentLayer <= LAYERIDXPARENT || m_currentLayer >= static_cast( m_layersVec.size() ) ){ + globalErrorStream() << m_currentLayer << " primitive layer index out of bounds\n"; + m_currentLayer = LAYERIDX0; + } + Tokeniser_skipToNextLine( tokeniser ); // skip possible format extension + return true; + } + Layer* getCurrentLayer(){ + return m_layersVec[ m_currentLayer ].operator->(); + } +}; inline MapImporter* Node_getMapImporter( scene::Node& node ){ return NodeTypeCast::cast( node ); @@ -52,7 +144,7 @@ NodeSmartReference Entity_create( EntityCreator& entityTable, EntityClass* entit return NodeSmartReference( entity ); } -NodeSmartReference Entity_parseTokens( Tokeniser& tokeniser, EntityCreator& entityTable, const PrimitiveParser& parser, int index ){ +NodeSmartReference Entity_parseTokens( Tokeniser& tokeniser, EntityCreator& entityTable, const PrimitiveParser& parser, int index, LayersParser& layersParser ){ NodeSmartReference entity( g_nullNode ); KeyValues keyValues; const char* classname = ""; @@ -61,8 +153,8 @@ NodeSmartReference Entity_parseTokens( Tokeniser& tokeniser, EntityCreator& enti while ( 1 ) { tokeniser.nextLine(); - const char* token = tokeniser.getToken(); - if ( token == 0 ) { + const char* token; + if ( !layersParser.read_layer( tokeniser ) || !( token = tokeniser.getToken() ) ) { Tokeniser_unexpectedError( tokeniser, token, "#entity-token" ); return g_nullNode; } @@ -86,6 +178,7 @@ NodeSmartReference Entity_parseTokens( Tokeniser& tokeniser, EntityCreator& enti globalErrorStream() << "brush " << count_primitives << ": parse error\n"; return g_nullNode; } + primitive.get().m_layer = layersParser.getCurrentLayer(); scene::Traversable* traversable = Node_getTraversable( entity ); if ( Node_getEntity( entity )->isContainer() && traversable != 0 ) { @@ -116,15 +209,23 @@ NodeSmartReference Entity_parseTokens( Tokeniser& tokeniser, EntityCreator& enti } void Map_Read( scene::Node& root, Tokeniser& tokeniser, EntityCreator& entityTable, const PrimitiveParser& parser ){ + LayersParser layersParser( root ); + if( !layersParser.read_layers( tokeniser ) ){ + layersParser.construct_tree(); // contruct anytime to have at least one layer, e.g. when empty .map + return; + } + layersParser.construct_tree(); + int count_entities = 0; for (;; ) { tokeniser.nextLine(); - if ( !tokeniser.getToken() ) { // { or 0 + if ( !layersParser.read_layer( tokeniser ) || !tokeniser.getToken() ) { // { or 0 break; } + Layer *entityLayer = layersParser.getCurrentLayer(); - NodeSmartReference entity( Entity_parseTokens( tokeniser, entityTable, parser, count_entities ) ); + NodeSmartReference entity( Entity_parseTokens( tokeniser, entityTable, parser, count_entities, layersParser ) ); if ( entity == g_nullNode ) { globalErrorStream() << "entity " << count_entities << ": parse error\n"; @@ -132,6 +233,8 @@ void Map_Read( scene::Node& root, Tokeniser& tokeniser, EntityCreator& entityTab } Node_getTraversable( root )->insert( entity ); + if( !Node_getEntity( entity )->isContainer() ) + entity.get().m_layer = entityLayer; ++count_entities; } diff --git a/plugins/mapq3/plugin.cpp b/plugins/mapq3/plugin.cpp index 1fdd8344..ff8bd6ba 100644 --- a/plugins/mapq3/plugin.cpp +++ b/plugins/mapq3/plugin.cpp @@ -101,7 +101,7 @@ public: return g_nullNode; } void readGraph( scene::Node& root, TextInputStream& inputStream, EntityCreator& entityTable ) const { - Tokeniser& tokeniser = GlobalScripLibModule::getTable().m_pfnNewSimpleTokeniser( inputStream ); + Tokeniser& tokeniser = GlobalScripLibModule::getTable().m_pfnNewMapTokeniser( inputStream ); tokeniser.nextLine(); if ( !Tokeniser_parseToken( tokeniser, "Version" ) ) { return; @@ -172,7 +172,7 @@ public: return g_nullNode; } void readGraph( scene::Node& root, TextInputStream& inputStream, EntityCreator& entityTable ) const { - Tokeniser& tokeniser = GlobalScripLibModule::getTable().m_pfnNewSimpleTokeniser( inputStream ); + Tokeniser& tokeniser = GlobalScripLibModule::getTable().m_pfnNewMapTokeniser( inputStream ); tokeniser.nextLine(); if ( !Tokeniser_parseToken( tokeniser, "Version" ) ) { return; @@ -291,7 +291,7 @@ public: } void readGraph( scene::Node& root, TextInputStream& inputStream, EntityCreator& entityTable ) const { - Tokeniser& tokeniser = GlobalScripLibModule::getTable().m_pfnNewSimpleTokeniser( inputStream ); + Tokeniser& tokeniser = GlobalScripLibModule::getTable().m_pfnNewMapTokeniser( inputStream ); m_formatDetected = false; Map_Read( root, tokeniser, entityTable, *this ); tokeniser.release(); @@ -368,7 +368,7 @@ public: return g_nullNode; } void readGraph( scene::Node& root, TextInputStream& inputStream, EntityCreator& entityTable ) const { - Tokeniser& tokeniser = GlobalScripLibModule::getTable().m_pfnNewSimpleTokeniser( inputStream ); + Tokeniser& tokeniser = GlobalScripLibModule::getTable().m_pfnNewMapTokeniser( inputStream ); m_formatDetected = false; Map_Read( root, tokeniser, entityTable, *this ); tokeniser.release(); @@ -412,7 +412,7 @@ public: return g_nullNode; } void readGraph( scene::Node& root, TextInputStream& inputStream, EntityCreator& entityTable ) const { - Tokeniser& tokeniser = GlobalScripLibModule::getTable().m_pfnNewSimpleTokeniser( inputStream ); + Tokeniser& tokeniser = GlobalScripLibModule::getTable().m_pfnNewMapTokeniser( inputStream ); Map_Read( root, tokeniser, entityTable, *this ); tokeniser.release(); } @@ -487,7 +487,7 @@ public: return g_nullNode; } void readGraph( scene::Node& root, TextInputStream& inputStream, EntityCreator& entityTable ) const { - Tokeniser& tokeniser = GlobalScripLibModule::getTable().m_pfnNewSimpleTokeniser( inputStream ); + Tokeniser& tokeniser = GlobalScripLibModule::getTable().m_pfnNewMapTokeniser( inputStream ); m_formatDetected = false; Map_Read( root, tokeniser, entityTable, *this ); tokeniser.release(); diff --git a/plugins/mapq3/write.cpp b/plugins/mapq3/write.cpp index 00d9d401..46d079e8 100644 --- a/plugins/mapq3/write.cpp +++ b/plugins/mapq3/write.cpp @@ -24,6 +24,31 @@ #include "ientity.h" #include "iscriplib.h" #include "scenelib.h" +#include "layers.h" + +void Layers_Write( Layers& layers, TokenWriter& writer ){ + layers.forEach( [&]( Layer& layer ){ + writer.writeToken( "//@$&" ); + writer.writeToken( "layerdef" ); + writer.writeString( layer.m_name.c_str() ); + writer.writeInteger( layer.m_parent->m_ownIndex ); + writer.writeInteger( layer.m_color[ 0 ] ); + writer.writeInteger( layer.m_color[ 1 ] ); + writer.writeInteger( layer.m_color[ 2 ] ); + writer.nextLine(); + } ); +} + +void Layer_Write( const Layer *layer, int& currentLayer, TokenWriter& writer ){ + if( layer != nullptr && currentLayer != layer->m_ownIndex ){ + currentLayer = layer->m_ownIndex; + writer.writeToken( "//@$&" ); + writer.writeToken( "layer" ); + writer.writeInteger( currentLayer ); + writer.nextLine(); + } +} + inline MapExporter* Node_getMapExporter( scene::Node& node ){ return NodeTypeCast::cast( node ); @@ -61,9 +86,13 @@ class WriteTokensWalker : public scene::Traversable::Walker mutable Stack m_stack; TokenWriter& m_writer; bool m_ignorePatches; + mutable int m_currentLayer = LAYERIDX0; public: - WriteTokensWalker( TokenWriter& writer, bool ignorePatches ) - : m_writer( writer ), m_ignorePatches( ignorePatches ){ + WriteTokensWalker( TokenWriter& writer, bool ignorePatches, Layers& layers ) + : m_writer( writer ), m_ignorePatches( ignorePatches ) + { + layers.update_ownIndices(); + Layers_Write( layers, m_writer ); } bool pre( scene::Node& node ) const { m_stack.push( false ); @@ -80,6 +109,8 @@ public: m_writer.writeUnsigned( g_count_entities++ ); m_writer.nextLine(); + Layer_Write( node.m_layer, m_currentLayer, m_writer ); + m_writer.writeToken( "{" ); m_writer.nextLine(); m_stack.top() = true; @@ -96,6 +127,8 @@ public: m_writer.writeUnsigned( g_count_brushes++ ); m_writer.nextLine(); + Layer_Write( node.m_layer, m_currentLayer, m_writer ); + exporter->exportTokens( m_writer ); } } @@ -113,5 +146,5 @@ public: void Map_Write( scene::Node& root, GraphTraversalFunc traverse, TokenWriter& writer, bool ignorePatches ){ g_count_entities = 0; - traverse( root, WriteTokensWalker( writer, ignorePatches ) ); + traverse( root, WriteTokensWalker( writer, ignorePatches, *Node_getLayers( root ) ) ); } diff --git a/plugins/md3model/model.h b/plugins/md3model/model.h index c47af9c2..c44340e2 100644 --- a/plugins/md3model/model.h +++ b/plugins/md3model/model.h @@ -456,7 +456,7 @@ public: typedef LazyStatic StaticTypeCasts; - ModelNode() : m_node( this, this, StaticTypeCasts::instance().get() ){ + ModelNode() : m_node( this, this, StaticTypeCasts::instance().get(), nullptr ){ } Model& model(){ diff --git a/plugins/model/model.cpp b/plugins/model/model.cpp index 70c40f78..0b79ca68 100644 --- a/plugins/model/model.cpp +++ b/plugins/model/model.cpp @@ -585,9 +585,9 @@ class PicoModelNode : public scene::Node::Symbiot, public scene::Instantiable public: typedef LazyStatic StaticTypeCasts; - PicoModelNode() : m_node( this, this, StaticTypeCasts::instance().get() ){ + PicoModelNode() : m_node( this, this, StaticTypeCasts::instance().get(), nullptr ){ } - PicoModelNode( picoModel_t* model ) : m_node( this, this, StaticTypeCasts::instance().get() ), m_picomodel( model ){ + PicoModelNode( picoModel_t* model ) : m_node( this, this, StaticTypeCasts::instance().get(), nullptr ), m_picomodel( model ){ } void release(){ diff --git a/radiant/brushnode.h b/radiant/brushnode.h index 2973f583..58a6acd9 100644 --- a/radiant/brushnode.h +++ b/radiant/brushnode.h @@ -95,7 +95,7 @@ public: } BrushNode() : - m_node( this, this, StaticTypeCasts::instance().get() ), + m_node( this, this, StaticTypeCasts::instance().get(), GlobalSceneGraph().currentLayer() ), m_brush( m_node, InstanceSetEvaluateTransform::Caller( m_instances ), InstanceSet::BoundsChangedCaller( m_instances ) ), m_mapImporter( m_brush ), m_mapExporter( m_brush ), @@ -106,7 +106,7 @@ public: scene::Node::Symbiot( other ), scene::Instantiable( other ), scene::Cloneable( other ), - m_node( this, this, StaticTypeCasts::instance().get() ), + m_node( this, this, StaticTypeCasts::instance().get(), other.m_node.m_layer ), m_brush( other.m_brush, m_node, InstanceSetEvaluateTransform::Caller( m_instances ), InstanceSet::BoundsChangedCaller( m_instances ) ), m_mapImporter( m_brush ), m_mapExporter( m_brush ), diff --git a/radiant/csg.cpp b/radiant/csg.cpp index a47bd91d..04e111dc 100644 --- a/radiant/csg.cpp +++ b/radiant/csg.cpp @@ -354,7 +354,7 @@ public: for( Brush* b : out ) { b->removeEmptyFaces(); if( b->hasContributingFaces() ) { - NodeSmartReference node( ( new BrushNode() )->node() ); + NodeSmartReference node( GlobalBrushCreator().createBrush() ); Node_getBrush( node )->copy( *b ); Node_getTraversable( path.parent() )->insert( node ); //path.push( makeReference( node.get() ) ); @@ -690,7 +690,7 @@ public: ++m_after; brush->removeEmptyFaces(); if ( !brush->empty() ) { - NodeSmartReference node( ( new BrushNode() )->node() ); + NodeSmartReference node( GlobalBrushCreator().createBrush() ); Node_getBrush( node )->copy( *brush ); delete brush; Node_getTraversable( path.parent() )->insert( node ); @@ -766,7 +766,7 @@ public: // the plane intersects this brush m_gj = true; if ( m_split ) { - NodeSmartReference node( ( new BrushNode() )->node() ); + NodeSmartReference node( GlobalBrushCreator().createBrush() ); Brush* fragment = Node_getBrush( node ); fragment->copy( *brush ); fragment->addPlane( m_points[0], m_points[2], m_points[1], m_shader, m_projection ); @@ -960,7 +960,7 @@ void CSG_Merge(){ UndoableCommand undo( "brushMerge" ); - NodeSmartReference node( ( new BrushNode() )->node() ); + NodeSmartReference node( GlobalBrushCreator().createBrush() ); Brush* brush = Node_getBrush( node ); // if the new brush would not be convex if ( !Brush_merge( *brush, selected_brushes, true ) ) { @@ -1166,7 +1166,7 @@ void CSG_WrapMerge( const ClipperPoints& clipperPoints ){ return; } - NodeSmartReference node( ( new BrushNode() )->node() ); + NodeSmartReference node( GlobalBrushCreator().createBrush() ); Brush* brush = GlobalSelectionSystem().countSelected() > 0? Node_getBrush( GlobalSelectionSystem().ultimateSelected().path().top() ) : 0; const bool oldbrush = brush && primit; if( oldbrush ) diff --git a/radiant/layerswindow.cpp b/radiant/layerswindow.cpp new file mode 100644 index 00000000..28774d70 --- /dev/null +++ b/radiant/layerswindow.cpp @@ -0,0 +1,537 @@ +/* + Copyright (C) 1999-2006 Id Software, Inc. and contributors. + For a list of contributors, see the accompanying CONTRIBUTORS file. + + This file is part of GtkRadiant. + + GtkRadiant is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + GtkRadiant is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with GtkRadiant; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "layerswindow.h" + +#include "layers.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "map.h" +#include "ientity.h" +#include "generic/callback.h" +#include "signal/signal.h" +#include "gtkutil/image.h" +#include "gtkmisc.h" +#include "windowobservers.h" + +/* + ?todo operator[] range check // checked during loading + layer color + restrict " in the name, empty name + 'move to layer' context menu on keybind +? tree 'move to layer' menu + def line format extensible +? extensibility with new root keywords + handle paste, clone, map import + new layer = make current + hide recursively + always nullptr layer for group ents node + hide/show group entity nodes automatically + don't select hidden nodes + non locked 0 layer //may be confusing for user when falling back to default layer in operations, it's implicitly 1st in the list//?write 0 layer for future +? feedback which layers are selected //highlight tree items +? feedback items count in a layer +? undo +button to add a layer +buttons to hide/show all layers +*/ + +class LayerAssignVisitor : public SelectionSystem::Visitor +{ + Layer *m_layer; +public: + LayerAssignVisitor( Layer *layer ) : m_layer( layer ){ + } + void visit( scene::Instance& instance ) const override { + scene::Node& node = instance.path().top().get(); + if( Entity *entity; !( ( entity = Node_getEntity( node ) ) && entity->isContainer() ) ) + node.m_layer = m_layer; + } +}; + +class LayerCollectVisitor : public SelectionSystem::Visitor +{ + std::vector& m_layers; +public: + LayerCollectVisitor( std::vector& layers ) : m_layers( layers ){ + } + void visit( scene::Instance& instance ) const override { + Layer *layer = instance.path().top().get().m_layer; + if( layer != nullptr && std::ranges::find( m_layers, layer ) == m_layers.cend() ) + m_layers.push_back( layer ); + } +}; + +class LayerHideWalker : public scene::Graph::Walker +{ + const std::vector& m_layers; +public: + LayerHideWalker( const std::vector& layers ) : m_layers( layers ){ + } + bool pre( const scene::Path& path, scene::Instance& instance ) const override { + scene::Node& node = path.top().get(); + if( std::ranges::find( m_layers, node.m_layer ) != m_layers.cend() ){ + node.enable( scene::Node::eLayerHidden ); + if( Selectable* selectable = Instance_getSelectable( instance ) ) + selectable->setSelected( false ); + } + return true; + } + void post( const scene::Path& path, scene::Instance& instance ) const override { + scene::Node& node = path.top().get(); + if( Node_isEntity( node ) ) // hide group entity labels, when their content is entirely hidden + if( scene::Traversable* traversable = Node_getTraversable( node ) ) + if( Traversable_all_of_children( traversable, []( const scene::Node& node ){ return node.excluded( scene::Node::eLayerHidden ); } ) ) + node.enable( scene::Node::eLayerHidden ); + } +}; +class LayerShowWalker : public scene::Graph::Walker +{ + const std::vector& m_layers; +public: + LayerShowWalker( const std::vector& layers ) : m_layers( layers ){ + } + bool pre( const scene::Path& path, scene::Instance& instance ) const override { + scene::Node& node = path.top().get(); + if( std::ranges::find( m_layers, node.m_layer ) != m_layers.cend() ){ + node.disable( scene::Node::eLayerHidden ); + } + return true; + } + void post( const scene::Path& path, scene::Instance& instance ) const override { + scene::Node& node = path.top().get(); + if( Node_isEntity( node ) ) // unhide group entity labels, when their content is partially visible + if( scene::Traversable* traversable = Node_getTraversable( node ) ) + if( Traversable_any_of_children( traversable, []( const scene::Node& node ){ return !node.excluded( scene::Node::eLayerHidden ); } ) ) + node.disable( scene::Node::eLayerHidden ); + } +}; + +class LayerSelectWalker : public scene::Graph::Walker +{ + const std::vector& m_layers; + const bool m_select; +public: + LayerSelectWalker( const std::vector& layers, bool select ) : m_layers( layers ), m_select( select ){ + } + bool pre( const scene::Path& path, scene::Instance& instance ) const override { + scene::Node& node = path.top().get(); + if( node.visible() && std::ranges::find( m_layers, node.m_layer ) != m_layers.cend() ){ + Instance_setSelected( instance, m_select ); + } + return true; + } +}; + +class LayerDeletetWalker : public scene::Graph::Walker +{ + const std::vector& m_layers; + Layer *m_currentLayer; +public: + LayerDeletetWalker( const std::vector& layers, Layer *currentLayer ) : m_layers( layers ), m_currentLayer( currentLayer ){ + } + bool pre( const scene::Path& path, scene::Instance& instance ) const override { + scene::Node& node = path.top().get(); + if( std::ranges::find( m_layers, node.m_layer ) != m_layers.cend() ){ + node.m_layer = m_currentLayer; + } + return true; + } +}; + + + +class LayersBrowser +{ +public: + QTreeWidget *m_tree = nullptr; + QMenu *m_movetoMenu = nullptr; + QIcon m_iconSelect; + QIcon m_iconSelectAdd; + QIcon m_iconSelectDeselect; + QIcon m_iconSelectMove; + QIcon m_iconVisibleOn; + QIcon m_iconVisibleOff; +}; + +LayersBrowser g_lbro; + + +namespace Column +{ +enum +{ + name, + current, + select, + visible, +}; +}; + +template + requires( std::invocable && std::is_void_v> ) +void items_iterate_recursively( QTreeWidgetItem *item, Functor&& functor ){ + functor( item ); + for( int i = 0; i < item->childCount(); ++i ) + items_iterate_recursively( item->child( i ), functor ); +} + +Layer::iterator item_getLayerIterator( QTreeWidgetItem *item ){ + static_assert( sizeof( Layer::iterator ) == sizeof( size_t ) ); + size_t layer = item->data( Column::name, Qt::ItemDataRole::UserRole ).value(); + return *reinterpret_cast( &layer ); +} +Layer* item_getLayer( QTreeWidgetItem *item ){ + return item_getLayerIterator( item ).operator->(); +} +std::vector item_getLayers( QTreeWidgetItem *item ){ + std::vector layers; + items_iterate_recursively( item, [&]( QTreeWidgetItem *item ){ layers.push_back( item_getLayer( item ) ); } ); + return layers; +} +std::vector item_getLayersIterators( QTreeWidgetItem *item ){ + std::vector layers; + items_iterate_recursively( item, [&]( QTreeWidgetItem *item ){ layers.push_back( item_getLayerIterator( item ) ); } ); + return layers; +} + +void item_setCurrent( QTreeWidgetItem *item ){ + for( QTreeWidgetItemIterator it( item->treeWidget() ); *it; ++it ) + ( *it )->setCheckState( Column::current, Qt::CheckState::Unchecked ); + + item->setCheckState( Column::current, Qt::CheckState::Checked ); + Node_getLayers( GlobalSceneGraph().root() )->m_currentLayer = item_getLayer( item ); +} + +void item_setIcons( QTreeWidgetItem *item ){ + item->setIcon( Column::select, g_lbro.m_iconSelect ); + item->setIcon( Column::visible, item->data( Column::visible, Qt::ItemDataRole::UserRole ).toBool()? g_lbro.m_iconVisibleOn : g_lbro.m_iconVisibleOff ); +} + +void item_setColor( QTreeWidgetItem *item, const int ( &color )[ 3 ] ){ + if( color[ 0 ] == 0 && color[ 1 ] == 0 && color[ 2 ] == 0 ) + item->setBackground( Column::current, {} ); + else + item->setBackground( Column::current, QColor( color[ 0 ], color[ 1 ], color[ 2 ] ) ); +} + +void item_construct( QTreeWidgetItem *item, Layer::iterator layer ){ + item->setText( Column::name, layer->m_name.c_str() ); + item->setData( Column::name, Qt::ItemDataRole::UserRole, QVariant::fromValue( *reinterpret_cast( &layer ) ) ); // Q_DECLARE_METATYPE( Layer* ) = crash during drag and drop + item->setData( Column::visible, Qt::ItemDataRole::UserRole, true ); // save toggle state + item->setToolTip( Column::current, "Set current layer" ); + item->setToolTip( Column::select, "CLICK = Select\nCTRL = multiple\nALT = deselect\nSHIFT = Move to layer" ); + item->setToolTip( Column::visible, "Toggle visibility" ); + item_setIcons( item ); + item_setColor( item, layer->m_color ); + item->setCheckState( Column::current, Qt::CheckState::Unchecked ); // make checkable +} + +void insert_children( QTreeWidgetItem *parentItem, std::list& children ){ + for( auto layer = children.begin(); layer != children.end(); ++layer ) + { + auto *item = new QTreeWidgetItem( parentItem ); + item_construct( item, layer ); + insert_children( item, layer->m_children ); + } +} + +void LayersBrowser_constructTree( QTreeWidget *tree ){ + if( Map_Valid( g_map ) ){ + Layers& layers = *Node_getLayers( GlobalSceneGraph().root() ); + tree->clear(); + insert_children( tree->invisibleRootItem(), layers.m_children ); + tree->topLevelItem( LAYERIDX0 )->setCheckState( Column::current, Qt::CheckState::Checked ); + tree->expandAll(); + } +} + + +class QTreeWidget_layers : public QTreeWidget, public WindowObserver +{ +protected: + bool m_drop = false; + void rowsInserted( const QModelIndex& parent, int start, int end ) override { + if( std::exchange( m_drop, false ) ){ // insertion source is drop + QTreeWidgetItem *parentItem = parent.isValid()? this->itemFromIndex( parent ) : this->invisibleRootItem(); + QTreeWidgetItem *item = parentItem->child( start ); + Layer *parentLayer = parent.isValid()? item_getLayer( parentItem ) : Node_getLayers( GlobalSceneGraph().root() ); + Layer::iterator layer = item_getLayerIterator( item ); + auto& srcList = layer->m_parent->m_children; + // move to the end of list 1st, so that resulting 'start' index can be used correctly, when moving inside a list farther + srcList.splice( srcList.cend(), srcList, layer ); + parentLayer->m_children.splice( std::next( parentLayer->m_children.cbegin(), start ), srcList, layer ); + layer->m_parent = parentLayer; + } + QTreeWidget::rowsInserted( parent, start, end ); + expandAll(); + } + // bool positionAboveItem0( const QPoint& pos ) const { + // const QModelIndex index = indexAt( pos ); + // return index.row() == LAYERIDX0 + // && !index.parent().isValid() // in the root + // && pos.y() < visualRect( index ).center().y(); + // } + void dragMoveEvent( QDragMoveEvent* event ) override { + // if( positionAboveItem0( event->pos() ) ){ + // event->ignore(); + // return; + // } + + QTreeWidget::dragMoveEvent( event ); + } + void dropEvent( QDropEvent* event ) override { + // if( positionAboveItem0( event->pos() ) ){ + // event->ignore(); + // return; + // } + ASSERT_MESSAGE( !m_drop, "dropEvent() without rowsInserted()" ); + m_drop = true; + QTreeWidget::dropEvent( event ); + } +private: + ModifierFlags m_modifiers; + void set_select_icons(){ + if( this->isVisible() ){ + QIcon& icon = m_modifiers == c_modifierControl? g_lbro.m_iconSelectAdd + : m_modifiers == c_modifierAlt? g_lbro.m_iconSelectDeselect + : m_modifiers == c_modifierShift? g_lbro.m_iconSelectMove + : g_lbro.m_iconSelect; + for( QTreeWidgetItemIterator it( this ); *it; ++it ) + ( *it )->setIcon( Column::select, icon ); + } + } +public: + void release() override {}; + void onSizeChanged( int width, int height ) override {}; + void onMouseDown( const WindowVector& position, ButtonIdentifier button, ModifierFlags modifiers ) override {}; + void onMouseUp( const WindowVector& position, ButtonIdentifier button, ModifierFlags modifiers ) override {}; + void onMouseMotion( const WindowVector& position, ModifierFlags modifiers ) override {}; + void onModifierDown( ModifierFlags modifier ) override { + m_modifiers = bitfield_enable( m_modifiers, modifier ); + set_select_icons(); + } + void onModifierUp( ModifierFlags modifier ) override { + m_modifiers = bitfield_disable( m_modifiers, modifier ); + set_select_icons(); + } +}; + + +void context_menu( const QPoint& pos ){ + auto *menu = new QMenu( g_lbro.m_tree ); + menu->setAttribute( Qt::WA_DeleteOnClose ); + + QTreeWidgetItem *item = g_lbro.m_tree->itemAt( pos ); + Layer *layer = item? item_getLayer( item ) : nullptr; + Layers& layers = *Node_getLayers( GlobalSceneGraph().root() ); + + menu->addAction( "Move Selection to This Layer", [&](){ + GlobalSelectionSystem().foreachSelected( LayerAssignVisitor( layer ) ); + } )->setDisabled( item == nullptr ); + + menu->addAction( g_lbro.m_iconSelectAdd, "New Layer", [&](){ + bool ok; + QString text = QInputDialog::getText( g_lbro.m_tree, "Layer Name", "Enter layer name:", QLineEdit::EchoMode::Normal, + "UnnamedpLayer", &ok ); + text.remove( '"' ); // remove token bounds symbol + if( ok && !text.isEmpty() ){ + QTreeWidgetItem *parentItem = item? item : g_lbro.m_tree->invisibleRootItem(); + Layer *parentLayer = layer? layer : &layers; + + parentLayer->m_children.emplace_back( text.toLatin1().constData(), parentLayer ); + auto *newItem = new QTreeWidgetItem( parentItem ); + item_construct( newItem, --parentLayer->m_children.end() ); + item_setCurrent( newItem ); + } + } ); + + int layersCount = 0; + layers.forEach( [&]( Layer& ){ ++layersCount; } ); + + menu->addAction( QApplication::style()->standardIcon( QStyle::SP_DialogCloseButton ), "Delete", [&](){ + auto dels = item_getLayers( item ); + auto it = item_getLayerIterator( item ); + + GlobalSceneGraph().traverse( LayerDeletetWalker( dels, layers.m_currentLayer ) ); + it->m_parent->m_children.erase( it ); + delete item; + + if( std::ranges::find( dels, layers.m_currentLayer ) != dels.cend() ){ + item_setCurrent( g_lbro.m_tree->topLevelItem( LAYERIDX0 ) ); + } + } )->setDisabled( item == nullptr || layersCount < 2 ); + + menu->addAction( "Rename", [&](){ + bool ok; + QString text = QInputDialog::getText( g_lbro.m_tree, "Layer Name", "Enter layer name:", QLineEdit::EchoMode::Normal, + item->text( Column::name ), &ok ); + text.remove( '"' ); // remove token bounds symbol + if( ok && !text.isEmpty() ){ + item->setText( Column::name, text ); + layer->m_name = text.toLatin1().constData(); + } + } )->setDisabled( item == nullptr ); + + menu->addAction( "Color", [&](){ + Vector3 color( layer->m_color[ 0 ] / 255.f, layer->m_color[ 1 ] / 255.f, layer->m_color[ 2 ] / 255.f ); + if( color_dialog( g_lbro.m_tree, color ) ){ + layer->m_color[ 0 ] = color[ 0 ] * 255; + layer->m_color[ 1 ] = color[ 1 ] * 255; + layer->m_color[ 2 ] = color[ 2 ] * 255; + item_setColor( item, layer->m_color ); + } + } )->setDisabled( item == nullptr ); + + menu->exec( g_lbro.m_tree->mapToGlobal( pos ) ); +} + +void context_moveto_menu(){ + if( !g_lbro.m_movetoMenu->isVisible() ){ + g_lbro.m_movetoMenu->clear(); + + Node_getLayers( GlobalSceneGraph().root() )->forEach( []( Layer& layer ){ + g_lbro.m_movetoMenu->addAction( QString( "-> " ) + layer.m_name.c_str(), [layer = &layer](){ + GlobalSelectionSystem().foreachSelected( LayerAssignVisitor( layer ) ); + } )->setDisabled( GlobalSelectionSystem().countSelected() == 0 ); + } ); + + g_lbro.m_movetoMenu->exec( QCursor::pos() ); + } +} + +void itemClicked( QTreeWidgetItem *item, int column ){ + if( item != nullptr ){ + if( column == Column::select ){ + const Qt::KeyboardModifiers kb = QApplication::keyboardModifiers(); + if( kb == Qt::KeyboardModifier::NoModifier || kb == Qt::KeyboardModifier::ControlModifier || kb == Qt::KeyboardModifier::AltModifier ){ + if( kb != Qt::KeyboardModifier::ControlModifier && kb != Qt::KeyboardModifier::AltModifier) + GlobalSelectionSystem().setSelectedAll( false ); + GlobalSceneGraph().traverse( LayerSelectWalker( item_getLayers( item ), kb != Qt::KeyboardModifier::AltModifier ) ); + } + else if( kb == Qt::KeyboardModifier::ShiftModifier ){ + GlobalSelectionSystem().foreachSelected( LayerAssignVisitor( item_getLayer( item ) ) ); + } + } + else if( column == Column::visible ){ + const bool visible = item->data( Column::visible, Qt::ItemDataRole::UserRole ).toBool(); + items_iterate_recursively( item, [&]( QTreeWidgetItem *item ){ + item->setData( Column::visible, Qt::ItemDataRole::UserRole, !visible ); + item_setIcons( item ); + } ); + + if( visible ){ + GlobalSceneGraph().traverse( LayerHideWalker( item_getLayers( item ) ) ); + /* not hiding worldspawn node so that newly created brushes are visible */ + if( scene::Node* w = Map_FindWorldspawn( g_map ) ) + w->disable( scene::Node::eLayerHidden ); + if( GlobalSelectionSystem().countSelectedComponents() != 0 ) + GlobalSelectionSystem().setSelectedAllComponents( false ); + } + else{ + GlobalSceneGraph().traverse( LayerShowWalker( item_getLayers( item ) ) ); + } + SceneChangeNotify(); + } + else if( column == Column::current ){ + item_setCurrent( item ); + } + } +} + +class QWidget* LayersBrowser_constructWindow( QWidget* toplevel ){ + auto *tree = new QTreeWidget_layers; + g_lbro.m_tree = tree; + + tree->setColumnCount( 4 ); + tree->setUniformRowHeights( true ); // optimization + tree->setHorizontalScrollBarPolicy( Qt::ScrollBarPolicy::ScrollBarAlwaysOff ); + tree->header()->setStretchLastSection( false ); // non greedy column sizing //makes it greedy for name section here + tree->header()->setSectionResizeMode( QHeaderView::ResizeMode::ResizeToContents ); + tree->header()->setSectionResizeMode( Column::name, QHeaderView::ResizeMode::Stretch ); + tree->setHeaderHidden( true ); + tree->setAutoScroll( true ); + tree->setAlternatingRowColors( true ); + const int iconSize = tree->style()->pixelMetric( QStyle::PixelMetric::PM_SmallIconSize, nullptr, tree ) * 1.3; + tree->setIconSize( QSize( iconSize, iconSize) ); + // tree->setStyleSheet( "QTreeWidget::item { border-bottom: 1px solid gray; }" ); + tree->setDragDropMode( QAbstractItemView::DragDropMode::InternalMove ); + + tree->setContextMenuPolicy( Qt::ContextMenuPolicy::CustomContextMenu ); + QObject::connect( tree, &QTreeWidget::customContextMenuRequested, context_menu ); + + tree->header()->swapSections( Column::current, Column::name ); // display current indicator 1st, tree second + + QObject::connect( tree, &QTreeWidget::itemClicked, itemClicked ); + QObject::connect( tree, &QTreeWidget::itemDoubleClicked, itemClicked ); // workaround fast clicks being not registered + + // Connect to itemCollapsed signal to prevent collapsing + QObject::connect( tree, &QTreeWidget::itemCollapsed, []( QTreeWidgetItem *item ){ item->setExpanded( true ); }); + + g_lbro.m_movetoMenu = new QMenu( tree ); + + g_lbro.m_iconSelect = new_local_icon( "check_mark.png" ); + g_lbro.m_iconSelectAdd = new_local_icon( "plus.png" ); + g_lbro.m_iconSelectDeselect = new_local_icon( "minus.png" ); + g_lbro.m_iconSelectMove = new_local_icon( "arrow_right.png" ); + g_lbro.m_iconVisibleOn = new_local_icon( "eye_opened.png" ); + g_lbro.m_iconVisibleOff = new_local_icon( "eye_closed.png" ); + + GlobalWindowObservers_add( tree ); // track modifiers for Column::select icons toggle + Map_addValidCallback( g_map, PointerCaller( g_lbro.m_tree ) ); + + return g_lbro.m_tree; +} + +void LayersBrowser_destroyWindow(){ + g_lbro.m_tree = nullptr; +} + + +void Scene_ExpandSelectionToLayers(){ + std::vector layers; + GlobalSelectionSystem().foreachSelected( LayerCollectVisitor( layers ) ); + for( size_t i = 0, end = layers.size(); i < end; ++i ){ // vector may be changed during loop + layers[ i ]->forEach( [&]( Layer& layer ){ + if( std::ranges::find( layers, &layer ) == layers.cend() ) + layers.push_back( &layer ); + } ); + } + GlobalSceneGraph().traverse( LayerSelectWalker( layers, true ) ); +} + +#include "commands.h" +void Layers_registerCommands(){ + GlobalCommands_insert( "ExpandSelectionToLayers", makeCallbackF( Scene_ExpandSelectionToLayers ), QKeySequence( "Ctrl+Q" ) ); + GlobalCommands_insert( "LayersMenu", makeCallbackF( context_moveto_menu ), QKeySequence( "Shift+Q" ) ); +} + +void Layers_registerShortcuts(){ + command_connect_accelerator( "LayersMenu" ); +} + diff --git a/radiant/layerswindow.h b/radiant/layerswindow.h new file mode 100644 index 00000000..60a608e7 --- /dev/null +++ b/radiant/layerswindow.h @@ -0,0 +1,28 @@ +/* + Copyright (C) 1999-2006 Id Software, Inc. and contributors. + For a list of contributors, see the accompanying CONTRIBUTORS file. + + This file is part of GtkRadiant. + + GtkRadiant is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + GtkRadiant is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with GtkRadiant; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#pragma once + +class QWidget* LayersBrowser_constructWindow( QWidget* toplevel ); +void LayersBrowser_destroyWindow(); + +void Layers_registerCommands(); +void Layers_registerShortcuts(); \ No newline at end of file diff --git a/radiant/mainframe.cpp b/radiant/mainframe.cpp index 0ba89f0b..134f9a01 100644 --- a/radiant/mainframe.cpp +++ b/radiant/mainframe.cpp @@ -114,6 +114,7 @@ #include "textures.h" #include "texwindow.h" #include "modelwindow.h" +#include "layerswindow.h" #include "url.h" #include "xywindow.h" #include "windowobservers.h" @@ -727,6 +728,12 @@ void ModelBrowser_ToggleShow(){ GroupDialog_showPage( g_page_models ); } +QWidget* g_page_layers; + +void LayersBrowser_ToggleShow(){ + GroupDialog_showPage( g_page_layers); +} + static class EverySecondTimer { @@ -924,6 +931,7 @@ void create_edit_menu( QMenuBar *menubar ){ create_menu_item_with_mnemonic( menu, "Select Textured", "SelectTextured" ); create_menu_item_with_mnemonic( menu, "&Expand Selection To Primitives", "ExpandSelectionToPrimitives" ); create_menu_item_with_mnemonic( menu, "&Expand Selection To Entities", "ExpandSelectionToEntities" ); + create_menu_item_with_mnemonic( menu, "&Expand Selection To Layers", "ExpandSelectionToLayers" ); create_menu_item_with_mnemonic( menu, "Select Connected Entities", "SelectConnectedEntities" ); menu->addSeparator(); @@ -951,8 +959,9 @@ void create_view_menu( QMenuBar *menubar, MainFrame::EViewStyle style ){ } create_menu_item_with_mnemonic( menu, "Model Browser", "ToggleModelBrowser" ); create_menu_item_with_mnemonic( menu, "Entity Inspector", "ToggleEntityInspector" ); + create_menu_item_with_mnemonic( menu, "Layers Browser", "ToggleLayersBrowser" ); create_menu_item_with_mnemonic( menu, "&Surface Inspector", "SurfaceInspector" ); - create_menu_item_with_mnemonic( menu, "Entity List", "EntityList" ); + create_menu_item_with_mnemonic( menu, "Entity List", "ToggleEntityList" ); menu->addSeparator(); { @@ -1322,6 +1331,7 @@ void register_shortcuts(){ TexBro_registerShortcuts(); Misc_registerShortcuts(); Entity_registerShortcuts(); + Layers_registerShortcuts(); } void File_constructToolbar( QToolBar* toolbar ){ @@ -1693,6 +1703,8 @@ void MainFrame::Create(){ g_page_models = GroupDialog_addPage( "Models", ModelBrowser_constructWindow( GroupDialog_getWindow() ), RawStringExportCaller( "Models" ) ); + g_page_layers = GroupDialog_addPage( "Layers", LayersBrowser_constructWindow( GroupDialog_getWindow() ), RawStringExportCaller( "Layers" ) ); + window->show(); if ( CurrentStyle() == eRegular || CurrentStyle() == eRegularLeft ) { @@ -1841,6 +1853,9 @@ void MainFrame::Create(){ m_hSplit->setStretchFactor( 1, 2222 ); m_hSplit->setStretchFactor( 2, 0 ); } + else{ // floating group dialog + GlobalWindowObservers_connectTopLevel( GroupDialog_getWindow() ); // for layers browser icons toggle + } EntityList_constructWindow( window ); PreferencesDialog_constructWindow( window ); @@ -1888,6 +1903,7 @@ void MainFrame::Shutdown(){ delete std::exchange( m_pXZWnd, nullptr ); ModelBrowser_destroyWindow(); + LayersBrowser_destroyWindow(); TextureBrowser_destroyWindow(); DeleteCamWnd( m_pCamWnd ); @@ -2055,9 +2071,11 @@ void MainFrame_Construct(){ GlobalCommands_insert( "ToggleConsole", makeCallbackF( Console_ToggleShow ), QKeySequence( "O" ) ); GlobalCommands_insert( "ToggleEntityInspector", makeCallbackF( EntityInspector_ToggleShow ), QKeySequence( "N" ) ); GlobalCommands_insert( "ToggleModelBrowser", makeCallbackF( ModelBrowser_ToggleShow ), QKeySequence( "/" ) ); - GlobalCommands_insert( "EntityList", makeCallbackF( EntityList_toggleShown ), QKeySequence( "L" ) ); + GlobalCommands_insert( "ToggleLayersBrowser", makeCallbackF( LayersBrowser_ToggleShow ), QKeySequence( "L" ) ); + GlobalCommands_insert( "ToggleEntityList", makeCallbackF( EntityList_toggleShown ), QKeySequence( "Shift+L" ) ); Select_registerCommands(); + Layers_registerCommands(); Tools_registerCommands(); diff --git a/radiant/map.cpp b/radiant/map.cpp index c043a161..cb7c8553 100644 --- a/radiant/map.cpp +++ b/radiant/map.cpp @@ -564,6 +564,46 @@ scene::Node& Map_FindOrInsertWorldspawn( Map& map ){ } +// assigns layers to ones from the other layers set of the same size +class LayersMergeWalker : public scene::Traversable::Walker +{ + std::vector m_layersVec; +public: + LayersMergeWalker( Layers *currentLayers ){ + currentLayers->forEach( [this]( Layer& layer ){ m_layersVec.push_back( &layer ); } ); + } + bool pre( scene::Node& node ) const { + if( node.m_layer != nullptr ) + node.m_layer = m_layersVec[ node.m_layer->m_ownIndex ]; + return true; + } +}; + +class LayersAssignWalker : public scene::Traversable::Walker +{ + Layer *m_current; +public: + LayersAssignWalker( Layer *current ) : m_current( current ){ + } + bool pre( scene::Node& node ) const { + if( Entity *entity; !( ( entity = Node_getEntity( node ) ) && entity->isContainer() ) ) + node.m_layer = m_current; + return true; + } +}; + +void Map_mergeLayers( scene::Node& newRoot, Layers *newLayers ){ + Layers *currLayers = Node_getLayers( GlobalSceneGraph().root() ); + newLayers->update_ownIndices(); + currLayers->update_ownIndices(); + if( newLayers->m_children == currLayers->m_children ){ // merge, likely same map copypaste or just one layer in total + Node_getTraversable( newRoot )->traverse( LayersMergeWalker( currLayers ) ); + } + else{ // set current + Node_getTraversable( newRoot )->traverse( LayersAssignWalker( GlobalSceneGraph().currentLayer() ) ); + } +} + class MapMergeAll : public scene::Traversable::Walker { mutable scene::Path m_path; @@ -631,6 +671,7 @@ class BasicContainer : public scene::Node::Symbiot public: TypeCasts(){ NodeContainedCast::install( m_casts ); + NodeContainedCast::install( m_casts ); } NodeTypeCastTable& get(){ return m_casts; @@ -639,6 +680,7 @@ class BasicContainer : public scene::Node::Symbiot scene::Node m_node; TraversableNodeSet m_traverse; + Layers m_layers; public: typedef LazyStatic StaticTypeCasts; @@ -646,8 +688,11 @@ public: scene::Traversable& get( NullType){ return m_traverse; } + Layers& get( NullType){ + return m_layers; + } - BasicContainer() : m_node( this, this, StaticTypeCasts::instance().get() ){ + BasicContainer() : m_node( this, this, StaticTypeCasts::instance().get(), nullptr ){ } void release(){ delete this; @@ -703,6 +748,7 @@ void Map_ImportSelected( TextInputStream& in, const MapFormat& format ){ Node_getTraversable( node )->traverse( Convert_Brushes( BrushType_getTexdefType( GlobalBrushCreator().getFormat() ), BrushType_getTexdefType( brush_type ) ) ); GlobalBrushCreator().toggleFormat( brush_type ); } + Map_mergeLayers( node, Node_getLayers( node ) ); Map_gatherNamespaced( node ); Map_mergeClonedNames(); MergeMap( node ); @@ -1167,11 +1213,17 @@ void Map_Traverse_Region( scene::Node& root, const scene::Traversable::Walker& w } +inline void swap_layers( Layers *from, Layers *to ){ + std::swap( from->m_children, to->m_children ); + to->forEach( [&]( Layer& layer ){ if( layer.m_parent == from ) layer.m_parent = to; } ); +} + void Map_RenameAbsolute( const char* absolute ){ Resource* resource = GlobalReferenceCache().capture( absolute ); NodeSmartReference clone( NewMapRoot( path_make_relative( absolute, GlobalFileSystem().findRoot( absolute ) ) ) ); resource->setNode( clone.get_pointer() ); + swap_layers( Node_getLayers( GlobalSceneGraph().root() ), Node_getLayers( clone ) ); { //ScopeTimer timer( "clone subgraph" ); Node_getTraversable( GlobalSceneGraph().root() )->traverse( CloneAll( clone ) ); @@ -1523,6 +1575,7 @@ bool Map_ImportFile( const char* filename ){ } NodeSmartReference clone( NewMapRoot( "" ) ); Node_getTraversable( *resource->getNode() )->traverse( CloneAll( clone ) ); + Map_mergeLayers( clone, Node_getLayers( *resource->getNode() ) ); resource->flush(); /* wipe map from cache to not spoil namespace */ GlobalReferenceCache().release( filename ); Map_gatherNamespaced( clone ); @@ -1575,6 +1628,7 @@ tryDecompile: } NodeSmartReference clone( NewMapRoot( "" ) ); Node_getTraversable( *resource->getNode() )->traverse( CloneAll( clone ) ); + Map_mergeLayers( clone, Node_getLayers( *resource->getNode() ) ); resource->flush(); /* wipe map from cache to not spoil namespace */ GlobalReferenceCache().release( filename ); Map_gatherNamespaced( clone ); diff --git a/radiant/modelwindow.cpp b/radiant/modelwindow.cpp index 9efe3025..2c035d3e 100644 --- a/radiant/modelwindow.cpp +++ b/radiant/modelwindow.cpp @@ -116,6 +116,10 @@ public: root.DecRef(); } + class Layer* currentLayer(){ + ASSERT_MESSAGE( 0, "Reached unreachable: currentLayer()" ); + return nullptr; + } void boundsChanged(){ ASSERT_MESSAGE( 0, "Reached unreachable: boundsChanged()" ); } @@ -303,7 +307,7 @@ public: return m_transform; } - ModelGraphRoot() : m_node( this, this, StaticTypeCasts::instance().get() ){ + ModelGraphRoot() : m_node( this, this, StaticTypeCasts::instance().get(), nullptr ){ m_node.m_isRoot = true; m_traverse.attach( this ); @@ -392,14 +396,14 @@ public: } ModelNode() : - m_node( this, this, StaticTypeCasts::instance().get() ){ + m_node( this, this, StaticTypeCasts::instance().get(), nullptr ){ construct(); } ModelNode( const ModelNode& other ) : scene::Node::Symbiot( other ), scene::Instantiable( other ), scene::Traversable::Observer( other ), - m_node( this, this, StaticTypeCasts::instance().get() ){ + m_node( this, this, StaticTypeCasts::instance().get(), nullptr ){ construct(); } ~ModelNode(){ diff --git a/radiant/nullmodel.cpp b/radiant/nullmodel.cpp index 14c7ce04..e18a0d13 100644 --- a/radiant/nullmodel.cpp +++ b/radiant/nullmodel.cpp @@ -150,7 +150,7 @@ public: typedef LazyStatic StaticTypeCasts; - NullModelNode() : m_node( this, this, StaticTypeCasts::instance().get() ){ + NullModelNode() : m_node( this, this, StaticTypeCasts::instance().get(), nullptr ){ m_node.m_isRoot = true; } diff --git a/radiant/parse.cpp b/radiant/parse.cpp index 36e3485f..aae260dc 100644 --- a/radiant/parse.cpp +++ b/radiant/parse.cpp @@ -33,6 +33,7 @@ public: ScriptLibraryAPI(){ m_scriptlibrary.m_pfnNewScriptTokeniser = &NewScriptTokeniser; + m_scriptlibrary.m_pfnNewMapTokeniser = &NewMapTokeniser; m_scriptlibrary.m_pfnNewSimpleTokeniser = &NewSimpleTokeniser; m_scriptlibrary.m_pfnNewSimpleTokenWriter = &NewSimpleTokenWriter; } diff --git a/radiant/patch.h b/radiant/patch.h index 5979fa0b..32af2a8f 100644 --- a/radiant/patch.h +++ b/radiant/patch.h @@ -1756,7 +1756,7 @@ public: } PatchNode( bool patchDef3 = false ) : - m_node( this, this, StaticTypeCasts::instance().get() ), + m_node( this, this, StaticTypeCasts::instance().get(), GlobalSceneGraph().currentLayer() ), m_patch( m_node, InstanceSetEvaluateTransform::Caller( m_instances ), InstanceSet::BoundsChangedCaller( m_instances ) ), m_importMap( m_patch ), m_exportMap( m_patch ){ @@ -1766,7 +1766,7 @@ public: scene::Node::Symbiot( other ), scene::Instantiable( other ), scene::Cloneable( other ), - m_node( this, this, StaticTypeCasts::instance().get() ), + m_node( this, this, StaticTypeCasts::instance().get(), other.m_node.m_layer ), m_patch( other.m_patch, m_node, InstanceSetEvaluateTransform::Caller( m_instances ), InstanceSet::BoundsChangedCaller( m_instances ) ), m_importMap( m_patch ), m_exportMap( m_patch ){ diff --git a/radiant/scenegraph.cpp b/radiant/scenegraph.cpp index 27f84c1a..f3c148e3 100644 --- a/radiant/scenegraph.cpp +++ b/radiant/scenegraph.cpp @@ -32,6 +32,7 @@ #include "scenelib.h" #include "instancelib.h" #include "treemodel.h" +#include "layers.h" template class TypeIdMap @@ -63,6 +64,8 @@ class CompiledGraph final : public scene::Graph, public scene::Instantiable::Obs Signal0 m_boundsChanged; scene::Path m_rootpath; Signal0 m_sceneChangedCallbacks; + Layer *m_currentLayer0 = nullptr; + Layer **m_currentLayer = &m_currentLayer0; TypeIdMap m_nodeTypeIds; TypeIdMap m_instanceTypeIds; @@ -94,6 +97,8 @@ public: Node_traverseSubgraph( root, InstanceSubgraphWalker( this, scene::Path(), 0 ) ); m_rootpath.push( makeReference( root ) ); + + m_currentLayer = &Node_getLayers( root )->m_currentLayer; } void erase_root(){ //globalOutputStream() << "erase_root\n"; @@ -107,6 +112,11 @@ public: Node_traverseSubgraph( root, UninstanceSubgraphWalker( this, scene::Path() ) ); root.DecRef(); + + m_currentLayer = &m_currentLayer0; + } + Layer* currentLayer(){ + return *m_currentLayer; } void boundsChanged(){ m_boundsChanged(); diff --git a/radiant/select.cpp b/radiant/select.cpp index 4a65dd54..27ae2146 100644 --- a/radiant/select.cpp +++ b/radiant/select.cpp @@ -941,26 +941,6 @@ void Select_FitTexture( float horizontal, float vertical, bool only_dimension ){ #include "commands.h" #include "dialog.h" -template -bool Traversable_all_of_children( scene::Traversable* traversable, const Check&& check ){ - class Check_all : public scene::Traversable::Walker - { - const Check m_check; - public: - mutable bool m_all = true; // true for empty container - Check_all( Check check ) : m_check( check ){ - } - bool pre( scene::Node& node ) const override { - if( !m_check( node ) ) - m_all = false; - return m_all; - } - } check_all( check ); - - traversable->traverse( check_all ); - return check_all.m_all; -} - inline void hide_node( scene::Node& node, bool hide ){ hide ? node.enable( scene::Node::eHidden ) diff --git a/radiant/texwindow.cpp b/radiant/texwindow.cpp index 4ef10eaa..85dba4b5 100644 --- a/radiant/texwindow.cpp +++ b/radiant/texwindow.cpp @@ -1401,7 +1401,7 @@ protected: } } bool editorEvent( QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index ) override { - /* let's do some infamous juggling to track user unduced CheckState change */ + /* let's do some infamous juggling to track user induced CheckState change */ if( event->type() == QEvent::MouseButtonRelease || event->type() == QEvent::KeyPress ){ if( const QVariant value = index.data( Qt::ItemDataRole::CheckStateRole ); value.isValid() ){ const bool ret = QItemDelegate::editorEvent( event, model, option, index ); diff --git a/setup/data/tools/bitmaps/arrow_right.png b/setup/data/tools/bitmaps/arrow_right.png new file mode 100644 index 00000000..b06f1b2a Binary files /dev/null and b/setup/data/tools/bitmaps/arrow_right.png differ diff --git a/setup/data/tools/bitmaps/check_mark.png b/setup/data/tools/bitmaps/check_mark.png new file mode 100644 index 00000000..8fc45acd Binary files /dev/null and b/setup/data/tools/bitmaps/check_mark.png differ diff --git a/setup/data/tools/bitmaps/eye_closed.png b/setup/data/tools/bitmaps/eye_closed.png new file mode 100644 index 00000000..23e9df42 Binary files /dev/null and b/setup/data/tools/bitmaps/eye_closed.png differ diff --git a/setup/data/tools/bitmaps/eye_opened.png b/setup/data/tools/bitmaps/eye_opened.png new file mode 100644 index 00000000..dca8be67 Binary files /dev/null and b/setup/data/tools/bitmaps/eye_opened.png differ diff --git a/setup/data/tools/bitmaps/minus.png b/setup/data/tools/bitmaps/minus.png new file mode 100644 index 00000000..d08c0176 Binary files /dev/null and b/setup/data/tools/bitmaps/minus.png differ diff --git a/setup/data/tools/bitmaps/plus.png b/setup/data/tools/bitmaps/plus.png new file mode 100644 index 00000000..9ba75524 Binary files /dev/null and b/setup/data/tools/bitmaps/plus.png differ