Gui enhancements

This commit is contained in:
Alessandro Pasotti 2022-10-21 08:55:29 +02:00 committed by Nyall Dawson
parent 3479a66e4e
commit 70c93d4aea
18 changed files with 487 additions and 202 deletions

View File

@ -261,7 +261,7 @@ Returns the (possibly NULL) raster attribute table for the given band ``bandNumb
.. versionadded:: 3.30
%End
int attributeTableCount( );
int attributeTableCount( ) const;
%Docstring
Returns the number of attribute tables for the raster by counting the number of bands that have an associated attribute table.

View File

@ -46,6 +46,33 @@ Sets the raster layer and an optional band number.
bool isDirty( ) const;
%Docstring
Returns ``True`` if the associated raster attribute table is dirty
%End
void setMessageBar( QgsMessageBar *bar );
%Docstring
Sets the message ``bar`` associated with the widget. This allows the widget to push feedback messages
to the appropriate message bar.
.. seealso:: :py:func:`messageBar`
%End
signals:
void rendererChanged( );
%Docstring
This signal is emitted after a successful classify operation which changed the raster renderer.
%End
public slots:
void saveChanges();
%Docstring
Save the changes in the raster attribute table.
%End
bool setEditable( bool editable );
%Docstring
Set the editable state, it may trigger save changes if the attribute table has unsave changes.
%End
};

View File

@ -4380,6 +4380,12 @@ void QgisApp::setupConnections()
connect( QgsProject::instance(), &QgsProject::transactionGroupsChanged, this, &QgisApp::onTransactionGroupsChanged );
// Handle dirty raster attribute tables
connect( QgsProject::instance(), qOverload<const QList< QgsMapLayer * > & >( &QgsProject::layersWillBeRemoved ), this, [ = ]( const QList< QgsMapLayer * > &layers )
{
checkUnsavedRasterAttributeTableEdits( layers, false );
} );
// connect preview modes actions
connect( mActionPreviewModeOff, &QAction::triggered, this, &QgisApp::disablePreviewMode );
connect( mActionPreviewModeMono, &QAction::triggered, this, &QgisApp::activateMonoPreview );
@ -5600,7 +5606,7 @@ void QgisApp::fileExit()
}
QgsCanvasRefreshBlocker refreshBlocker;
if ( checkUnsavedLayerEdits() && checkMemoryLayers() && saveDirty() && checkExitBlockers() )
if ( checkUnsavedLayerEdits() && checkMemoryLayers() && saveDirty() && checkExitBlockers() && checkUnsavedRasterAttributeTableEdits() )
{
closeProject();
userProfileManager()->setDefaultFromActive();
@ -5638,7 +5644,7 @@ bool QgisApp::fileNew( bool promptToSaveFlag, bool forceBlank )
if ( promptToSaveFlag )
{
if ( !checkUnsavedLayerEdits() || !checkMemoryLayers() || !saveDirty() )
if ( !checkUnsavedLayerEdits() || !checkMemoryLayers() || !saveDirty() || !checkUnsavedRasterAttributeTableEdits() )
{
return false; //cancel pressed
}
@ -5710,7 +5716,7 @@ bool QgisApp::fileNewFromTemplate( const QString &fileName )
if ( checkTasksDependOnProject() )
return false;
if ( !checkUnsavedLayerEdits() || !checkMemoryLayers() || !saveDirty() )
if ( !checkUnsavedLayerEdits() || !checkMemoryLayers() || !saveDirty() || !checkUnsavedRasterAttributeTableEdits() )
{
return false; //cancel pressed
}
@ -6206,7 +6212,7 @@ void QgisApp::fileOpen()
return;
// possibly save any pending work before opening a new project
if ( checkUnsavedLayerEdits() && checkMemoryLayers() && saveDirty() )
if ( checkUnsavedLayerEdits() && checkMemoryLayers() && saveDirty() && checkUnsavedRasterAttributeTableEdits() )
{
// Retrieve last used project dir from persistent settings
QgsSettings settings;
@ -6262,7 +6268,7 @@ void QgisApp::fileRevert()
QMessageBox::Yes | QMessageBox::No, QMessageBox::No ) == QMessageBox::No )
return;
if ( !checkUnsavedLayerEdits() || !checkMemoryLayers() )
if ( !checkUnsavedLayerEdits() || !checkMemoryLayers() || ! checkUnsavedRasterAttributeTableEdits() )
return;
// re-open the current project
@ -6760,7 +6766,7 @@ void QgisApp::openProject( QAction *action )
if ( checkTasksDependOnProject() )
return;
if ( checkUnsavedLayerEdits() && checkMemoryLayers() && saveDirty() )
if ( checkUnsavedLayerEdits() && checkMemoryLayers() && saveDirty() && checkUnsavedRasterAttributeTableEdits() )
addProject( project );
}
@ -6805,7 +6811,7 @@ void QgisApp::openProject( const QString &fileName )
return;
// possibly save any pending work before opening a different project
if ( checkUnsavedLayerEdits() && checkMemoryLayers() && saveDirty() )
if ( checkUnsavedLayerEdits() && checkMemoryLayers() && saveDirty() && checkUnsavedRasterAttributeTableEdits() )
{
// error handling and reporting is in addProject() function
addProject( fileName );
@ -12027,6 +12033,30 @@ void QgisApp::openRasterAttributeTable()
}
}
void QgisApp::loadRasterAttributeTableFromFile( )
{
if ( !mLayerTreeView )
return;
//find current Layer
QgsMapLayer *currentLayer = mLayerTreeView->currentLayer();
if ( !currentLayer )
return;
QgsRasterLayer *layer = qobject_cast<QgsRasterLayer *>( currentLayer );
int bandNumber { 0 };
if ( layer )
{
QgsCreateRasterAttributeTableDialog dlg { layer };
if ( dlg.exec() == QDialog::Accepted )
{
// TODO!
}
}
}
void QgisApp::createRasterAttributeTable()
{
if ( !mLayerTreeView )
@ -12053,7 +12083,7 @@ void QgisApp::createRasterAttributeTable()
if ( ! rat )
{
visibleMessageBar()->pushMessage( tr( "Error Creating Raster Attribute Table" ),
tr( "The Raster Attribute Table could not be created." ),
tr( "The raster attribute table could not be created." ),
Qgis::MessageLevel::Critical );
return;
}
@ -13381,6 +13411,94 @@ bool QgisApp::checkUnsavedLayerEdits()
return true;
}
bool QgisApp::checkUnsavedRasterAttributeTableEdits( const QList<QgsMapLayer *> &mapLayers, bool allowCancel )
{
bool retVal { true };
QVector<QgsRasterLayer *> rasterLayers;
if ( ! mapLayers.isEmpty() )
{
for ( QgsMapLayer *mapLayer : std::as_const( mapLayers ) )
{
if ( QgsRasterLayer *rasterLayer = qobject_cast< QgsRasterLayer *>( mapLayer ) )
{
rasterLayers.push_back( rasterLayer );
}
}
}
else
{
rasterLayers = QgsProject::instance()->layers<QgsRasterLayer *>();
}
for ( QgsRasterLayer *rasterLayer : std::as_const( rasterLayers ) )
{
QStringList dirtyBands;
QList<QgsRasterAttributeTable *> dirtyRats;
for ( int bandNo = 1; bandNo < rasterLayer->bandCount(); ++bandNo )
{
if ( QgsRasterAttributeTable *rat = rasterLayer->attributeTable( bandNo ); rat && rat->isDirty() )
{
dirtyBands.push_back( QString::number( bandNo ) );
dirtyRats.push_back( rat );
}
}
if ( ! dirtyBands.isEmpty( ) )
{
QMessageBox::StandardButtons buttons = QMessageBox::Save | QMessageBox::Discard;
if ( allowCancel )
{
buttons |= QMessageBox::Cancel;
}
switch ( QMessageBox::question( nullptr,
tr( "Save Raster Attribute Table" ),
tr( "Do you want to save the changes to the attribute tables (bands: %1) associated with layer '%2'?" ).arg( dirtyBands.join( QStringLiteral( ", " ) ), rasterLayer->name() ),
buttons ) )
{
case QMessageBox::Save:
{
for ( QgsRasterAttributeTable *rat : std::as_const( dirtyRats ) )
{
QString errorMessage;
if ( rat->filePath().isEmpty( ) )
{
if ( ! rasterLayer->dataProvider()->writeNativeAttributeTable( &errorMessage ) )
{
visibleMessageBar()->pushMessage( tr( "Error Saving Raster Attribute Table" ),
tr( "An error occourred while saving raster attribute table for layer '%1': %2" ).arg( rasterLayer->name(), errorMessage ),
Qgis::MessageLevel::Critical );
retVal = false;
}
}
else
{
if ( ! rat->writeToFile( rat->filePath(), &errorMessage ) )
{
visibleMessageBar()->pushMessage( tr( "Error Saving Raster Attribute Table" ),
tr( "An error occourred while saving raster attribute table for layer '%1' to VAT.DBF file '%2': %3" ).arg( rasterLayer->name(), rat->filePath(), errorMessage ),
Qgis::MessageLevel::Critical );
retVal = false;
}
}
}
break;
}
case QMessageBox::Cancel:
retVal = false;
break;
case QMessageBox::Discard:
default:
break;
}
}
}
return retVal;
}
bool QgisApp::checkMemoryLayers()
{
if ( !QgsSettings().value( QStringLiteral( "askToSaveMemoryLayers" ), true, QgsSettings::App ).toBool() )
@ -16175,6 +16293,8 @@ void QgisApp::showLayerProperties( QgsMapLayer *mapLayer, const QString &page )
{
rasterLayerPropertiesDialog->deleteLater();
} );
break;
}

View File

@ -1106,7 +1106,7 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow
void legendLayerStretchUsingCurrentExtent();
/**
* Open the RasterAttributeTable for the raster layer.
* Open the Raster Attribute Table for the raster layer.
* Only works on raster layers.
*
* \since QGIS 3.30
@ -1114,7 +1114,7 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow
void openRasterAttributeTable();
/**
* Creates a new RasterAttributeTable from the raster layer renderer if the
* Creates a new Raster Attribute Table from the raster layer renderer if the
* renderer supports it.
*
* Only works on raster layers.
@ -1123,6 +1123,15 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow
*/
void createRasterAttributeTable();
/**
* Loads a Raster Attribute Table from a VAT.DBF file.
*
* Only works on raster layers.
*
* \since QGIS 3.30
*/
void loadRasterAttributeTableFromFile();
//! Watch for QFileOpenEvent.
bool event( QEvent *event ) override;
@ -1339,6 +1348,19 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow
*/
bool checkUnsavedLayerEdits();
/**
* Checks for unsaved changes in raster attribute tables and prompts the user to save
* or discard these changes for each raster attribute table.
*
* Returns TRUE if there are no unsaved raster attribute table remaining, or the user
* opted to discard them all. Returns FALSE if the user opted to cancel
* on any raster attribute table.
*
* \param mapLayers optional list of layers to check, if empty all project raster layers will be checked.
* \param allowCancel optional flag (default TRUE) that switches on the "Cancel" button.
*/
bool checkUnsavedRasterAttributeTableEdits( const QList<QgsMapLayer *> &mapLayers = QList<QgsMapLayer *>(), bool allowCancel = true );
/**
* Checks whether memory layers (with features) exist in the project, and if so
* shows a warning to users that their contents will be lost on

View File

@ -229,8 +229,10 @@ QMenu *QgsAppLayerTreeViewMenuProvider::createContextMenu()
}
else if ( rlayer->canCreateRasterAttributeTable() )
{
menu->addAction( QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddTable.svg" ) ), tr( "Create Raster Attribute Table" ), QgisApp::instance(), &QgisApp::createRasterAttributeTable );
menu->addAction( QgsApplication::getThemeIcon( QStringLiteral( "/mActionCreateTable.svg" ) ), tr( "Create Raster Attribute Table" ), QgisApp::instance(), &QgisApp::createRasterAttributeTable );
}
menu->addAction( QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddTable.svg" ) ), tr( "Load Raster Attribute Table from VAT.DBF" ), QgisApp::instance(), &QgisApp::loadRasterAttributeTableFromFile );
}
// No raster support in createSqlVectorLayer (yet)

View File

@ -3212,25 +3212,10 @@ bool QgsGdalProvider::writeNativeAttributeTable( QString *errorMessage ) //#spel
}
// Needs to be in write mode for HFA and perhaps other formats!
if ( GDALGetAccess( mGdalDataset ) == GA_ReadOnly )
if ( ! isEditable() )
{
QgsDebugMsg( QStringLiteral( "re-opening the dataset in read/write mode" ) );
GDALClose( mGdalDataset );
mGdalBaseDataset = gdalOpen( dataSourceUri( true ), GDAL_OF_UPDATE );
// if the dataset couldn't be opened in read / write mode, tell the user
if ( !mGdalBaseDataset )
{
mGdalBaseDataset = gdalOpen( dataSourceUri( true ), GDAL_OF_READONLY );
//Since we are not a virtual warped dataset, mGdalDataSet and mGdalBaseDataset are supposed to be the same
mGdalDataset = mGdalBaseDataset;
if ( errorMessage )
{
*errorMessage = tr( "GDAL Error reopening dataset in write mode, raster attribute table could not be saved." );
}
return false;
}
setEditable( true );
wasReopenedReadWrite = true;
}
@ -3327,17 +3312,7 @@ bool QgsGdalProvider::writeNativeAttributeTable( QString *errorMessage ) //#spel
if ( wasReopenedReadWrite )
{
QgsDebugMsg( QStringLiteral( "re-opening the dataset in read-only mode" ) );
GDALClose( mGdalDataset );
mGdalBaseDataset = gdalOpen( dataSourceUri( true ), GDAL_OF_READONLY );
// if the dataset couldn't be opened in read / write mode, tell the user
if ( !mGdalBaseDataset )
{
mGdalBaseDataset = gdalOpen( dataSourceUri( true ), GDAL_OF_UPDATE );
//Since we are not a virtual warped dataset, mGdalDataSet and mGdalBaseDataset are supposed to be the same
mGdalDataset = mGdalBaseDataset;
}
setEditable( false );
}
return success;

View File

@ -479,6 +479,7 @@ bool QgsRasterAttributeTable::removeRow( int position, QString *errorMessage )
return false;
}
mData.removeAt( position );
setDirty( true );
return true;
}
@ -723,8 +724,12 @@ bool QgsRasterAttributeTable::setValue( const int row, const int column, const Q
}
const QVariant oldVal = mData[ row ][ column ];
mData[ row ][ column ] = newVal;
setDirty( newVal != oldVal );
if ( newVal != oldVal )
{
mData[ row ][ column ] = newVal;
setDirty( true );
}
return true;
}
@ -1508,7 +1513,7 @@ QgsRasterRenderer *QgsRasterAttributeTable::createRenderer( QgsRasterDataProvide
pseudoColorRenderer->setClassificationMin( minValue() );
pseudoColorRenderer->setClassificationMax( maxValue() );
// Use discrete for single colors, interpolated for ramps
pseudoColorRenderer->createShader( ramp, hasColor() ? QgsColorRampShader::Type::Discrete : QgsColorRampShader::Type::Interpolated, QgsColorRampShader::ClassificationMode::Continuous, ramp->stops().count() + 2, true );
pseudoColorRenderer->createShader( ramp, hasRamp() ? QgsColorRampShader::Type::Interpolated : QgsColorRampShader::Type::Discrete, QgsColorRampShader::ClassificationMode::Continuous, ramp->stops().count() + 2, true );
pseudoColorRenderer->shader()->setMaximumValue( maxValue() );
pseudoColorRenderer->shader()->setMinimumValue( minValue() );
// Set labels
@ -1516,10 +1521,30 @@ QgsRasterRenderer *QgsRasterAttributeTable::createRenderer( QgsRasterDataProvide
{
shaderFunction->setMinimumValue( minValue() );
shaderFunction->setMaximumValue( maxValue() );
const QList<QgsColorRampShader::ColorRampItem> itemList { shaderFunction->colorRampItemList() };
const bool labelsAreUsable { labels.count() == itemList.count() };
if ( ! itemList.isEmpty() )
const bool labelsAreUsable { ramp->count() > 2 && labels.count() == ramp->count() - 1 };
if ( labelsAreUsable )
{
QList<QgsColorRampShader::ColorRampItem> deduplicatedDitemList;
const double range { maxValue() - minValue() };
int stopIdx { 0 };
for ( const QString &label : std::as_const( labels ) )
{
if ( stopIdx >= ramp->count() - 2 )
{
break;
}
double value { minValue() + ramp->stops().at( stopIdx ).offset * range };
QgsColorRampShader::ColorRampItem item { value, ramp->stops().at( stopIdx ).color, label };
deduplicatedDitemList.push_back( item );
stopIdx++;
}
QgsColorRampShader::ColorRampItem item { maxValue(), ramp->color2(), labels.last() };
deduplicatedDitemList.push_back( item );
#if 0
QList<QgsColorRampShader::ColorRampItem> deduplicatedDitemList;
// Deduplicate entries, this is necessary when the ramp for athematic
// ramp rasters creates a stop for each class limit (min and max) which
@ -1548,6 +1573,8 @@ QgsRasterRenderer *QgsRasterAttributeTable::createRenderer( QgsRasterDataProvide
deduplicatedDitemList.push_back( item );
}
}
#endif
shaderFunction->setColorRampItemList( deduplicatedDitemList );
}
}

View File

@ -257,7 +257,7 @@ QgsRasterAttributeTable *QgsRasterLayer::attributeTable( int bandNoInt ) const
return mDataProvider->attributeTable( bandNoInt );
}
int QgsRasterLayer::attributeTableCount()
int QgsRasterLayer::attributeTableCount() const
{
if ( !mDataProvider )
return 0;

View File

@ -326,7 +326,7 @@ class CORE_EXPORT QgsRasterLayer : public QgsMapLayer, public QgsAbstractProfile
* Returns the number of attribute tables for the raster by counting the number of bands that have an associated attribute table.
* \since QGIS 3.30
*/
int attributeTableCount( );
int attributeTableCount( ) const;
/**
* Returns TRUE if the raster renderer is suitable for creation of a raster attribute table. The supported renderers are QgsPalettedRasterRenderer and QgsSingleBandPseudoColorRenderer.

View File

@ -16,11 +16,9 @@
#include "qgsrasterattributetabledialog.h"
#include "qgsrasterlayer.h"
#include <QMessageBox>
QgsRasterAttributeTableDialog::QgsRasterAttributeTableDialog( QgsRasterLayer *rasterLayer, int bandNumber, QWidget *parent )
: QDialog( parent )
, mRasterLayer( rasterLayer )
{
Q_ASSERT( rasterLayer );
setupUi( this );
@ -28,40 +26,15 @@ QgsRasterAttributeTableDialog::QgsRasterAttributeTableDialog( QgsRasterLayer *ra
setWindowTitle( tr( "Raster Attribute Table for %1" ).arg( rasterLayer->name() ) );
connect( mButtonBox, &QDialogButtonBox::rejected, this, &QgsRasterAttributeTableDialog::reject );
connect( rasterLayer, &QgsRasterLayer::dataSourceChanged, this, &QgsRasterAttributeTableDialog::reject );
connect( rasterLayer, &QgsRasterLayer::willBeDeleted, this, &QgsRasterAttributeTableDialog::reject );
}
void QgsRasterAttributeTableDialog::reject()
{
QList<int> dirtyBands;
for ( int bandNo = 1; bandNo <= mRasterLayer->bandCount(); ++bandNo )
if ( mRatWidget->setEditable( false ) )
{
if ( QgsRasterAttributeTable *rat = mRasterLayer->attributeTable( bandNo ) )
{
if ( rat->isDirty() )
{
dirtyBands.push_back( bandNo );
}
}
QDialog::reject();
}
if ( ! dirtyBands.isEmpty() )
{
QString bandsStr;
for ( int i = 0; i < dirtyBands.size(); i++ )
{
bandsStr.append( QString::number( dirtyBands[i] ) );
if ( i < dirtyBands.size() - 1 )
bandsStr.append( QStringLiteral( ", " ) );
}
QString msg { dirtyBands.count( ) > 1 ? tr( "Attribute table bands (%1) contain unsaved changes, close without saving?" ).arg( bandsStr ) : tr( "Attribute table band %1 contains unsaved changes, close without saving?" ).arg( bandsStr ) };
if ( QMessageBox::question( nullptr, tr( "Save Attribute Table" ), msg ) != QMessageBox::Yes )
{
return;
}
}
QDialog::reject();
}

View File

@ -26,13 +26,13 @@ class QgsRasterLayer;
/**
* \ingroup gui
* \brief The QgsRasterAttributeTableDialog class embeds an attribute table widget
* and contains logic to handle confirmation when closing with unsaved changes.
* \brief The QgsRasterAttributeTableDialog class embeds an attribute table widget.
* \since QGIS 3.30
*/
class GUI_EXPORT QgsRasterAttributeTableDialog: public QDialog, private Ui::QRasterAttributeTableDialogBase
{
Q_OBJECT
public:
QgsRasterAttributeTableDialog( QgsRasterLayer *rasterLayer, int bandNumber = 0, QWidget *parent SIP_TRANSFERTHIS = nullptr );
@ -42,9 +42,6 @@ class GUI_EXPORT QgsRasterAttributeTableDialog: public QDialog, private Ui::QRas
void reject() override;
private:
QgsRasterLayer *mRasterLayer = nullptr;
};
#endif // QGSRASTERATTRIBUTETABLEDIALOG_H

View File

@ -90,14 +90,14 @@ void QgsRasterAttributeTableWidget::setRasterLayer( QgsRasterLayer *rasterLayer,
bool QgsRasterAttributeTableWidget::isDirty() const
{
return mAttributeTable && mAttributeTable->isDirty();
return mAttributeTableBuffer && mAttributeTableBuffer->isDirty();
}
void QgsRasterAttributeTableWidget::init( int bandNumber )
{
disconnect( mRasterBandsComboBox, qOverload<int>( &QComboBox::currentIndexChanged ), this, &QgsRasterAttributeTableWidget::bandChanged );
mAttributeTable = nullptr;
mAttributeTableBuffer = nullptr;
mCurrentBand = 0;
mRasterBandsComboBox->clear();
@ -120,22 +120,22 @@ void QgsRasterAttributeTableWidget::init( int bandNumber )
if ( availableRats.contains( bandNumber ) )
{
mCurrentBand = bandNumber;
mAttributeTable = mRasterLayer->attributeTable( mCurrentBand );
mRasterBandsComboBox->setCurrentIndex( availableRats.indexOf( mCurrentBand ) );
}
else if ( ! availableRats.isEmpty() )
{
mCurrentBand = availableRats.first();
mAttributeTable = mRasterLayer->attributeTable( mCurrentBand );
mRasterBandsComboBox->setCurrentIndex( availableRats.indexOf( mCurrentBand ) );
}
mAttributeTableBuffer = std::make_unique<QgsRasterAttributeTable>( *mRasterLayer->attributeTable( mCurrentBand ) );
mRasterBandsComboBox->setCurrentIndex( availableRats.indexOf( mCurrentBand ) );
}
}
if ( mAttributeTable )
if ( mAttributeTableBuffer )
{
mModel.reset( new QgsRasterAttributeTableModel( mAttributeTable ) );
mModel.reset( new QgsRasterAttributeTableModel( mAttributeTableBuffer.get() ) );
mModel->setEditable( mEditable );
connect( mModel.get(), &QgsRasterAttributeTableModel::dataChanged, this, [ = ]( const QModelIndex &, const QModelIndex &, const QVector<int> & )
{
updateButtons();
@ -166,14 +166,15 @@ void QgsRasterAttributeTableWidget::init( int bandNumber )
void QgsRasterAttributeTableWidget::updateButtons()
{
const bool enableEditingButtons { static_cast<bool>( mAttributeTable ) &&mEditable &&mRATView->selectionModel()->currentIndex().isValid() };
const bool enableEditingButtons( static_cast<bool>( mAttributeTableBuffer ) && mEditable && mRATView->selectionModel()->currentIndex().isValid() );
mActionToggleEditing->setChecked( mEditable );
mActionAddColumn->setEnabled( mEditable );
mActionRemoveColumn->setEnabled( enableEditingButtons );
mActionAddRow->setEnabled( enableEditingButtons );
mActionRemoveRow->setEnabled( enableEditingButtons );
mActionSaveChanges->setEnabled( mAttributeTable && mAttributeTable->isDirty() );
mClassifyButton->setEnabled( mAttributeTable && mRasterLayer );
mClassifyComboBox->setEnabled( mAttributeTable && mRasterLayer );
mActionSaveChanges->setEnabled( mAttributeTableBuffer && mAttributeTableBuffer->isDirty() );
mClassifyButton->setEnabled( mAttributeTableBuffer && mRasterLayer );
mClassifyComboBox->setEnabled( mAttributeTableBuffer && mRasterLayer );
}
void QgsRasterAttributeTableWidget::setMessageBar( QgsMessageBar *bar )
@ -181,83 +182,109 @@ void QgsRasterAttributeTableWidget::setMessageBar( QgsMessageBar *bar )
mMessageBar = bar;
}
void QgsRasterAttributeTableWidget::setEditable( bool editable )
bool QgsRasterAttributeTableWidget::setEditable( bool editable )
{
bool isDirty { false };
for ( int bandNo = 1; bandNo <= mRasterLayer->bandCount(); ++bandNo )
const bool isDirty { mAttributeTableBuffer &&mAttributeTableBuffer->isDirty() &&mCurrentBand > 0 && mRasterLayer->attributeTable( mCurrentBand ) };
bool retVal { true };
// Switch to read-only
if ( ! editable && isDirty )
{
if ( mRasterLayer->dataProvider()->attributeTable( bandNo ) && mRasterLayer->dataProvider()->attributeTable( bandNo )->isDirty() )
const QMessageBox::StandardButtons buttons { QMessageBox::Button::Cancel | QMessageBox::Button::Yes | QMessageBox::Button::No };
switch ( QMessageBox::question( nullptr, tr( "Save Attribute Table" ), tr( "Attribute table contains unsaved changes, do you want to save the changes?" ), buttons ) )
{
isDirty = true;
break;
case QMessageBox::Button::Cancel:
{
retVal = false;
break;
}
case QMessageBox::Button::Yes:
{
saveChanges();
retVal = true;
break;
}
case QMessageBox::Button::No:
default:
{
// Reset to its original state
mAttributeTableBuffer = std::make_unique<QgsRasterAttributeTable>( *mRasterLayer->attributeTable( mCurrentBand ) );
init( mCurrentBand );
retVal = true;
break;
}
}
}
if ( isDirty && QMessageBox::question( nullptr, tr( "Save Attribute Table" ), tr( "Attribute table contains unsaved changes, do you want to save the changes?" ) ) == QMessageBox::Yes )
if ( retVal )
{
saveChanges();
mEditable = editable;
mModel->setEditable( editable );
}
mEditable = editable;
mModel->setEditable( editable );
updateButtons();
return retVal;
}
void QgsRasterAttributeTableWidget::saveChanges()
{
if ( mRasterLayer )
if ( mRasterLayer && mAttributeTableBuffer && mAttributeTableBuffer->isDirty() && mCurrentBand > 0 )
{
for ( int bandNo = 1; bandNo <= mRasterLayer->bandCount(); ++bandNo )
QgsRasterAttributeTable *attributeTable { mRasterLayer->dataProvider()->attributeTable( mCurrentBand ) };
if ( ! attributeTable )
{
QgsRasterAttributeTable *attributeTable { mRasterLayer->dataProvider()->attributeTable( bandNo ) };
if ( attributeTable && attributeTable->isDirty() )
QgsDebugMsg( QStringLiteral( "Error saving RAT: RAT for band %1 is unexpectedly gone!" ).arg( mCurrentBand ) );
}
else
{
*attributeTable = *mAttributeTableBuffer;
QString errorMessage;
QString newPath { attributeTable->filePath() };
const bool nativeRatSupported = mRasterLayer->dataProvider()->providerCapabilities().testFlag( QgsRasterDataProvider::ProviderCapability::NativeRasterAttributeTable );
bool saveToNative { false };
if ( newPath.isEmpty() && ! nativeRatSupported )
{
QString errorMessage;
QString newPath { attributeTable->filePath() };
const bool nativeRatSupported = mRasterLayer->dataProvider()->providerCapabilities().testFlag( QgsRasterDataProvider::ProviderCapability::NativeRasterAttributeTable );
bool saveToNative { false };
if ( newPath.isEmpty() && ! nativeRatSupported )
{
newPath = QFileDialog::getOpenFileName( nullptr, tr( "Save Raster Attribute Table (band %1) To File" ).arg( bandNo ), QFile::exists( mRasterLayer->dataProvider()->dataSourceUri( ) ) ? mRasterLayer->dataProvider()->dataSourceUri( ) + ".vat.dbf" : QString(), QStringLiteral( "VAT DBF Files (*.vat.dbf)" ) );
if ( newPath.isEmpty() )
{
// Aborted by user
return;
}
}
else if ( newPath.isEmpty() )
{
saveToNative = true;
}
bool writeSuccess { false };
// Save to file
if ( ! saveToNative && ! newPath.isEmpty() )
{
writeSuccess = attributeTable->writeToFile( attributeTable->filePath(), &errorMessage );
}
else if ( saveToNative )
{
writeSuccess = mRasterLayer->dataProvider()->writeNativeAttributeTable( &errorMessage );
}
if ( writeSuccess )
{
notify( tr( "Attribute Table Write Success" ), tr( "The raster attibute table has been successfully saved." ), Qgis::MessageLevel::Success );
}
else
{
notify( tr( "Attribute Table Write Error" ), errorMessage, Qgis::MessageLevel::Critical );
}
// Save to native saves RATs for all bands, no need to loop further.
if ( saveToNative )
newPath = QFileDialog::getOpenFileName( nullptr, tr( "Save Raster Attribute Table (band %1) To File" ).arg( mCurrentBand ), QFile::exists( mRasterLayer->dataProvider()->dataSourceUri( ) ) ? mRasterLayer->dataProvider()->dataSourceUri( ) + ".vat.dbf" : QString(), QStringLiteral( "VAT DBF Files (*.vat.dbf)" ) );
if ( newPath.isEmpty() )
{
// Aborted by user
return;
}
}
else if ( newPath.isEmpty() )
{
saveToNative = true;
}
bool writeSuccess { false };
// Save to file
if ( ! saveToNative && ! newPath.isEmpty() )
{
writeSuccess = attributeTable->writeToFile( attributeTable->filePath(), &errorMessage );
}
else if ( saveToNative )
{
writeSuccess = mRasterLayer->dataProvider()->writeNativeAttributeTable( &errorMessage );
}
if ( writeSuccess )
{
mAttributeTableBuffer->setDirty( false );
notify( tr( "Attribute Table Write Success" ), tr( "The raster attibute table has been successfully saved." ), Qgis::MessageLevel::Success );
}
else
{
notify( tr( "Attribute Table Write Error" ), errorMessage, Qgis::MessageLevel::Critical );
}
// Save to native saves RATs for all bands, no need to loop further.
if ( saveToNative )
{
return;
}
}
}
@ -267,7 +294,7 @@ void QgsRasterAttributeTableWidget::saveChanges()
void QgsRasterAttributeTableWidget::classify()
{
if ( ! mAttributeTable )
if ( ! mAttributeTableBuffer )
{
notify( tr( "Classification Error" ), tr( "The raster attribute table is not set." ), Qgis::MessageLevel::Critical );
return;
@ -279,13 +306,22 @@ void QgsRasterAttributeTableWidget::classify()
return;
}
if ( QMessageBox::question( nullptr, tr( "Apply Style From Attribute Table" ), tr( "The existing style for the raster will be replaced by a new style from the attribute table and any unsaved changes to the current style will be lost, do you want to proceed?" ) ) == QMessageBox::Yes )
QString confirmMessage;
QString errorMessage;
if ( ! mAttributeTableBuffer->isValid( &errorMessage ) )
{
confirmMessage = tr( "The attribute table does not seem to be valid and it may produce an unusable symbology, validation errors:<br>%1<br>" ).arg( errorMessage );
}
if ( QMessageBox::question( nullptr, tr( "Apply Style From Attribute Table" ), confirmMessage.append( tr( "The existing symbology for the raster will be replaced by a new symbology from the attribute table and any unsaved changes to the current symbology will be lost, do you want to proceed?" ) ) ) == QMessageBox::Yes )
{
if ( QgsRasterRenderer *renderer = mAttributeTable->createRenderer( mRasterLayer->dataProvider(), mCurrentBand, mClassifyComboBox->currentData().toInt() ) )
if ( QgsRasterRenderer *renderer = mAttributeTableBuffer->createRenderer( mRasterLayer->dataProvider(), mCurrentBand, mClassifyComboBox->currentData().toInt() ) )
{
mRasterLayer->setRenderer( renderer );
mRasterLayer->triggerRepaint( );
emit rendererChanged();
}
else
{
@ -296,9 +332,9 @@ void QgsRasterAttributeTableWidget::classify()
void QgsRasterAttributeTableWidget::addColumn()
{
if ( mAttributeTable )
if ( mAttributeTableBuffer )
{
QgsRasterAttributeTableAddColumnDialog dlg { mAttributeTable };
QgsRasterAttributeTableAddColumnDialog dlg { mAttributeTableBuffer.get() };
if ( dlg.exec() == QDialog::Accepted )
{
QString errorMessage;
@ -330,7 +366,7 @@ void QgsRasterAttributeTableWidget::addColumn()
void QgsRasterAttributeTableWidget::removeColumn()
{
const QModelIndex currentIndex { mProxyModel->mapToSource( mRATView->selectionModel()->currentIndex() ) };
if ( mAttributeTable && currentIndex.isValid() && currentIndex.column() < mAttributeTable->fields().count() )
if ( mAttributeTableBuffer && currentIndex.isValid() && currentIndex.column() < mAttributeTableBuffer->fields().count() )
{
if ( QMessageBox::question( nullptr, tr( "Remove Column" ), tr( "Do you want to remove the selected column? This action cannot be undone." ) ) == QMessageBox::Yes )
{
@ -345,7 +381,7 @@ void QgsRasterAttributeTableWidget::removeColumn()
void QgsRasterAttributeTableWidget::addRow()
{
if ( mAttributeTable )
if ( mAttributeTableBuffer )
{
// Default to append
int position { mModel->rowCount( QModelIndex() ) };
@ -372,7 +408,7 @@ void QgsRasterAttributeTableWidget::addRow()
QVariantList rowData;
QList<QgsRasterAttributeTable::Field> fields { mAttributeTable->fields() };
QList<QgsRasterAttributeTable::Field> fields { mAttributeTableBuffer->fields() };
for ( const QgsRasterAttributeTable::Field &field : std::as_const( fields ) )
{
rowData.push_back( QVariant( field.type ) );
@ -393,7 +429,7 @@ void QgsRasterAttributeTableWidget::addRow()
void QgsRasterAttributeTableWidget::removeRow()
{
if ( mAttributeTable && mRATView->selectionModel()->currentIndex().isValid() )
if ( mAttributeTableBuffer && mRATView->selectionModel()->currentIndex().isValid() )
{
if ( QMessageBox::question( nullptr, tr( "Remove Row" ), tr( "Do you want to remove the selected row? This action cannot be undone." ) ) == QMessageBox::Yes )
{
@ -409,8 +445,13 @@ void QgsRasterAttributeTableWidget::removeRow()
void QgsRasterAttributeTableWidget::bandChanged( const int index )
{
const QVariant itemData = mRasterBandsComboBox->itemData( index );
if ( itemData.isValid() )
{
if ( mEditable )
{
setEditable( false );
}
init( itemData.toInt( ) );
}
}
@ -449,9 +490,9 @@ void QgsRasterAttributeTableWidget::notify( const QString &title, const QString
void QgsRasterAttributeTableWidget::setDelegates()
{
mClassifyComboBox->clear();
if ( mAttributeTable )
if ( mAttributeTableBuffer )
{
const QList<QgsRasterAttributeTable::Field> tableFields { mAttributeTable->fields() };
const QList<QgsRasterAttributeTable::Field> tableFields { mAttributeTableBuffer->fields() };
int fieldIdx { 0 };
const QHash<Qgis::RasterAttributeTableFieldUsage, QgsRasterAttributeTable::UsageInformation> usageInfo { QgsRasterAttributeTable::usageInformation() };
for ( const QgsRasterAttributeTable::Field &f : std::as_const( tableFields ) )
@ -465,26 +506,26 @@ void QgsRasterAttributeTableWidget::setDelegates()
fieldIdx++;
}
if ( mAttributeTable->hasColor() )
if ( mAttributeTableBuffer->hasColor() )
{
if ( mAttributeTable->usages().contains( Qgis::RasterAttributeTableFieldUsage::Alpha ) )
if ( mAttributeTableBuffer->usages().contains( Qgis::RasterAttributeTableFieldUsage::Alpha ) )
{
mRATView->setItemDelegateForColumn( mAttributeTable->fields().count( ), new ColorAlphaDelegate( mRATView ) );
mRATView->setItemDelegateForColumn( mAttributeTableBuffer->fields().count( ), new ColorAlphaDelegate( mRATView ) );
}
else
{
mRATView->setItemDelegateForColumn( mAttributeTable->fields().count( ), new ColorDelegate( mRATView ) );
mRATView->setItemDelegateForColumn( mAttributeTableBuffer->fields().count( ), new ColorDelegate( mRATView ) );
}
}
else if ( mAttributeTable->hasRamp() )
else if ( mAttributeTableBuffer->hasRamp() )
{
if ( mAttributeTable->usages().contains( Qgis::RasterAttributeTableFieldUsage::AlphaMin ) )
if ( mAttributeTableBuffer->usages().contains( Qgis::RasterAttributeTableFieldUsage::AlphaMin ) )
{
mRATView->setItemDelegateForColumn( mAttributeTable->fields().count( ), new ColorRampAlphaDelegate( mRATView ) );
mRATView->setItemDelegateForColumn( mAttributeTableBuffer->fields().count( ), new ColorRampAlphaDelegate( mRATView ) );
}
else
{
mRATView->setItemDelegateForColumn( mAttributeTable->fields().count( ), new ColorRampDelegate( mRATView ) );
mRATView->setItemDelegateForColumn( mAttributeTableBuffer->fields().count( ), new ColorRampDelegate( mRATView ) );
}
}
}

View File

@ -122,10 +122,48 @@ class GUI_EXPORT QgsRasterAttributeTableWidget : public QWidget, private Ui::Qgs
*/
bool isDirty( ) const;
/**
* Sets the message \a bar associated with the widget. This allows the widget to push feedback messages
* to the appropriate message bar.
* \see messageBar()
*/
void setMessageBar( QgsMessageBar *bar );
signals:
/**
* This signal is emitted after a successful classify operation which changed the raster renderer.
*/
void rendererChanged( );
public slots:
/**
* Save the changes in the raster attribute table.
*/
void saveChanges();
/**
* Set the editable state, it may trigger save changes if the attribute table has unsave changes.
*/
bool setEditable( bool editable );
private slots:
void classify();
void addColumn();
void removeColumn();
void addRow();
void removeRow();
void bandChanged( const int index );
void notify( const QString &title, const QString &message, Qgis::MessageLevel level = Qgis::MessageLevel::Info );
void setDelegates( );
private:
QgsRasterLayer *mRasterLayer = nullptr;
QgsRasterAttributeTable *mAttributeTable = nullptr;
std::unique_ptr<QgsRasterAttributeTable> mAttributeTableBuffer;
// Default to invalid (bands are 1-indexed)
int mCurrentBand = 0;
bool mEditable = false;
@ -144,28 +182,6 @@ class GUI_EXPORT QgsRasterAttributeTableWidget : public QWidget, private Ui::Qgs
void init( int bandNumber = 0 );
void updateButtons();
/**
* Sets the message \a bar associated with the widget. This allows the widget to push feedback messages
* to the appropriate message bar.
* \see messageBar()
*/
void setMessageBar( QgsMessageBar *bar );
private slots:
void setEditable( bool editable );
void saveChanges();
void classify();
void addColumn();
void removeColumn();
void addRow();
void removeRow();
void bandChanged( const int index );
void notify( const QString &title, const QString &message, Qgis::MessageLevel level = Qgis::MessageLevel::Info );
void setDelegates( );
};
#endif // QGSRASTERATTRIBUTETABLEWIDGET_H

View File

@ -68,6 +68,7 @@
#include "qgsdoublevalidator.h"
#include "qgsmaplayerconfigwidgetfactory.h"
#include "qgsprojectutils.h"
#include "qgsrasterattributetablewidget.h"
#include "qgsrasterlayertemporalpropertieswidget.h"
#include "qgsprojecttimesettings.h"
@ -234,6 +235,21 @@ QgsRasterLayerProperties::QgsRasterLayerProperties( QgsMapLayer *lyr, QgsMapCanv
return;
}
// Setup raster attribute table
if ( mRasterLayer->attributeTableCount() > 0 )
{
mRasterAttributeTableWidget = new QgsRasterAttributeTableWidget( this, mRasterLayer );
mOptsPage_RasterAttributeTable->layout()->addWidget( mRasterAttributeTableWidget );
// When the renderer changes we need to sync the style options page
connect( mRasterAttributeTableWidget, &QgsRasterAttributeTableWidget::rendererChanged, this, &QgsRasterLayerProperties::syncToLayer );
mNoRasterAttributeTableWidget->hide();
}
else
{
mNoRasterAttributeTableWidget->show();
}
mBackupCrs = mRasterLayer->crs();
// Handles window modality raising canvas

View File

@ -46,6 +46,7 @@ class QgsMapLayerConfigWidgetFactory;
class QgsMapLayerConfigWidget;
class QgsPropertyOverrideButton;
class QgsRasterTransparencyWidget;
class QgsRasterAttributeTableWidget;
/**
@ -296,5 +297,7 @@ class GUI_EXPORT QgsRasterLayerProperties : public QgsOptionsDialogBase, private
friend class QgsAppScreenShots;
QgsCoordinateReferenceSystem mBackupCrs;
QgsRasterAttributeTableWidget *mRasterAttributeTableWidget = nullptr;
};
#endif

View File

@ -93,7 +93,7 @@
<bool>true</bool>
</property>
<property name="currentRow">
<number>0</number>
<number>-1</number>
</property>
<item>
<property name="text">
@ -203,6 +203,18 @@
<normaloff>:/images/themes/default/legend.svg</normaloff>:/images/themes/default/legend.svg</iconset>
</property>
</item>
<item>
<property name="text">
<string>Attribute Tables</string>
</property>
<property name="toolTip">
<string>Manage Raster Attribute Tables</string>
</property>
<property name="icon">
<iconset resource="../../images/images.qrc">
<normaloff>:/images/themes/default/propertyicons/attributes.svg</normaloff>:/images/themes/default/propertyicons/attributes.svg</iconset>
</property>
</item>
<item>
<property name="text">
<string>QGIS Server</string>
@ -254,7 +266,7 @@
</sizepolicy>
</property>
<property name="currentIndex">
<number>0</number>
<number>10</number>
</property>
<widget class="QWidget" name="mOptsPage_Information">
<layout class="QVBoxLayout" name="verticalLayout_20">
@ -299,8 +311,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>253</width>
<height>392</height>
<width>648</width>
<height>690</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_7">
@ -1438,6 +1450,55 @@ p, li { white-space: pre-wrap; }
</item>
</layout>
</widget>
<widget class="QWidget" name="mOptsPage_RasterAttributeTable">
<layout class="QVBoxLayout" name="verticalLayout_rat">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QWidget" name="mNoRasterAttributeTableWidget" native="true">
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QLabel" name="mRasterAttributeTableLabel">
<property name="text">
<string>There are no raster attribute tables associated with this data source.
If the current symbology can be converted to an attribute table you can create a new attribute table using the context menu available in the layer tree.
From the layer tree context menu it you can also import an existing raster attribute table from a VAT.DBF file and associate it with a raster band.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="mOptsPage_Server">
<layout class="QVBoxLayout" name="verticalLayout_8">
<property name="leftMargin">
@ -1466,7 +1527,7 @@ p, li { white-space: pre-wrap; }
<x>0</x>
<y>0</y>
<width>263</width>
<height>566</height>
<height>600</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout_12">
@ -1994,6 +2055,8 @@ p, li { white-space: pre-wrap; }
</tabstops>
<resources>
<include location="../../images/images.qrc"/>
<include location="../../images/images.qrc"/>
<include location="../../images/images.qrc"/>
</resources>
<connections>
<connection>

View File

@ -17,7 +17,7 @@
<item>
<widget class="QLabel" name="mCreateInfoLabel">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Create a new Raster Attribute Table (RAT) from the current symbology.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Create a new Raster Attribute Table from the current symbology.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>

View File

@ -111,6 +111,9 @@
<property name="text">
<string>After</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="0">
@ -119,7 +122,7 @@
<string>Before</string>
</property>
<property name="checked">
<bool>true</bool>
<bool>false</bool>
</property>
</widget>
</item>