//################################################################################################## // // Custom Visualization Core library // // This library may be used under the terms of either the GNU General Public License or // the GNU Lesser General Public License as follows: // // GNU General Public License Usage // This library 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 3 of the License, or // (at your option) any later version. // // This library 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 at <> // for more details. // // GNU Lesser General Public License Usage // This library is free software; you can redistribute it and/or modify // it under the terms of the GNU Lesser General Public License as published by // the Free Software Foundation; either version 2.1 of the License, or // (at your option) any later version. // // This library 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 Lesser General Public License at <> // for more details. // //################################################################################################## #include "cafPdmUiTreeSelectionEditor.h" #include "cafAssert.h" #include "cafPdmObject.h" #include "cafPdmUiCommandSystemProxy.h" #include "cafPdmUiTreeSelectionQModel.h" #include "cafQShortenedLabel.h" #include #include #include #include #include #include #include #include #include #include #include #include #include //================================================================================================== /// Helper class used to control height of size hint //================================================================================================== class QTreeViewHeightHint : public QTreeView { public: explicit QTreeViewHeightHint( QWidget* parent = nullptr ) : m_heightHint( -1 ) { } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QSize sizeHint() const override { QSize mySize = QTreeView::sizeHint(); if ( m_heightHint > 0 ) { mySize.setHeight( m_heightHint ); } return mySize; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void setHeightHint( int heightHint ) { m_heightHint = heightHint; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void keyPressEvent( QKeyEvent* event ) override { QTreeView::keyPressEvent( event ); if ( event->key() == Qt::Key_Down || event->key() == Qt::Key_Up || event->key() == Qt::Key_Home || event->key() == Qt::Key_End || event->key() == Qt::Key_PageDown || event->key() == Qt::Key_PageUp ) { emit clicked( currentIndex() ); } } private: int m_heightHint; }; //================================================================================================== /// //================================================================================================== class FilterLeafNodesOnlyProxyModel : public QSortFilterProxyModel { public: FilterLeafNodesOnlyProxyModel( QObject* parent = nullptr ) : QSortFilterProxyModel( parent ) { } protected: //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- bool filterAcceptsRow( int source_row, const QModelIndex& source_parent ) const override { QModelIndex index = sourceModel()->index( source_row, 0, source_parent ); if ( sourceModel()->hasChildren( index ) ) { // Always include node if node has children return true; } return QSortFilterProxyModel::filterAcceptsRow( source_row, source_parent ); } }; namespace caf { //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- CAF_PDM_UI_FIELD_EDITOR_SOURCE_INIT( PdmUiTreeSelectionEditor ); //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- PdmUiTreeSelectionEditor::PdmUiTreeSelectionEditor() : m_model( nullptr ) , m_proxyModel( nullptr ) { } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- PdmUiTreeSelectionEditor::~PdmUiTreeSelectionEditor() { } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void PdmUiTreeSelectionEditor::configureAndUpdateUi( const QString& uiConfigName ) { // Label CAF_ASSERT( !m_label.isNull() ); PdmUiFieldEditorHandle::updateLabelFromField( m_label, uiConfigName ); if ( !m_model ) { m_model = new caf::PdmUiTreeSelectionQModel( this ); m_proxyModel = new FilterLeafNodesOnlyProxyModel( this ); m_proxyModel->setSourceModel( m_model ); m_proxyModel->setFilterCaseSensitivity( Qt::CaseInsensitive ); m_treeView->setModel( m_proxyModel ); } bool optionsOnly = true; QList options = uiField()->valueOptions( &optionsOnly ); bool itemCountHasChaged = false; if ( m_model->optionItemCount() != options.size() ) itemCountHasChaged = true; QVariant fieldValue = uiField()->uiValue(); m_model->setUiValueCache( &fieldValue ); // TODO: If the count is different between incoming and current list of items, // use cafQTreeViewStateSerializer to restore collapsed state m_model->setOptions( this, options ); if ( itemCountHasChaged ) { m_treeView->expandAll(); } if ( PdmUiTreeSelectionQModel::isSingleValueField( fieldValue ) ) { m_toggleAllCheckBox->hide(); } else if ( PdmUiTreeSelectionQModel::isMultipleValueField( fieldValue ) ) { caf::PdmUiObjectHandle* uiObject = uiObj( uiField()->fieldHandle()->ownerObject() ); if ( uiObject ) { uiObject->editorAttribute( uiField()->fieldHandle(), uiConfigName, &m_attributes ); } if ( m_attributes.singleSelectionMode ) { m_treeView->setSelectionMode( QAbstractItemView::SingleSelection ); m_treeView->setContextMenuPolicy( Qt::NoContextMenu ); m_model->enableSingleSelectionMode( m_attributes.singleSelectionMode ); } else { m_treeView->setSelectionMode( QAbstractItemView::ExtendedSelection ); } connect( m_treeView, SIGNAL( clicked( QModelIndex ) ), this, SLOT( slotClicked( QModelIndex ) ), Qt::UniqueConnection ); if ( !m_attributes.showTextFilter ) { m_textFilterLineEdit->hide(); } if ( m_attributes.singleSelectionMode || !m_attributes.showToggleAllCheckbox ) { m_toggleAllCheckBox->hide(); } else { if ( options.size() == 0 ) { m_toggleAllCheckBox->setChecked( false ); } else { QModelIndexList indices = allVisibleSourceModelIndices(); if ( indices.size() > 0 ) { size_t editableItems = 0u; size_t checkedEditableItems = 0u; for ( auto mi : indices ) { if ( !m_model->isReadOnly( mi ) ) { editableItems++; if ( m_model->isChecked( mi ) ) { checkedEditableItems++; } } } bool allItemsChecked = ( editableItems > 0u && checkedEditableItems == editableItems ); m_toggleAllCheckBox->setChecked( allItemsChecked ); } } } } // If the tree doesn't have grand children we treat this as a straight list m_treeView->setRootIsDecorated( m_model->hasGrandChildren() ); m_model->resetUiValueCache(); if ( m_attributes.currentIndexFieldHandle ) { PdmUiFieldHandle* uiFieldHandle = m_attributes.currentIndexFieldHandle->uiCapability(); if ( uiFieldHandle ) { QModelIndexList indices = allVisibleSourceModelIndices(); QVariant currentItemValue = uiFieldHandle->uiValue(); for ( const auto& mi : indices ) { QVariant itemValue = m_model->data( mi, PdmUiTreeSelectionQModel::optionItemValueRole() ); if ( currentItemValue == itemValue ) { QModelIndex treeViewIndex = m_proxyModel->mapFromSource( mi ); m_treeView->setCurrentIndex( treeViewIndex ); } } } } // It is required to use a timer here, as the layout of the widgets are handled by events // Calling scrollTo() here has no effect, or scrolls to wrong location QTimer::singleShot( 150, this, SLOT( slotScrollToFirstCheckedItem() ) ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QWidget* PdmUiTreeSelectionEditor::createEditorWidget( QWidget* parent ) { QFrame* frame = new QFrame( parent ); QVBoxLayout* layout = new QVBoxLayout; layout->setContentsMargins( 0, 0, 0, 0 ); frame->setLayout( layout ); { QHBoxLayout* headerLayout = new QHBoxLayout; headerLayout->setContentsMargins( 0, 0, 0, 0 ); layout->addLayout( headerLayout ); PdmUiTreeSelectionEditorAttribute attrib; m_toggleAllCheckBox = new QCheckBox(); headerLayout->addWidget( m_toggleAllCheckBox ); connect( m_toggleAllCheckBox, SIGNAL( clicked( bool ) ), this, SLOT( slotToggleAll() ) ); m_textFilterLineEdit = new QLineEdit(); m_textFilterLineEdit->setPlaceholderText( "Click to add filter" ); headerLayout->addWidget( m_textFilterLineEdit ); connect( m_textFilterLineEdit, SIGNAL( textChanged( QString ) ), this, SLOT( slotTextFilterChanged() ) ); } QTreeViewHeightHint* treeViewHeightHint = new QTreeViewHeightHint( parent ); treeViewHeightHint->setHeightHint( 2000 ); treeViewHeightHint->setHeaderHidden( true ); m_treeView = treeViewHeightHint; m_treeView->setContextMenuPolicy( Qt::CustomContextMenu ); connect( m_treeView, SIGNAL( customContextMenuRequested( QPoint ) ), SLOT( customMenuRequested( QPoint ) ) ); layout->addWidget( treeViewHeightHint ); return frame; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QWidget* PdmUiTreeSelectionEditor::createLabelWidget( QWidget* parent ) { m_label = new QShortenedLabel( parent ); return m_label; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QMargins PdmUiTreeSelectionEditor::calculateLabelContentMargins() const { QSize editorSize = m_textFilterLineEdit->sizeHint(); QSize labelSize = m_label->sizeHint(); int heightDiff = editorSize.height() - labelSize.height(); QMargins contentMargins = m_label->contentsMargins(); if ( heightDiff > 0 ) { contentMargins.setTop( contentMargins.top() + heightDiff / 2 ); contentMargins.setBottom( contentMargins.bottom() + heightDiff / 2 ); } return contentMargins; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- bool PdmUiTreeSelectionEditor::isMultiRowEditor() const { return true; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void PdmUiTreeSelectionEditor::customMenuRequested( const QPoint& pos ) { QMenu menu; QModelIndexList selectedIndexes = m_treeView->selectionModel()->selectedIndexes(); bool onlyHeadersInSelection = true; for ( auto mi : selectedIndexes ) { QVariant v = m_proxyModel->data( mi, PdmUiTreeSelectionQModel::headingRole() ); if ( v.toBool() == false ) { onlyHeadersInSelection = false; } } if ( onlyHeadersInSelection && selectedIndexes.size() > 0 ) { { QAction* act = new QAction( "Sub Items On", this ); connect( act, SIGNAL( triggered() ), SLOT( slotSetSubItemsOn() ) ); menu.addAction( act ); } { QAction* act = new QAction( "Sub Items Off", this ); connect( act, SIGNAL( triggered() ), SLOT( slotSetSubItemsOff() ) ); menu.addAction( act ); } } else if ( selectedIndexes.size() > 0 ) { { QAction* act = new QAction( "Set Selected On", this ); connect( act, SIGNAL( triggered() ), SLOT( slotSetSelectedOn() ) ); menu.addAction( act ); } { QAction* act = new QAction( "Set Selected Off", this ); connect( act, SIGNAL( triggered() ), SLOT( slotSetSelectedOff() ) ); menu.addAction( act ); } } if ( menu.actions().size() > 0 ) { // Qt doc: QAbstractScrollArea and its subclasses that map the context menu event to coordinates of the viewport(). QPoint globalPos = m_treeView->viewport()->mapToGlobal( pos ); menu.exec( globalPos ); } } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void PdmUiTreeSelectionEditor::slotSetSelectedOn() { this->setCheckedStateOfSelected( true ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void PdmUiTreeSelectionEditor::slotSetSelectedOff() { this->setCheckedStateOfSelected( false ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void PdmUiTreeSelectionEditor::setCheckedStateOfSelected( bool checked ) { if ( !m_proxyModel ) return; QItemSelection selectionInProxyModel = m_treeView->selectionModel()->selection(); QItemSelection selectionInSourceModel = m_proxyModel->mapSelectionToSource( selectionInProxyModel ); m_model->setCheckedStateForItems( selectionInSourceModel.indexes(), checked ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void PdmUiTreeSelectionEditor::slotSetSubItemsOn() { this->setCheckedStateForSubItemsOfSelected( true ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void PdmUiTreeSelectionEditor::slotSetSubItemsOff() { this->setCheckedStateForSubItemsOfSelected( false ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void PdmUiTreeSelectionEditor::setCheckedStateForSubItemsOfSelected( bool checked ) { QModelIndexList selectedProxyIndexes = m_treeView->selectionModel()->selectedIndexes(); QModelIndexList sourceIndexesToSubItems; for ( auto mi : selectedProxyIndexes ) { for ( int i = 0; i < m_proxyModel->rowCount( mi ); i++ ) { QModelIndex childProxyIndex = m_proxyModel->index( i, 0, mi ); sourceIndexesToSubItems.push_back( m_proxyModel->mapToSource( childProxyIndex ) ); } } m_model->setCheckedStateForItems( sourceIndexesToSubItems, checked ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void PdmUiTreeSelectionEditor::slotToggleAll() { if ( m_toggleAllCheckBox->isChecked() ) { checkAllItems(); } else { unCheckAllItems(); } } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void PdmUiTreeSelectionEditor::slotTextFilterChanged() { QString searchString = m_textFilterLineEdit->text(); searchString += "*"; // Escape the characters '[' and ']' as these have special meaning for a search string // To be able to search for vector names in brackets, these must be escaped // See "Wildcard Matching" in Qt documentation searchString.replace( "[", "\\[" ); searchString.replace( "]", "\\]" ); QRegExp searcher( searchString, Qt::CaseInsensitive, QRegExp::WildcardUnix ); m_proxyModel->setFilterRegExp( searcher ); updateUi(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void PdmUiTreeSelectionEditor::slotClicked( const QModelIndex& index ) { QModelIndex lastUncheckedIndex = m_model->indexForLastUncheckedItem(); m_model->clearIndexForLastUncheckedItem(); QModelIndex proxyModelIndex = m_proxyModel->mapFromSource( lastUncheckedIndex ); if ( proxyModelIndex == index ) { // Early return to avoid changing the current item if an item was unchecked return; } if ( m_attributes.setCurrentIndexWhenItemIsChecked && index.isValid() ) { QModelIndexList selectedIndexes = m_treeView->selectionModel()->selectedIndexes(); if ( selectedIndexes.size() < 2 ) { QVariant v = m_proxyModel->data( index, Qt::CheckStateRole ); if ( v == Qt::Checked ) { m_treeView->setCurrentIndex( index ); } } } currentChanged( index ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void PdmUiTreeSelectionEditor::slotScrollToFirstCheckedItem() { auto firstVisibleIndex = m_treeView->indexAt( m_treeView->viewport()->rect().topLeft() ); auto lastVisibleIndex = m_treeView->indexAt( m_treeView->viewport()->rect().bottomRight() ); if ( !firstVisibleIndex.isValid() ) { return; } if ( !lastVisibleIndex.isValid() ) { return; } for ( int i = firstVisibleIndex.row(); i < lastVisibleIndex.row(); i++ ) { auto treeViewIndex = m_proxyModel->index( i, 0 ); if ( m_proxyModel->data( treeViewIndex, Qt::CheckStateRole ).toBool() ) { // Do nothing if there is a checked and visible item in the view return; } } for ( int i = 0; i < m_proxyModel->rowCount(); i++ ) { auto treeViewIndex = m_proxyModel->index( i, 0 ); if ( m_proxyModel->data( treeViewIndex, Qt::CheckStateRole ).toBool() ) { // Scroll to the first checked item if no checked items are visible m_treeView->scrollTo( treeViewIndex ); return; } } } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void PdmUiTreeSelectionEditor::currentChanged( const QModelIndex& current ) { if ( m_attributes.singleSelectionMode ) { m_proxyModel->setData( current, true, Qt::CheckStateRole ); } if ( m_attributes.currentIndexFieldHandle ) { PdmUiFieldHandle* uiFieldHandle = m_attributes.currentIndexFieldHandle->uiCapability(); if ( uiFieldHandle ) { QVariant v = m_proxyModel->data( current, PdmUiTreeSelectionQModel::optionItemValueRole() ); PdmUiCommandSystemProxy::instance()->setUiValueToField( uiFieldHandle, v ); } } } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void PdmUiTreeSelectionEditor::checkAllItems() { QModelIndexList indices = allVisibleSourceModelIndices(); m_model->setCheckedStateForItems( indices, true ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void PdmUiTreeSelectionEditor::unCheckAllItems() { QModelIndexList indices = allVisibleSourceModelIndices(); m_model->setCheckedStateForItems( indices, false ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QModelIndexList PdmUiTreeSelectionEditor::allVisibleSourceModelIndices() const { QModelIndexList indices; recursiveAppendVisibleSourceModelIndices( QModelIndex(), &indices ); return indices; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void PdmUiTreeSelectionEditor::recursiveAppendVisibleSourceModelIndices( const QModelIndex& parent, QModelIndexList* sourceModelIndices ) const { for ( int row = 0; row < m_proxyModel->rowCount( parent ); row++ ) { QModelIndex mi = m_proxyModel->index( row, 0, parent ); if ( mi.isValid() ) { QVariant v = m_proxyModel->data( mi, PdmUiTreeSelectionQModel::headingRole() ); if ( v.toBool() == false ) { sourceModelIndices->push_back( m_proxyModel->mapToSource( mi ) ); } recursiveAppendVisibleSourceModelIndices( mi, sourceModelIndices ); } } } } // end namespace caf