From 06cd1e98ff595a4923fa1b576ed0d667ba7db6a8 Mon Sep 17 00:00:00 2001 From: Garux Date: Sun, 26 Apr 2020 07:28:23 +0300 Subject: [PATCH] * Model Browser: default bind '/' m1 = assign model to selected entity nodes (shift + e to select nodes of group entities) m1x2 = insert "misc_model" with given model m1 drag = rotate model Preferences.Model Browser: list of * separated folderToLoad/depth values, e.g. *models/mapobjects/99*maps/1*; */99* loads root --- Makefile | 1 + libs/gtkutil/cursor.h | 42 ++ radiant/dialog.cpp | 12 + radiant/dialog.h | 4 + radiant/groupdialog.cpp | 3 + radiant/mainframe.cpp | 21 +- radiant/map.cpp | 3 + radiant/modelwindow.cpp | 1413 +++++++++++++++++++++++++++++++++++++++ radiant/modelwindow.h | 41 ++ radiant/plugin.cpp | 3 + radiant/preferences.h | 6 + radiant/texwindow.cpp | 37 - 12 files changed, 1547 insertions(+), 39 deletions(-) create mode 100644 radiant/modelwindow.cpp create mode 100644 radiant/modelwindow.h diff --git a/Makefile b/Makefile index 12f51c65..e271e855 100644 --- a/Makefile +++ b/Makefile @@ -705,6 +705,7 @@ $(INSTALLDIR)/radiant.$(EXE): \ radiant/mainframe.o \ radiant/main.o \ radiant/map.o \ + radiant/modelwindow.o \ $(if $(findstring $(OS),Win32),radiant/multimon.o,) \ radiant/mru.o \ radiant/nullmodel.o \ diff --git a/libs/gtkutil/cursor.h b/libs/gtkutil/cursor.h index fd8330e8..0d813397 100644 --- a/libs/gtkutil/cursor.h +++ b/libs/gtkutil/cursor.h @@ -225,4 +225,46 @@ void unfreeze_pointer( GtkWindow* window, bool centerize ){ } }; + + + + +class DeferredAdjustment +{ +gdouble m_value; +guint m_handler; +typedef void ( *ValueChangedFunction )( void* data, gdouble value ); +ValueChangedFunction m_function; +void* m_data; + +static gboolean deferred_value_changed( gpointer data ){ + reinterpret_cast( data )->m_function( + reinterpret_cast( data )->m_data, + reinterpret_cast( data )->m_value + ); + reinterpret_cast( data )->m_handler = 0; + reinterpret_cast( data )->m_value = 0; + return FALSE; +} +public: +DeferredAdjustment( ValueChangedFunction function, void* data ) : m_value( 0 ), m_handler( 0 ), m_function( function ), m_data( data ){ +} +void flush(){ + if ( m_handler != 0 ) { + g_source_remove( m_handler ); + deferred_value_changed( this ); + } +} +void value_changed( gdouble value ){ + m_value = value; + if ( m_handler == 0 ) { + m_handler = g_idle_add( deferred_value_changed, this ); + } +} +static void adjustment_value_changed( GtkAdjustment *adjustment, DeferredAdjustment* self ){ + self->value_changed( adjustment->value ); +} +}; + + #endif diff --git a/radiant/dialog.cpp b/radiant/dialog.cpp index 006948b2..8c0549d1 100644 --- a/radiant/dialog.cpp +++ b/radiant/dialog.cpp @@ -642,6 +642,18 @@ GtkWidget* Dialog::addFloatEntry( GtkWidget* vbox, const char* name, const Float return row.m_row; } +GtkWidget* Dialog::addTextEntry( GtkWidget* vbox, const char* name, const StringImportCallback& importViewer, const StringExportCallback& exportViewer ){ + GtkEntry* entry = DialogEntry_new(); + gtk_widget_set_size_request( GTK_WIDGET( entry ), -1, -1 ); // unset + + AddTextEntryData( *entry, importViewer, exportViewer ); + + GtkTable* row = DialogRow_new( name, GTK_WIDGET( entry ) ); + DialogVBox_packRow( GTK_VBOX( vbox ), GTK_WIDGET( row ) ); + + return GTK_WIDGET( row ); +} + GtkWidget* Dialog::addPathEntry( GtkWidget* vbox, const char* name, bool browse_directory, const StringImportCallback& importViewer, const StringExportCallback& exportViewer ){ PathEntry pathEntry = PathEntry_new(); g_signal_connect( G_OBJECT( pathEntry.m_button ), "clicked", G_CALLBACK( browse_directory ? button_clicked_entry_browse_directory : button_clicked_entry_browse_file ), pathEntry.m_entry ); diff --git a/radiant/dialog.h b/radiant/dialog.h index 7a562c68..34b4aa59 100644 --- a/radiant/dialog.h +++ b/radiant/dialog.h @@ -160,6 +160,10 @@ GtkWidget* addFloatEntry( GtkWidget* vbox, const char* name, const FloatImportCa GtkWidget* addEntry( GtkWidget* vbox, const char* name, float& data ){ return addFloatEntry( vbox, name, FloatImportCaller( data ), FloatExportCaller( data ) ); } +GtkWidget* addTextEntry( GtkWidget* vbox, const char* name, const StringImportCallback& importCallback, const StringExportCallback& exportCallback ); +GtkWidget* addEntry( GtkWidget* vbox, const char* name, CopiedString& data ){ + return addTextEntry( vbox, name, StringImportCallback( StringImportCaller( data ) ), StringExportCallback( StringExportCaller( data ) ) ); +} GtkWidget* addPathEntry( GtkWidget* vbox, const char* name, bool browse_directory, const StringImportCallback& importCallback, const StringExportCallback& exportCallback ); GtkWidget* addPathEntry( GtkWidget* vbox, const char* name, CopiedString& data, bool directory ); GtkWidget* addSpinner( GtkWidget* vbox, const char* name, int& data, double value, double lower, double upper ); diff --git a/radiant/groupdialog.cpp b/radiant/groupdialog.cpp index c7d9700a..10147c05 100644 --- a/radiant/groupdialog.cpp +++ b/radiant/groupdialog.cpp @@ -92,6 +92,9 @@ static gboolean switch_page( GtkNotebook *notebook, GtkNotebookPage *page, guint GroupDialog_updatePageTitle( GTK_WINDOW( data ), page_num ); g_current_page = page_num; + /* workaround for gtk 2.24 issue: not displayed glwidget after toggle */ + g_object_set_data( G_OBJECT( g_GroupDlg.m_window ), "glwidget", g_object_get_data( G_OBJECT( gtk_notebook_get_nth_page( notebook, page_num ) ), "glwidget" ) ); + return FALSE; } diff --git a/radiant/mainframe.cpp b/radiant/mainframe.cpp index 14bac918..daaa60c5 100644 --- a/radiant/mainframe.cpp +++ b/radiant/mainframe.cpp @@ -110,6 +110,7 @@ #include "surfacedialog.h" #include "textures.h" #include "texwindow.h" +#include "modelwindow.h" #include "url.h" #include "xywindow.h" #include "windowobservers.h" @@ -1088,6 +1089,12 @@ void EntityInspector_ToggleShow(){ GroupDialog_showPage( g_page_entity ); } +GtkWidget* g_page_models; + +void ModelBrowser_ToggleShow(){ + GroupDialog_showPage( g_page_models ); +} + void SetClipMode( bool enable ); @@ -2095,6 +2102,7 @@ GtkMenuItem* create_view_menu( MainFrame::EViewStyle style ){ create_menu_item_with_mnemonic( menu, "Console", "ToggleConsole" ); create_menu_item_with_mnemonic( menu, "Texture Browser", "ToggleTextures" ); } + create_menu_item_with_mnemonic( menu, "Model Browser", "ToggleModelBrowser" ); create_menu_item_with_mnemonic( menu, "Entity Inspector", "ToggleEntityInspector" ); create_menu_item_with_mnemonic( menu, "_Surface Inspector", "SurfaceInspector" ); create_menu_item_with_mnemonic( menu, "_Patch Inspector", "PatchInspector" ); @@ -3119,6 +3127,13 @@ void MainFrame::Create(){ g_page_console = GroupDialog_addPage( "Console", Console_constructWindow( GroupDialog_getWindow() ), RawStringExportCaller( "Console" ) ); } + { + GtkFrame* frame = create_framed_widget( ModelBrowser_constructWindow( GroupDialog_getWindow() ) ); + g_page_models = GroupDialog_addPage( "Models", GTK_WIDGET( frame ), RawStringExportCaller( "Models" ) ); + /* workaround for gtk 2.24 issue: not displayed glwidget after toggle */ + g_object_set_data( G_OBJECT( g_page_models ), "glwidget", ModelBrowser_getGLWidget() ); + } + #ifdef WIN32 if ( g_multimon_globals.m_bStartOnPrimMon ) { PositionWindowOnPrimaryScreen( g_layout_globals.m_position ); @@ -3266,7 +3281,7 @@ void MainFrame::Create(){ GtkFrame* frame = create_framed_widget( TextureBrowser_constructWindow( GroupDialog_getWindow() ) ); g_page_textures = GroupDialog_addPage( "Textures", GTK_WIDGET( frame ), TextureBrowserExportTitleCaller() ); /* workaround for gtk 2.24 issue: not displayed glwidget after toggle */ - g_object_set_data( G_OBJECT( GroupDialog_getWindow() ), "glwidget", TextureBrowser_getGLWidget() ); + g_object_set_data( G_OBJECT( g_page_textures ), "glwidget", TextureBrowser_getGLWidget() ); } m_vSplit = 0; @@ -3305,7 +3320,7 @@ void MainFrame::Create(){ GtkFrame* frame = create_framed_widget( TextureBrowser_constructWindow( GroupDialog_getWindow() ) ); g_page_textures = GroupDialog_addPage( "Textures", GTK_WIDGET( frame ), TextureBrowserExportTitleCaller() ); /* workaround for gtk 2.24 issue: not displayed glwidget after toggle */ - g_object_set_data( G_OBJECT( GroupDialog_getWindow() ), "glwidget", TextureBrowser_getGLWidget() ); + g_object_set_data( G_OBJECT( g_page_textures ), "glwidget", TextureBrowser_getGLWidget() ); } } @@ -3392,6 +3407,7 @@ void MainFrame::Shutdown(){ delete m_pXZWnd; m_pXZWnd = 0; + ModelBrowser_destroyWindow(); TextureBrowser_destroyWindow(); DeleteCamWnd( m_pCamWnd ); @@ -3605,6 +3621,7 @@ void MainFrame_Construct(){ GlobalCommands_insert( "ToggleConsole", FreeCaller(), Accelerator( 'O' ) ); GlobalCommands_insert( "ToggleEntityInspector", FreeCaller(), Accelerator( 'N' ) ); + GlobalCommands_insert( "ToggleModelBrowser", FreeCaller(), Accelerator( '/' ) ); GlobalCommands_insert( "EntityList", FreeCaller(), Accelerator( 'L' ) ); // GlobalCommands_insert( "ShowHidden", FreeCaller(), Accelerator( 'H', (GdkModifierType)GDK_SHIFT_MASK ) ); diff --git a/radiant/map.cpp b/radiant/map.cpp index 5558bc6d..ff085b9d 100644 --- a/radiant/map.cpp +++ b/radiant/map.cpp @@ -409,6 +409,7 @@ void RemoveRegionBrushes( void ); free all map elements, reinitialize the structures that depend on them ================ */ +#include "modelwindow.h" void Map_Free(){ Map_RegionOff(); Select_ShowAllHidden(); @@ -419,6 +420,8 @@ void Map_Free(){ GlobalReferenceCache().release( g_map.m_name.c_str() ); g_map.m_resource = 0; + ModelBrowser_flushReferences(); + FlushReferences(); g_currentMap = 0; diff --git a/radiant/modelwindow.cpp b/radiant/modelwindow.cpp new file mode 100644 index 00000000..e1cfd6e1 --- /dev/null +++ b/radiant/modelwindow.cpp @@ -0,0 +1,1413 @@ +/* + 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 "modelwindow.h" + +#include +#include +#include "ifiletypes.h" +#include "ifilesystem.h" +#include "iarchive.h" +#include "imodel.h" +#include "igl.h" +#include "irender.h" +#include "renderable.h" +#include "render.h" +#include "renderer.h" +#include "view.h" +#include "os/path.h" +#include "string/string.h" +#include "stringio.h" +#include "stream/stringstream.h" +#include "generic/callback.h" + +#include +#include "gtkutil/glwidget.h" +#include "gtkutil/button.h" +#include "gtkutil/toolbar.h" +#include "gtkutil/cursor.h" +#include "gtkmisc.h" + +#include "mainframe.h" +#include "camwindow.h" +#include "grid.h" +#include "instancelib.h" +#include "traverselib.h" +#include "selectionlib.h" + + +/* specialized copy of class CompiledGraph */ +class ModelGraph final : public scene::Graph, public scene::Instantiable::Observer +{ +typedef std::map InstanceMap; + +InstanceMap m_instances; +scene::Path m_rootpath; + +scene::Instantiable::Observer& m_observer; + +public: + +ModelGraph( scene::Instantiable::Observer& observer ) : m_observer( observer ){ +} + +void addSceneChangedCallback( const SignalHandler& handler ){ + ASSERT_MESSAGE( 0, "Reached unreachable: addSceneChangedCallback()" ); +} +void sceneChanged(){ + ASSERT_MESSAGE( 0, "Reached unreachable: sceneChanged()" ); +} + +scene::Node& root(){ + ASSERT_MESSAGE( !m_rootpath.empty(), "scenegraph root does not exist" ); + return m_rootpath.top(); +} +void insert_root( scene::Node& root ){ + //globalOutputStream() << "insert_root\n"; + + ASSERT_MESSAGE( m_rootpath.empty(), "scenegraph root already exists" ); + + root.IncRef(); + + Node_traverseSubgraph( root, InstanceSubgraphWalker( this, scene::Path(), 0 ) ); + + m_rootpath.push( makeReference( root ) ); +} +void erase_root(){ + //globalOutputStream() << "erase_root\n"; + + ASSERT_MESSAGE( !m_rootpath.empty(), "scenegraph root does not exist" ); + + scene::Node& root = m_rootpath.top(); + + m_rootpath.pop(); + + Node_traverseSubgraph( root, UninstanceSubgraphWalker( this, scene::Path() ) ); + + root.DecRef(); +} +void boundsChanged(){ + ASSERT_MESSAGE( 0, "Reached unreachable: boundsChanged()" ); +} + +void traverse( const Walker& walker ){ + ASSERT_MESSAGE( 0, "Reached unreachable: traverse()" ); +} + +void traverse_subgraph( const Walker& walker, const scene::Path& start ){ + ASSERT_MESSAGE( 0, "Reached unreachable: traverse_subgraph()" ); +} + +scene::Instance* find( const scene::Path& path ){ + ASSERT_MESSAGE( 0, "Reached unreachable: find()" ); + return nullptr; +} + +void insert( scene::Instance* instance ){ + m_instances.insert( InstanceMap::value_type( PathConstReference( instance->path() ), instance ) ); + m_observer.insert( instance ); +} +void erase( scene::Instance* instance ){ + m_instances.erase( PathConstReference( instance->path() ) ); + m_observer.erase( instance ); +} + +SignalHandlerId addBoundsChangedCallback( const SignalHandler& boundsChanged ){ + ASSERT_MESSAGE( 0, "Reached unreachable: addBoundsChangedCallback()" ); + return Handle>( nullptr ); +} +void removeBoundsChangedCallback( SignalHandlerId id ){ + ASSERT_MESSAGE( 0, "Reached unreachable: removeBoundsChangedCallback()" ); +} + +TypeId getNodeTypeId( const char* name ){ + ASSERT_MESSAGE( 0, "Reached unreachable: getNodeTypeId()" ); + return 0; +} + +TypeId getInstanceTypeId( const char* name ){ + ASSERT_MESSAGE( 0, "Reached unreachable: getInstanceTypeId()" ); + return 0; +} + +void clear(){ + DeleteSubgraph( root() ); +} + +}; + +/* specialized copy of class TraversableNodeSet */ +/// \brief A sequence of node references which notifies an observer of inserts and deletions, and uses the global undo system to provide undo for modifications. +class TraversableModelNodeSet : public scene::Traversable +{ +UnsortedNodeSet m_children; +Observer* m_observer; + +void copy( const TraversableModelNodeSet& other ){ + m_children = other.m_children; +} +void notifyInsertAll(){ + if ( m_observer ) { + for ( UnsortedNodeSet::iterator i = m_children.begin(); i != m_children.end(); ++i ) + { + m_observer->insert( *i ); + } + } +} +void notifyEraseAll(){ + if ( m_observer ) { + for ( UnsortedNodeSet::iterator i = m_children.begin(); i != m_children.end(); ++i ) + { + m_observer->erase( *i ); + } + } +} +public: +TraversableModelNodeSet() + : m_observer( 0 ){ +} +TraversableModelNodeSet( const TraversableModelNodeSet& other ) + : scene::Traversable( other ), m_observer( 0 ){ + copy( other ); + notifyInsertAll(); +} +~TraversableModelNodeSet(){ + notifyEraseAll(); +} +TraversableModelNodeSet& operator=( const TraversableModelNodeSet& other ){ +#if 1 // optimised change-tracking using diff algorithm + if ( m_observer ) { + nodeset_diff( m_children, other.m_children, m_observer ); + } + copy( other ); +#else + TraversableModelNodeSet tmp( other ); + tmp.swap( *this ); +#endif + return *this; +} +void swap( TraversableModelNodeSet& other ){ + std::swap( m_children, other.m_children ); + std::swap( m_observer, other.m_observer ); +} + +void attach( Observer* observer ){ + ASSERT_MESSAGE( m_observer == 0, "TraversableModelNodeSet::attach: observer cannot be attached" ); + m_observer = observer; + notifyInsertAll(); +} +void detach( Observer* observer ){ + ASSERT_MESSAGE( m_observer == observer, "TraversableModelNodeSet::detach: observer cannot be detached" ); + notifyEraseAll(); + m_observer = 0; +} +/// \brief \copydoc scene::Traversable::insert() +void insert( scene::Node& node ){ + ASSERT_MESSAGE( (volatile intptr_t)&node != 0, "TraversableModelNodeSet::insert: sanity check failed" ); + + ASSERT_MESSAGE( m_children.find( NodeSmartReference( node ) ) == m_children.end(), "TraversableModelNodeSet::insert - element already exists" ); + + m_children.insert( NodeSmartReference( node ) ); + + if ( m_observer ) { + m_observer->insert( node ); + } +} +/// \brief \copydoc scene::Traversable::erase() +void erase( scene::Node& node ){ + ASSERT_MESSAGE( (volatile intptr_t)&node != 0, "TraversableModelNodeSet::erase: sanity check failed" ); + + ASSERT_MESSAGE( m_children.find( NodeSmartReference( node ) ) != m_children.end(), "TraversableModelNodeSet::erase - failed to find element" ); + + if ( m_observer ) { + m_observer->erase( node ); + } + + m_children.erase( NodeSmartReference( node ) ); +} +/// \brief \copydoc scene::Traversable::traverse() +void traverse( const Walker& walker ){ + UnsortedNodeSet::iterator i = m_children.begin(); + while ( i != m_children.end() ) + { + // post-increment the iterator + Node_traverseSubgraph( *i++, walker ); + // the Walker can safely remove the current node from + // this container without invalidating the iterator + } +} +/// \brief \copydoc scene::Traversable::empty() +bool empty() const { + return m_children.empty(); +} +}; + +/* specialized copy of class MapRoot */ +class ModelGraphRoot : public scene::Node::Symbiot, public scene::Instantiable, public scene::Traversable::Observer +{ +class TypeCasts +{ +NodeTypeCastTable m_casts; +public: +TypeCasts(){ + NodeStaticCast::install( m_casts ); + NodeContainedCast::install( m_casts ); + NodeContainedCast::install( m_casts ); +} +NodeTypeCastTable& get(){ + return m_casts; +} +}; + +scene::Node m_node; +IdentityTransform m_transform; +TraversableModelNodeSet m_traverse; +InstanceSet m_instances; +public: +typedef LazyStatic StaticTypeCasts; + +scene::Traversable& get( NullType){ + return m_traverse; +} +TransformNode& get( NullType){ + return m_transform; +} + +ModelGraphRoot() : m_node( this, this, StaticTypeCasts::instance().get() ){ + m_node.m_isRoot = true; + + m_traverse.attach( this ); +} +~ModelGraphRoot(){ +} +void release(){ + m_traverse.detach( this ); + delete this; +} +scene::Node& node(){ + return m_node; +} + +void insert( scene::Node& child ){ + m_instances.insert( child ); +} +void erase( scene::Node& child ){ + m_instances.erase( child ); +} + +scene::Node& clone() const { + return ( new ModelGraphRoot( *this ) )->node(); +} + +scene::Instance* create( const scene::Path& path, scene::Instance* parent ){ + return new SelectableInstance( path, parent ); +} +void forEachInstance( const scene::Instantiable::Visitor& visitor ){ + m_instances.forEachInstance( visitor ); +} +void insert( scene::Instantiable::Observer* observer, const scene::Path& path, scene::Instance* instance ){ + m_instances.insert( observer, path, instance ); +} +scene::Instance* erase( scene::Instantiable::Observer* observer, const scene::Path& path ){ + return m_instances.erase( observer, path ); +} +}; + + + + + +#include "../plugins/entity/model.h" + +class ModelNode : + public scene::Node::Symbiot, + public scene::Instantiable, + public scene::Traversable::Observer +{ +class TypeCasts +{ +NodeTypeCastTable m_casts; +public: +TypeCasts(){ + NodeStaticCast::install( m_casts ); + NodeContainedCast::install( m_casts ); + NodeContainedCast::install( m_casts ); +} +NodeTypeCastTable& get(){ + return m_casts; +} +}; + + +scene::Node m_node; +InstanceSet m_instances; +SingletonModel m_model; +MatrixTransform m_transform; + +void construct(){ + m_model.attach( this ); +} +void destroy(){ + m_model.detach( this ); +} + +public: +typedef LazyStatic StaticTypeCasts; + +scene::Traversable& get( NullType){ + return m_model.getTraversable(); +} +TransformNode& get( NullType){ + return m_transform; +} + +ModelNode() : + m_node( this, this, StaticTypeCasts::instance().get() ){ + construct(); +} +ModelNode( const ModelNode& other ) : + scene::Node::Symbiot( other ), + scene::Instantiable( other ), + scene::Traversable::Observer( other ), + m_node( this, this, StaticTypeCasts::instance().get() ){ + construct(); +} +~ModelNode(){ + destroy(); +} + +void release(){ + delete this; +} +scene::Node& node(){ + return m_node; +} + +void insert( scene::Node& child ){ + m_instances.insert( child ); +} +void erase( scene::Node& child ){ + m_instances.erase( child ); +} + +scene::Instance* create( const scene::Path& path, scene::Instance* parent ){ + return new SelectableInstance( path, parent ); +} +void forEachInstance( const scene::Instantiable::Visitor& visitor ){ + m_instances.forEachInstance( visitor ); +} +void insert( scene::Instantiable::Observer* observer, const scene::Path& path, scene::Instance* instance ){ + m_instances.insert( observer, path, instance ); +} +scene::Instance* erase( scene::Instantiable::Observer* observer, const scene::Path& path ){ + return m_instances.erase( observer, path ); +} + +void setModel( const char* modelname ){ + m_model.modelChanged( modelname ); +} +}; + + +ModelGraph* g_modelGraph = nullptr; + + + + + +void ModelGraph_clear(){ + g_modelGraph->clear(); +} + + + + +class ModelFS +{ +public: + const CopiedString m_folderName; + ModelFS() = default; + ModelFS( const StringRange range ) : m_folderName( range ){ + } + bool operator<( const ModelFS& other ) const { + return string_less( m_folderName.c_str(), other.m_folderName.c_str() ); + } + mutable std::set m_folders; + mutable std::set m_files; + void insert( const char* filepath ) const { + const char* slash = strchr( filepath, '/' ); + if( slash == nullptr ){ + m_files.emplace( filepath ); + } + else{ + m_folders.emplace( StringRange( filepath, slash ) ).first->insert( slash + 1 ); + } + } +}; + +class CellPos +{ + const int m_cellSize; //half size of model square (radius) + const int m_fontHeight; + const int m_plusWidth; //pre offset on the left + const int m_plusHeight; //above text + const int m_cellsInRow; + + int m_index = 0; +public: + CellPos( int width, int cellSize, int fontHeight ) + : m_cellSize( cellSize ), m_fontHeight( fontHeight ), + m_plusWidth( 8 ), + m_plusHeight( 4 ), + m_cellsInRow( std::max( 1, ( width - m_plusWidth ) / ( m_cellSize * 2 + m_plusWidth ) ) ){ + } + void operator++(){ + ++m_index; + } + Vector3 getOrigin( int index ) const { // origin of model square + const int x = ( index % m_cellsInRow ) * m_cellSize * 2 + m_cellSize + ( index % m_cellsInRow + 1 ) * m_plusWidth; + const int z = ( index / m_cellsInRow ) * m_cellSize * 2 + m_cellSize + ( index / m_cellsInRow + 1 ) * ( m_fontHeight + m_plusHeight ); + return Vector3( x, 0, -z ); + } + Vector3 getOrigin() const { // origin of model square + return getOrigin( m_index ); + } + Vector3 getTextPos( int index ) const { + const int x = ( index % m_cellsInRow ) * m_cellSize * 2 + ( index % m_cellsInRow + 1 ) * m_plusWidth; + const int z = ( index / m_cellsInRow ) * m_cellSize * 2 + ( index / m_cellsInRow + 1 ) * ( m_fontHeight + m_plusHeight ) - 1; + return Vector3( x, 0, -z ); + } + Vector3 getTextPos() const { + return getTextPos( m_index ); + } + int getCellSize() const { + return m_cellSize; + } + int totalHeight( int height, int cellCount ) const { + return std::max( height, ( ( cellCount - 1 ) / m_cellsInRow + 1 ) * ( m_cellSize * 2 + m_fontHeight + m_plusHeight ) + m_fontHeight ); + } + int testSelect( int x, int z ) const { // index of cell at ( x, z ) + return std::min( m_cellsInRow - 1, ( x / ( m_cellSize * 2 + m_plusWidth ) ) ) + ( m_cellsInRow * ( z / ( m_cellSize * 2 + m_fontHeight + m_plusHeight ) ) ); + } +}; + +void ModelBrowser_scrollChanged( void* data, gdouble value ); + +class ModelBrowser : public scene::Instantiable::Observer +{ + // track instances in the order of insertion + std::vector m_modelInstances; +public: + ModelFS m_modelFS; + CopiedString m_prefFoldersToLoad = "*models/99*"; + ModelBrowser() : m_scrollAdjustment( ModelBrowser_scrollChanged, this ){ + } + ~ModelBrowser(){ + } + + FBO* m_fbo = nullptr; + FBO* fbo_get(){ + return m_fbo = m_fbo? m_fbo : GlobalOpenGL().support_ARB_framebuffer_object? new FBO : new FBO_fallback; + } + const int m_MSAA = 8; + + GtkWindow* m_parent = nullptr; + GtkWidget* m_gl_widget = nullptr; + GtkWidget* m_gl_scroll = nullptr; + GtkWidget* m_treeViewTree = nullptr; + + guint m_sizeHandler; + guint m_exposeHandler; + int m_width; + int m_height; + + int m_originZ = 0; // <= 0 + DeferredAdjustment m_scrollAdjustment; + + int m_cellSize = 80; + + CopiedString m_currentFolderPath; + const ModelFS* m_currentFolder = nullptr; + int m_currentModelId = -1; // selected model index in m_modelInstances, m_currentFolder->m_files; these must be in sync! + + CellPos constructCellPos() const { + return CellPos( m_width, m_cellSize, GlobalOpenGL().m_font->getPixelHeight() ); + } + void testSelect( int x, int z ){ + m_currentModelId = constructCellPos().testSelect( x, z - m_originZ ); + if( m_currentModelId >= static_cast( m_modelInstances.size() ) ) + m_currentModelId = -1; + } +private: + int totalHeight() const { + return constructCellPos().totalHeight( m_height, m_modelInstances.size() ); + } + void updateScroll() const { + GtkAdjustment *vadjustment = gtk_range_get_adjustment( GTK_RANGE( m_gl_scroll ) ); + + vadjustment->value = -m_originZ; + vadjustment->page_size = m_height; + vadjustment->page_increment = m_height / 2; + vadjustment->step_increment = 20; + vadjustment->lower = 0; + vadjustment->upper = totalHeight(); + + g_signal_emit_by_name( G_OBJECT( vadjustment ), "changed" ); + } +public: + void setOriginZ( int origin ){ + m_originZ = origin; + m_originInvalid = true; + queueDraw(); + } + void queueDraw() const { + if ( m_gl_widget != nullptr ) + gtk_widget_queue_draw( m_gl_widget ); + } + bool m_originInvalid = true; + void validate(){ + if( m_originInvalid ){ + m_originInvalid = false; + const int lowest = std::min( m_height - totalHeight(), 0 ); + m_originZ = std::max( lowest, std::min( m_originZ, 0 ) ); + updateScroll(); + } + } + +private: + FreezePointer m_freezePointer; + bool m_move_started = false; +public: + int m_move_amount; + static void trackingDelta( int x, int y, unsigned int state, void* data ){ + ModelBrowser& modelBrowser = *reinterpret_cast( data ); + modelBrowser.m_move_amount += std::abs( x ) + std::abs( y ); + if ( ( state & GDK_BUTTON3_MASK ) && y != 0 ) { // scroll view + const int scale = ( state & GDK_SHIFT_MASK )? 4 : 1; + modelBrowser.setOriginZ( modelBrowser.m_originZ + y * scale ); + } + else if ( ( state & GDK_BUTTON1_MASK ) && ( x != 0 || y != 0 ) && modelBrowser.m_currentModelId >= 0 ) { // rotate selected model + ASSERT_MESSAGE( modelBrowser.m_currentModelId < static_cast( modelBrowser.m_modelInstances.size() ), "modelBrowser.m_currentModelId out of range" ); + scene::Instance *instance = modelBrowser.m_modelInstances[modelBrowser.m_currentModelId]; + if( TransformNode *transformNode = Node_getTransformNode( instance->path().parent() ) ){ + Matrix4 rot( g_matrix4_identity ); + matrix4_pivoted_rotate_by_euler_xyz_degrees( rot, Vector3( y, 0, x ) * ( 45.f / modelBrowser.m_cellSize ), modelBrowser.constructCellPos().getOrigin( modelBrowser.m_currentModelId ) ); + matrix4_premultiply_by_matrix4( const_cast( transformNode->localToParent() ), rot ); + instance->parent()->transformChangedLocal(); + instance->transformChangedLocal(); + modelBrowser.queueDraw(); + } + } + } + void tracking_MouseUp(){ + if( m_move_started ){ + m_move_started = false; + m_freezePointer.unfreeze_pointer( m_parent, false ); + } + } + void tracking_MouseDown(){ + tracking_MouseUp(); + m_move_started = true; + m_move_amount = 0; + m_freezePointer.freeze_pointer( m_parent, m_gl_widget, trackingDelta, this ); + } + + void insert( scene::Instance* instance ) override { + if( instance->path().size() == 3 ) + m_modelInstances.push_back( instance ); + } + void erase( scene::Instance* instance ) override { // just invalidate everything (also happens on resource flush and refresh) //FIXME: redraw on resource refresh + m_modelInstances.clear(); + m_currentFolder = nullptr; + m_originZ = 0; + m_originInvalid = true; + } + template + void forEachModelInstance( const Functor& functor ) const { + for( scene::Instance* instance : m_modelInstances ) + functor( instance ); + } +}; + +ModelBrowser g_ModelBrowser; + + + + + +class models_set_transforms +{ + mutable CellPos m_cellPos = g_ModelBrowser.constructCellPos(); +public: + void operator()( scene::Instance* instance ) const { + if( TransformNode *transformNode = Node_getTransformNode( instance->path().parent() ) ){ + if( Bounded *bounded = Instance_getBounded( *instance ) ){ + AABB aabb = bounded->localAABB(); + const float scale = m_cellPos.getCellSize() / aabb.extents[ vector3_max_abs_component_index( aabb.extents ) ]; + aabb.extents.z() *= 2; // prioritize Z for orientation + const Matrix4 rotation = matrix4_rotation_for_euler_xyz_degrees( + vector3_min_abs_component_index( aabb.extents ) == 0? Vector3( 0, 0, -90 ) + : vector3_min_abs_component_index( aabb.extents ) == 2? Vector3( 90, 0, 0 ) + : g_vector3_identity ); + const_cast( transformNode->localToParent() ) = matrix4_multiplied_by_matrix4( + matrix4_translation_for_vec3( m_cellPos.getOrigin() ), + matrix4_multiplied_by_matrix4( + rotation, + matrix4_multiplied_by_matrix4( + matrix4_scale_for_vec3( Vector3( scale, scale, scale ) ), + matrix4_translation_for_vec3( -aabb.origin ) + ) + ) + ); + instance->parent()->transformChangedLocal(); + instance->transformChangedLocal(); +//% globalOutputStream() << transformNode->localToParent() << " transformNode->localToParent()\n"; + ++m_cellPos; + } + } + } +}; + + +class ModelRenderer : public Renderer +{ +struct state_type +{ + state_type() : + m_state( 0 ){ + } + Shader* m_state; +}; +public: +ModelRenderer( RenderStateFlags globalstate ) : + m_globalstate( globalstate ){ + m_state_stack.push_back( state_type() ); +} + +void SetState( Shader* state, EStyle style ){ + ASSERT_NOTNULL( state ); + if ( style == eFullMaterials ) { + m_state_stack.back().m_state = state; + } +} +EStyle getStyle() const { + return eFullMaterials; +} +void PushState(){ + m_state_stack.push_back( m_state_stack.back() ); +} +void PopState(){ + ASSERT_MESSAGE( !m_state_stack.empty(), "popping empty stack" ); + m_state_stack.pop_back(); +} +void Highlight( EHighlightMode mode, bool bEnable = true ){ +} +void addRenderable( const OpenGLRenderable& renderable, const Matrix4& localToWorld ){ + m_state_stack.back().m_state->addRenderable( renderable, localToWorld ); +} + +void render( const Matrix4& modelview, const Matrix4& projection ){ + GlobalShaderCache().render( m_globalstate, modelview, projection ); +} +private: +std::vector m_state_stack; +RenderStateFlags m_globalstate; +}; + +/* +x=0, z<=0 +origin -----> +x + | -- + | | | + | -- + \/ -z +*/ +void ModelBrowser_render(){ + g_ModelBrowser.validate(); + + g_ModelBrowser.fbo_get()->start(); + + const int W = g_ModelBrowser.m_width; + const int H = g_ModelBrowser.m_height; + glViewport( 0, 0, W, H ); + + // enable depth buffer writes + glDepthMask( GL_TRUE ); + glPolygonMode( GL_FRONT_AND_BACK, GL_FILL ); + + glClearColor( .25f, .25f, .25f, 0 ); + glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT ); + + const unsigned int globalstate = RENDER_DEPTHTEST + | RENDER_COLOURWRITE + | RENDER_DEPTHWRITE + | RENDER_ALPHATEST + | RENDER_BLEND + | RENDER_CULLFACE + | RENDER_COLOURARRAY + | RENDER_POLYGONSMOOTH + | RENDER_LINESMOOTH + | RENDER_FOG + | RENDER_COLOURCHANGE + | RENDER_FILL + | RENDER_LIGHTING + | RENDER_TEXTURE + | RENDER_SMOOTH + | RENDER_SCALED; + + + Matrix4 m_projection; + + m_projection[0] = 1.0f / static_cast( W / 2.f ); + m_projection[5] = 1.0f / static_cast( H / 2.f ); + m_projection[10] = 1.0f / ( 9999 ); + + m_projection[12] = 0.0f; + m_projection[13] = 0.0f; + m_projection[14] = -1.0f; + + m_projection[1] = + m_projection[2] = + m_projection[3] = + + m_projection[4] = + m_projection[6] = + m_projection[7] = + + m_projection[8] = + m_projection[9] = + m_projection[11] = 0.0f; + + m_projection[15] = 1.0f; + + + Matrix4 m_modelview; + // translation + m_modelview[12] = -W / 2.f; + m_modelview[13] = H / 2.f - g_ModelBrowser.m_originZ; + m_modelview[14] = 9999; + + // axis base + //XZ: + m_modelview[0] = 1; + m_modelview[1] = 0; + m_modelview[2] = 0; + + m_modelview[4] = 0; + m_modelview[5] = 0; + m_modelview[6] = 1; + + m_modelview[8] = 0; + m_modelview[9] = 1; + m_modelview[10] = 0; + + + m_modelview[3] = m_modelview[7] = m_modelview[11] = 0; + m_modelview[15] = 1; + + + + View m_view( true ); + m_view.Construct( m_projection, m_modelview, W, H ); + + + glMatrixMode( GL_PROJECTION ); + glLoadMatrixf( reinterpret_cast( &m_projection ) ); + + glMatrixMode( GL_MODELVIEW ); + glLoadMatrixf( reinterpret_cast( &m_modelview ) ); + + + if( g_ModelBrowser.m_currentFolder != nullptr ){ + { // prepare for 2d stuff + glDisable( GL_BLEND ); + + if ( GlobalOpenGL().GL_1_3() ) { + glClientActiveTexture( GL_TEXTURE0 ); + glActiveTexture( GL_TEXTURE0 ); + } + + glDisableClientState( GL_TEXTURE_COORD_ARRAY ); + glDisableClientState( GL_NORMAL_ARRAY ); + glDisableClientState( GL_COLOR_ARRAY ); + + glDisable( GL_TEXTURE_2D ); + glDisable( GL_LIGHTING ); + glDisable( GL_COLOR_MATERIAL ); + glDisable( GL_DEPTH_TEST ); + } + + { // brighter background squares + glColor4f( 0.3f, 0.3f, 0.3f, 1.f ); + glDepthMask( GL_FALSE ); + glPolygonMode( GL_FRONT_AND_BACK, GL_FILL ); + glDisable( GL_CULL_FACE ); + + CellPos cellPos = g_ModelBrowser.constructCellPos(); + glBegin( GL_QUADS ); + for( std::size_t i = g_ModelBrowser.m_currentFolder->m_files.size(); i != 0; --i ){ + const Vector3 origin = cellPos.getOrigin(); + const float minx = origin.x() - cellPos.getCellSize(); + const float maxx = origin.x() + cellPos.getCellSize(); + const float minz = origin.z() - cellPos.getCellSize(); + const float maxz = origin.z() + cellPos.getCellSize(); + glVertex3f( minx, 0, maxz ); + glVertex3f( minx, 0, minz ); + glVertex3f( maxx, 0, minz ); + glVertex3f( maxx, 0, maxz ); + ++cellPos; + } + glEnd(); + } + + // one directional light source directly behind the viewer + { + GLfloat inverse_cam_dir[4], ambient[4], diffuse[4]; + + ambient[0] = ambient[1] = ambient[2] = 0.4f; + ambient[3] = 1.0f; + diffuse[0] = diffuse[1] = diffuse[2] = 0.4f; + diffuse[3] = 1.0f; + + inverse_cam_dir[0] = -m_view.getViewDir()[0]; + inverse_cam_dir[1] = -m_view.getViewDir()[1]; + inverse_cam_dir[2] = -m_view.getViewDir()[2]; + inverse_cam_dir[3] = 0; + + glLightfv( GL_LIGHT0, GL_POSITION, inverse_cam_dir ); + + glLightfv( GL_LIGHT0, GL_AMBIENT, ambient ); + glLightfv( GL_LIGHT0, GL_DIFFUSE, diffuse ); + + glEnable( GL_LIGHT0 ); + } + + { + ModelRenderer renderer( globalstate ); + + g_ModelBrowser.forEachModelInstance( [&renderer, &m_view]( scene::Instance* instance ){ + if( Renderable *renderable = Instance_getRenderable( *instance ) ) + renderable->renderSolid( renderer, m_view ); + } ); + + renderer.render( m_modelview, m_projection ); + } + + { // prepare for 2d stuff + glColor4f( 1, 1, 1, 1 ); + glDisable( GL_BLEND ); + + if ( GlobalOpenGL().GL_1_3() ) { + glClientActiveTexture( GL_TEXTURE0 ); + glActiveTexture( GL_TEXTURE0 ); + } + + glDisableClientState( GL_TEXTURE_COORD_ARRAY ); + glDisableClientState( GL_NORMAL_ARRAY ); + glDisableClientState( GL_COLOR_ARRAY ); + + glDisable( GL_TEXTURE_2D ); + glDisable( GL_LIGHTING ); + glDisable( GL_COLOR_MATERIAL ); + glDisable( GL_DEPTH_TEST ); + glLineWidth( 1 ); + } + { // render model file names + CellPos cellPos = g_ModelBrowser.constructCellPos(); + for( const CopiedString& string : g_ModelBrowser.m_currentFolder->m_files ){ + const Vector3 pos = cellPos.getTextPos(); + if( m_view.TestPoint( pos ) ){ + glRasterPos3f( pos.x(), pos.y(), pos.z() ); + GlobalOpenGL().drawString( string.c_str() ); + } + ++cellPos; + } + } + } + + // bind back to the default texture so that we don't have problems + // elsewhere using/modifying texture maps between contexts + glBindTexture( GL_TEXTURE_2D, 0 ); + + g_ModelBrowser.fbo_get()->save(); +} + + +gboolean ModelBrowser_size_allocate( GtkWidget* widget, GtkAllocation* allocation, ModelBrowser* modelBrowser ){ + modelBrowser->fbo_get()->reset( allocation->width, allocation->height, modelBrowser->m_MSAA, true ); + modelBrowser->m_width = allocation->width; + modelBrowser->m_height = allocation->height; + modelBrowser->m_originInvalid = true; + modelBrowser->forEachModelInstance( models_set_transforms() ); + modelBrowser->queueDraw(); + return FALSE; +} + +gboolean ModelBrowser_expose( GtkWidget* widget, GdkEventExpose* event, ModelBrowser* modelBrowser ){ + if ( glwidget_make_current( modelBrowser->m_gl_widget ) != FALSE ) { + GlobalOpenGL_debugAssertNoErrors(); + ModelBrowser_render(); + GlobalOpenGL_debugAssertNoErrors(); + glwidget_swap_buffers( modelBrowser->m_gl_widget ); + } + return FALSE; +} + + + + +gboolean ModelBrowser_mouseScroll( GtkWidget* widget, GdkEventScroll* event, ModelBrowser* modelBrowser ){ + gtk_widget_grab_focus( widget ); + if( !gtk_window_is_active( modelBrowser->m_parent ) ) + gtk_window_present( modelBrowser->m_parent ); + + if ( event->direction == GDK_SCROLL_UP ) { + modelBrowser->setOriginZ( modelBrowser->m_originZ + 64 ); + } + else if ( event->direction == GDK_SCROLL_DOWN ) { + modelBrowser->setOriginZ( modelBrowser->m_originZ - 64 ); + } + return FALSE; +} + +void ModelBrowser_scrollChanged( void* data, gdouble value ){ + //globalOutputStream() << "vertical scroll\n"; + reinterpret_cast( data )->setOriginZ( -(int)value ); +} + +static void ModelBrowser_scrollbarScroll( GtkAdjustment *adjustment, ModelBrowser* modelBrowser ){ + modelBrowser->m_scrollAdjustment.value_changed( adjustment->value ); +} + + +gboolean ModelBrowser_button_press( GtkWidget* widget, GdkEventButton* event, ModelBrowser* modelBrowser ){ + if ( event->type == GDK_BUTTON_PRESS ) { + gtk_widget_grab_focus( widget ); + if ( event->button == 1 || event->button == 3 ) { + modelBrowser->tracking_MouseDown(); + } + if ( event->button == 1 ) { + modelBrowser->testSelect( static_cast( event->x ), static_cast( event->y ) ); + } + } + /* create misc_model */ + else if ( event->type == GDK_2BUTTON_PRESS && event->button == 1 && modelBrowser->m_currentFolder != nullptr && modelBrowser->m_currentModelId >= 0 ) { + UndoableCommand undo( "insertModel" ); + // todo + // GlobalEntityClassManager() seach for "misc_model" + // otherwise search for entityClass->miscmodel_is + // otherwise go with GlobalEntityClassManager().findOrInsert( "misc_model", false ); + EntityClass* entityClass = GlobalEntityClassManager().findOrInsert( "misc_model", false ); + NodeSmartReference node( GlobalEntityCreator().createEntity( entityClass ) ); + + Node_getTraversable( GlobalSceneGraph().root() )->insert( node ); + + scene::Path entitypath( makeReference( GlobalSceneGraph().root() ) ); + entitypath.push( makeReference( node.get() ) ); + scene::Instance& instance = findInstance( entitypath ); + + if ( Transformable* transform = Instance_getTransformable( instance ) ) { // might be cool to consider model aabb here + transform->setType( TRANSFORM_PRIMITIVE ); + transform->setTranslation( vector3_snapped( Camera_getOrigin( *g_pParentWnd->GetCamWnd() ) - Camera_getViewVector( *g_pParentWnd->GetCamWnd() ) * 128.f, GetSnapGridSize() ) ); + transform->freezeTransform(); + } + + GlobalSelectionSystem().setSelectedAll( false ); + Instance_setSelected( instance, true ); + + StringOutputStream sstream( 128 ); + sstream << modelBrowser->m_currentFolderPath.c_str() << std::next( modelBrowser->m_currentFolder->m_files.begin(), modelBrowser->m_currentModelId )->c_str(); + Node_getEntity( node )->setKeyValue( entityClass->miscmodel_key(), sstream.c_str() ); + } + return FALSE; +} + +gboolean ModelBrowser_button_release( GtkWidget* widget, GdkEventButton* event, ModelBrowser* modelBrowser ){ + if ( event->type == GDK_BUTTON_RELEASE ) { + if ( event->button == 1 || event->button == 3 ) { + modelBrowser->tracking_MouseUp(); + } + if ( event->button == 1 && modelBrowser->m_move_amount < 16 && modelBrowser->m_currentFolder != nullptr && modelBrowser->m_currentModelId >= 0 ) { // assign model to selected entity nodes + StringOutputStream sstream( 128 ); + sstream << modelBrowser->m_currentFolderPath.c_str() << std::next( modelBrowser->m_currentFolder->m_files.begin(), modelBrowser->m_currentModelId )->c_str(); + class EntityVisitor : public SelectionSystem::Visitor + { + const char* m_filePath; + public: + EntityVisitor( const char* filePath ) : m_filePath( filePath ){ + } + void visit( scene::Instance& instance ) const override { + if( Entity* entity = Node_getEntity( instance.path().top() ) ){ + entity->setKeyValue( entity->getEntityClass().miscmodel_key(), m_filePath ); + } + } + } visitor( sstream.c_str() ); + UndoableCommand undo( "entityAssignModel" ); + GlobalSelectionSystem().foreachSelected( visitor ); + } + } + return FALSE; +} + + + +static void TreeView_onRowActivated( GtkTreeView* treeview, GtkTreePath* path, GtkTreeViewColumn* col, gpointer userdata ){ + GtkTreeModel* model = gtk_tree_view_get_model( GTK_TREE_VIEW( treeview ) ); + GtkTreeIter iter; + if ( gtk_tree_model_get_iter( model, &iter, path ) ) { + std::deque iters; + iters.push_front( iter ); + GtkTreeIter parent; + while( gtk_tree_model_iter_parent( model, &parent, &iters.front() ) ) + iters.push_front( parent ); + StringOutputStream sstream( 64 ); + const ModelFS *modelFS = &g_ModelBrowser.m_modelFS; + for( GtkTreeIter& i : iters ){ + gchar* buffer; + gtk_tree_model_get( model, &i, 0, &buffer, -1 ); + const auto found = modelFS->m_folders.find( ModelFS( StringRange( buffer, buffer + strlen( buffer ) ) ) ); + if( found != modelFS->m_folders.end() ){ // ok to not find, while loading root + modelFS = &( *found ); + sstream << buffer << "/"; + } + g_free( buffer ); + } + +//% globalOutputStream() << sstream.c_str() << " sstream.c_str()\n"; + + ModelGraph_clear(); // this goes 1st: resets m_currentFolder + + g_ModelBrowser.m_currentFolder = modelFS; + g_ModelBrowser.m_currentFolderPath = sstream.c_str(); + + ScopeDisableScreenUpdates disableScreenUpdates( g_ModelBrowser.m_currentFolderPath.c_str(), "Loading Models" ); + { + for( const CopiedString& filename : g_ModelBrowser.m_currentFolder->m_files ){ + sstream.clear(); + sstream << g_ModelBrowser.m_currentFolderPath.c_str() << filename.c_str(); + ModelNode *modelNode = new ModelNode; + modelNode->setModel( sstream.c_str() ); + NodeSmartReference node( modelNode->node() ); + Node_getTraversable( g_modelGraph->root() )->insert( node ); + } + g_ModelBrowser.forEachModelInstance( models_set_transforms() ); + } + + g_ModelBrowser.queueDraw(); + + //deactivate, so SPACE and RETURN wont be broken for 2d + gtk_window_set_focus( GTK_WINDOW( gtk_widget_get_toplevel( GTK_WIDGET( treeview ) ) ), NULL ); + } +} + + + +#if 0 +void modelFS_traverse( const ModelFS& modelFS ){ + static int depth = -1; + ++depth; + for( int i = 0; i < depth; ++i ){ + globalOutputStream() << "\t"; + } + globalOutputStream() << modelFS.m_folderName.c_str() << "\n"; + for( const ModelFS& m : modelFS.m_folders ) + modelFS_traverse( m ); + + --depth; +} +#endif +void ModelBrowser_constructTreeModel( const ModelFS& modelFS, GtkTreeStore* store, GtkTreeIter* parent ){ + GtkTreeIter iter; + gtk_tree_store_append( store, &iter, parent ); + gtk_tree_store_set( store, &iter, 0, modelFS.m_folderName.c_str(), -1 ); + for( const ModelFS& m : modelFS.m_folders ) + ModelBrowser_constructTreeModel( m, store, &iter ); //recursion +} + +typedef std::map ModelFoldersMap; + +class ModelFolders +{ +public: + ModelFoldersMap m_modelFoldersMap; + // parse string of format *pathToLoad/depth*path2ToLoad/depth* + // */depth* for root path + ModelFolders( const char* pathsString ){ + char* str = strdup( pathsString ); + for( char* s = str; *s; ++s ) + if( *s == '\\' ) + *s = '/'; + + char* start = str; + while( 1 ){ + while( *start == '*' ) + ++start; + char* end = start; + while( *end && *end != '*' ) + ++end; + if( start == end ) + break; + char* slash = nullptr; + for( char* s = start; s != end; ++s ) + if( *s == '/' ) + slash = s; + if( slash != nullptr && end - slash > 1 ){ + std::size_t depth; + Size_importString( depth, CopiedString( StringRange( slash + 1, end ) ).c_str() ); + StringRange folder( start, ( start == slash )? slash : slash + 1 ); + m_modelFoldersMap.emplace( folder, depth ); + } + start = end; + } + + free( str ); + if( m_modelFoldersMap.empty() ) + m_modelFoldersMap.emplace( "models/", 99 ); + } +}; + + +typedef std::set StringSetWithLambda; + +class ModelPaths_ArchiveVisitor : public Archive::Visitor +{ + const StringSetWithLambda& m_modelExtensions; + ModelFS& m_modelFS; +public: + const ModelFoldersMap& m_modelFoldersMap; + bool m_avoid_pk3dir; + ModelPaths_ArchiveVisitor( const StringSetWithLambda& modelExtensions, ModelFS& modelFS, const ModelFoldersMap& modelFoldersMap ) + : m_modelExtensions( modelExtensions ), m_modelFS( modelFS ), m_modelFoldersMap( modelFoldersMap ){ + } + void visit( const char* name ) override { + if( m_modelExtensions.find( path_get_extension( name ) ) != m_modelExtensions.end() && ( !m_avoid_pk3dir || !string_in_string_nocase( name, ".pk3dir/" ) ) ){ + m_modelFS.insert( name ); +//% globalOutputStream() << name << " name\n"; + } + } +}; + +void ModelPaths_addFromArchive( ModelPaths_ArchiveVisitor& visitor, const char *archiveName ){ +//% globalOutputStream() << "\t\t" << archiveName << " archiveName\n"; + Archive *archive = GlobalFileSystem().getArchive( archiveName, false ); + if ( archive != nullptr ) { + for( const auto& folder : visitor.m_modelFoldersMap ){ + /* should better avoid .pk3dir traversal right in archive implementation for normal folders */ + visitor.m_avoid_pk3dir = string_empty( folder.first.c_str() ) // root + && folder.second > 1 // deep nuff + && string_equal_suffix( archiveName, "/" ) // normal folder, not archive + && !string_equal_suffix_nocase( archiveName, ".pk3dir/" ); // not .pk3dir + archive->forEachFile( Archive::VisitorFunc( visitor, Archive::eFiles, folder.second ), folder.first.c_str() ); + } + } +} +typedef ReferenceCaller1 ModelPaths_addFromArchiveCaller; + +void ModelBrowser_constructTree(){ + g_ModelBrowser.m_modelFS.m_folders.clear(); + g_ModelBrowser.m_modelFS.m_files.clear(); + ModelGraph_clear(); + g_ModelBrowser.queueDraw(); + + class : public IFileTypeList + { + public: + StringSetWithLambda m_modelExtensions{ []( const CopiedString& lhs, const CopiedString& rhs )->bool{ + return string_less_nocase( lhs.c_str(), rhs.c_str() ); + } }; + void addType( const char* moduleName, filetype_t type ) override { + m_modelExtensions.emplace( moduleName ); + } + } typelist; + GlobalFiletypes().getTypeList( ModelLoader::Name(), &typelist, true, false, false ); + + ModelFolders modelFolders( g_ModelBrowser.m_prefFoldersToLoad.c_str() ); + + ModelPaths_ArchiveVisitor visitor( typelist.m_modelExtensions, g_ModelBrowser.m_modelFS, modelFolders.m_modelFoldersMap ); + GlobalFileSystem().forEachArchive( ModelPaths_addFromArchiveCaller( visitor ), false, false ); + +//% modelFS_traverse( g_ModelBrowser.m_modelFS ); + + + GtkTreeStore* store = gtk_tree_store_new( 1, G_TYPE_STRING ); + + { + if( !g_ModelBrowser.m_modelFS.m_files.empty() ){ // models in the root + GtkTreeIter iter; + gtk_tree_store_append( store, &iter, nullptr ); + gtk_tree_store_set( store, &iter, 0, "", -1 ); + } + + for( const ModelFS& m : g_ModelBrowser.m_modelFS.m_folders ) + ModelBrowser_constructTreeModel( m, store, nullptr ); + } + + gtk_tree_view_set_model( GTK_TREE_VIEW( g_ModelBrowser.m_treeViewTree ), GTK_TREE_MODEL( store ) ); + + g_object_unref( G_OBJECT( store ) ); +} + +GtkWidget* ModelBrowser_constructWindow( GtkWindow* toplevel ){ + g_ModelBrowser.m_parent = toplevel; + + GtkWidget* table = gtk_table_new( 1, 3, FALSE ); + GtkWidget* vbox = gtk_vbox_new( FALSE, 0 ); + gtk_table_attach( GTK_TABLE( table ), vbox, 0, 1, 0, 1, GTK_FILL, GTK_FILL, 0, 0 ); + gtk_widget_show( vbox ); + + { // menu bar + GtkToolbar* toolbar = GTK_TOOLBAR( gtk_toolbar_new() ); + gtk_box_pack_start( GTK_BOX( vbox ), GTK_WIDGET( toolbar ), FALSE, FALSE, 0 ); + + GtkButton* button = toolbar_append_button( toolbar, "Reload Model Folders Tree View", "texbro_refresh.png", FreeCaller() ); + gtk_widget_set_size_request( GTK_WIDGET( button ), 22, 22 ); + gtk_widget_show( GTK_WIDGET( toolbar ) ); + } + { // TreeView + GtkWidget* scr = gtk_scrolled_window_new( NULL, NULL ); + gtk_container_set_border_width( GTK_CONTAINER( scr ), 0 ); + // vertical only scrolling for treeview + gtk_scrolled_window_set_policy( GTK_SCROLLED_WINDOW( scr ), GTK_POLICY_NEVER, GTK_POLICY_ALWAYS ); + gtk_widget_show( scr ); + + g_ModelBrowser.m_treeViewTree = gtk_tree_view_new(); + GtkTreeView* treeview = GTK_TREE_VIEW( g_ModelBrowser.m_treeViewTree ); + //gtk_tree_view_set_enable_search( treeview, FALSE ); + + gtk_tree_view_set_headers_visible( treeview, FALSE ); + g_signal_connect( treeview, "row-activated", (GCallback) TreeView_onRowActivated, NULL ); + + GtkCellRenderer* renderer = gtk_cell_renderer_text_new(); + //g_object_set( G_OBJECT( renderer ), "ellipsize", PANGO_ELLIPSIZE_START, NULL ); + gtk_tree_view_insert_column_with_attributes( treeview, -1, "", renderer, "text", 0, NULL ); + + + ModelBrowser_constructTree(); + + + //gtk_scrolled_window_add_with_viewport( GTK_SCROLLED_WINDOW( scr ), g_ModelBrowser.m_treeViewTree ); + gtk_container_add( GTK_CONTAINER( scr ), g_ModelBrowser.m_treeViewTree ); //GtkTreeView has native scrolling support; should not be used with the GtkViewport proxy. + gtk_widget_show( g_ModelBrowser.m_treeViewTree ); + + gtk_box_pack_start( GTK_BOX( vbox ), scr, TRUE, TRUE, 0 ); + } + { // gl_widget scrollbar + GtkWidget* w = g_ModelBrowser.m_gl_scroll = gtk_vscrollbar_new( GTK_ADJUSTMENT( gtk_adjustment_new( 0, 0, 0, 1, 1, 0 ) ) ); + gtk_table_attach( GTK_TABLE( table ), w, 2, 3, 0, 1, GTK_SHRINK, GTK_FILL, 0, 0 ); + gtk_widget_show( w ); + + GtkAdjustment *vadjustment = gtk_range_get_adjustment( GTK_RANGE( g_ModelBrowser.m_gl_scroll ) ); + g_signal_connect( G_OBJECT( vadjustment ), "value_changed", G_CALLBACK( ModelBrowser_scrollbarScroll ), &g_ModelBrowser ); + } + { // gl_widget + GtkWidget* w = g_ModelBrowser.m_gl_widget = glwidget_new( TRUE ); + gtk_widget_ref( w ); + + gtk_widget_set_events( w, GDK_DESTROY | GDK_EXPOSURE_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK | GDK_SCROLL_MASK ); + GTK_WIDGET_SET_FLAGS( w, GTK_CAN_FOCUS ); + + gtk_table_attach_defaults( GTK_TABLE( table ), w, 1, 2, 0, 1 ); + gtk_widget_show( w ); + + g_ModelBrowser.m_sizeHandler = g_signal_connect( G_OBJECT( w ), "size_allocate", G_CALLBACK( ModelBrowser_size_allocate ), &g_ModelBrowser ); + g_ModelBrowser.m_exposeHandler = g_signal_connect( G_OBJECT( w ), "expose_event", G_CALLBACK( ModelBrowser_expose ), &g_ModelBrowser ); + + g_signal_connect( G_OBJECT( w ), "button_press_event", G_CALLBACK( ModelBrowser_button_press ), &g_ModelBrowser ); + g_signal_connect( G_OBJECT( w ), "button_release_event", G_CALLBACK( ModelBrowser_button_release ), &g_ModelBrowser ); + g_signal_connect( G_OBJECT( w ), "scroll_event", G_CALLBACK( ModelBrowser_mouseScroll ), &g_ModelBrowser ); + } + + //prevent focusing on filter entry or tex dirs treeview after click on tab of floating group dialog (np, if called via hotkey) + gtk_container_set_focus_chain( GTK_CONTAINER( table ), NULL ); + + return table; +} + +void ModelBrowser_destroyWindow(){ + g_signal_handler_disconnect( G_OBJECT( g_ModelBrowser.m_gl_widget ), g_ModelBrowser.m_sizeHandler ); + g_signal_handler_disconnect( G_OBJECT( g_ModelBrowser.m_gl_widget ), g_ModelBrowser.m_exposeHandler ); + + gtk_widget_unref( g_ModelBrowser.m_gl_widget ); + g_ModelBrowser.m_gl_widget = nullptr; + + delete g_ModelBrowser.m_fbo; +} + + +#include "preferencesystem.h" +#include "preferences.h" +#include "stringio.h" + +void CellSizeImport( int& oldvalue, int value ){ + if( oldvalue != value ){ + oldvalue = value; + g_ModelBrowser.forEachModelInstance( models_set_transforms() ); + g_ModelBrowser.m_originInvalid = true; + g_ModelBrowser.queueDraw(); + } +} +typedef ReferenceCaller1 CellSizeImportCaller; + +void FoldersToLoadImport( CopiedString& self, const char* value ){ + if( self != value ){ + self = value; + ModelBrowser_constructTree(); + } +} +typedef ReferenceCaller1 FoldersToLoadImportCaller; + +void ModelBrowser_constructPage( PreferenceGroup& group ){ + PreferencesPage page( group.createPage( "Model Browser", "Model Browser Preferences" ) ); + + page.appendSpinner( "Model View Size", 80.0, 16.0, 8192.0, + IntImportCallback( CellSizeImportCaller( g_ModelBrowser.m_cellSize ) ), + IntExportCallback( IntExportCaller( g_ModelBrowser.m_cellSize ) ) ); + page.appendEntry( "List of *folderToLoad/depth*", + StringImportCallback( FoldersToLoadImportCaller( g_ModelBrowser.m_prefFoldersToLoad ) ), + StringExportCallback( StringExportCaller( g_ModelBrowser.m_prefFoldersToLoad ) ) ); +} +void ModelBrowser_registerPreferencesPage(){ + PreferencesDialog_addSettingsPage( FreeCaller1() ); +} + +void ModelBrowser_Construct(){ + GlobalPreferenceSystem().registerPreference( "ModelBrowserFolders", CopiedStringImportStringCaller( g_ModelBrowser.m_prefFoldersToLoad ), CopiedStringExportStringCaller( g_ModelBrowser.m_prefFoldersToLoad ) ); + GlobalPreferenceSystem().registerPreference( "ModelBrowserCellSize", IntImportStringCaller( g_ModelBrowser.m_cellSize ), IntExportStringCaller( g_ModelBrowser.m_cellSize ) ); + + ModelBrowser_registerPreferencesPage(); + + g_modelGraph = new ModelGraph( g_ModelBrowser ); + g_modelGraph->insert_root( ( new ModelGraphRoot )->node() ); +} + +void ModelBrowser_Destroy(){ + g_modelGraph->erase_root(); + delete g_modelGraph; +} + +GtkWidget* ModelBrowser_getGLWidget(){ + return g_ModelBrowser.m_gl_widget; +} + +void ModelBrowser_flushReferences(){ + ModelGraph_clear(); + g_ModelBrowser.queueDraw(); +} diff --git a/radiant/modelwindow.h b/radiant/modelwindow.h new file mode 100644 index 00000000..5c7afeee --- /dev/null +++ b/radiant/modelwindow.h @@ -0,0 +1,41 @@ +/* + 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 + */ + +#if !defined( INCLUDED_MODELWINDOW_H ) +#define INCLUDED_MODELWINDOW_H + + +void ModelBrowser_Construct(); +void ModelBrowser_Destroy(); + + +typedef struct _GtkWidget GtkWidget; +typedef struct _GtkWindow GtkWindow; + +GtkWidget* ModelBrowser_constructWindow( GtkWindow* toplevel ); +void ModelBrowser_destroyWindow(); + +GtkWidget* ModelBrowser_getGLWidget(); + +void ModelBrowser_flushReferences(); + + +#endif diff --git a/radiant/plugin.cpp b/radiant/plugin.cpp index 73d66a76..8b2d48d1 100644 --- a/radiant/plugin.cpp +++ b/radiant/plugin.cpp @@ -59,6 +59,7 @@ #include "points.h" #include "gtkmisc.h" #include "texwindow.h" +#include "modelwindow.h" #include "mainframe.h" #include "build.h" #include "mru.h" @@ -246,6 +247,7 @@ Radiant(){ XYWindow_Construct(); BuildMonitor_Construct(); TextureBrowser_Construct(); + ModelBrowser_Construct(); Entity_Construct(); Autosave_Construct(); EntityInspector_construct(); @@ -265,6 +267,7 @@ Radiant(){ EntityInspector_destroy(); Autosave_Destroy(); Entity_Destroy(); + ModelBrowser_Destroy(); TextureBrowser_Destroy(); BuildMonitor_Destroy(); XYWindow_Destroy(); diff --git a/radiant/preferences.h b/radiant/preferences.h index 96a96931..7d3aabb7 100644 --- a/radiant/preferences.h +++ b/radiant/preferences.h @@ -91,6 +91,12 @@ GtkWidget* appendEntry( const char* name, const FloatImportCallback& importCallb GtkWidget* appendEntry( const char* name, float& data ){ return m_dialog.addEntry( m_vbox, name, data ); } +GtkWidget* appendEntry( const char* name, const StringImportCallback& importCallback, const StringExportCallback& exportCallback ){ + return m_dialog.addTextEntry( m_vbox, name, importCallback, exportCallback ); +} +GtkWidget* appendEntry( const char* name, CopiedString& data ){ + return m_dialog.addEntry( m_vbox, name, data ); +} GtkWidget* appendPathEntry( const char* name, bool browse_directory, const StringImportCallback& importCallback, const StringExportCallback& exportCallback ){ return m_dialog.addPathEntry( m_vbox, name, browse_directory, importCallback, exportCallback ); } diff --git a/radiant/texwindow.cpp b/radiant/texwindow.cpp index 934cfc0d..9213fd94 100644 --- a/radiant/texwindow.cpp +++ b/radiant/texwindow.cpp @@ -133,43 +133,6 @@ bool g_TextureBrowser_enableAlpha = false; bool g_TextureBrowser_filter_searchFromStart = false; } -class DeferredAdjustment -{ -gdouble m_value; -guint m_handler; -typedef void ( *ValueChangedFunction )( void* data, gdouble value ); -ValueChangedFunction m_function; -void* m_data; - -static gboolean deferred_value_changed( gpointer data ){ - reinterpret_cast( data )->m_function( - reinterpret_cast( data )->m_data, - reinterpret_cast( data )->m_value - ); - reinterpret_cast( data )->m_handler = 0; - reinterpret_cast( data )->m_value = 0; - return FALSE; -} -public: -DeferredAdjustment( ValueChangedFunction function, void* data ) : m_value( 0 ), m_handler( 0 ), m_function( function ), m_data( data ){ -} -void flush(){ - if ( m_handler != 0 ) { - g_source_remove( m_handler ); - deferred_value_changed( this ); - } -} -void value_changed( gdouble value ){ - m_value = value; - if ( m_handler == 0 ) { - m_handler = g_idle_add( deferred_value_changed, this ); - } -} -static void adjustment_value_changed( GtkAdjustment *adjustment, DeferredAdjustment* self ){ - self->value_changed( adjustment->value ); -} -}; - class TextureBrowser;