31 changes: 31 additions & 0 deletions src/core/project/qgsprojectstylesettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include <memory.h>
#include <QAbstractListModel>
#include <QSortFilterProxyModel>
#include <QPointer>

class QDomElement;
class QgsReadWriteContext;
Expand Down Expand Up @@ -52,6 +53,8 @@ class CORE_EXPORT QgsProjectStyleSettings : public QObject
*/
QgsProjectStyleSettings( QgsProject *project = nullptr );

~QgsProjectStyleSettings() override;

/**
* Returns the project default symbol for a given type.
* \param symbolType the symbol type
Expand Down Expand Up @@ -119,6 +122,20 @@ class CORE_EXPORT QgsProjectStyleSettings : public QObject
*/
void reset();

/**
* Sets the style database to use for the project style.
*
* \see projectStyle()
*/
void setProjectStyle( QgsStyle *style SIP_TRANSFER );

/**
* Returns the style database to use for project specific styles.
*
* \see setProjectStyle()
*/
QgsStyle *projectStyle();

/**
* Reads the settings's state from a DOM element.
* \see writeXml()
Expand Down Expand Up @@ -237,6 +254,13 @@ class CORE_EXPORT QgsProjectStyleSettings : public QObject
*/
void styleDatabaseRemoved( const QString &path );

/**
* Emitted when the style returned by projectStyle() is changed.
*
* \note Not available in Python bindings
*/
void projectStyleChanged();

#endif
private:

Expand All @@ -251,6 +275,7 @@ class CORE_EXPORT QgsProjectStyleSettings : public QObject
bool mRandomizeDefaultSymbolColor = true;
double mDefaultSymbolOpacity = 1.0;

QgsStyle *mProjectStyle = nullptr;
QStringList mStyleDatabases;
QList< QPointer< QgsStyle > > mStyles;

Expand Down Expand Up @@ -322,9 +347,15 @@ class CORE_EXPORT QgsProjectStyleDatabaseModel : public QAbstractListModel
void styleDatabaseAdded( const QString &path );
void styleDatabaseRemoved( const QString &path );

void setProjectStyle( QgsStyle *style );
void projectStyleAboutToBeDestroyed();
void projectStyleDestroyed();
void projectStyleChanged();

private:
QgsProjectStyleSettings *mSettings = nullptr;
bool mShowDefault = false;
QPointer< QgsStyle > mProjectStyle;
};

/**
Expand Down
78 changes: 77 additions & 1 deletion tests/src/python/test_qgsprojectstylesettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,13 @@
QgsTextFormat,
Qgis)

from qgis.PyQt.QtCore import Qt, QModelIndex
from qgis.PyQt.QtCore import (
Qt,
QModelIndex,
QTemporaryDir,
QCoreApplication,
QEvent
)
from qgis.PyQt.QtGui import QFont, QColor

from qgis.PyQt.QtTest import QSignalSpy
Expand Down Expand Up @@ -90,6 +96,34 @@ def testDefaultSymbolOpacity(self):
p.setDefaultSymbolOpacity(0.25)
self.assertEqual(p.defaultSymbolOpacity(), 0.25)

def testProjectStyle(self):
project = QgsProject()
settings = project.styleSettings()
self.assertIsInstance(settings.projectStyle(), QgsStyle)
self.assertEqual(settings.projectStyle().name(), 'Project Styles')

text_format = QgsTextFormat()
text_format.setColor(QColor(255,0,0))
self.assertTrue(settings.projectStyle().addTextFormat( 'my text format', text_format ))
self.assertTrue(settings.projectStyle().saveTextFormat( 'my text format', text_format, True, [] ))
self.assertEqual(settings.projectStyle().textFormatCount(), 1)

tmp_dir = QTemporaryDir()
tmp_project_file = "{}/project.qgs".format(tmp_dir.path())
self.assertTrue(project.write(tmp_project_file))

project.deleteLater()
del project
QCoreApplication.sendPostedEvents(None, QEvent.DeferredDelete)

project2 = QgsProject()
self.assertTrue(project2.read(tmp_project_file))
self.assertEqual(project2.styleSettings().projectStyle().textFormatCount(), 1)
self.assertEqual(project2.styleSettings().projectStyle().textFormat('my text format').color().name(), '#ff0000')

project2.clear()
self.assertEqual(project2.styleSettings().projectStyle().textFormatCount(), 0)

@unittest.skipIf(QgsCombinedStyleModel is None, "QgsCombinedStyleModel not available")
def testStylePaths(self):
p = QgsProjectStyleSettings()
Expand All @@ -101,6 +135,12 @@ def testStylePaths(self):
proxy_model = QgsProjectStyleDatabaseProxyModel(model_with_default)
proxy_model.setFilters(QgsProjectStyleDatabaseProxyModel.Filter.FilterHideReadOnly)

project_style = QgsStyle()
project_style.setName('project')
model_with_project_style = QgsProjectStyleDatabaseModel(p)
model_with_project_style.setShowDefaultStyle(True)
model_with_project_style.setProjectStyle(project_style)

self.assertFalse(p.styleDatabasePaths())
self.assertFalse(p.styles())
self.assertEqual(p.combinedStyleModel().rowCount(), 0)
Expand All @@ -109,6 +149,13 @@ def testStylePaths(self):
self.assertEqual(model_with_default.rowCount(QModelIndex()), 1)
self.assertEqual(model_with_default.data(model_with_default.index(0, 0, QModelIndex()), Qt.DisplayRole), 'Default')
self.assertEqual(model_with_default.data(model_with_default.index(0, 0, QModelIndex()), QgsProjectStyleDatabaseModel.StyleRole), QgsStyle.defaultStyle())

self.assertEqual(model_with_project_style.rowCount(QModelIndex()), 2)
self.assertEqual(model_with_project_style.data(model_with_project_style.index(0, 0, QModelIndex()), Qt.DisplayRole), 'project')
self.assertEqual(model_with_project_style.data(model_with_project_style.index(0, 0, QModelIndex()), QgsProjectStyleDatabaseModel.StyleRole), project_style)
self.assertEqual(model_with_project_style.data(model_with_project_style.index(1, 0, QModelIndex()), Qt.DisplayRole), 'Default')
self.assertEqual(model_with_project_style.data(model_with_project_style.index(1, 0, QModelIndex()), QgsProjectStyleDatabaseModel.StyleRole), QgsStyle.defaultStyle())

self.assertEqual(proxy_model.rowCount(QModelIndex()), 1)
self.assertEqual(proxy_model.data(proxy_model.index(0, 0, QModelIndex()), Qt.DisplayRole), 'Default')
self.assertEqual(proxy_model.data(proxy_model.index(0, 0, QModelIndex()), QgsProjectStyleDatabaseModel.StyleRole), QgsStyle.defaultStyle())
Expand All @@ -134,6 +181,17 @@ def testStylePaths(self):
self.assertEqual(model_with_default.data(model_with_default.index(1, 0, QModelIndex()), QgsProjectStyleDatabaseModel.StyleRole), p.styles()[0])
self.assertEqual(model_with_default.data(model_with_default.index(1, 0, QModelIndex()),
QgsProjectStyleDatabaseModel.PathRole), unitTestDataPath() + '/style1.db')

self.assertEqual(model_with_project_style.rowCount(QModelIndex()), 3)
self.assertEqual(model_with_project_style.data(model_with_project_style.index(0, 0, QModelIndex()), Qt.DisplayRole), 'project')
self.assertEqual(model_with_project_style.data(model_with_project_style.index(0, 0, QModelIndex()), QgsProjectStyleDatabaseModel.StyleRole), project_style)
self.assertEqual(model_with_project_style.data(model_with_project_style.index(1, 0, QModelIndex()), Qt.DisplayRole), 'Default')
self.assertEqual(model_with_project_style.data(model_with_project_style.index(1, 0, QModelIndex()), QgsProjectStyleDatabaseModel.StyleRole), QgsStyle.defaultStyle())
self.assertEqual(model_with_project_style.data(model_with_project_style.index(2, 0, QModelIndex()), Qt.DisplayRole), 'style1')
self.assertEqual(model_with_project_style.data(model_with_project_style.index(2, 0, QModelIndex()), QgsProjectStyleDatabaseModel.StyleRole), p.styles()[0])
self.assertEqual(model_with_project_style.data(model_with_project_style.index(2, 0, QModelIndex()),
QgsProjectStyleDatabaseModel.PathRole), unitTestDataPath() + '/style1.db')

self.assertEqual(proxy_model.rowCount(QModelIndex()), 2)
self.assertEqual(proxy_model.data(proxy_model.index(0, 0, QModelIndex()), Qt.DisplayRole), 'Default')
self.assertEqual(proxy_model.data(proxy_model.index(0, 0, QModelIndex()), QgsProjectStyleDatabaseModel.StyleRole), QgsStyle.defaultStyle())
Expand Down Expand Up @@ -180,6 +238,16 @@ def testStylePaths(self):
self.assertEqual(model_with_default.data(model_with_default.index(2, 0, QModelIndex()), Qt.DisplayRole), 'style2')
self.assertEqual(model_with_default.data(model_with_default.index(2, 0, QModelIndex()), QgsProjectStyleDatabaseModel.StyleRole), p.styles()[1])

self.assertEqual(model_with_project_style.rowCount(QModelIndex()), 4)
self.assertEqual(model_with_project_style.data(model_with_project_style.index(0, 0, QModelIndex()), Qt.DisplayRole), 'project')
self.assertEqual(model_with_project_style.data(model_with_project_style.index(0, 0, QModelIndex()), QgsProjectStyleDatabaseModel.StyleRole), project_style)
self.assertEqual(model_with_project_style.data(model_with_project_style.index(1, 0, QModelIndex()), Qt.DisplayRole), 'Default')
self.assertEqual(model_with_project_style.data(model_with_project_style.index(1, 0, QModelIndex()), QgsProjectStyleDatabaseModel.StyleRole), QgsStyle.defaultStyle())
self.assertEqual(model_with_project_style.data(model_with_project_style.index(2, 0, QModelIndex()), Qt.DisplayRole), 'style1')
self.assertEqual(model_with_project_style.data(model_with_project_style.index(2, 0, QModelIndex()), QgsProjectStyleDatabaseModel.StyleRole), p.styles()[0])
self.assertEqual(model_with_project_style.data(model_with_project_style.index(3, 0, QModelIndex()), Qt.DisplayRole), 'style2')
self.assertEqual(model_with_project_style.data(model_with_project_style.index(3, 0, QModelIndex()), QgsProjectStyleDatabaseModel.StyleRole), p.styles()[1])

self.assertEqual(p.styleAtPath(unitTestDataPath() + '/style1.db'), p.styles()[0])
self.assertEqual(p.styleAtPath(unitTestDataPath() + '/style2.db'), p.styles()[1])
self.assertFalse(p.styleAtPath('.xxx'))
Expand All @@ -200,6 +268,14 @@ def testStylePaths(self):
self.assertEqual(model_with_default.data(model_with_default.index(1, 0, QModelIndex()), Qt.DisplayRole), 'style3')
self.assertEqual(model_with_default.data(model_with_default.index(1, 0, QModelIndex()), QgsProjectStyleDatabaseModel.StyleRole), p.styles()[0])

self.assertEqual(model_with_project_style.rowCount(QModelIndex()), 3)
self.assertEqual(model_with_project_style.data(model_with_project_style.index(0, 0, QModelIndex()), Qt.DisplayRole), 'project')
self.assertEqual(model_with_project_style.data(model_with_project_style.index(0, 0, QModelIndex()), QgsProjectStyleDatabaseModel.StyleRole), project_style)
self.assertEqual(model_with_project_style.data(model_with_project_style.index(1, 0, QModelIndex()), Qt.DisplayRole), 'Default')
self.assertEqual(model_with_project_style.data(model_with_project_style.index(1, 0, QModelIndex()), QgsProjectStyleDatabaseModel.StyleRole), QgsStyle.defaultStyle())
self.assertEqual(model_with_project_style.data(model_with_project_style.index(2, 0, QModelIndex()), Qt.DisplayRole), 'style3')
self.assertEqual(model_with_project_style.data(model_with_project_style.index(2, 0, QModelIndex()), QgsProjectStyleDatabaseModel.StyleRole), p.styles()[0])

self.assertEqual(p.styles()[0].fileName(), unitTestDataPath() + '/style3.db')
self.assertEqual(p.styles()[0].name(), 'style3')

Expand Down