netradiant-custom/radiant/entityinspector.cpp

1393 lines
44 KiB
C++

/*
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 "entityinspector.h"
#include "debugging/debugging.h"
#include "ientity.h"
#include "ifilesystem.h"
#include "imodel.h"
#include "iscenegraph.h"
#include "iselection.h"
#include "iundo.h"
#include <map>
#include <set>
#include <variant>
#include <gtkutil/guisettings.h>
#include <QSplitter>
#include <QTreeWidget>
#include <QHeaderView>
#include <QPlainTextEdit>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGridLayout>
#include <QScrollArea>
#include <QCheckBox>
#include "gtkutil/lineedit.h"
#include <QLabel>
#include <QPushButton>
#include <QToolButton>
#include <QKeyEvent>
#include <QApplication>
#include <QButtonGroup>
#include "gtkutil/combobox.h"
#include "os/path.h"
#include "eclasslib.h"
#include "scenelib.h"
#include "generic/callback.h"
#include "os/file.h"
#include "stream/stringstream.h"
#include "moduleobserver.h"
#include "stringio.h"
#include "gtkutil/accelerator.h"
#include "gtkutil/dialog.h"
#include "gtkutil/filechooser.h"
#include "gtkutil/messagebox.h"
#include "gtkutil/nonmodal.h"
#include "gtkutil/entry.h"
#include "qe3.h"
#include "gtkmisc.h"
#include "gtkdlgs.h"
#include "entity.h"
#include "mainframe.h"
#include "textureentry.h"
#include "groupdialog.h"
#include "select.h"
namespace
{
typedef std::map<CopiedString, CopiedString> KeyValues;
KeyValues g_selectedKeyValues;
KeyValues g_selectedDefaultKeyValues;
}
const char* SelectedEntity_getValueForKey( const char* key ){
{
KeyValues::const_iterator i = g_selectedKeyValues.find( key );
if ( i != g_selectedKeyValues.end() ) {
return ( *i ).second.c_str();
}
}
{
KeyValues::const_iterator i = g_selectedDefaultKeyValues.find( key );
if ( i != g_selectedDefaultKeyValues.end() ) {
return ( *i ).second.c_str();
}
}
return "";
}
void Scene_EntitySetKeyValue_Selected_Undoable( const char* key, const char* value ){
const auto command = StringStream( "entitySetKeyValue -key ", makeQuoted( key ), " -value ", makeQuoted( value ) );
UndoableCommand undo( command );
Scene_EntitySetKeyValue_Selected( key, value );
}
class EntityAttribute
{
public:
virtual QWidget* getWidget() const = 0;
virtual void update() = 0;
virtual void release() = 0;
};
class BooleanAttribute final : public EntityAttribute
{
const CopiedString m_key;
QCheckBox* m_check;
public:
BooleanAttribute( const char* key ) :
m_key( key ),
m_check( new QCheckBox )
{
QObject::connect( m_check, &QAbstractButton::clicked, [this](){ apply(); } );
update();
}
QWidget* getWidget() const override {
return m_check;
}
void release() override {
delete this;
}
void apply(){
Scene_EntitySetKeyValue_Selected_Undoable( m_key.c_str(), m_check->isChecked() ? "1" : "" );
}
typedef MemberCaller<BooleanAttribute, &BooleanAttribute::apply> ApplyCaller;
void update() override {
const char* value = SelectedEntity_getValueForKey( m_key.c_str() );
m_check->setChecked( atoi( value ) != 0 ); // atoi( empty ) is also 0
}
typedef MemberCaller<BooleanAttribute, &BooleanAttribute::update> UpdateCaller;
};
class StringAttribute : public EntityAttribute
{
const CopiedString m_key;
NonModalEntry *m_entry;
public:
StringAttribute( const char* key ) :
m_key( key ),
m_entry( new NonModalEntry( ApplyCaller( *this ), UpdateCaller( *this ) ) ){
}
virtual ~StringAttribute() = default;
QWidget* getWidget() const override {
return m_entry;
}
QLineEdit* getEntry() const {
return m_entry;
}
void release() override {
delete this;
}
void apply(){
const auto value = m_entry->text().toLatin1();
Scene_EntitySetKeyValue_Selected_Undoable( m_key.c_str(), value.constData() );
}
typedef MemberCaller<StringAttribute, &StringAttribute::apply> ApplyCaller;
void update() override {
m_entry->setText( SelectedEntity_getValueForKey( m_key.c_str() ) );
}
typedef MemberCaller<StringAttribute, &StringAttribute::update> UpdateCaller;
};
class ShaderAttribute : public StringAttribute
{
public:
ShaderAttribute( const char* key ) : StringAttribute( key ){
GlobalShaderEntryCompletion::instance().connect( StringAttribute::getEntry() );
}
};
class TextureAttribute : public StringAttribute
{
public:
TextureAttribute( const char* key ) : StringAttribute( key ){
if( string_empty( GlobalRadiant().getGameDescriptionKeyValue( "show_wads" ) ) )
GlobalAllShadersEntryCompletion::instance().connect( StringAttribute::getEntry() ); // with textures/
else
GlobalTextureEntryCompletion::instance().connect( StringAttribute::getEntry() ); // w/o
}
};
class ColorAttribute final : public EntityAttribute
{
const CopiedString m_key;
NonModalEntry *m_entry;
public:
ColorAttribute( const char* key ) :
m_key( key ),
m_entry( new NonModalEntry( ApplyCaller( *this ), UpdateCaller( *this ) ) ){
auto button = m_entry->addAction( QApplication::style()->standardIcon( QStyle::SP_ArrowRight ), QLineEdit::ActionPosition::TrailingPosition );
QObject::connect( button, &QAction::triggered, [this](){ browse(); } );
}
void release() override {
delete this;
}
QWidget* getWidget() const override {
return m_entry;
}
void apply(){
Scene_EntitySetKeyValue_Selected_Undoable( m_key.c_str(), m_entry->text().toLatin1().constData() );
}
typedef MemberCaller<ColorAttribute, &ColorAttribute::apply> ApplyCaller;
void update() override {
m_entry->setText( SelectedEntity_getValueForKey( m_key.c_str() ) );
}
typedef MemberCaller<ColorAttribute, &ColorAttribute::update> UpdateCaller;
void browse(){
Vector3 color( 1, 1, 1 );
string_parse_vector3( m_entry->text().toLatin1().constData(), color );
if( color_dialog( m_entry->window(), color ) ){
char buffer[64];
sprintf( buffer, "%g %g %g", color[0], color[1], color[2] );
m_entry->setText( buffer );
apply();
}
}
};
class ModelAttribute final : public EntityAttribute
{
const CopiedString m_key;
NonModalEntry *m_entry;
public:
ModelAttribute( const char* key ) :
m_key( key ),
m_entry( new NonModalEntry( ApplyCaller( *this ), UpdateCaller( *this ) ) ){
auto button = m_entry->addAction( QApplication::style()->standardIcon( QStyle::SP_DialogOpenButton ), QLineEdit::ActionPosition::TrailingPosition );
QObject::connect( button, &QAction::triggered, [this](){ browse(); } );
}
void release() override {
delete this;
}
QWidget* getWidget() const override {
return m_entry;
}
void apply(){
Scene_EntitySetKeyValue_Selected_Undoable( m_key.c_str(), m_entry->text().toLatin1().constData() );
}
typedef MemberCaller<ModelAttribute, &ModelAttribute::apply> ApplyCaller;
void update() override {
m_entry->setText( SelectedEntity_getValueForKey( m_key.c_str() ) );
}
typedef MemberCaller<ModelAttribute, &ModelAttribute::update> UpdateCaller;
void browse(){
const char *filename = misc_model_dialog( m_entry->window(), m_entry->text().toLatin1().constData() );
if ( filename != 0 ) {
m_entry->setText( filename );
apply();
}
}
};
const char* browse_sound( QWidget* parent, const char* filepath ){
StringOutputStream buffer( 256 );
if( !string_empty( filepath ) ){
const char* root = GlobalFileSystem().findFile( filepath );
if( !string_empty( root ) && file_is_directory( root ) )
buffer << root << filepath;
}
if( buffer.empty() ){
buffer << g_qeglobals.m_userGamePath << "sound/";
if ( !file_readable( buffer ) ) {
// just go to fsmain
buffer( g_qeglobals.m_userGamePath );
}
}
const char* filename = file_dialog( parent, true, "Open Sound File", buffer, "sound" );
if ( filename != 0 ) {
const char* relative = path_make_relative( filename, GlobalFileSystem().findRoot( filename ) );
if ( relative == filename ) {
globalWarningStream() << "WARNING: could not extract the relative path, using full path instead\n";
}
return relative;
}
return filename;
}
class SoundAttribute final : public EntityAttribute
{
const CopiedString m_key;
NonModalEntry *m_entry;
public:
SoundAttribute( const char* key ) :
m_key( key ),
m_entry( new NonModalEntry( ApplyCaller( *this ), UpdateCaller( *this ) ) ){
auto button = m_entry->addAction( QApplication::style()->standardIcon( QStyle::SP_MediaVolume ), QLineEdit::ActionPosition::TrailingPosition );
QObject::connect( button, &QAction::triggered, [this](){ browse(); } );
}
void release() override {
delete this;
}
QWidget* getWidget() const override {
return m_entry;
}
void apply(){
Scene_EntitySetKeyValue_Selected_Undoable( m_key.c_str(), m_entry->text().toLatin1().constData() );
}
typedef MemberCaller<SoundAttribute, &SoundAttribute::apply> ApplyCaller;
void update() override {
m_entry->setText( SelectedEntity_getValueForKey( m_key.c_str() ) );
}
typedef MemberCaller<SoundAttribute, &SoundAttribute::update> UpdateCaller;
void browse(){
const char *filename = browse_sound( m_entry->window(), m_entry->text().toLatin1().constData() );
if ( filename != 0 ) {
m_entry->setText( filename );
apply();
}
}
};
inline double angle_normalised( double angle ){
return float_mod( angle, 360.0 );
}
#include "camwindow.h"
class CamAnglesButton
{
typedef Callback1<const Vector3&> ApplyCallback;
ApplyCallback m_apply;
public:
QPushButton* m_button;
CamAnglesButton( const ApplyCallback& apply ) : m_apply( apply ), m_button( new QPushButton( "<-cam" ) ){
QObject::connect( m_button, &QAbstractButton::clicked, [this](){
Vector3 angles( Camera_getAngles( *g_pParentWnd->GetCamWnd() ) );
if( !string_equal( GlobalRadiant().getRequiredGameDescriptionKeyValue( "entities" ), "quake" ) ) /* stupid quake bug */
angles[0] = -angles[0];
m_apply( angles );
} );
}
};
inline QWidget *new_container_widget(){
QWidget *w = new QWidget;
auto l = new QHBoxLayout( w );
l->setContentsMargins( 0, 0, 0, 0 );
return w;
}
class AngleAttribute final : public EntityAttribute
{
const CopiedString m_key;
NonModalEntry* m_entry;
CamAnglesButton m_butt;
QWidget *m_hbox;
public:
AngleAttribute( const char* key ) :
m_key( key ),
m_entry( new NonModalEntry( ApplyCaller( *this ), UpdateCaller( *this ) ) ),
m_butt( ApplyVecCaller( *this ) ),
m_hbox( new_container_widget() ){
m_hbox->layout()->addWidget( m_entry );
m_hbox->layout()->addWidget( m_butt.m_button );
}
void release() override {
delete this;
}
QWidget* getWidget() const override {
return m_hbox;
}
void apply(){
const auto angle = StringStream<32>( angle_normalised( entry_get_float( m_entry ) ) );
Scene_EntitySetKeyValue_Selected_Undoable( m_key.c_str(), angle );
}
typedef MemberCaller<AngleAttribute, &AngleAttribute::apply> ApplyCaller;
void update() override {
const char* value = SelectedEntity_getValueForKey( m_key.c_str() );
if ( !string_empty( value ) ) {
const auto angle = StringStream<32>( angle_normalised( atof( value ) ) );
m_entry->setText( angle.c_str() );
}
else
{
m_entry->setText( "0" );
}
}
typedef MemberCaller<AngleAttribute, &AngleAttribute::update> UpdateCaller;
void apply( const Vector3& angles ){
entry_set_float( m_entry, angles[1] );
apply();
}
typedef MemberCaller1<AngleAttribute, const Vector3&, &AngleAttribute::apply> ApplyVecCaller;
};
class DirectionAttribute final : public EntityAttribute
{
const CopiedString m_key;
NonModalEntry* m_entry;
RadioHBox m_radio;
CamAnglesButton m_butt;
QWidget* m_hbox;
static constexpr const char *const buttons[] = { "up", "down", "yaw" };
public:
DirectionAttribute( const char* key ) :
m_key( key ),
m_entry( new NonModalEntry( ApplyCaller( *this ), UpdateCaller( *this ) ) ),
m_radio( RadioHBox_new( StringArrayRange( buttons ) ) ),
m_butt( ApplyVecCaller( *this ) ),
m_hbox( new_container_widget() ){
static_cast<QHBoxLayout*>( m_hbox->layout() )->addLayout( m_radio.m_hbox );
m_hbox->layout()->addWidget( m_entry );
m_hbox->layout()->addWidget( m_butt.m_button );
QObject::connect( m_radio.m_radio, QOverload<int>::of( &QButtonGroup::buttonClicked ), ApplyRadioCaller( *this ) );
}
void release() override {
delete this;
}
QWidget* getWidget() const override {
return m_hbox;
}
void apply(){
const auto angle = StringStream<32>( angle_normalised( entry_get_float( m_entry ) ) );
Scene_EntitySetKeyValue_Selected_Undoable( m_key.c_str(), angle );
}
typedef MemberCaller<DirectionAttribute, &DirectionAttribute::apply> ApplyCaller;
void update() override {
const char* value = SelectedEntity_getValueForKey( m_key.c_str() );
if ( !string_empty( value ) ) {
const float f = atof( value );
if ( f == -1 ) {
m_entry->setEnabled( false );
m_radio.m_radio->button( 0 )->setChecked( true );
m_entry->clear();
}
else if ( f == -2 ) {
m_entry->setEnabled( false );
m_radio.m_radio->button( 1 )->setChecked( true );
m_entry->clear();
}
else
{
m_entry->setEnabled( true );
m_radio.m_radio->button( 2 )->setChecked( true );
const auto angle = StringStream<32>( angle_normalised( f ) );
m_entry->setText( angle.c_str() );
}
}
else
{
m_radio.m_radio->button( 2 )->setChecked( true );
m_entry->setText( "0" );
}
}
typedef MemberCaller<DirectionAttribute, &DirectionAttribute::update> UpdateCaller;
void applyRadio( int id ){
if ( id == 0 ) {
Scene_EntitySetKeyValue_Selected_Undoable( m_key.c_str(), "-1" );
}
else if ( id == 1 ) {
Scene_EntitySetKeyValue_Selected_Undoable( m_key.c_str(), "-2" );
}
else if ( id == 2 ) {
apply();
}
}
typedef MemberCaller1<DirectionAttribute, int, &DirectionAttribute::applyRadio> ApplyRadioCaller;
void apply( const Vector3& angles ){
entry_set_float( m_entry, angles[1] );
apply();
}
typedef MemberCaller1<DirectionAttribute, const Vector3&, &DirectionAttribute::apply> ApplyVecCaller;
};
class AnglesEntry
{
public:
QLineEdit* m_roll;
QLineEdit* m_pitch;
QLineEdit* m_yaw;
AnglesEntry() : m_roll( 0 ), m_pitch( 0 ), m_yaw( 0 ){
}
};
class AnglesAttribute final : public EntityAttribute
{
const CopiedString m_key;
AnglesEntry m_angles;
CamAnglesButton m_butt;
QWidget* m_hbox;
public:
AnglesAttribute( const char* key ) :
m_key( key ),
m_butt( ApplyVecCaller( *this ) ),
m_hbox( new_container_widget() ){
m_hbox->layout()->addWidget( m_angles.m_pitch = new NonModalEntry( ApplyCaller( *this ), UpdateCaller( *this ) ) );
m_hbox->layout()->addWidget( m_angles.m_yaw = new NonModalEntry( ApplyCaller( *this ), UpdateCaller( *this ) ) );
m_hbox->layout()->addWidget( m_angles.m_roll = new NonModalEntry( ApplyCaller( *this ), UpdateCaller( *this ) ) );
m_hbox->layout()->addWidget( m_butt.m_button );
}
void release() override {
delete this;
}
QWidget* getWidget() const override {
return m_hbox;
}
void apply(){
const auto angles = StringStream<64>( angle_normalised( entry_get_float( m_angles.m_pitch ) ),
' ', angle_normalised( entry_get_float( m_angles.m_yaw ) ),
' ', angle_normalised( entry_get_float( m_angles.m_roll ) ) );
Scene_EntitySetKeyValue_Selected_Undoable( m_key.c_str(), angles );
}
typedef MemberCaller<AnglesAttribute, &AnglesAttribute::apply> ApplyCaller;
void update() override {
const char* value = SelectedEntity_getValueForKey( m_key.c_str() );
if ( !string_empty( value ) ) {
DoubleVector3 pitch_yaw_roll;
if ( !string_parse_vector3( value, pitch_yaw_roll ) ) {
pitch_yaw_roll = DoubleVector3( 0, 0, 0 );
}
StringOutputStream angle( 32 );
angle( angle_normalised( pitch_yaw_roll.x() ) );
m_angles.m_pitch->setText( angle.c_str() );
angle( angle_normalised( pitch_yaw_roll.y() ) );
m_angles.m_yaw->setText( angle.c_str() );
angle( angle_normalised( pitch_yaw_roll.z() ) );
m_angles.m_roll->setText( angle.c_str() );
}
else
{
m_angles.m_pitch->setText( "0" );
m_angles.m_yaw->setText( "0" );
m_angles.m_roll->setText( "0" );
}
}
typedef MemberCaller<AnglesAttribute, &AnglesAttribute::update> UpdateCaller;
void apply( const Vector3& angles ){
entry_set_float( m_angles.m_pitch, angles[0] );
entry_set_float( m_angles.m_yaw, angles[1] );
entry_set_float( m_angles.m_roll, 0 );
apply();
}
typedef MemberCaller1<AnglesAttribute, const Vector3&, &AnglesAttribute::apply> ApplyVecCaller;
};
class Vector3Entry
{
public:
QLineEdit* m_x;
QLineEdit* m_y;
QLineEdit* m_z;
Vector3Entry() : m_x( 0 ), m_y( 0 ), m_z( 0 ){
}
};
class Vector3Attribute final : public EntityAttribute
{
const CopiedString m_key;
Vector3Entry m_vector3;
QWidget* m_hbox;
public:
Vector3Attribute( const char* key ) :
m_key( key ),
m_hbox( new_container_widget() ){
m_hbox->layout()->addWidget( m_vector3.m_x = new NonModalEntry( ApplyCaller( *this ), UpdateCaller( *this ) ) );
m_hbox->layout()->addWidget( m_vector3.m_y = new NonModalEntry( ApplyCaller( *this ), UpdateCaller( *this ) ) );
m_hbox->layout()->addWidget( m_vector3.m_z = new NonModalEntry( ApplyCaller( *this ), UpdateCaller( *this ) ) );
}
void release() override {
delete this;
}
QWidget* getWidget() const override {
return m_hbox;
}
void apply(){
const auto vector3 = StringStream<64>( entry_get_float( m_vector3.m_x ),
' ', entry_get_float( m_vector3.m_y ),
' ', entry_get_float( m_vector3.m_z ) );
Scene_EntitySetKeyValue_Selected_Undoable( m_key.c_str(), vector3 );
}
typedef MemberCaller<Vector3Attribute, &Vector3Attribute::apply> ApplyCaller;
void update() override {
const char* value = SelectedEntity_getValueForKey( m_key.c_str() );
if ( !string_empty( value ) ) {
DoubleVector3 x_y_z;
if ( !string_parse_vector3( value, x_y_z ) ) {
x_y_z = DoubleVector3( 0, 0, 0 );
}
StringOutputStream buffer( 32 );
buffer( x_y_z.x() );
m_vector3.m_x->setText( buffer.c_str() );
buffer( x_y_z.y() );
m_vector3.m_y->setText( buffer.c_str() );
buffer( x_y_z.z() );
m_vector3.m_z->setText( buffer.c_str() );
}
else
{
m_vector3.m_x->setText( "0" );
m_vector3.m_y->setText( "0" );
m_vector3.m_z->setText( "0" );
}
}
typedef MemberCaller<Vector3Attribute, &Vector3Attribute::update> UpdateCaller;
};
class ListAttribute final : public EntityAttribute
{
const CopiedString m_key;
QComboBox* m_combo;
const ListAttributeType& m_type;
public:
ListAttribute( const char* key, const ListAttributeType& type ) :
m_key( key ),
m_combo( new ComboBox ),
m_type( type ){
for ( const auto&[ name, value ] : type )
{
m_combo->addItem( name.c_str() );
}
QObject::connect( m_combo, QOverload<int>::of( &QComboBox::activated ), ApplyCaller( *this ) );
}
void release() override {
delete this;
}
QWidget* getWidget() const override {
return m_combo;
}
void apply(){
// looks safe to assume that user actions wont make m_combo->currentIndex() -1
Scene_EntitySetKeyValue_Selected_Undoable( m_key.c_str(), m_type[m_combo->currentIndex()].second.c_str() );
}
typedef MemberCaller<ListAttribute, &ListAttribute::apply> ApplyCaller;
void update() override {
const char* value = SelectedEntity_getValueForKey( m_key.c_str() );
ListAttributeType::const_iterator i = m_type.findValue( value );
if ( i != m_type.end() ) {
m_combo->setCurrentIndex( static_cast<int>( std::distance( m_type.begin(), i ) ) );
}
else
{
m_combo->setCurrentIndex( 0 );
}
}
typedef MemberCaller<ListAttribute, &ListAttribute::update> UpdateCaller;
};
namespace
{
bool g_entityInspector_windowConstructed = false;
QTreeWidget* g_entityClassList;
QPlainTextEdit* g_entityClassComment;
QCheckBox* g_entitySpawnflagsCheck[MAX_FLAGS];
QLineEdit* g_entityKeyEntry;
QLineEdit* g_entityValueEntry;
QToolButton* g_focusToggleButton;
QTreeWidget* g_entprops_store;
const EntityClass* g_current_flags = 0;
const EntityClass* g_current_comment = 0;
const EntityClass* g_current_attributes = 0;
// the number of active spawnflags
int g_spawnflag_count;
// table: index, match spawnflag item to the spawnflag index (i.e. which bit)
int spawn_table[MAX_FLAGS];
// we change the layout depending on how many spawn flags we need to display
// the table is a 4x4 in which we need to put the comment box g_entityClassComment and the spawn flags..
QGridLayout* g_spawnflagsTable;
QGridLayout* g_attributeBox = nullptr;
typedef std::vector<EntityAttribute*> EntityAttributes;
EntityAttributes g_entityAttributes;
}
void GlobalEntityAttributes_clear(){
for ( EntityAttribute* attr : g_entityAttributes )
{
attr->release();
}
g_entityAttributes.clear();
}
class GetKeyValueVisitor : public Entity::Visitor
{
KeyValues& m_keyvalues;
public:
GetKeyValueVisitor( KeyValues& keyvalues )
: m_keyvalues( keyvalues ){
}
void visit( const char* key, const char* value ){
m_keyvalues.insert( KeyValues::value_type( CopiedString( key ), CopiedString( value ) ) );
}
};
void Entity_GetKeyValues( const Entity& entity, KeyValues& keyvalues, KeyValues& defaultValues ){
GetKeyValueVisitor visitor( keyvalues );
entity.forEachKeyValue( visitor );
const EntityClassAttributes& attributes = entity.getEntityClass().m_attributes;
for ( EntityClassAttributes::const_iterator i = attributes.begin(); i != attributes.end(); ++i )
{
defaultValues.insert( KeyValues::value_type( ( *i ).first, ( *i ).second.m_value ) );
}
}
void Entity_GetKeyValues_Selected( KeyValues& keyvalues, KeyValues& defaultValues ){
class EntityGetKeyValues : public SelectionSystem::Visitor
{
KeyValues& m_keyvalues;
KeyValues& m_defaultValues;
mutable std::set<Entity*> m_visited;
public:
EntityGetKeyValues( KeyValues& keyvalues, KeyValues& defaultValues )
: m_keyvalues( keyvalues ), m_defaultValues( defaultValues ){
}
void visit( scene::Instance& instance ) const {
Entity* entity = Node_getEntity( instance.path().top() );
if ( entity == 0 && instance.path().size() != 1 ) {
entity = Node_getEntity( instance.path().parent() );
}
if ( entity != 0 && m_visited.insert( entity ).second ) {
Entity_GetKeyValues( *entity, m_keyvalues, m_defaultValues );
}
}
} visitor( keyvalues, defaultValues );
GlobalSelectionSystem().foreachSelected( visitor );
}
const char* keyvalues_valueforkey( KeyValues& keyvalues, const char* key ){
KeyValues::iterator i = keyvalues.find( CopiedString( key ) );
if ( i != keyvalues.end() ) {
return ( *i ).second.c_str();
}
return "";
}
// required to store EntityClass* in QVariant
Q_DECLARE_METATYPE( EntityClass* )
class EntityClassListStoreAppend : public EntityClassVisitor
{
QTreeWidget* tree;
public:
EntityClassListStoreAppend( QTreeWidget* tree_ ) : tree( tree_ ){
}
void visit( EntityClass* e ){
auto item = new QTreeWidgetItem( tree );
item->setData( 0, Qt::ItemDataRole::DisplayRole, e->name() );
item->setData( 0, Qt::ItemDataRole::UserRole, QVariant::fromValue( e ) );
}
};
void EntityClassList_fill(){
EntityClassListStoreAppend append( g_entityClassList );
GlobalEntityClassManager().forEach( append );
}
void EntityClassList_clear(){
g_entityClassList->clear();
}
void SetComment( EntityClass* eclass ){
if ( eclass == g_current_comment ) {
return;
}
g_current_comment = eclass;
if( eclass == nullptr ){
g_entityClassComment->clear();
return;
}
g_entityClassComment->setPlainText( eclass->comments() );
{ // Catch patterns like "\nstuff :" used to describe keys and spawnflags, and make them bold for readability.
QTextCharFormat format;
format.setFontWeight( QFont::Weight::Bold );
QTextDocument *document = g_entityClassComment->document();
const QRegularExpression rx( "^\\s*\\w+(?=\\s*:)", QRegularExpression::PatternOption::MultilineOption );
for( QTextCursor cursor( document ); cursor = document->find( rx, cursor ), !cursor.isNull(); )
cursor.mergeCharFormat( format );
}
}
void EntityAttribute_setTooltip( QWidget* widget, const char* name, const char* description ){
StringOutputStream stream( 256 );
if( string_not_empty( name ) )
stream << "<b>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;" << name << "</b>&nbsp;&nbsp;&nbsp;&nbsp;";
if( string_not_empty( description ) ){
stream << "<br>" << description;
}
if( !stream.empty() )
widget->setToolTip( stream.c_str() );
}
void SpawnFlags_setEntityClass( EntityClass* eclass ){
if ( eclass == g_current_flags ) {
return;
}
g_current_flags = eclass;
g_spawnflag_count = 0;
// do a first pass to count the spawn flags, don't touch the widgets, we don't know in what state they are
for ( int i = 0; i < MAX_FLAGS; i++ )
{
if ( !string_empty( eclass->flagnames[i] ) ) {
spawn_table[g_spawnflag_count++] = i;
}
// hide all boxes
g_entitySpawnflagsCheck[i]->hide();
}
for ( int i = 0; i < g_spawnflag_count; ++i )
{
const auto str = StringStream<16>( LowerCase( eclass->flagnames[spawn_table[i]] ) );
QCheckBox *check = g_entitySpawnflagsCheck[i];
check->setText( str.c_str() );
check->show();
if( const EntityClassAttribute* attribute = eclass->flagAttributes[spawn_table[i]] ){
EntityAttribute_setTooltip( check, attribute->m_name.c_str(), attribute->m_description.c_str() );
}
}
}
void EntityClassList_selectEntityClass( EntityClass* eclass ){
const auto list = g_entityClassList->findItems( eclass->name(), Qt::MatchFlag::MatchFixedString );
g_entityClassList->setCurrentItem( !list.isEmpty()
? list.first()
: nullptr );
// g_entityClassComment is only updated via g_entityClassList selection change
// using special nullprt case to also update it on selection of unknown entity added during runtime
// hence this->EntityClassList_selection_changed()->SetComment() must handle nullptr
}
void EntityInspector_appendAttribute( const EntityClassAttributePair& attributePair, EntityAttribute& attribute ){
const char* keyname = attributePair.first.c_str(); //EntityClassAttributePair_getName( attributePair );
auto label = new QLabel( keyname );
EntityAttribute_setTooltip( label, attributePair.second.m_name.c_str(), attributePair.second.m_description.c_str() );
DialogGrid_packRow( g_attributeBox, attribute.getWidget(), label );
}
template<typename Attribute>
class StatelessAttributeCreator
{
public:
static EntityAttribute* create( const char* name ){
return new Attribute( name );
}
};
class EntityAttributeFactory
{
typedef EntityAttribute* ( *CreateFunc )( const char* name );
typedef std::map<const char*, CreateFunc, RawStringLess> Creators;
Creators m_creators;
public:
EntityAttributeFactory(){
m_creators.insert( Creators::value_type( "string", &StatelessAttributeCreator<StringAttribute>::create ) );
m_creators.insert( Creators::value_type( "array", &StatelessAttributeCreator<StringAttribute>::create ) );
m_creators.insert( Creators::value_type( "integer", &StatelessAttributeCreator<StringAttribute>::create ) );
m_creators.insert( Creators::value_type( "boolean", &StatelessAttributeCreator<BooleanAttribute>::create ) );
m_creators.insert( Creators::value_type( "real", &StatelessAttributeCreator<StringAttribute>::create ) );
m_creators.insert( Creators::value_type( "angle", &StatelessAttributeCreator<AngleAttribute>::create ) );
m_creators.insert( Creators::value_type( "direction", &StatelessAttributeCreator<DirectionAttribute>::create ) );
m_creators.insert( Creators::value_type( "vector3", &StatelessAttributeCreator<Vector3Attribute>::create ) );
m_creators.insert( Creators::value_type( "real3", &StatelessAttributeCreator<Vector3Attribute>::create ) );
m_creators.insert( Creators::value_type( "angles", &StatelessAttributeCreator<AnglesAttribute>::create ) );
m_creators.insert( Creators::value_type( "color", &StatelessAttributeCreator<ColorAttribute>::create ) );
m_creators.insert( Creators::value_type( "target", &StatelessAttributeCreator<StringAttribute>::create ) );
m_creators.insert( Creators::value_type( "targetname", &StatelessAttributeCreator<StringAttribute>::create ) );
m_creators.insert( Creators::value_type( "sound", &StatelessAttributeCreator<SoundAttribute>::create ) );
m_creators.insert( Creators::value_type( "shader", &StatelessAttributeCreator<ShaderAttribute>::create ) );
m_creators.insert( Creators::value_type( "texture", &StatelessAttributeCreator<TextureAttribute>::create ) );
m_creators.insert( Creators::value_type( "model", &StatelessAttributeCreator<ModelAttribute>::create ) );
m_creators.insert( Creators::value_type( "skin", &StatelessAttributeCreator<StringAttribute>::create ) );
}
EntityAttribute* create( const char* type, const char* name ){
Creators::iterator i = m_creators.find( type );
if ( i != m_creators.end() ) {
return ( *i ).second( name );
}
const ListAttributeType* listType = GlobalEntityClassManager().findListType( type );
if ( listType != 0 ) {
return new ListAttribute( name, *listType );
}
return 0;
}
};
typedef Static<EntityAttributeFactory> GlobalEntityAttributeFactory;
void EntityInspector_setEntityClass( EntityClass *eclass ){
EntityClassList_selectEntityClass( eclass );
SpawnFlags_setEntityClass( eclass );
if ( eclass != g_current_attributes ) {
g_current_attributes = eclass;
while( QLayoutItem *item = g_attributeBox->takeAt( 0 ) ){
delete item->widget();
delete item;
}
g_attributeBox->update(); // trigger scrollbar update
GlobalEntityAttributes_clear();
for ( const EntityClassAttributePair &pair : eclass->m_attributes )
{
EntityAttribute* attribute = GlobalEntityAttributeFactory::instance().create( pair.second.m_type.c_str(), pair.first.c_str() );
if ( attribute != 0 ) {
g_entityAttributes.push_back( attribute );
EntityInspector_appendAttribute( pair, *g_entityAttributes.back() );
}
}
}
}
void EntityInspector_updateSpawnflags(){
{
const int f = atoi( SelectedEntity_getValueForKey( "spawnflags" ) );
for ( int i = 0; i < g_spawnflag_count; ++i )
{
const bool v = !!( f & ( 1 << spawn_table[i] ) );
g_entitySpawnflagsCheck[i]->setChecked( v );
}
}
}
void EntityInspector_applySpawnflags(){
int f = 0;
for ( int i = 0; i < g_spawnflag_count; ++i )
{
const int v = g_entitySpawnflagsCheck[i]->isChecked();
f |= v << spawn_table[i];
}
char value[32] = {};
if( f != 0 )
sprintf( value, "%i", f );
{
const auto command = StringStream<64>( "entitySetSpawnflags -flags ", f );
UndoableCommand undo( command );
Scene_EntitySetKeyValue_Selected( "spawnflags", value );
}
}
void EntityInspector_updateKeyValues(){
g_selectedKeyValues.clear();
g_selectedDefaultKeyValues.clear();
Entity_GetKeyValues_Selected( g_selectedKeyValues, g_selectedDefaultKeyValues );
EntityInspector_setEntityClass( GlobalEntityClassManager().findOrInsert( keyvalues_valueforkey( g_selectedKeyValues, "classname" ), false ) );
EntityInspector_updateSpawnflags();
g_entprops_store->clear();
// Walk through list and add pairs
for ( const auto&[ key, value ] : g_selectedKeyValues )
{
g_entprops_store->addTopLevelItem( new QTreeWidgetItem( { key.c_str(), value.c_str() } ) );
}
for ( EntityAttribute *attr : g_entityAttributes )
{
attr->update();
}
}
class EntityInspectorDraw
{
IdleDraw m_idleDraw;
public:
EntityInspectorDraw() : m_idleDraw( FreeCaller<EntityInspector_updateKeyValues>( ) ){
}
void queueDraw(){
m_idleDraw.queueDraw();
}
};
EntityInspectorDraw g_EntityInspectorDraw;
void EntityInspector_keyValueChanged(){
g_EntityInspectorDraw.queueDraw();
}
void EntityInspector_selectionChanged( const Selectable& ){
EntityInspector_keyValueChanged();
}
void EntityInspector_applyKeyValue(){
// Get current selection text
const auto key = g_entityKeyEntry->text().toLatin1();
const auto value = g_entityValueEntry->text().toLatin1();
// TTimo: if you change the classname to worldspawn you won't merge back in the structural brushes but create a parasite entity
// if ( !strcmp( key.c_str(), "classname" ) && !strcmp( value.c_str(), "worldspawn" ) ) {
// qt_MessageBox( g_entityKeyEntry->window(), "Cannot change \"classname\" key back to worldspawn." );
// return;
// }
// RR2DO2: we don't want spaces and special symbols in entity keys
if ( std::any_of( key.cbegin(), key.cend(), []( const char c ){ return strchr( " \n\r\t\v\"", c ) != nullptr; } ) ) {
qt_MessageBox( g_entityKeyEntry->window(), "No spaces, newlines, tabs, quotes are allowed in entity key names." );
return;
}
if ( std::any_of( value.cbegin(), value.cend(), []( const char c ){ return strchr( "\n\r\"", c ) != nullptr; } ) ) {
qt_MessageBox( g_entityKeyEntry->window(), "No newlines & quotes are allowed in entity key values." );
return;
}
// avoid empty key name; empty value is okay: deletes key
if( key.isEmpty() )
return;
if ( string_equal( key.constData(), "classname" ) ) {
Scene_EntitySetClassname_Selected( value.constData() );
}
else
{
Scene_EntitySetKeyValue_Selected_Undoable( key.constData(), value.constData() );
}
}
void EntityInspector_clearKeyValue(){
// Get current selection text
if( const auto item = g_entprops_store->currentItem() ){
const auto key = item->text( 0 ).toLatin1();
if ( !string_equal( key.constData(), "classname" ) ) {
const auto command = StringStream<64>( "entityDeleteKey -key ", key.constData() );
UndoableCommand undo( command );
Scene_EntitySetKeyValue_Selected( key.constData(), "" );
}
}
}
class : public QObject
{
protected:
bool eventFilter( QObject *obj, QEvent *event ) override {
if( event->type() == QEvent::ShortcutOverride ) {
QKeyEvent *keyEvent = static_cast<QKeyEvent *>( event );
if( keyEvent->key() == Qt::Key_Delete ){
EntityInspector_clearKeyValue();
event->accept();
}
}
return QObject::eventFilter( obj, event ); // standard event processing
}
}
g_EntityProperties_keypress;
void EntityInspector_clearAllKeyValues(){
UndoableCommand undo( "entityClear" );
// remove all keys except classname and origin
for ( const auto&[ key, value ] : g_selectedKeyValues )
{
if ( !string_equal( key.c_str(), "classname" ) && !string_equal( key.c_str(), "origin" ) ) {
Scene_EntitySetKeyValue_Selected( key.c_str(), "" );
}
}
}
// =============================================================================
// callbacks
static void EntityClassList_selection_changed( QTreeWidgetItem *current, QTreeWidgetItem *previous ){
SetComment( current != nullptr
? current->data( 0, Qt::ItemDataRole::UserRole ).value<EntityClass*>()
: nullptr );
}
static void EntityProperties_selection_changed( QTreeWidgetItem *item, int column ){
if( item != nullptr ){
g_entityKeyEntry->setText( item->text( 0 ) );
g_entityValueEntry->setText( item->text( 1 ) );
}
}
class : public QObject
{
protected:
bool eventFilter( QObject *obj, QEvent *event ) override {
if( event->type() == QEvent::ShortcutOverride ) {
QKeyEvent *keyEvent = static_cast<QKeyEvent *>( event );
if( keyEvent->key() == Qt::Key_Return
|| keyEvent->key() == Qt::Key_Enter
|| keyEvent->key() == Qt::Key_Tab
|| keyEvent->key() == Qt::Key_Up
|| keyEvent->key() == Qt::Key_Down
|| keyEvent->key() == Qt::Key_PageUp
|| keyEvent->key() == Qt::Key_PageDown ){
event->accept();
}
}
// clear focus widget while showing to keep global shortcuts working
else if( event->type() == QEvent::Show ) {
QTimer::singleShot( 0, [obj](){
if( static_cast<QWidget*>( obj )->focusWidget() != nullptr )
static_cast<QWidget*>( obj )->focusWidget()->clearFocus();
} );
}
return QObject::eventFilter( obj, event ); // standard event processing
}
}
g_pressedKeysFilter;
void EntityInspector_destroyWindow(){
g_entityInspector_windowConstructed = false;
GlobalEntityAttributes_clear();
}
QWidget* EntityInspector_constructWindow( QWidget* toplevel ){
auto splitter = new QSplitter( Qt::Vertical );
QObject::connect( splitter, &QObject::destroyed, EntityInspector_destroyWindow );
splitter->installEventFilter( &g_pressedKeysFilter );
{
// class list
auto tree = g_entityClassList = new QTreeWidget;
tree->setColumnCount( 1 );
tree->setSortingEnabled( true );
tree->sortByColumn( 0, Qt::SortOrder::AscendingOrder );
tree->setUniformRowHeights( true ); // optimization
tree->setHorizontalScrollBarPolicy( Qt::ScrollBarPolicy::ScrollBarAlwaysOff );
tree->setSizeAdjustPolicy( QAbstractScrollArea::SizeAdjustPolicy::AdjustToContents ); // scroll area will inherit column size
tree->header()->setStretchLastSection( false ); // non greedy column sizing
tree->header()->setSectionResizeMode( QHeaderView::ResizeMode::ResizeToContents ); // no text elision
tree->setHeaderHidden( true );
tree->setRootIsDecorated( false );
tree->setEditTriggers( QAbstractItemView::EditTrigger::NoEditTriggers );
tree->setAutoScroll( true );
QObject::connect( tree, &QTreeWidget::itemActivated, []( QTreeWidgetItem *item, int column ){
Scene_EntitySetClassname_Selected( item->text( 0 ).toLatin1().constData() );
} );
QObject::connect( tree, &QTreeWidget::currentItemChanged, EntityClassList_selection_changed );
splitter->addWidget( tree );
}
{
auto text = g_entityClassComment = new QPlainTextEdit;
text->setReadOnly( true );
text->setUndoRedoEnabled( false );
splitter->addWidget( text );
}
{
QWidget *containerWidget = new QWidget; // Adding a QLayout to a QSplitter is not supported, use proxy widget
splitter->addWidget( containerWidget );
auto vbox = new QVBoxLayout( containerWidget );
vbox->setContentsMargins( 0, 0, 0, 0 );
{
// Spawnflags (4 colums wide max, or window gets too wide.)
auto grid = g_spawnflagsTable = new QGridLayout;
grid->setAlignment( Qt::AlignmentFlag::AlignLeft );
vbox->addLayout( grid );
for ( int i = 0; i < MAX_FLAGS; i++ )
{
auto check = g_entitySpawnflagsCheck[i] = new QCheckBox;
grid->addWidget( check, i / 4, i % 4 );
check->hide();
QObject::connect( check, &QAbstractButton::clicked, EntityInspector_applySpawnflags );
}
}
{
// key/value list
auto tree = g_entprops_store = new QTreeWidget;
tree->setColumnCount( 2 );
tree->setUniformRowHeights( true ); // optimization
tree->setHorizontalScrollBarPolicy( Qt::ScrollBarPolicy::ScrollBarAlwaysOff );
tree->header()->setSectionResizeMode( 0, QHeaderView::ResizeMode::ResizeToContents ); // no text elision
tree->setHeaderHidden( true );
tree->setRootIsDecorated( false );
tree->setEditTriggers( QAbstractItemView::EditTrigger::NoEditTriggers );
QObject::connect( tree, &QTreeWidget::itemPressed, EntityProperties_selection_changed );
tree->installEventFilter( &g_EntityProperties_keypress );
vbox->addWidget( tree );
}
{
// key/value entry
auto grid = new QGridLayout;
grid->setContentsMargins( 4, 0, 4, 0 );
vbox->addLayout( grid );
{
grid->addWidget( new QLabel( "Key" ), 0, 0 );
grid->addWidget( new QLabel( "Value" ), 1, 0 );
}
{
auto line = g_entityKeyEntry = new LineEdit;
grid->addWidget( line, 0, 1 );
QObject::connect( line, &QLineEdit::returnPressed, [](){ g_entityValueEntry->setFocus(); g_entityValueEntry->selectAll(); } );
}
{
auto line = g_entityValueEntry = new LineEdit;
grid->addWidget( line, 1, 1 );
QObject::connect( line, &QLineEdit::returnPressed, [](){ EntityInspector_applyKeyValue(); } );
}
/* select by key/value buttons */
{
auto b = new QToolButton;
b->setText( "+" );
b->setToolTip( "Select by key" );
grid->addWidget( b, 0, 2 );
QObject::connect( b, &QAbstractButton::clicked, [](){
Select_EntitiesByKeyValue( g_entityKeyEntry->text().toLatin1().constData(), nullptr );
} );
}
{
auto b = new QToolButton;
b->setText( "+" );
b->setToolTip( "Select by value" );
grid->addWidget( b, 1, 2 );
QObject::connect( b, &QAbstractButton::clicked, [](){
Select_EntitiesByKeyValue( nullptr, g_entityValueEntry->text().toLatin1().constData() );
} );
}
{
auto b = new QToolButton;
b->setText( "+" );
b->setToolTip( "Select by key + value" );
grid->addWidget( b, 0, 3, 2, 1 );
QObject::connect( b, &QAbstractButton::clicked, [](){
Select_EntitiesByKeyValue( g_entityKeyEntry->text().toLatin1().constData(), g_entityValueEntry->text().toLatin1().constData() );
} );
}
}
{
auto hbox = new QHBoxLayout;
hbox->setContentsMargins( 4, 0, 4, 0 );
vbox->addLayout( hbox );
{
auto b = new QPushButton( "Clear All" );
hbox->addWidget( b );
QObject::connect( b, &QAbstractButton::clicked, EntityInspector_clearAllKeyValues );
}
{
auto b = new QPushButton( "Delete Key" );
hbox->addWidget( b );
QObject::connect( b, &QAbstractButton::clicked, EntityInspector_clearKeyValue );
}
{
auto b = new QToolButton;
hbox->addWidget( b );
b->setText( "<" );
b->setToolTip( "Select targeting entities" );
QObject::connect( b, &QAbstractButton::clicked, [](){ Select_ConnectedEntities( true, false, g_focusToggleButton->isChecked() ); } );
}
{
auto b = new QToolButton;
hbox->addWidget( b );
b->setText( ">" );
b->setToolTip( "Select targets" );
QObject::connect( b, &QAbstractButton::clicked, [](){ Select_ConnectedEntities( false, true, g_focusToggleButton->isChecked() ); } );
}
{
auto b = new QToolButton;
hbox->addWidget( b );
b->setText( "<->" );
b->setToolTip( "Select connected entities" );
QObject::connect( b, &QAbstractButton::clicked, [](){ Select_ConnectedEntities( true, true, g_focusToggleButton->isChecked() ); } );
}
{
auto b = g_focusToggleButton = new QToolButton;
hbox->addWidget( b );
b->setText( u8"👀" );
b->setToolTip( "AutoFocus on Selection" );
b->setCheckable( true );
QObject::connect( b, &QAbstractButton::clicked, []( bool checked ){ if( checked ) FocusAllViews(); } );
}
}
}
{
auto scroll = new QScrollArea;
scroll->setHorizontalScrollBarPolicy( Qt::ScrollBarPolicy::ScrollBarAlwaysOff );
scroll->setWidgetResizable( true );
splitter->addWidget( scroll );
QWidget *containerWidget = new QWidget; // Adding a QLayout to a QScrollArea is not supported, use proxy widget
g_attributeBox = new QGridLayout( containerWidget );
g_attributeBox->setAlignment( Qt::AlignmentFlag::AlignTop );
g_attributeBox->setColumnStretch( 0, 111 );
g_attributeBox->setColumnStretch( 1, 333 );
scroll->setWidget( containerWidget ); // widget's layout must be set b4 this!
}
g_entityInspector_windowConstructed = true;
EntityClassList_fill();
typedef FreeCaller1<const Selectable&, EntityInspector_selectionChanged> EntityInspectorSelectionChangedCaller;
GlobalSelectionSystem().addSelectionChangeCallback( EntityInspectorSelectionChangedCaller() );
GlobalEntityCreator().setKeyValueChangedFunc( EntityInspector_keyValueChanged );
g_guiSettings.addSplitter( splitter, "EntityInspector/splitter", { 55, 175, 255, 255 } );
return splitter;
}
class EntityInspector : public ModuleObserver
{
std::size_t m_unrealised;
public:
EntityInspector() : m_unrealised( 1 ){
}
void realise(){
if ( --m_unrealised == 0 ) {
if ( g_entityInspector_windowConstructed ) {
//globalOutputStream() << "Entity Inspector: realise\n";
EntityClassList_fill();
}
}
}
void unrealise(){
if ( ++m_unrealised == 1 ) {
if ( g_entityInspector_windowConstructed ) {
//globalOutputStream() << "Entity Inspector: unrealise\n";
EntityClassList_clear();
}
}
}
};
EntityInspector g_EntityInspector;
#include "preferencesystem.h"
#include "stringio.h"
void EntityInspector_construct(){
GlobalEntityClassManager().attach( g_EntityInspector );
}
void EntityInspector_destroy(){
GlobalEntityClassManager().detach( g_EntityInspector );
}