Compare commits

...

14 Commits

Author SHA1 Message Date
Germap
4daa7545b1
Merge 84827615881fa14ee7b5586825a3b3ae70ef2b59 into 74549aad26c3358101e88477d9dfa1caae013d72 2025-07-01 08:32:09 -05:00
Juergen E. Fischer
74549aad26 Reapply "Allow free naming of project properties (#60855)"
This reverts commit fb11239112adfc321b3bbacbb20da888a7a37c23.
2025-07-01 09:08:44 +00:00
Juergen E. Fischer
eac401c009 if a plugin supports qt6 it should also support QGIS4 (fixes #62359) 2025-07-01 09:07:26 +00:00
Loïc Bartoletti
1f0166d35e
Merge pull request #62365 from benoitdm-oslandia/fix/cgibin_dir
fix: update default cgi-bin installation paths according to OS
2025-07-01 07:26:25 +02:00
Loïc Bartoletti
ada589bb1d
Merge pull request #62052 from martin-s42/visitPointsByRegularDistance-segfault-fix
fix segfault due to calling qgsDoubleNear with tollerance that can ne…
2025-07-01 06:59:03 +02:00
Alexander Bruy
08dd318614
Merge pull request #62463 from qgis/m-kuhn-patch-2
Add qtimageformats
2025-06-30 12:19:41 +01:00
Matthias Kuhn
89dbd40b8f
Add qtimageformats 2025-06-29 13:02:03 +02:00
bdm-oslandia
b1c8ef3265 fixup! fix: update default cgi-bin installation paths according to OS 2025-06-27 09:35:48 +02:00
Germán Carrillo
8482761588 When there's a PostGIS layer that couldn't be loaded for any reason, take users to the PostGIS tab in the Log Message Viewer via link. There they can find the concrete reason of failure. 2025-06-25 16:22:36 -05:00
bdm-oslandia
f4cf09d4b0 fixup! fix: update default cgi-bin installation paths according to OS 2025-06-25 08:29:52 +02:00
Germán Carrillo
1552fe8887 [ux] In the Data Source Manager, when loading PG views, make sure the 'PK' warning is only shown when there is no selection in the Feature id combo box. Since QGIS will do some heuristics to attempt to help users to load PG views (namely, select the first available column as Feature id), that means that the warning is hidden by default. 2025-06-24 22:06:10 -05:00
bdm-oslandia
9db58e3726 fixup! fix: update default cgi-bin installation paths according to OS 2025-06-24 17:37:57 +02:00
bdm-oslandia
551aa20f20 fix: update default cgi-bin installation paths according to OS 2025-06-23 10:44:49 +02:00
Martin Siegert
a156c43f7b
fix segfault due to calling qgsDoubleNear with tollerance that can never been reached
see issue #60772: function interpolatePoint in qgsabstractprofilesurfacegenerator.cpp:157 can return a nullpointer causing a segfault because qgsDoubleNear in visitPointsByRegularDistance in qgslinestring.cpp is called with a tolerance that is too small so that qgsDoubleNear always returns false. Solution: scale epsilon by the size of the numbers to be compared.
2025-05-29 13:37:03 -07:00
11 changed files with 177 additions and 182 deletions

View File

@ -979,7 +979,31 @@ if (WITH_CORE)
else() else()
# UNIX # UNIX
set (DEFAULT_BIN_SUBDIR bin) set (DEFAULT_BIN_SUBDIR bin)
set (DEFAULT_CGIBIN_SUBDIR bin)
# From https://www.cyberciti.biz/faq/how-do-i-find-the-url-for-my-cgi-bin/
execute_process(COMMAND lsb_release -a OUTPUT_VARIABLE LSB_RELEASE_A)
if(EXISTS "/etc/fedora-release")
# in /var/www/cgi-bin
set (DEFAULT_CGIBIN_SUBDIR www/cgi-bin)
elseif (${CMAKE_HOST_SYSTEM_NAME} MATCHES "FreeBSD")
# in /usr/local/www/cgi-bin/
set (DEFAULT_CGIBIN_SUBDIR www/cgi-bin)
elseif (${CMAKE_HOST_SYSTEM_NAME} MATCHES "BSD")
# in /usr/local/libexec/cgi-bin/
set (DEFAULT_CGIBIN_SUBDIR libexec/cgi-bin)
elseif ("${LSB_RELEASE_A}" MATCHES "Ubuntu" OR "${LSB_RELEASE_A}" MATCHES "Debian" OR "${LSB_RELEASE_A}" MATCHES "Mint")
# in /usr/lib/cgi-bin/
set (DEFAULT_CGIBIN_SUBDIR lib/cgi-bin)
else()
# others: Red Hat/CentOS/Rocky/Alma Linux
# in /var/www/cgi-bin/
set (DEFAULT_CGIBIN_SUBDIR www/cgi-bin)
endif()
set (DEFAULT_LIB_SUBDIR lib${LIB_SUFFIX}) set (DEFAULT_LIB_SUBDIR lib${LIB_SUFFIX})
set (DEFAULT_DATA_SUBDIR share/qgis) set (DEFAULT_DATA_SUBDIR share/qgis)
set (DEFAULT_LIBEXEC_SUBDIR lib${LIB_SUFFIX}/qgis) set (DEFAULT_LIBEXEC_SUBDIR lib${LIB_SUFFIX}/qgis)

View File

@ -659,6 +659,9 @@ class Repositories(QObject):
.strip() .strip()
) )
if not qgisMaximumVersion: if not qgisMaximumVersion:
if qgisMinimumVersion[0] == "3" and supports_qt6:
qgisMaximumVersion = "4.99"
else:
qgisMaximumVersion = qgisMinimumVersion[0] + ".99" qgisMaximumVersion = qgisMinimumVersion[0] + ".99"
# if compatible, add the plugin to the list # if compatible, add the plugin to the list
if not pluginNodes.item(i).firstChildElement( if not pluginNodes.item(i).firstChildElement(
@ -845,6 +848,9 @@ class Plugins(QObject):
qgisMinimumVersion = "0" qgisMinimumVersion = "0"
qgisMaximumVersion = pluginMetadata("qgisMaximumVersion").strip() qgisMaximumVersion = pluginMetadata("qgisMaximumVersion").strip()
if not qgisMaximumVersion: if not qgisMaximumVersion:
if qgisMinimumVersion[0] == "3" and supports_qt6:
qgisMaximumVersion = "4.99"
else:
qgisMaximumVersion = qgisMinimumVersion[0] + ".99" qgisMaximumVersion = qgisMinimumVersion[0] + ".99"
# if compatible, add the plugin to the list # if compatible, add the plugin to the list
if not isCompatible( if not isCompatible(

View File

@ -1329,7 +1329,18 @@ QList<QgsMapLayer *> QgsAppLayerHandling::addDatabaseLayers( const QStringList &
QgsMessageLog::logMessage( QObject::tr( "%1 is an invalid layer - not loaded" ).arg( layerPath ) ); QgsMessageLog::logMessage( QObject::tr( "%1 is an invalid layer - not loaded" ).arg( layerPath ) );
QLabel *msgLabel = new QLabel( QObject::tr( "%1 is an invalid layer and cannot be loaded. Please check the <a href=\"#messageLog\">message log</a> for further info." ).arg( layerPath ), QgisApp::instance()->messageBar() ); QLabel *msgLabel = new QLabel( QObject::tr( "%1 is an invalid layer and cannot be loaded. Please check the <a href=\"#messageLog\">message log</a> for further info." ).arg( layerPath ), QgisApp::instance()->messageBar() );
msgLabel->setWordWrap( true ); msgLabel->setWordWrap( true );
if ( providerKey == QLatin1String( "postgres" ) )
{
QObject::connect( msgLabel, &QLabel::linkActivated, QgisApp::instance(), [] {
QgisApp::instance()->openMessageLog( QObject::tr( "PostGIS" ) );
} );
}
else
{
QObject::connect( msgLabel, &QLabel::linkActivated, QgisApp::instance()->logDock(), &QWidget::show ); QObject::connect( msgLabel, &QLabel::linkActivated, QgisApp::instance()->logDock(), &QWidget::show );
}
QgsMessageBarItem *item = new QgsMessageBarItem( msgLabel, Qgis::MessageLevel::Warning ); QgsMessageBarItem *item = new QgsMessageBarItem( msgLabel, Qgis::MessageLevel::Warning );
QgisApp::instance()->messageBar()->pushItem( item ); QgisApp::instance()->messageBar()->pushItem( item );
delete layer; delete layer;

View File

@ -727,19 +727,25 @@ bool QgsPluginRegistry::checkPythonPlugin( const QString &packageName )
bool QgsPluginRegistry::isPythonPluginCompatible( const QString &packageName ) const bool QgsPluginRegistry::isPythonPluginCompatible( const QString &packageName ) const
{ {
#ifdef WITH_BINDINGS #ifdef WITH_BINDINGS
#if QT_VERSION >= QT_VERSION_CHECK( 6, 0, 0 ) bool supportsQgis4 = true;
const QString supportsQt6 = mPythonUtils->getPluginMetadata( packageName, QStringLiteral( "supportsQt6" ) ).trimmed(); const QString supportsQt6 = mPythonUtils->getPluginMetadata( packageName, QStringLiteral( "supportsQt6" ) ).trimmed();
if ( supportsQt6.compare( QLatin1String( "YES" ), Qt::CaseInsensitive ) != 0 && supportsQt6.compare( QLatin1String( "TRUE" ), Qt::CaseInsensitive ) != 0 ) if ( supportsQt6.compare( QLatin1String( "YES" ), Qt::CaseInsensitive ) != 0 && supportsQt6.compare( QLatin1String( "TRUE" ), Qt::CaseInsensitive ) != 0 )
{ {
#if QT_VERSION >= QT_VERSION_CHECK( 6, 0, 0 )
if ( !getenv( "QGIS_DISABLE_SUPPORTS_QT6_CHECK" ) ) if ( !getenv( "QGIS_DISABLE_SUPPORTS_QT6_CHECK" ) )
{ {
return false; return false;
} }
}
#endif #endif
supportsQgis4 = false;
}
const QString minVersion = mPythonUtils->getPluginMetadata( packageName, QStringLiteral( "qgisMinimumVersion" ) ); const QString minVersion = mPythonUtils->getPluginMetadata( packageName, QStringLiteral( "qgisMinimumVersion" ) );
// try to read qgisMaximumVersion. Note checkQgisVersion can cope with "__error__" value. // try to read qgisMaximumVersion. Note checkQgisVersion can cope with "__error__" value.
const QString maxVersion = mPythonUtils->getPluginMetadata( packageName, QStringLiteral( "qgisMaximumVersion" ) ); QString maxVersion = mPythonUtils->getPluginMetadata( packageName, QStringLiteral( "qgisMaximumVersion" ) );
if ( maxVersion == QLatin1String( "__error__" ) && minVersion.startsWith( QLatin1String( "3." ) ) && supportsQgis4 )
{
maxVersion = QLatin1String( "4.99.0" );
}
return minVersion != QLatin1String( "__error__" ) && checkQgisVersion( minVersion, maxVersion ); return minVersion != QLatin1String( "__error__" ) && checkQgisVersion( minVersion, maxVersion );
#else #else
Q_UNUSED( packageName ) Q_UNUSED( packageName )

View File

@ -1532,6 +1532,7 @@ void QgsLineString::visitPointsByRegularDistance( const double distance, const s
double pZ = std::numeric_limits<double>::quiet_NaN(); double pZ = std::numeric_limits<double>::quiet_NaN();
double pM = std::numeric_limits<double>::quiet_NaN(); double pM = std::numeric_limits<double>::quiet_NaN();
double nextPointDistance = distance; double nextPointDistance = distance;
const double eps = 4 * nextPointDistance * std::numeric_limits<double>::epsilon ();
for ( int i = 1; i < totalPoints; ++i ) for ( int i = 1; i < totalPoints; ++i )
{ {
double thisX = *x++; double thisX = *x++;
@ -1540,7 +1541,7 @@ void QgsLineString::visitPointsByRegularDistance( const double distance, const s
double thisM = m ? *m++ : 0.0; double thisM = m ? *m++ : 0.0;
const double segmentLength = QgsGeometryUtilsBase::distance2D( thisX, thisY, prevX, prevY ); const double segmentLength = QgsGeometryUtilsBase::distance2D( thisX, thisY, prevX, prevY );
while ( nextPointDistance < distanceTraversed + segmentLength || qgsDoubleNear( nextPointDistance, distanceTraversed + segmentLength ) ) while ( nextPointDistance < distanceTraversed + segmentLength || qgsDoubleNear( nextPointDistance, distanceTraversed + segmentLength, eps ) )
{ {
// point falls on this segment - truncate to segment length if qgsDoubleNear test was actually > segment length // point falls on this segment - truncate to segment length if qgsDoubleNear test was actually > segment length
const double distanceToPoint = std::min( nextPointDistance - distanceTraversed, segmentLength ); const double distanceToPoint = std::min( nextPointDistance - distanceTraversed, segmentLength );

View File

@ -116,21 +116,6 @@ QStringList makeKeyTokens_( const QString &scope, const QString &key )
// be sure to include the canonical root node // be sure to include the canonical root node
keyTokens.push_front( QStringLiteral( "properties" ) ); keyTokens.push_front( QStringLiteral( "properties" ) );
//check validy of keys since an invalid xml name will will be dropped upon saving the xml file. If not valid, we print a message to the console.
for ( int i = 0; i < keyTokens.size(); ++i )
{
const QString keyToken = keyTokens.at( i );
//invalid chars in XML are found at http://www.w3.org/TR/REC-xml/#NT-NameChar
//note : it seems \x10000-\xEFFFF is valid, but it when added to the regexp, a lot of unwanted characters remain
const thread_local QRegularExpression sInvalidRegexp = QRegularExpression( QStringLiteral( "([^:A-Z_a-z\\x{C0}-\\x{D6}\\x{D8}-\\x{F6}\\x{F8}-\\x{2FF}\\x{370}-\\x{37D}\\x{37F}-\\x{1FFF}\\x{200C}-\\x{200D}\\x{2070}-\\x{218F}\\x{2C00}-\\x{2FEF}\\x{3001}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFFD}\\-\\.0-9\\x{B7}\\x{0300}-\\x{036F}\\x{203F}-\\x{2040}]|^[^:A-Z_a-z\\x{C0}-\\x{D6}\\x{D8}-\\x{F6}\\x{F8}-\\x{2FF}\\x{370}-\\x{37D}\\x{37F}-\\x{1FFF}\\x{200C}-\\x{200D}\\x{2070}-\\x{218F}\\x{2C00}-\\x{2FEF}\\x{3001}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFFD}])" ) );
if ( keyToken.contains( sInvalidRegexp ) )
{
const QString errorString = QObject::tr( "Entry token invalid : '%1'. The token will not be saved to file." ).arg( keyToken );
QgsMessageLog::logMessage( errorString, QString(), Qgis::MessageLevel::Critical );
}
}
return keyTokens; return keyTokens;
} }
@ -1322,20 +1307,20 @@ void dump_( const QgsProjectPropertyKey &topQgsPropertyKey )
* scope. "layers" is a list containing three string values. * scope. "layers" is a list containing three string values.
* *
* \code{.xml} * \code{.xml}
* <properties> * <properties name="properties">
* <fsplugin> * <properties name="fsplugin">
* <foo type="int" >42</foo> * <properties name="foo" type="int" >42</properties>
* <baz type="int" >1</baz> * <properties name="baz" type="int" >1</properties>
* <layers type="QStringList" > * <properties name="layers" type="QStringList">
* <value>railroad</value> * <value>railroad</value>
* <value>airport</value> * <value>airport</value>
* </layers> * </properties>
* <xyqzzy type="int" >1</xyqzzy> * <properties name="xyqzzy" type="int" >1</properties>
* <bar type="double" >123.456</bar> * <properties name="bar" type="double" >123.456</properties>
* <feature_types type="QStringList" > * <properties name="feature_types" type="QStringList">
* <value>type</value> * <value>type</value>
* </feature_types> * </properties>
* </fsplugin> * </properties>
* </properties> * </properties>
* \endcode * \endcode
* *
@ -3992,10 +3977,25 @@ bool QgsProject::createEmbeddedLayer( const QString &layerId, const QString &pro
const QDomElement propertiesElem = sProjectDocument.documentElement().firstChildElement( QStringLiteral( "properties" ) ); const QDomElement propertiesElem = sProjectDocument.documentElement().firstChildElement( QStringLiteral( "properties" ) );
if ( !propertiesElem.isNull() ) if ( !propertiesElem.isNull() )
{ {
const QDomElement absElem = propertiesElem.firstChildElement( QStringLiteral( "Paths" ) ).firstChildElement( QStringLiteral( "Absolute" ) ); QDomElement e = propertiesElem.firstChildElement( QStringLiteral( "Paths" ) );
if ( !absElem.isNull() ) if ( e.isNull() )
{ {
useAbsolutePaths = absElem.text().compare( QLatin1String( "true" ), Qt::CaseInsensitive ) == 0; e = propertiesElem.firstChildElement( QStringLiteral( "properties" ) );
while ( !e.isNull() && e.attribute( QStringLiteral( "name" ) ) != QStringLiteral( "Paths" ) )
e = e.nextSiblingElement( QStringLiteral( "properties" ) );
e = e.firstChildElement( QStringLiteral( "properties" ) );
while ( !e.isNull() && e.attribute( QStringLiteral( "name" ) ) != QStringLiteral( "Absolute" ) )
e = e.nextSiblingElement( QStringLiteral( "properties" ) );
}
else
{
e = e.firstChildElement( QStringLiteral( "Absolute" ) );
}
if ( !e.isNull() )
{
useAbsolutePaths = e.text().compare( QLatin1String( "true" ), Qt::CaseInsensitive ) == 0;
} }
} }

View File

@ -236,12 +236,12 @@ bool QgsProjectPropertyValue::writeXml( QString const &nodeName,
QDomElement &keyElement, QDomElement &keyElement,
QDomDocument &document ) QDomDocument &document )
{ {
QDomElement valueElement = document.createElement( nodeName ); QDomElement valueElement = document.createElement( QStringLiteral( "properties" ) );
// remember the type so that we can rebuild it when the project is read in // remember the type so that we can rebuild it when the project is read in
valueElement.setAttribute( QStringLiteral( "name" ), nodeName );
valueElement.setAttribute( QStringLiteral( "type" ), mValue.typeName() ); valueElement.setAttribute( QStringLiteral( "type" ), mValue.typeName() );
// we handle string lists differently from other types in that we // we handle string lists differently from other types in that we
// create a sequence of repeated elements to cover all the string list // create a sequence of repeated elements to cover all the string list
// members; each value will be in a <value></value> tag. // members; each value will be in a <value></value> tag.
@ -362,33 +362,41 @@ bool QgsProjectPropertyKey::readXml( const QDomNode &keyNode )
while ( i < subkeys.count() ) while ( i < subkeys.count() )
{ {
const QDomNode subkey = subkeys.item( i );
QString name;
if ( subkey.nodeName() == QStringLiteral( "properties" ) &&
subkey.hasAttributes() && // if we have attributes
subkey.isElement() && // and we're an element
subkey.toElement().hasAttribute( QStringLiteral( "name" ) ) ) // and we have a "name" attribute
name = subkey.toElement().attribute( QStringLiteral( "name" ) );
else
name = subkey.nodeName();
// if the current node is an element that has a "type" attribute, // if the current node is an element that has a "type" attribute,
// then we know it's a leaf node; i.e., a subkey _value_, and not // then we know it's a leaf node; i.e., a subkey _value_, and not
// a subkey // a subkey
if ( subkeys.item( i ).hasAttributes() && // if we have attributes if ( subkey.hasAttributes() && // if we have attributes
subkeys.item( i ).isElement() && // and we're an element subkey.isElement() && // and we're an element
subkeys.item( i ).toElement().hasAttribute( QStringLiteral( "type" ) ) ) // and we have a "type" attribute subkey.toElement().hasAttribute( QStringLiteral( "type" ) ) ) // and we have a "type" attribute
{ {
// then we're a key value // then we're a key value
delete mProperties.take( subkeys.item( i ).nodeName() ); //
mProperties.insert( subkeys.item( i ).nodeName(), new QgsProjectPropertyValue ); delete mProperties.take( name );
mProperties.insert( name, new QgsProjectPropertyValue );
QDomNode subkey = subkeys.item( i ); if ( !mProperties[name]->readXml( subkey ) )
if ( !mProperties[subkeys.item( i ).nodeName()]->readXml( subkey ) )
{ {
QgsDebugError( QStringLiteral( "unable to parse key value %1" ).arg( subkeys.item( i ).nodeName() ) ); QgsDebugError( QStringLiteral( "unable to parse key value %1" ).arg( name ) );
} }
} }
else // otherwise it's a subkey, so just recurse on down the remaining keys else // otherwise it's a subkey, so just recurse on down the remaining keys
{ {
addKey( subkeys.item( i ).nodeName() ); addKey( name );
QDomNode subkey = subkeys.item( i ); if ( !mProperties[name]->readXml( subkey ) )
if ( !mProperties[subkeys.item( i ).nodeName()]->readXml( subkey ) )
{ {
QgsDebugError( QStringLiteral( "unable to parse subkey %1" ).arg( subkeys.item( i ).nodeName() ) ); QgsDebugError( QStringLiteral( "unable to parse subkey %1" ).arg( name ) );
} }
} }
@ -408,7 +416,8 @@ bool QgsProjectPropertyKey::writeXml( QString const &nodeName, QDomElement &elem
// If it's an _empty_ node (i.e., one with no properties) we need to emit // If it's an _empty_ node (i.e., one with no properties) we need to emit
// an empty place holder; else create new Dom elements as necessary. // an empty place holder; else create new Dom elements as necessary.
QDomElement keyElement = document.createElement( nodeName ); // Dom element for this property key QDomElement keyElement = document.createElement( "properties" ); // Dom element for this property key
keyElement.toElement().setAttribute( QStringLiteral( "name" ), nodeName );
if ( ! mProperties.isEmpty() ) if ( ! mProperties.isEmpty() )
{ {

View File

@ -96,7 +96,7 @@ void QgsPgTableModel::addTableEntry( const QgsPostgresLayerProperty &layerProper
} }
QString tip; QString tip;
bool withTipButSelectable = false; bool layerNeedsFeatureId = false;
if ( !layerProperty.isRaster ) if ( !layerProperty.isRaster )
{ {
if ( wkbType == Qgis::WkbType::Unknown ) if ( wkbType == Qgis::WkbType::Unknown )
@ -110,7 +110,7 @@ void QgsPgTableModel::addTableEntry( const QgsPostgresLayerProperty &layerProper
else if ( !layerProperty.pkCols.isEmpty() ) else if ( !layerProperty.pkCols.isEmpty() )
{ {
tip = tr( "Select columns in the '%1' column that uniquely identify features of this layer" ).arg( tr( "Feature id" ) ); tip = tr( "Select columns in the '%1' column that uniquely identify features of this layer" ).arg( tr( "Feature id" ) );
withTipButSelectable = true; layerNeedsFeatureId = true;
} }
} }
@ -160,7 +160,9 @@ void QgsPgTableModel::addTableEntry( const QgsPostgresLayerProperty &layerProper
pkItem->setFlags( pkItem->flags() | Qt::ItemIsEditable ); pkItem->setFlags( pkItem->flags() | Qt::ItemIsEditable );
} }
else else
{
pkItem->setFlags( pkItem->flags() & ~Qt::ItemIsEditable ); pkItem->setFlags( pkItem->flags() & ~Qt::ItemIsEditable );
}
pkItem->setData( layerProperty.pkCols, Qt::UserRole + 1 ); pkItem->setData( layerProperty.pkCols, Qt::UserRole + 1 );
@ -176,8 +178,17 @@ void QgsPgTableModel::addTableEntry( const QgsPostgresLayerProperty &layerProper
pkItem->setData( defPk, Qt::UserRole + 2 ); pkItem->setData( defPk, Qt::UserRole + 2 );
if ( !defPk.isEmpty() ) if ( !defPk.isEmpty() )
{
pkItem->setText( defPk.join( ',' ) ); pkItem->setText( defPk.join( ',' ) );
// Reset the tip since we're pre-selecting fields in the Feature id combo box.
// Note we don't reset the tip if Geom type or SRID need some action from users.
if ( layerNeedsFeatureId )
{
tip = QString();
}
}
QStandardItem *selItem = new QStandardItem( QString() ); QStandardItem *selItem = new QStandardItem( QString() );
selItem->setFlags( selItem->flags() | Qt::ItemIsUserCheckable ); selItem->setFlags( selItem->flags() | Qt::ItemIsUserCheckable );
selItem->setCheckState( Qt::Checked ); selItem->setCheckState( Qt::Checked );
@ -237,28 +248,10 @@ void QgsPgTableModel::addTableEntry( const QgsPostgresLayerProperty &layerProper
childItemList << checkPkUnicityItem; childItemList << checkPkUnicityItem;
childItemList << sqlItem; childItemList << sqlItem;
const auto constChildItemList = childItemList; for ( int column = 0; column < childItemList.count(); column++ )
for ( QStandardItem *item : constChildItemList )
{ {
if ( tip.isEmpty() || withTipButSelectable ) QStandardItem *item = childItemList.at( column );
item->setFlags( item->flags() | Qt::ItemIsSelectable ); setItemStatus( item, tip, column );
else
item->setFlags( item->flags() & ~Qt::ItemIsSelectable );
if ( item->toolTip().isEmpty() && tip.isEmpty() && item != checkPkUnicityItem && item != selItem )
{
item->setToolTip( QString() );
}
else
{
if ( item == schemaNameItem )
item->setData( QgsApplication::getThemeIcon( QStringLiteral( "/mIconWarning.svg" ) ), Qt::DecorationRole );
if ( item == schemaNameItem || item == tableItem || item == geomItem )
{
item->setToolTip( tip );
}
}
} }
if ( !schemaItem ) if ( !schemaItem )
@ -285,6 +278,38 @@ void QgsPgTableModel::addTableEntry( const QgsPostgresLayerProperty &layerProper
} }
} }
void QgsPgTableModel::setItemStatus( QStandardItem *item, const QString &tip, int column )
{
if ( tip.isEmpty() )
{
item->setFlags( item->flags() | Qt::ItemIsSelectable );
if ( column == DbtmSchema || column == DbtmTable || column == DbtmGeomCol )
{
item->setToolTip( QString() );
if ( column == DbtmSchema )
{
item->setData( QVariant(), Qt::DecorationRole );
}
}
}
else
{
item->setFlags( item->flags() & ~Qt::ItemIsSelectable );
if ( column == DbtmSchema || column == DbtmTable || column == DbtmGeomCol )
{
item->setToolTip( tip );
if ( column == DbtmSchema )
{
item->setData( QgsApplication::getThemeIcon( QStringLiteral( "/mIconWarning.svg" ) ), Qt::DecorationRole );
}
}
}
}
void QgsPgTableModel::setSql( const QModelIndex &index, const QString &sql ) void QgsPgTableModel::setSql( const QModelIndex &index, const QString &sql )
{ {
if ( !index.isValid() || !index.parent().isValid() ) if ( !index.isValid() || !index.parent().isValid() )
@ -362,6 +387,7 @@ bool QgsPgTableModel::setData( const QModelIndex &idx, const QVariant &value, in
if ( !QStandardItemModel::setData( idx, value, role ) ) if ( !QStandardItemModel::setData( idx, value, role ) )
return false; return false;
// After changes in type, srid, or feature id columns, update other sibling columns
if ( idx.column() == DbtmType || idx.column() == DbtmSrid || idx.column() == DbtmPkCol ) if ( idx.column() == DbtmType || idx.column() == DbtmSrid || idx.column() == DbtmPkCol )
{ {
const Qgis::WkbType wkbType = static_cast<Qgis::WkbType>( idx.sibling( idx.row(), DbtmType ).data( Qt::UserRole + 2 ).toInt() ); const Qgis::WkbType wkbType = static_cast<Qgis::WkbType>( idx.sibling( idx.row(), DbtmType ).data( Qt::UserRole + 2 ).toInt() );
@ -386,35 +412,15 @@ bool QgsPgTableModel::setData( const QModelIndex &idx, const QVariant &value, in
const QSet<QString> s0( qgis::listToSet( idx.sibling( idx.row(), DbtmPkCol ).data( Qt::UserRole + 2 ).toStringList() ) ); const QSet<QString> s0( qgis::listToSet( idx.sibling( idx.row(), DbtmPkCol ).data( Qt::UserRole + 2 ).toStringList() ) );
const QSet<QString> s1( qgis::listToSet( pkCols ) ); const QSet<QString> s1( qgis::listToSet( pkCols ) );
if ( !s0.intersects( s1 ) ) if ( !s0.intersects( s1 ) )
{
tip = tr( "Select columns in the '%1' column that uniquely identify features of this layer" ).arg( tr( "Feature id" ) ); tip = tr( "Select columns in the '%1' column that uniquely identify features of this layer" ).arg( tr( "Feature id" ) );
} }
for ( int i = 0; i < columnCount(); i++ )
{
QStandardItem *item = itemFromIndex( idx.sibling( idx.row(), i ) );
if ( tip.isEmpty() )
{
if ( i == DbtmSchema )
{
item->setData( QVariant(), Qt::DecorationRole );
} }
item->setFlags( item->flags() | Qt::ItemIsSelectable ); for ( int column = 0; column < columnCount(); column++ )
item->setToolTip( QString() );
}
else
{ {
item->setFlags( item->flags() & ~Qt::ItemIsSelectable ); QStandardItem *item = itemFromIndex( idx.sibling( idx.row(), column ) );
setItemStatus( item, tip, column );
if ( i == DbtmSchema )
item->setData( QgsApplication::getThemeIcon( QStringLiteral( "/mIconWarning.svg" ) ), Qt::DecorationRole );
if ( i == DbtmSchema || i == DbtmTable || i == DbtmGeomCol )
{
item->setFlags( item->flags() );
item->setToolTip( tip );
}
}
} }
} }

View File

@ -69,6 +69,15 @@ class QgsPgTableModel : public QgsAbstractDbTableModel
void setConnectionName( const QString &connName ) { mConnName = connName; } void setConnectionName( const QString &connName ) { mConnName = connName; }
/**
* Sets flags, tool tips and decorators to the schema, table and geometry column items.
*
* \param item Item to be modified.
* \param tip Tool tip to be applied to the item.
* \param column Column where the item is located in the current row.
*/
void setItemStatus( QStandardItem *item, const QString &tip, int column );
private: private:
//! Number of tables in the model //! Number of tables in the model
int mTableCount = 0; int mTableCount = 0;

View File

@ -65,84 +65,6 @@ class TestQgsProject(QgisTestCase):
QgisTestCase.__init__(self, methodName) QgisTestCase.__init__(self, methodName)
self.messageCaught = False self.messageCaught = False
def test_makeKeyTokens_(self):
# see http://www.w3.org/TR/REC-xml/#d0e804 for a list of valid characters
invalidTokens = []
validTokens = []
# all test tokens will be generated by prepending or inserting characters to this token
validBase = "valid"
# some invalid characters, not allowed anywhere in a token
# note that '/' must not be added here because it is taken as a separator by makeKeyTokens_()
invalidChars = "+*,;<>|!$%()=?#\x01"
# generate the characters that are allowed at the start of a token (and at every other position)
validStartChars = ":_"
charRanges = [
(ord("a"), ord("z")),
(ord("A"), ord("Z")),
(0x00F8, 0x02FF),
(0x0370, 0x037D),
(0x037F, 0x1FFF),
(0x200C, 0x200D),
(0x2070, 0x218F),
(0x2C00, 0x2FEF),
(0x3001, 0xD7FF),
(0xF900, 0xFDCF),
(0xFDF0, 0xFFFD),
# (0x10000, 0xEFFFF), while actually valid, these are not yet accepted by makeKeyTokens_()
]
for r in charRanges:
for c in range(r[0], r[1]):
validStartChars += chr(c)
# generate the characters that are only allowed inside a token, not at the start
validInlineChars = "-.\xB7"
charRanges = [
(ord("0"), ord("9")),
(0x0300, 0x036F),
(0x203F, 0x2040),
]
for r in charRanges:
for c in range(r[0], r[1]):
validInlineChars += chr(c)
# test forbidden start characters
for c in invalidChars + validInlineChars:
invalidTokens.append(c + validBase)
# test forbidden inline characters
for c in invalidChars:
invalidTokens.append(validBase[:4] + c + validBase[4:])
# test each allowed start character
for c in validStartChars:
validTokens.append(c + validBase)
# test each allowed inline character
for c in validInlineChars:
validTokens.append(validBase[:4] + c + validBase[4:])
logger = QgsApplication.messageLog()
logger.messageReceived.connect(self.catchMessage)
prj = QgsProject.instance()
for token in validTokens:
self.messageCaught = False
prj.readEntry("test", token)
myMessage = f"valid token '{token}' not accepted"
assert not self.messageCaught, myMessage
for token in invalidTokens:
self.messageCaught = False
prj.readEntry("test", token)
myMessage = f"invalid token '{token}' accepted"
assert self.messageCaught, myMessage
logger.messageReceived.disconnect(self.catchMessage)
def catchMessage(self): def catchMessage(self):
self.messageCaught = True self.messageCaught = True

View File

@ -84,6 +84,7 @@
] ]
}, },
"qtdeclarative", "qtdeclarative",
"qtimageformats",
"qtkeychain-qt6", "qtkeychain-qt6",
"qtlocation", "qtlocation",
"qtquickcontrols2", "qtquickcontrols2",