351 lines
11 KiB
C++
351 lines
11 KiB
C++
/*
|
|
Copyright (C) 2001-2006, William Joseph.
|
|
All Rights Reserved.
|
|
|
|
This file is part of GtkRadiant.
|
|
|
|
GtkRadiant is free software; you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation; either version 2 of the License, or
|
|
(at your option) any later version.
|
|
|
|
GtkRadiant is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with GtkRadiant; if not, write to the Free Software
|
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
*/
|
|
|
|
#include "entitylist.h"
|
|
|
|
#include "iselection.h"
|
|
|
|
#include <QVBoxLayout>
|
|
#include <QHBoxLayout>
|
|
#include <QTreeView>
|
|
#include <QCheckBox>
|
|
#include <QLineEdit>
|
|
#include <QHeaderView>
|
|
#include <QApplication>
|
|
#include <QAction>
|
|
#include <QKeyEvent>
|
|
|
|
#include "string/string.h"
|
|
#include "scenelib.h"
|
|
#include "nameable.h"
|
|
#include "signal/isignal.h"
|
|
#include "generic/object.h"
|
|
|
|
#include "gtkutil/widget.h"
|
|
#include "gtkutil/idledraw.h"
|
|
#include "gtkutil/accelerator.h"
|
|
#include "gtkutil/guisettings.h"
|
|
|
|
#include "treemodel.h"
|
|
|
|
#include "mainframe.h"
|
|
|
|
void RedrawEntityList();
|
|
typedef FreeCaller<RedrawEntityList> RedrawEntityListCaller;
|
|
|
|
class EntityList
|
|
{
|
|
public:
|
|
enum EDirty
|
|
{
|
|
eDefault,
|
|
eSelection,
|
|
eInsertRemove,
|
|
};
|
|
|
|
EDirty m_dirty;
|
|
|
|
IdleDraw m_idleDraw;
|
|
|
|
QWidget* m_window;
|
|
QCheckBox* m_check;
|
|
QTreeView* m_tree_view;
|
|
QAbstractItemModel* m_tree_model;
|
|
bool m_selection_disabled;
|
|
|
|
bool m_search_from_start;
|
|
|
|
EntityList() :
|
|
m_dirty( EntityList::eDefault ),
|
|
m_idleDraw( RedrawEntityListCaller() ),
|
|
m_window( 0 ),
|
|
m_selection_disabled( false ),
|
|
m_search_from_start( false ){
|
|
}
|
|
|
|
bool visible() const {
|
|
return m_window->isVisible();
|
|
}
|
|
};
|
|
|
|
namespace
|
|
{
|
|
EntityList* g_EntityList;
|
|
|
|
inline EntityList& getEntityList(){
|
|
ASSERT_NOTNULL( g_EntityList );
|
|
return *g_EntityList;
|
|
}
|
|
}
|
|
|
|
void entitylist_focusSelected( bool checked ){
|
|
if( checked )
|
|
FocusAllViews();
|
|
}
|
|
|
|
template<typename Functor>
|
|
void item_model_foreach( Functor f, QAbstractItemModel* model, QModelIndex parent = QModelIndex() )
|
|
{
|
|
if ( !parent.isValid() )
|
|
parent = model->index( 0, 0, QModelIndex() );
|
|
|
|
const int numRows = model->rowCount( parent );
|
|
|
|
for ( int i = 0; i < numRows; ++i )
|
|
item_model_foreach( f, model, model->index( i, 0, parent ) );
|
|
|
|
f( parent );
|
|
}
|
|
|
|
void EntityList_UpdateSelection( QAbstractItemModel* model, QTreeView* view ){
|
|
item_model_foreach( [view]( QModelIndex &index ){
|
|
scene::Instance* instance = static_cast<scene::Instance*>( index.data( c_ItemDataRole_Instance ).value<void*>() );
|
|
if ( Selectable* selectable = Instance_getSelectable( *instance ) ) {
|
|
view->selectionModel()->select( index, selectable->isSelected()
|
|
? QItemSelectionModel::SelectionFlag::Select
|
|
: QItemSelectionModel::SelectionFlag::Deselect );
|
|
}
|
|
}, model );
|
|
}
|
|
|
|
|
|
void RedrawEntityList(){
|
|
switch ( getEntityList().m_dirty )
|
|
{
|
|
case EntityList::eInsertRemove:
|
|
case EntityList::eSelection:
|
|
EntityList_UpdateSelection( getEntityList().m_tree_model, getEntityList().m_tree_view );
|
|
default:
|
|
break;
|
|
}
|
|
getEntityList().m_dirty = EntityList::eDefault;
|
|
}
|
|
|
|
void entitylist_queue_draw(){
|
|
getEntityList().m_idleDraw.queueDraw();
|
|
}
|
|
|
|
void EntityList_SelectionUpdate(){
|
|
if( getEntityList().visible() ){
|
|
// deselect tree items on deseletion in the scene for some degree of consistency
|
|
if( getEntityList().m_tree_view->selectionModel()->hasSelection() ){
|
|
QTimer::singleShot( 0, [](){
|
|
for( const auto& index : getEntityList().m_tree_view->selectionModel()->selectedRows( 0 ) ){
|
|
scene::Instance* instance = static_cast<scene::Instance*>( index.data( c_ItemDataRole_Instance ).value<void*>() );
|
|
if ( Selectable* selectable = Instance_getSelectable( *instance ) )
|
|
if( !selectable->isSelected() )
|
|
getEntityList().m_tree_view->selectionModel()->select( index, QItemSelectionModel::SelectionFlag::Deselect );
|
|
}
|
|
} );
|
|
}
|
|
|
|
getEntityList().m_tree_view->viewport()->update(); // reads in Qt::ItemDataRole::BackgroundRole of visible items
|
|
}
|
|
return; //. making actual selection is ULTRA SLOW :F thus using Qt::ItemDataRole::BackgroundRole instead
|
|
|
|
if ( getEntityList().m_selection_disabled ) {
|
|
return;
|
|
}
|
|
|
|
if ( getEntityList().m_dirty < EntityList::eSelection ) {
|
|
getEntityList().m_dirty = EntityList::eSelection;
|
|
}
|
|
entitylist_queue_draw();
|
|
}
|
|
|
|
void EntityList_SelectionChanged( const Selectable& selectable ){
|
|
EntityList_SelectionUpdate();
|
|
}
|
|
|
|
|
|
void EntityList_SetShown( bool shown ){
|
|
getEntityList().m_window->setVisible( shown );
|
|
if( shown ){ /* expand map's root node for convenience */
|
|
auto index = getEntityList().m_tree_model->index( 0, 0 );
|
|
if( index.isValid() && !getEntityList().m_tree_view->isExpanded( index ) )
|
|
getEntityList().m_tree_view->expand( index );
|
|
}
|
|
}
|
|
|
|
void EntityList_toggleShown(){
|
|
EntityList_SetShown( !getEntityList().visible() );
|
|
}
|
|
|
|
class Filter_QLineEdit : public QLineEdit
|
|
{
|
|
protected:
|
|
void enterEvent( QEvent *event ) override {
|
|
setFocus();
|
|
}
|
|
void leaveEvent( QEvent *event ) override {
|
|
clearFocus();
|
|
}
|
|
bool event( QEvent *event ) override {
|
|
if( event->type() == QEvent::ShortcutOverride ){
|
|
QKeyEvent *keyEvent = static_cast<QKeyEvent*>( event );
|
|
if( keyEvent->key() == Qt::Key_Escape ){
|
|
clear();
|
|
event->accept();
|
|
}
|
|
}
|
|
else if( event->type() == QEvent::Hide ){
|
|
if( !text().isEmpty() )
|
|
clear(); // workaround: reset filtering, as modification of tree after setRowHidden() is ULTRA SLOW
|
|
}
|
|
return QLineEdit::event( event );
|
|
}
|
|
};
|
|
|
|
void searchEntrySetModeIcon( QAction *action, bool search_from_start ){
|
|
action->setIcon( QApplication::style()->standardIcon(
|
|
search_from_start
|
|
? QStyle::StandardPixmap::SP_CommandLink
|
|
: QStyle::StandardPixmap::SP_FileDialogContentsView ) );
|
|
}
|
|
|
|
/* search */
|
|
void tree_view_filter( const char *string, bool from_start ){
|
|
const auto traverse = [=]( const auto& self, QAbstractItemModel* model, QTreeView *tree, QModelIndex parent = QModelIndex() ) -> bool {
|
|
if ( !parent.isValid() )
|
|
parent = model->index( 0, 0, QModelIndex() );
|
|
|
|
const int numRows = model->rowCount( parent );
|
|
|
|
bool childVisible = false;
|
|
|
|
for ( int i = 0; i < numRows; ++i )
|
|
childVisible |= self( self, model, tree, model->index( i, 0, parent ) );
|
|
|
|
const bool visible = childVisible || string_empty( string )
|
|
|| ( from_start && string_equal_prefix_nocase( parent.data( Qt::ItemDataRole::DisplayRole ).toString().toLatin1().constData(), string ) )
|
|
|| ( !from_start && string_in_string_nocase( parent.data( Qt::ItemDataRole::DisplayRole ).toString().toLatin1().constData(), string ) );
|
|
tree->setRowHidden( parent.row(), parent.parent(), !visible );
|
|
|
|
return childVisible || visible;
|
|
};
|
|
|
|
traverse( traverse, getEntityList().m_tree_model, getEntityList().m_tree_view );
|
|
}
|
|
|
|
|
|
|
|
extern QAbstractItemModel* scene_graph_get_tree_model();
|
|
void AttachEntityTreeModel(){
|
|
getEntityList().m_tree_model = scene_graph_get_tree_model();
|
|
getEntityList().m_tree_view->setModel( getEntityList().m_tree_model );
|
|
|
|
QObject::connect( getEntityList().m_tree_view->selectionModel(), &QItemSelectionModel::selectionChanged,
|
|
[]( const QItemSelection &selected, const QItemSelection &deselected ){
|
|
for( const auto& index : deselected.indexes() ){
|
|
scene::Instance* instance = static_cast<scene::Instance*>( index.data( c_ItemDataRole_Instance ).value<void*>() );
|
|
if( Selectable* selectable = Instance_getSelectable( *instance ) )
|
|
selectable->setSelected( false );
|
|
}
|
|
for( const auto& index : selected.indexes() ){
|
|
scene::Instance* instance = static_cast<scene::Instance*>( index.data( c_ItemDataRole_Instance ).value<void*>() );
|
|
if( Selectable* selectable = Instance_getSelectable( *instance ) )
|
|
selectable->setSelected( true );
|
|
}
|
|
if( !selected.empty() && getEntityList().m_check->isChecked() )
|
|
FocusAllViews();
|
|
} );
|
|
}
|
|
|
|
void DetachEntityTreeModel(){
|
|
getEntityList().m_tree_model = 0;
|
|
getEntityList().m_tree_view->setModel( nullptr );
|
|
}
|
|
|
|
void EntityList_constructWindow( QWidget* main_window ){
|
|
ASSERT_MESSAGE( getEntityList().m_window == 0, "error" );
|
|
|
|
auto window = getEntityList().m_window = new QWidget( main_window, Qt::Dialog | Qt::WindowCloseButtonHint );
|
|
window->setWindowTitle( "Entity List" );
|
|
|
|
g_guiSettings.addWindow( window, "EntityList/geometry", 350, 500 );
|
|
{
|
|
auto *vbox = new QVBoxLayout( window );
|
|
vbox->setContentsMargins( 4, 0, 4, 4 );
|
|
{
|
|
auto *tree = getEntityList().m_tree_view = new QTreeView;
|
|
tree->setHeaderHidden( true );
|
|
tree->setEditTriggers( QAbstractItemView::EditTrigger::NoEditTriggers );
|
|
tree->setUniformRowHeights( true ); // optimization
|
|
tree->setSizeAdjustPolicy( QAbstractScrollArea::SizeAdjustPolicy::AdjustToContents ); // scroll area will inherit column size
|
|
tree->header()->setStretchLastSection( false ); // non greedy column sizing; + QHeaderView::ResizeMode::ResizeToContents = no text elision 🤷♀️
|
|
tree->header()->setSectionResizeMode( QHeaderView::ResizeMode::ResizeToContents );
|
|
tree->setSelectionMode( QAbstractItemView::SelectionMode::ExtendedSelection );
|
|
vbox->addWidget( tree );
|
|
}
|
|
{
|
|
auto *hbox = new QHBoxLayout;
|
|
vbox->addLayout( hbox );
|
|
{
|
|
auto *check = getEntityList().m_check = new QCheckBox( "AutoFocus on Selection" );
|
|
hbox->addWidget( check );
|
|
QObject::connect( check, &QAbstractButton::clicked, entitylist_focusSelected );
|
|
}
|
|
{ //search entry
|
|
QLineEdit *entry = new Filter_QLineEdit;
|
|
hbox->addWidget( entry );
|
|
entry->setClearButtonEnabled( true );
|
|
entry->setFocusPolicy( Qt::FocusPolicy::ClickFocus );
|
|
|
|
QAction *action = entry->addAction( QApplication::style()->standardIcon( QStyle::StandardPixmap::SP_CommandLink ), QLineEdit::LeadingPosition );
|
|
searchEntrySetModeIcon( action, getEntityList().m_search_from_start );
|
|
action->setToolTip( "toggle match mode ( start / any position )" );
|
|
|
|
QObject::connect( entry, &QLineEdit::textChanged, []( const QString& text ){
|
|
tree_view_filter( text.toLatin1().constData(), getEntityList().m_search_from_start );
|
|
} );
|
|
QObject::connect( action, &QAction::triggered, [action, entry](){
|
|
getEntityList().m_search_from_start ^= 1;
|
|
searchEntrySetModeIcon( action, getEntityList().m_search_from_start );
|
|
entry->textChanged( entry->text() ); // trigger filtering update
|
|
} );
|
|
}
|
|
}
|
|
}
|
|
|
|
AttachEntityTreeModel();
|
|
}
|
|
|
|
void EntityList_destroyWindow(){
|
|
DetachEntityTreeModel();
|
|
delete getEntityList().m_window;
|
|
}
|
|
|
|
#include "preferencesystem.h"
|
|
|
|
#include "stringio.h"
|
|
|
|
void EntityList_Construct(){
|
|
g_EntityList = new EntityList;
|
|
|
|
GlobalPreferenceSystem().registerPreference( "EntListSearchFromStart", BoolImportStringCaller( getEntityList().m_search_from_start ), BoolExportStringCaller( getEntityList().m_search_from_start ) );
|
|
|
|
typedef FreeCaller1<const Selectable&, EntityList_SelectionChanged> EntityListSelectionChangedCaller;
|
|
GlobalSelectionSystem().addSelectionChangeCallback( EntityListSelectionChangedCaller() );
|
|
}
|
|
void EntityList_Destroy(){
|
|
delete g_EntityList;
|
|
}
|