QGIS/src/gui/attributetable/qgsdualview.cpp
2019-12-17 17:10:43 +01:00

1111 lines
38 KiB
C++

/***************************************************************************
qgsdualview.cpp
--------------------------------------
Date : 10.2.2013
Copyright : (C) 2013 Matthias Kuhn
Email : matthias at opengis dot ch
***************************************************************************
* *
* This program 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. *
* *
***************************************************************************/
#include <QClipboard>
#include <QDialog>
#include <QMenu>
#include <QMessageBox>
#include <QProgressDialog>
#include <QGroupBox>
#include <QInputDialog>
#include <QTimer>
#include <QShortcut>
#include "qgsapplication.h"
#include "qgsactionmanager.h"
#include "qgsattributetablemodel.h"
#include "qgsdualview.h"
#include "qgsexpressionbuilderdialog.h"
#include "qgsfeaturelistmodel.h"
#include "qgsifeatureselectionmanager.h"
#include "qgsmapcanvas.h"
#include "qgsmaplayeractionregistry.h"
#include "qgsmessagelog.h"
#include "qgsvectordataprovider.h"
#include "qgsvectorlayercache.h"
#include "qgsorganizetablecolumnsdialog.h"
#include "qgseditorwidgetregistry.h"
#include "qgssettings.h"
#include "qgsscrollarea.h"
#include "qgsgui.h"
#include "qgsexpressioncontextutils.h"
#include "qgsshortcutsmanager.h"
#include "qgsfieldconditionalformatwidget.h"
QgsDualView::QgsDualView( QWidget *parent )
: QStackedWidget( parent )
{
setupUi( this );
connect( mFeatureListView, &QgsFeatureListView::aboutToChangeEditSelection, this, &QgsDualView::featureListAboutToChangeEditSelection );
connect( mFeatureListView, &QgsFeatureListView::currentEditSelectionChanged, this, &QgsDualView::featureListCurrentEditSelectionChanged );
connect( mFeatureListView, &QgsFeatureListView::currentEditSelectionProgressChanged, this, &QgsDualView::updateEditSelectionProgress );
mConditionalFormatWidgetStack->hide();
mConditionalFormatWidget = new QgsFieldConditionalFormatWidget( this );
mConditionalFormatWidgetStack->setMainPanel( mConditionalFormatWidget );
mConditionalFormatWidget->setDockMode( true );
QgsSettings settings;
mConditionalSplitter->restoreState( settings.value( QStringLiteral( "/qgis/attributeTable/splitterState" ), QByteArray() ).toByteArray() );
mPreviewColumnsMenu = new QMenu( this );
mActionPreviewColumnsMenu->setMenu( mPreviewColumnsMenu );
// Set preview icon
mActionExpressionPreview->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mIconExpressionPreview.svg" ) ) );
// Connect layer list preview signals
connect( mActionExpressionPreview, &QAction::triggered, this, &QgsDualView::previewExpressionBuilder );
connect( mFeatureListView, &QgsFeatureListView::displayExpressionChanged, this, &QgsDualView::previewExpressionChanged );
// browsing toolbar
connect( mNextFeatureButton, &QToolButton::clicked, mFeatureListView, &QgsFeatureListView::editNextFeature );
connect( mPreviousFeatureButton, &QToolButton::clicked, mFeatureListView, &QgsFeatureListView::editPreviousFeature );
connect( mFirstFeatureButton, &QToolButton::clicked, mFeatureListView, &QgsFeatureListView::editFirstFeature );
connect( mLastFeatureButton, &QToolButton::clicked, mFeatureListView, &QgsFeatureListView::editLastFeature );
auto createShortcuts = [ = ]( const QString & objectName, void ( QgsFeatureListView::* slot )() )
{
QShortcut *sc = QgsGui::shortcutsManager()->shortcutByName( objectName );
// do not assert for sc as it would lead to crashes in testing
// or when using custom widgets lib if built with Debug
if ( sc )
connect( sc, &QShortcut::activated, mFeatureListView, slot );
};
createShortcuts( QStringLiteral( "mAttributeTableFirstEditedFeature" ), &QgsFeatureListView::editFirstFeature );
createShortcuts( QStringLiteral( "mAttributeTablePreviousEditedFeature" ), &QgsFeatureListView::editPreviousFeature );
createShortcuts( QStringLiteral( "mAttributeTableNextEditedFeature" ), &QgsFeatureListView::editNextFeature );
createShortcuts( QStringLiteral( "mAttributeTableLastEditedFeature" ), &QgsFeatureListView::editLastFeature );
QButtonGroup *buttonGroup = new QButtonGroup( this );
buttonGroup->setExclusive( false );
buttonGroup->addButton( mAutoPanButton, PanToFeature );
buttonGroup->addButton( mAutoZoomButton, ZoomToFeature );
FeatureListBrowsingAction action = QgsSettings().enumValue( QStringLiteral( "/qgis/attributeTable/featureListBrowsingAction" ), NoAction );
QAbstractButton *bt = buttonGroup->button( static_cast<int>( action ) );
if ( bt )
bt->setChecked( true );
connect( buttonGroup, qgis::overload< QAbstractButton *, bool >::of( &QButtonGroup::buttonToggled ), this, &QgsDualView::panZoomGroupButtonToggled );
mFlashButton->setChecked( QgsSettings().value( QStringLiteral( "/qgis/attributeTable/featureListHighlightFeature" ), true ).toBool() );
connect( mFlashButton, &QToolButton::clicked, this, &QgsDualView::flashButtonClicked );
}
QgsDualView::~QgsDualView()
{
QgsSettings settings;
settings.setValue( QStringLiteral( "/qgis/attributeTable/splitterState" ), mConditionalSplitter->saveState() );
}
void QgsDualView::init( QgsVectorLayer *layer, QgsMapCanvas *mapCanvas, const QgsFeatureRequest &request,
const QgsAttributeEditorContext &context, bool loadFeatures )
{
if ( !layer )
return;
mLayer = layer;
mEditorContext = context;
connect( mTableView, &QgsAttributeTableView::willShowContextMenu, this, &QgsDualView::viewWillShowContextMenu );
mTableView->horizontalHeader()->setContextMenuPolicy( Qt::CustomContextMenu );
connect( mTableView->horizontalHeader(), &QHeaderView::customContextMenuRequested, this, &QgsDualView::showViewHeaderMenu );
connect( mTableView, &QgsAttributeTableView::columnResized, this, &QgsDualView::tableColumnResized );
connect( mFeatureListView, &QgsFeatureListView::willShowContextMenu, this, &QgsDualView::widgetWillShowContextMenu );
initLayerCache( !( request.flags() & QgsFeatureRequest::NoGeometry ) || !request.filterRect().isNull() );
initModels( mapCanvas, request, loadFeatures );
mConditionalFormatWidget->setLayer( mLayer );
mTableView->setModel( mFilterModel );
mFeatureListView->setModel( mFeatureListModel );
delete mAttributeForm;
mAttributeForm = new QgsAttributeForm( mLayer, mTempAttributeFormFeature, mEditorContext );
mTempAttributeFormFeature = QgsFeature();
if ( !context.parentContext() )
{
mAttributeEditorScrollArea = new QgsScrollArea();
mAttributeEditorScrollArea->setWidgetResizable( true );
mAttributeEditor->layout()->addWidget( mAttributeEditorScrollArea );
mAttributeEditorScrollArea->setWidget( mAttributeForm );
}
else
{
mAttributeEditor->layout()->addWidget( mAttributeForm );
}
connect( mAttributeForm, &QgsAttributeForm::widgetValueChanged, this, &QgsDualView::featureFormAttributeChanged );
connect( mAttributeForm, &QgsAttributeForm::modeChanged, this, &QgsDualView::formModeChanged );
connect( mMasterModel, &QgsAttributeTableModel::modelChanged, mAttributeForm, &QgsAttributeForm::refreshFeature );
connect( mAttributeForm, &QgsAttributeForm::filterExpressionSet, this, &QgsDualView::filterExpressionSet );
connect( mFilterModel, &QgsAttributeTableFilterModel::sortColumnChanged, this, &QgsDualView::onSortColumnChanged );
if ( mFeatureListPreviewButton->defaultAction() )
mFeatureListView->setDisplayExpression( mDisplayExpression );
else
columnBoxInit();
// This slows down load of the attribute table heaps and uses loads of memory.
//mTableView->resizeColumnsToContents();
}
void QgsDualView::columnBoxInit()
{
// load fields
QList<QgsField> fields = mLayer->fields().toList();
QString defaultField;
// default expression: saved value
QString displayExpression = mLayer->displayExpression();
if ( displayExpression.isEmpty() )
{
// ... there isn't really much to display
displayExpression = QStringLiteral( "'[Please define preview text]'" );
}
mFeatureListPreviewButton->addAction( mActionExpressionPreview );
mFeatureListPreviewButton->addAction( mActionPreviewColumnsMenu );
const auto constFields = fields;
for ( const QgsField &field : constFields )
{
int fieldIndex = mLayer->fields().lookupField( field.name() );
if ( fieldIndex == -1 )
continue;
QString fieldName = field.name();
if ( QgsGui::editorWidgetRegistry()->findBest( mLayer, fieldName ).type() != QLatin1String( "Hidden" ) )
{
QIcon icon = mLayer->fields().iconForField( fieldIndex );
QString text = mLayer->attributeDisplayName( fieldIndex );
// Generate action for the preview popup button of the feature list
QAction *previewAction = new QAction( icon, text, mFeatureListPreviewButton );
connect( previewAction, &QAction::triggered, this, [ = ] { previewColumnChanged( previewAction, fieldName ); } );
mPreviewColumnsMenu->addAction( previewAction );
if ( text == defaultField )
{
mFeatureListPreviewButton->setDefaultAction( previewAction );
}
}
}
QAction *sortByPreviewExpression = new QAction( QgsApplication::getThemeIcon( QStringLiteral( "sort.svg" ) ), tr( "Sort by preview expression" ), this );
connect( sortByPreviewExpression, &QAction::triggered, this, &QgsDualView::sortByPreviewExpression );
mFeatureListPreviewButton->addAction( sortByPreviewExpression );
QAction *separator = new QAction( mFeatureListPreviewButton );
separator->setSeparator( true );
mFeatureListPreviewButton->addAction( separator );
restoreRecentDisplayExpressions();
// If there is no single field found as preview
if ( !mFeatureListPreviewButton->defaultAction() )
{
mFeatureListView->setDisplayExpression( displayExpression );
mFeatureListPreviewButton->setDefaultAction( mActionExpressionPreview );
setDisplayExpression( mFeatureListView->displayExpression() );
}
else
{
mFeatureListPreviewButton->defaultAction()->trigger();
}
}
void QgsDualView::setView( QgsDualView::ViewMode view )
{
setCurrentIndex( view );
}
QgsDualView::ViewMode QgsDualView::view() const
{
return static_cast< QgsDualView::ViewMode >( currentIndex() );
}
void QgsDualView::setFilterMode( QgsAttributeTableFilterModel::FilterMode filterMode )
{
// cleanup any existing connections
switch ( mFilterModel->filterMode() )
{
case QgsAttributeTableFilterModel::ShowVisible:
disconnect( mFilterModel->mapCanvas(), &QgsMapCanvas::extentsChanged, this, &QgsDualView::extentChanged );
break;
case QgsAttributeTableFilterModel::ShowAll:
case QgsAttributeTableFilterModel::ShowEdited:
case QgsAttributeTableFilterModel::ShowFilteredList:
break;
case QgsAttributeTableFilterModel::ShowSelected:
disconnect( masterModel()->layer(), &QgsVectorLayer::selectionChanged, this, &QgsDualView::updateSelectedFeatures );
break;
}
QgsFeatureRequest r = mMasterModel->request();
bool needsGeometry = filterMode == QgsAttributeTableFilterModel::ShowVisible;
bool requiresTableReload = ( r.filterType() != QgsFeatureRequest::FilterNone || !r.filterRect().isNull() ) // previous request was subset
|| ( needsGeometry && r.flags() & QgsFeatureRequest::NoGeometry ) // no geometry for last request
|| ( mMasterModel->rowCount() == 0 ); // no features
if ( !needsGeometry )
r.setFlags( r.flags() | QgsFeatureRequest::NoGeometry );
else
r.setFlags( r.flags() & ~( QgsFeatureRequest::NoGeometry ) );
r.setFilterFids( QgsFeatureIds() );
r.setFilterRect( QgsRectangle() );
r.disableFilter();
// setup new connections and filter request parameters
switch ( filterMode )
{
case QgsAttributeTableFilterModel::ShowVisible:
connect( mFilterModel->mapCanvas(), &QgsMapCanvas::extentsChanged, this, &QgsDualView::extentChanged );
if ( mFilterModel->mapCanvas() )
{
QgsRectangle rect = mFilterModel->mapCanvas()->mapSettings().mapToLayerCoordinates( mLayer, mFilterModel->mapCanvas()->extent() );
r.setFilterRect( rect );
}
break;
case QgsAttributeTableFilterModel::ShowAll:
case QgsAttributeTableFilterModel::ShowEdited:
case QgsAttributeTableFilterModel::ShowFilteredList:
break;
case QgsAttributeTableFilterModel::ShowSelected:
connect( masterModel()->layer(), &QgsVectorLayer::selectionChanged, this, &QgsDualView::updateSelectedFeatures );
r.setFilterFids( masterModel()->layer()->selectedFeatureIds() );
break;
}
if ( requiresTableReload )
{
mMasterModel->setRequest( r );
whileBlocking( mLayerCache )->setCacheGeometry( needsGeometry );
mMasterModel->loadLayer();
}
//update filter model
mFilterModel->setFilterMode( filterMode );
emit filterChanged();
}
void QgsDualView::setSelectedOnTop( bool selectedOnTop )
{
mFilterModel->setSelectedOnTop( selectedOnTop );
}
void QgsDualView::initLayerCache( bool cacheGeometry )
{
// Initialize the cache
QgsSettings settings;
int cacheSize = settings.value( QStringLiteral( "qgis/attributeTableRowCache" ), "10000" ).toInt();
mLayerCache = new QgsVectorLayerCache( mLayer, cacheSize, this );
mLayerCache->setCacheGeometry( cacheGeometry );
if ( 0 == cacheSize || 0 == ( QgsVectorDataProvider::SelectAtId & mLayer->dataProvider()->capabilities() ) )
{
connect( mLayerCache, &QgsVectorLayerCache::invalidated, this, &QgsDualView::rebuildFullLayerCache );
rebuildFullLayerCache();
}
}
void QgsDualView::initModels( QgsMapCanvas *mapCanvas, const QgsFeatureRequest &request, bool loadFeatures )
{
delete mFeatureListModel;
delete mFilterModel;
delete mMasterModel;
mMasterModel = new QgsAttributeTableModel( mLayerCache, this );
mMasterModel->setRequest( request );
mMasterModel->setEditorContext( mEditorContext );
mMasterModel->setExtraColumns( 1 ); // Add one extra column which we can "abuse" as an action column
connect( mMasterModel, &QgsAttributeTableModel::progress, this, &QgsDualView::progress );
connect( mMasterModel, &QgsAttributeTableModel::finished, this, &QgsDualView::finished );
connect( mConditionalFormatWidget, &QgsFieldConditionalFormatWidget::rulesUpdated, mMasterModel, &QgsAttributeTableModel::fieldConditionalStyleChanged );
if ( loadFeatures )
mMasterModel->loadLayer();
mFilterModel = new QgsAttributeTableFilterModel( mapCanvas, mMasterModel, mMasterModel );
// The following connections to invalidate() are necessary to keep the filter model in sync
// see regression https://github.com/qgis/QGIS/issues/23890
connect( mMasterModel, &QgsAttributeTableModel::rowsRemoved, mFilterModel, &QgsAttributeTableFilterModel::invalidate );
connect( mMasterModel, &QgsAttributeTableModel::rowsInserted, mFilterModel, &QgsAttributeTableFilterModel::invalidate );
connect( mFeatureListView, &QgsFeatureListView::displayExpressionChanged, this, &QgsDualView::displayExpressionChanged );
mFeatureListModel = new QgsFeatureListModel( mFilterModel, mFilterModel );
mFeatureListModel->setSortByDisplayExpression( true );
}
void QgsDualView::restoreRecentDisplayExpressions()
{
const QVariantList previewExpressions = mLayer->customProperty( QStringLiteral( "dualview/previewExpressions" ) ).toList();
for ( const QVariant &previewExpression : previewExpressions )
insertRecentlyUsedDisplayExpression( previewExpression.toString() );
}
void QgsDualView::saveRecentDisplayExpressions() const
{
if ( ! mLayer )
{
return;
}
QList<QAction *> actions = mFeatureListPreviewButton->actions();
// Remove existing same action
int index = actions.indexOf( mLastDisplayExpressionAction );
if ( index != -1 )
{
QVariantList previewExpressions;
for ( ; index < actions.length(); ++index )
{
QAction *action = actions.at( index );
previewExpressions << action->property( "previewExpression" );
}
mLayer->setCustomProperty( QStringLiteral( "dualview/previewExpressions" ), previewExpressions );
}
}
void QgsDualView::setDisplayExpression( const QString &expression )
{
mDisplayExpression = expression;
insertRecentlyUsedDisplayExpression( expression );
}
void QgsDualView::insertRecentlyUsedDisplayExpression( const QString &expression )
{
QList<QAction *> actions = mFeatureListPreviewButton->actions();
// Remove existing same action
int index = actions.indexOf( mLastDisplayExpressionAction );
if ( index != -1 )
{
for ( int i = 0; index + i < actions.length(); ++i )
{
QAction *action = actions.at( index );
if ( action->text() == expression || i >= 9 )
{
if ( action == mLastDisplayExpressionAction )
mLastDisplayExpressionAction = nullptr;
mFeatureListPreviewButton->removeAction( action );
}
else
{
if ( !mLastDisplayExpressionAction )
mLastDisplayExpressionAction = action;
}
}
}
QString name = expression;
QIcon icon = QgsApplication::getThemeIcon( QStringLiteral( "/mIconExpressionPreview.svg" ) );
if ( expression.startsWith( QLatin1String( "COALESCE( \"" ) ) && expression.endsWith( QLatin1String( ", '<NULL>' )" ) ) )
{
name = expression.mid( 11, expression.length() - 24 ); // Numbers calculated from the COALESCE / <NULL> parts
int fieldIndex = mLayer->fields().indexOf( name );
if ( fieldIndex != -1 )
{
name = mLayer->attributeDisplayName( fieldIndex );
icon = mLayer->fields().iconForField( fieldIndex );
}
else
{
name = expression;
}
}
QAction *previewAction = new QAction( icon, name, mFeatureListPreviewButton );
previewAction->setProperty( "previewExpression", expression );
connect( previewAction, &QAction::triggered, this, [expression, this]( bool )
{
setDisplayExpression( expression );
mFeatureListPreviewButton->setText( expression );
}
);
mFeatureListPreviewButton->insertAction( mLastDisplayExpressionAction, previewAction );
mLastDisplayExpressionAction = previewAction;
}
void QgsDualView::updateEditSelectionProgress( int progress, int count )
{
mProgressCount->setText( QStringLiteral( "%1 / %2" ).arg( progress + 1 ).arg( count ) );
mPreviousFeatureButton->setEnabled( progress > 0 );
mNextFeatureButton->setEnabled( progress + 1 < count );
mFirstFeatureButton->setEnabled( progress > 0 );
mLastFeatureButton->setEnabled( progress + 1 < count );
}
void QgsDualView::panOrZoomToFeature( const QgsFeatureIds &featureset )
{
QgsMapCanvas *canvas = mFilterModel->mapCanvas();
if ( canvas && view() == AttributeEditor && featureset != mLastFeatureSet )
{
if ( filterMode() != QgsAttributeTableFilterModel::ShowVisible )
{
if ( mAutoPanButton->isChecked() )
QTimer::singleShot( 0, this, [ = ]()
{
canvas->panToFeatureIds( mLayer, featureset, false );
} );
else if ( mAutoZoomButton->isChecked() )
QTimer::singleShot( 0, this, [ = ]()
{
canvas->zoomToFeatureIds( mLayer, featureset );
} );
}
if ( mFlashButton->isChecked() )
QTimer::singleShot( 0, this, [ = ]()
{
canvas->flashFeatureIds( mLayer, featureset );
} );
mLastFeatureSet = featureset;
}
}
void QgsDualView::panZoomGroupButtonToggled( QAbstractButton *button, bool checked )
{
if ( button == mAutoPanButton && checked )
{
QgsSettings().setEnumValue( QStringLiteral( "/qgis/attributeTable/featureListBrowsingAction" ), PanToFeature );
mAutoZoomButton->setChecked( false );
}
else if ( button == mAutoZoomButton && checked )
{
QgsSettings().setEnumValue( QStringLiteral( "/qgis/attributeTable/featureListBrowsingAction" ), ZoomToFeature );
mAutoPanButton->setChecked( false );
}
else
{
QgsSettings().setEnumValue( QStringLiteral( "/qgis/attributeTable/featureListBrowsingAction" ), NoAction );
}
if ( checked )
panOrZoomToFeature( mFeatureListView->currentEditSelection() );
}
void QgsDualView::flashButtonClicked( bool clicked )
{
QgsSettings().setValue( QStringLiteral( "/qgis/attributeTable/featureListHighlightFeature" ), clicked );
if ( !clicked )
return;
QgsMapCanvas *canvas = mFilterModel->mapCanvas();
if ( canvas )
canvas->flashFeatureIds( mLayer, mFeatureListView->currentEditSelection() );
}
void QgsDualView::featureListAboutToChangeEditSelection( bool &ok )
{
if ( mLayer->isEditable() && !mAttributeForm->save() )
ok = false;
}
void QgsDualView::featureListCurrentEditSelectionChanged( const QgsFeature &feat )
{
if ( !mAttributeForm )
{
mTempAttributeFormFeature = feat;
}
else if ( !mLayer->isEditable() || mAttributeForm->save() )
{
mAttributeForm->setFeature( feat );
QgsFeatureIds featureset;
featureset << feat.id();
setCurrentEditSelection( featureset );
panOrZoomToFeature( featureset );
}
else
{
// Couldn't save feature
}
}
void QgsDualView::setCurrentEditSelection( const QgsFeatureIds &fids )
{
mFeatureListView->setCurrentFeatureEdited( false );
mFeatureListView->setEditSelection( fids );
}
bool QgsDualView::saveEditChanges()
{
return mAttributeForm->save();
}
void QgsDualView::openConditionalStyles()
{
mConditionalFormatWidgetStack->setVisible( !mConditionalFormatWidgetStack->isVisible() );
}
void QgsDualView::setMultiEditEnabled( bool enabled )
{
if ( enabled )
setView( AttributeEditor );
mAttributeForm->setMode( enabled ? QgsAttributeEditorContext::MultiEditMode : QgsAttributeEditorContext::SingleEditMode );
}
void QgsDualView::toggleSearchMode( bool enabled )
{
if ( enabled )
{
setView( AttributeEditor );
mAttributeForm->setMode( QgsAttributeEditorContext::SearchMode );
}
else
{
mAttributeForm->setMode( QgsAttributeEditorContext::SingleEditMode );
}
}
void QgsDualView::previewExpressionBuilder()
{
// Show expression builder
QgsExpressionContext context( QgsExpressionContextUtils::globalProjectLayerScopes( mLayer ) );
QgsExpressionBuilderDialog dlg( mLayer, mFeatureListView->displayExpression(), this, QStringLiteral( "generic" ), context );
dlg.setWindowTitle( tr( "Expression Based Preview" ) );
dlg.setExpressionText( mFeatureListView->displayExpression() );
if ( dlg.exec() == QDialog::Accepted )
{
mFeatureListView->setDisplayExpression( dlg.expressionText() );
mFeatureListPreviewButton->setDefaultAction( mActionExpressionPreview );
mFeatureListPreviewButton->setPopupMode( QToolButton::MenuButtonPopup );
}
setDisplayExpression( mFeatureListView->displayExpression() );
}
void QgsDualView::previewColumnChanged( QAction *previewAction, const QString &expression )
{
if ( !mFeatureListView->setDisplayExpression( QStringLiteral( "COALESCE( \"%1\", '<NULL>' )" ).arg( expression ) ) )
{
QMessageBox::warning( this,
tr( "Column Preview" ),
tr( "Could not set column '%1' as preview column.\nParser error:\n%2" )
.arg( previewAction->text(), mFeatureListView->parserErrorString() )
);
}
else
{
mFeatureListPreviewButton->setText( previewAction->text() );
mFeatureListPreviewButton->setIcon( previewAction->icon() );
mFeatureListPreviewButton->setPopupMode( QToolButton::InstantPopup );
}
setDisplayExpression( mFeatureListView->displayExpression() );
}
int QgsDualView::featureCount()
{
return mMasterModel->rowCount();
}
int QgsDualView::filteredFeatureCount()
{
return mFilterModel->rowCount();
}
void QgsDualView::copyCellContent() const
{
QAction *action = qobject_cast<QAction *>( sender() );
if ( action && action->data().isValid() && action->data().canConvert<QModelIndex>() )
{
QModelIndex index = action->data().toModelIndex();
QVariant var = masterModel()->data( index, Qt::DisplayRole );
QApplication::clipboard()->setText( var.toString() );
}
}
void QgsDualView::cancelProgress()
{
if ( mProgressDlg )
mProgressDlg->cancel();
}
void QgsDualView::hideEvent( QHideEvent *event )
{
Q_UNUSED( event )
saveRecentDisplayExpressions();
}
void QgsDualView::viewWillShowContextMenu( QMenu *menu, const QgsFeatureId featureId )
{
if ( !menu )
{
return;
}
QModelIndex sourceIndex = mFilterModel->fidToIndex( featureId );
if ( ! sourceIndex.isValid() )
{
return;
}
QAction *copyContentAction = new QAction( tr( "Copy Cell Content" ), this );
copyContentAction->setData( QVariant::fromValue<QModelIndex>( sourceIndex ) );
menu->addAction( copyContentAction );
connect( copyContentAction, &QAction::triggered, this, &QgsDualView::copyCellContent );
QgsVectorLayer *vl = mFilterModel->layer();
QgsMapCanvas *canvas = mFilterModel->mapCanvas();
if ( canvas && vl && vl->geometryType() != QgsWkbTypes::NullGeometry )
{
menu->addAction( tr( "Zoom to Feature" ), this, SLOT( zoomToCurrentFeature() ) );
menu->addAction( tr( "Pan to Feature" ), this, SLOT( panToCurrentFeature() ) );
menu->addAction( tr( "Flash Feature" ), this, SLOT( flashCurrentFeature() ) );
}
//add user-defined actions to context menu
QList<QgsAction> actions = mLayer->actions()->actions( QStringLiteral( "Field" ) );
if ( !actions.isEmpty() )
{
QAction *a = menu->addAction( tr( "Run Layer Action" ) );
a->setEnabled( false );
const auto constActions = actions;
for ( const QgsAction &action : constActions )
{
if ( !action.runable() )
continue;
if ( vl && !vl->isEditable() && action.isEnabledOnlyWhenEditable() )
continue;
QgsAttributeTableAction *a = new QgsAttributeTableAction( action.name(), this, action.id(), sourceIndex );
menu->addAction( action.name(), a, &QgsAttributeTableAction::execute );
}
}
//add actions from QgsMapLayerActionRegistry to context menu
QList<QgsMapLayerAction *> registeredActions = QgsGui::mapLayerActionRegistry()->mapLayerActions( mLayer );
if ( !registeredActions.isEmpty() )
{
//add a separator between user defined and standard actions
menu->addSeparator();
const auto constRegisteredActions = registeredActions;
for ( QgsMapLayerAction *action : constRegisteredActions )
{
QgsAttributeTableMapLayerAction *a = new QgsAttributeTableMapLayerAction( action->text(), this, action, sourceIndex );
menu->addAction( action->text(), a, &QgsAttributeTableMapLayerAction::execute );
}
}
menu->addSeparator();
QgsAttributeTableAction *a = new QgsAttributeTableAction( tr( "Open Form" ), this, QString(), sourceIndex );
menu->addAction( tr( "Open Form" ), a, &QgsAttributeTableAction::featureForm );
}
void QgsDualView::widgetWillShowContextMenu( QgsActionMenu *menu, const QgsFeatureId featureId )
{
emit showContextMenuExternally( menu, featureId );
}
void QgsDualView::showViewHeaderMenu( QPoint point )
{
int col = mTableView->columnAt( point.x() );
delete mHorizontalHeaderMenu;
mHorizontalHeaderMenu = new QMenu( this );
QAction *hide = new QAction( tr( "&Hide Column" ), mHorizontalHeaderMenu );
connect( hide, &QAction::triggered, this, &QgsDualView::hideColumn );
hide->setData( col );
mHorizontalHeaderMenu->addAction( hide );
QAction *setWidth = new QAction( tr( "&Set Width…" ), mHorizontalHeaderMenu );
connect( setWidth, &QAction::triggered, this, &QgsDualView::resizeColumn );
setWidth->setData( col );
mHorizontalHeaderMenu->addAction( setWidth );
QAction *optimizeWidth = new QAction( tr( "&Autosize" ), mHorizontalHeaderMenu );
connect( optimizeWidth, &QAction::triggered, this, &QgsDualView::autosizeColumn );
optimizeWidth->setData( col );
mHorizontalHeaderMenu->addAction( optimizeWidth );
mHorizontalHeaderMenu->addSeparator();
QAction *organize = new QAction( tr( "&Organize Columns…" ), mHorizontalHeaderMenu );
connect( organize, &QAction::triggered, this, &QgsDualView::organizeColumns );
mHorizontalHeaderMenu->addAction( organize );
QAction *sort = new QAction( tr( "&Sort…" ), mHorizontalHeaderMenu );
connect( sort, &QAction::triggered, this, &QgsDualView::modifySort );
mHorizontalHeaderMenu->addAction( sort );
mHorizontalHeaderMenu->popup( mTableView->horizontalHeader()->mapToGlobal( point ) );
}
void QgsDualView::organizeColumns()
{
if ( !mLayer )
{
return;
}
QgsOrganizeTableColumnsDialog dialog( mLayer, attributeTableConfig(), this );
if ( dialog.exec() == QDialog::Accepted )
{
QgsAttributeTableConfig config = dialog.config();
setAttributeTableConfig( config );
}
}
void QgsDualView::tableColumnResized( int column, int width )
{
QgsAttributeTableConfig config = mConfig;
int sourceCol = config.mapVisibleColumnToIndex( column );
if ( sourceCol >= 0 && config.columnWidth( sourceCol ) != width )
{
config.setColumnWidth( sourceCol, width );
setAttributeTableConfig( config );
}
}
void QgsDualView::hideColumn()
{
QAction *action = qobject_cast<QAction *>( sender() );
int col = action->data().toInt();
QgsAttributeTableConfig config = mConfig;
int sourceCol = mConfig.mapVisibleColumnToIndex( col );
if ( sourceCol >= 0 )
{
config.setColumnHidden( sourceCol, true );
setAttributeTableConfig( config );
}
}
void QgsDualView::resizeColumn()
{
QAction *action = qobject_cast<QAction *>( sender() );
int col = action->data().toInt();
if ( col < 0 )
return;
QgsAttributeTableConfig config = mConfig;
int sourceCol = config.mapVisibleColumnToIndex( col );
if ( sourceCol >= 0 )
{
bool ok = false;
int width = QInputDialog::getInt( this, tr( "Set column width" ), tr( "Enter column width" ),
mTableView->columnWidth( col ),
0, 1000, 10, &ok );
if ( ok )
{
config.setColumnWidth( sourceCol, width );
setAttributeTableConfig( config );
}
}
}
void QgsDualView::autosizeColumn()
{
QAction *action = qobject_cast<QAction *>( sender() );
int col = action->data().toInt();
mTableView->resizeColumnToContents( col );
}
void QgsDualView::modifySort()
{
if ( !mLayer )
return;
QgsAttributeTableConfig config = mConfig;
QDialog orderByDlg;
orderByDlg.setWindowTitle( tr( "Configure Attribute Table Sort Order" ) );
QDialogButtonBox *dialogButtonBox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel );
QGridLayout *layout = new QGridLayout();
connect( dialogButtonBox, &QDialogButtonBox::accepted, &orderByDlg, &QDialog::accept );
connect( dialogButtonBox, &QDialogButtonBox::rejected, &orderByDlg, &QDialog::reject );
orderByDlg.setLayout( layout );
QGroupBox *sortingGroupBox = new QGroupBox();
sortingGroupBox->setTitle( tr( "Defined sort order in attribute table" ) );
sortingGroupBox->setCheckable( true );
sortingGroupBox->setChecked( !sortExpression().isEmpty() );
layout->addWidget( sortingGroupBox );
sortingGroupBox->setLayout( new QGridLayout() );
QgsExpressionBuilderWidget *expressionBuilder = new QgsExpressionBuilderWidget();
QgsExpressionContext context( QgsExpressionContextUtils::globalProjectLayerScopes( mLayer ) );
expressionBuilder->setExpressionContext( context );
expressionBuilder->setLayer( mLayer );
expressionBuilder->loadFieldNames();
expressionBuilder->loadRecent( QStringLiteral( "generic" ) );
expressionBuilder->loadUserExpressions( QStringLiteral( "generic" ) );
expressionBuilder->setExpressionText( sortExpression().isEmpty() ? mLayer->displayExpression() : sortExpression() );
sortingGroupBox->layout()->addWidget( expressionBuilder );
QCheckBox *cbxSortAscending = new QCheckBox( tr( "Sort ascending" ) );
cbxSortAscending->setChecked( config.sortOrder() == Qt::AscendingOrder );
sortingGroupBox->layout()->addWidget( cbxSortAscending );
layout->addWidget( dialogButtonBox );
if ( orderByDlg.exec() )
{
Qt::SortOrder sortOrder = cbxSortAscending->isChecked() ? Qt::AscendingOrder : Qt::DescendingOrder;
if ( sortingGroupBox->isChecked() )
{
setSortExpression( expressionBuilder->expressionText(), sortOrder );
config.setSortExpression( expressionBuilder->expressionText() );
config.setSortOrder( sortOrder );
}
else
{
setSortExpression( QString(), sortOrder );
config.setSortExpression( QString() );
}
setAttributeTableConfig( config );
}
}
void QgsDualView::zoomToCurrentFeature()
{
QModelIndex currentIndex = mTableView->currentIndex();
if ( !currentIndex.isValid() )
{
return;
}
QgsFeatureIds ids;
ids.insert( mFilterModel->rowToId( currentIndex ) );
QgsMapCanvas *canvas = mFilterModel->mapCanvas();
if ( canvas )
{
canvas->zoomToFeatureIds( mLayer, ids );
}
}
void QgsDualView::panToCurrentFeature()
{
QModelIndex currentIndex = mTableView->currentIndex();
if ( !currentIndex.isValid() )
{
return;
}
QgsFeatureIds ids;
ids.insert( mFilterModel->rowToId( currentIndex ) );
QgsMapCanvas *canvas = mFilterModel->mapCanvas();
if ( canvas )
{
canvas->panToFeatureIds( mLayer, ids );
}
}
void QgsDualView::flashCurrentFeature()
{
QModelIndex currentIndex = mTableView->currentIndex();
if ( !currentIndex.isValid() )
{
return;
}
QgsFeatureIds ids;
ids.insert( mFilterModel->rowToId( currentIndex ) );
QgsMapCanvas *canvas = mFilterModel->mapCanvas();
if ( canvas )
{
canvas->flashFeatureIds( mLayer, ids );
}
}
void QgsDualView::rebuildFullLayerCache()
{
connect( mLayerCache, &QgsVectorLayerCache::progress, this, &QgsDualView::progress, Qt::UniqueConnection );
connect( mLayerCache, &QgsVectorLayerCache::finished, this, &QgsDualView::finished, Qt::UniqueConnection );
mLayerCache->setFullCache( true );
}
void QgsDualView::previewExpressionChanged( const QString &expression )
{
mLayer->setDisplayExpression( expression );
}
void QgsDualView::onSortColumnChanged()
{
QgsAttributeTableConfig cfg = attributeTableConfig();
if ( cfg.sortExpression() != mFilterModel->sortExpression() ||
cfg.sortOrder() != mFilterModel->sortOrder() )
{
cfg.setSortExpression( mFilterModel->sortExpression() );
cfg.setSortOrder( mFilterModel->sortOrder() );
setAttributeTableConfig( cfg );
}
}
void QgsDualView::sortByPreviewExpression()
{
Qt::SortOrder sortOrder = Qt::AscendingOrder;
if ( mFeatureListView->displayExpression() == sortExpression() )
{
sortOrder = mConfig.sortOrder() == Qt::AscendingOrder ? Qt::DescendingOrder : Qt::AscendingOrder;
}
setSortExpression( mFeatureListView->displayExpression(), sortOrder );
}
void QgsDualView::updateSelectedFeatures()
{
QgsFeatureRequest r = mMasterModel->request();
if ( r.filterType() == QgsFeatureRequest::FilterNone && r.filterRect().isNull() )
return; // already requested all features
r.setFilterFids( masterModel()->layer()->selectedFeatureIds() );
mMasterModel->setRequest( r );
mMasterModel->loadLayer();
emit filterChanged();
}
void QgsDualView::extentChanged()
{
QgsFeatureRequest r = mMasterModel->request();
if ( mFilterModel->mapCanvas() && ( r.filterType() != QgsFeatureRequest::FilterNone || !r.filterRect().isNull() ) )
{
QgsRectangle rect = mFilterModel->mapCanvas()->mapSettings().mapToLayerCoordinates( mLayer, mFilterModel->mapCanvas()->extent() );
r.setFilterRect( rect );
mMasterModel->setRequest( r );
mMasterModel->loadLayer();
}
emit filterChanged();
}
void QgsDualView::featureFormAttributeChanged( const QString &attribute, const QVariant &value, bool attributeChanged )
{
Q_UNUSED( attribute )
Q_UNUSED( value )
if ( attributeChanged )
mFeatureListView->setCurrentFeatureEdited( true );
}
void QgsDualView::setFilteredFeatures( const QgsFeatureIds &filteredFeatures )
{
mFilterModel->setFilteredFeatures( filteredFeatures );
}
void QgsDualView::setRequest( const QgsFeatureRequest &request )
{
mMasterModel->setRequest( request );
}
void QgsDualView::setFeatureSelectionManager( QgsIFeatureSelectionManager *featureSelectionManager )
{
mTableView->setFeatureSelectionManager( featureSelectionManager );
mFeatureListView->setFeatureSelectionManager( featureSelectionManager );
if ( mFeatureSelectionManager && mFeatureSelectionManager->parent() == this )
delete mFeatureSelectionManager;
mFeatureSelectionManager = featureSelectionManager;
}
void QgsDualView::setAttributeTableConfig( const QgsAttributeTableConfig &config )
{
mConfig = config;
mConfig.update( mLayer->fields() );
mLayer->setAttributeTableConfig( mConfig );
mFilterModel->setAttributeTableConfig( mConfig );
mTableView->setAttributeTableConfig( mConfig );
}
void QgsDualView::setSortExpression( const QString &sortExpression, Qt::SortOrder sortOrder )
{
if ( sortExpression.isNull() )
mFilterModel->sort( -1 );
else
mFilterModel->sort( sortExpression, sortOrder );
mConfig.setSortExpression( sortExpression );
mConfig.setSortOrder( sortOrder );
setAttributeTableConfig( mConfig );
}
QString QgsDualView::sortExpression() const
{
return mFilterModel->sortExpression();
}
QgsAttributeTableConfig QgsDualView::attributeTableConfig() const
{
return mConfig;
}
void QgsDualView::progress( int i, bool &cancel )
{
if ( !mProgressDlg )
{
mProgressDlg = new QProgressDialog( tr( "Loading features…" ), tr( "Abort" ), 0, 0, this );
mProgressDlg->setWindowTitle( tr( "Attribute Table" ) );
mProgressDlg->setWindowModality( Qt::WindowModal );
mProgressDlg->show();
}
mProgressDlg->setLabelText( tr( "%1 features loaded." ).arg( i ) );
QCoreApplication::processEvents();
cancel = mProgressDlg && mProgressDlg->wasCanceled();
}
void QgsDualView::finished()
{
delete mProgressDlg;
mProgressDlg = nullptr;
}
/*
* QgsAttributeTableAction
*/
void QgsAttributeTableAction::execute()
{
mDualView->masterModel()->executeAction( mAction, mFieldIdx );
}
void QgsAttributeTableAction::featureForm()
{
QgsFeatureIds editedIds;
editedIds << mDualView->masterModel()->rowToId( mFieldIdx.row() );
mDualView->setCurrentEditSelection( editedIds );
mDualView->setView( QgsDualView::AttributeEditor );
}
/*
* QgsAttributeTableMapLayerAction
*/
void QgsAttributeTableMapLayerAction::execute()
{
mDualView->masterModel()->executeMapLayerAction( mAction, mFieldIdx );
}