forked from mirror/openmw-tes3mp
Replace nonconst getId with setId, add template specialization and specialized derived classes for LandTexture
This commit is contained in:
parent
5d14a2afcc
commit
9e41f1340a
15 changed files with 308 additions and 22 deletions
|
@ -70,7 +70,7 @@ opencs_units (view/world
|
|||
cellcreator pathgridcreator referenceablecreator startscriptcreator referencecreator scenesubview
|
||||
infocreator scriptedit dialoguesubview previewsubview regionmap dragrecordtable nestedtable
|
||||
dialoguespinbox recordbuttonbar tableeditidaction scripterrortable extendedcommandconfigurator
|
||||
bodypartcreator
|
||||
bodypartcreator landtexturecreator
|
||||
)
|
||||
|
||||
opencs_units_noqt (view/world
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <functional>
|
||||
|
||||
#include <QVariant>
|
||||
|
@ -13,8 +14,8 @@
|
|||
#include <components/misc/stringops.hpp>
|
||||
|
||||
#include "columnbase.hpp"
|
||||
|
||||
#include "collectionbase.hpp"
|
||||
#include "landtexture.hpp"
|
||||
|
||||
namespace CSMWorld
|
||||
{
|
||||
|
@ -22,15 +23,14 @@ namespace CSMWorld
|
|||
template<typename ESXRecordT>
|
||||
struct IdAccessor
|
||||
{
|
||||
std::string& getId (ESXRecordT& record);
|
||||
|
||||
void setId(ESXRecordT& record, const std::string& id) const;
|
||||
const std::string getId (const ESXRecordT& record) const;
|
||||
};
|
||||
|
||||
template<typename ESXRecordT>
|
||||
std::string& IdAccessor<ESXRecordT>::getId (ESXRecordT& record)
|
||||
void IdAccessor<ESXRecordT>::setId(ESXRecordT& record, const std::string& id) const
|
||||
{
|
||||
return record.mId;
|
||||
record.mId = id;
|
||||
}
|
||||
|
||||
template<typename ESXRecordT>
|
||||
|
@ -39,6 +39,23 @@ namespace CSMWorld
|
|||
return record.mId;
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void IdAccessor<LandTexture>::setId (LandTexture& record, const std::string& id) const
|
||||
{
|
||||
int plugin = 0;
|
||||
int index = 0;
|
||||
|
||||
LandTexture::parseUniqueRecordId(id, plugin, index);
|
||||
record.mPluginIndex = plugin;
|
||||
record.mIndex = index;
|
||||
}
|
||||
|
||||
template<>
|
||||
inline const std::string IdAccessor<LandTexture>::getId (const LandTexture& record) const
|
||||
{
|
||||
return LandTexture::createUniqueRecordId(record.mPluginIndex, record.mIndex);
|
||||
}
|
||||
|
||||
/// \brief Single-type record collection
|
||||
template<typename ESXRecordT, typename IdAccessorT = IdAccessor<ESXRecordT> >
|
||||
class Collection : public CollectionBase
|
||||
|
@ -213,7 +230,7 @@ namespace CSMWorld
|
|||
Record<ESXRecordT> copy;
|
||||
copy.mModified = getRecord(origin).get();
|
||||
copy.mState = RecordBase::State_ModifiedOnly;
|
||||
copy.get().mId = destination;
|
||||
IdAccessorT().setId(copy.get(), destination);
|
||||
|
||||
insertRecord(copy, getAppendIndex(destination, type));
|
||||
}
|
||||
|
@ -366,7 +383,7 @@ namespace CSMWorld
|
|||
UniversalId::Type type)
|
||||
{
|
||||
ESXRecordT record;
|
||||
IdAccessorT().getId (record) = id;
|
||||
IdAccessorT().setId(record, id);
|
||||
record.blank();
|
||||
|
||||
Record<ESXRecordT> record2;
|
||||
|
|
|
@ -63,6 +63,13 @@ namespace CSMWorld
|
|||
}
|
||||
};
|
||||
|
||||
template<>
|
||||
inline QVariant StringIdColumn<LandTexture>::get(const Record<LandTexture>& record) const
|
||||
{
|
||||
const LandTexture& ltex = record.get();
|
||||
return QString::fromUtf8(std::string('L' + std::to_string(ltex.mPluginIndex) + '#' + std::to_string(ltex.mIndex)).c_str());
|
||||
}
|
||||
|
||||
template<typename ESXRecordT>
|
||||
struct RecordStateColumn : public Column<ESXRecordT>
|
||||
{
|
||||
|
@ -2421,6 +2428,31 @@ namespace CSMWorld
|
|||
}
|
||||
};
|
||||
|
||||
template<typename ESXRecordT>
|
||||
struct TextureHandleColumn : public Column<ESXRecordT>
|
||||
{
|
||||
TextureHandleColumn()
|
||||
: Column<ESXRecordT> (Columns::ColumnId_TextureHandle, ColumnBase::Display_String)
|
||||
{}
|
||||
|
||||
QVariant get(const Record<ESXRecordT>& record) const override
|
||||
{
|
||||
return QString::fromUtf8(record.get().mId.c_str());
|
||||
}
|
||||
|
||||
void set(Record<ESXRecordT>& record, const QVariant& data) override
|
||||
{
|
||||
ESXRecordT copy = record.get();
|
||||
copy.mId = data.toString().toUtf8().constData();
|
||||
record.setModified(copy);
|
||||
}
|
||||
|
||||
bool isEditable() const override
|
||||
{
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
template<typename ESXRecordT>
|
||||
struct TextureIndexColumn : public Column<ESXRecordT>
|
||||
{
|
||||
|
@ -2428,31 +2460,30 @@ namespace CSMWorld
|
|||
: Column<ESXRecordT> (Columns::ColumnId_TextureIndex, ColumnBase::Display_Integer)
|
||||
{}
|
||||
|
||||
QVariant get (const Record<ESXRecordT>& record) const
|
||||
QVariant get(const Record<ESXRecordT>& record) const override
|
||||
{
|
||||
return record.get().mIndex;
|
||||
}
|
||||
|
||||
virtual bool isEditable() const
|
||||
bool isEditable() const override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// TODO remove
|
||||
template<typename ESXRecordT>
|
||||
struct PluginIndexColumn : public Column<ESXRecordT>
|
||||
{
|
||||
PluginIndexColumn()
|
||||
: Column<ESXRecordT> (Columns::ColumnId_PluginIndex, ColumnBase::Display_Integer)
|
||||
: Column<ESXRecordT> (Columns::ColumnId_PluginIndex, ColumnBase::Display_Integer,0)
|
||||
{}
|
||||
|
||||
QVariant get (const Record<ESXRecordT>& record) const
|
||||
QVariant get(const Record<ESXRecordT>& record) const override
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
virtual bool isEditable() const
|
||||
virtual bool isEditable() const override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -330,6 +330,7 @@ namespace CSMWorld
|
|||
{ ColumnId_WeatherChance, "Percent Chance" },
|
||||
|
||||
{ ColumnId_Text, "Text" },
|
||||
{ ColumnId_TextureHandle, "Texture Handle" },
|
||||
{ ColumnId_PluginIndex, "Plugin Index" },
|
||||
{ ColumnId_TextureIndex, "Texture Index" },
|
||||
|
||||
|
|
|
@ -329,8 +329,9 @@ namespace CSMWorld
|
|||
|
||||
ColumnId_Text = 297,
|
||||
|
||||
ColumnId_PluginIndex = 298,
|
||||
ColumnId_TextureIndex = 299,
|
||||
ColumnId_TextureHandle = 298,
|
||||
ColumnId_PluginIndex = 299,
|
||||
ColumnId_TextureIndex = 300,
|
||||
|
||||
// Allocated to a separate value range, so we don't get a collision should we ever need
|
||||
// to extend the number of use values.
|
||||
|
|
|
@ -417,9 +417,10 @@ CSMWorld::Data::Data (ToUTF8::FromType encoding, bool fsStrict, const Files::Pat
|
|||
mLand.addColumn (new FixedRecordTypeColumn<Land>(UniversalId::Type_Land));
|
||||
mLand.addColumn (new PluginIndexColumn<Land>);
|
||||
|
||||
mLandTextures.addColumn (new StringIdColumn<LandTexture>);
|
||||
mLandTextures.addColumn (new StringIdColumn<LandTexture>(true));
|
||||
mLandTextures.addColumn (new RecordStateColumn<LandTexture>);
|
||||
mLandTextures.addColumn (new FixedRecordTypeColumn<LandTexture>(UniversalId::Type_LandTexture));
|
||||
mLandTextures.addColumn (new TextureHandleColumn<LandTexture>);
|
||||
mLandTextures.addColumn (new PluginIndexColumn<LandTexture>);
|
||||
mLandTextures.addColumn (new TextureIndexColumn<LandTexture>);
|
||||
mLandTextures.addColumn (new TextureColumn<LandTexture>);
|
||||
|
@ -544,7 +545,7 @@ CSMWorld::Data::Data (ToUTF8::FromType encoding, bool fsStrict, const Files::Pat
|
|||
addModel (new IdTable (&mSoundGens), UniversalId::Type_SoundGen);
|
||||
addModel (new IdTable (&mMagicEffects), UniversalId::Type_MagicEffect);
|
||||
addModel (new IdTable (&mLand), UniversalId::Type_Land);
|
||||
addModel (new IdTable (&mLandTextures), UniversalId::Type_LandTexture);
|
||||
addModel (new LandTextureIdTable (&mLandTextures), UniversalId::Type_LandTexture);
|
||||
addModel (new IdTree (&mPathgrids, &mPathgrids), UniversalId::Type_Pathgrid);
|
||||
addModel (new IdTable (&mStartScripts), UniversalId::Type_StartScript);
|
||||
addModel (new IdTree (&mReferenceables, &mReferenceables, IdTable::Feature_Preview),
|
||||
|
|
|
@ -281,3 +281,34 @@ CSMWorld::CollectionBase *CSMWorld::IdTable::idCollection() const
|
|||
{
|
||||
return mIdCollection;
|
||||
}
|
||||
|
||||
CSMWorld::LandTextureIdTable::LandTextureIdTable(CollectionBase* idCollection, unsigned int features)
|
||||
: IdTable(idCollection, features)
|
||||
{
|
||||
}
|
||||
|
||||
QVariant CSMWorld::LandTextureIdTable::data(const QModelIndex& index, int role) const
|
||||
{
|
||||
if (role==Qt::EditRole && !idCollection()->getRecord(index.row()).isModified())
|
||||
return QVariant();
|
||||
|
||||
return IdTable::data(index, role);
|
||||
}
|
||||
|
||||
bool CSMWorld::LandTextureIdTable::setData(const QModelIndex& index, const QVariant& value, int role)
|
||||
{
|
||||
if (!idCollection()->getRecord(index.row()).isModified())
|
||||
return false;
|
||||
else
|
||||
return IdTable::setData(index, value, role);
|
||||
}
|
||||
|
||||
Qt::ItemFlags CSMWorld::LandTextureIdTable::flags(const QModelIndex& index) const
|
||||
{
|
||||
Qt::ItemFlags flags = IdTable::flags(index);
|
||||
|
||||
if (!idCollection()->getRecord(index.row()).isModified())
|
||||
flags &= ~Qt::ItemIsEditable;
|
||||
|
||||
return flags;
|
||||
}
|
||||
|
|
|
@ -93,6 +93,21 @@ namespace CSMWorld
|
|||
|
||||
virtual CollectionBase *idCollection() const;
|
||||
};
|
||||
|
||||
/// An IdTable customized to handle the more unique needs of LandTextureId's which behave
|
||||
/// differently from other records.
|
||||
class LandTextureIdTable : public IdTable
|
||||
{
|
||||
public:
|
||||
|
||||
LandTextureIdTable(CollectionBase* idCollection, unsigned int features=0);
|
||||
|
||||
QVariant data(const QModelIndex& index, int role=Qt::DisplayRole) const override;
|
||||
|
||||
bool setData(const QModelIndex& index, const QVariant& value, int role) override;
|
||||
|
||||
Qt::ItemFlags flags (const QModelIndex & index) const override;
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
#include "landtexture.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <components/esm/esmreader.hpp>
|
||||
|
||||
namespace CSMWorld
|
||||
|
@ -11,4 +13,19 @@ namespace CSMWorld
|
|||
mPluginIndex = esm.getIndex();
|
||||
}
|
||||
|
||||
std::string LandTexture::createUniqueRecordId(int plugin, int index)
|
||||
{
|
||||
return 'L' + std::to_string(plugin) + '#' + std::to_string(index);
|
||||
}
|
||||
|
||||
void LandTexture::parseUniqueRecordId(const std::string& id, int& plugin, int& index)
|
||||
{
|
||||
size_t middle = id.find('#');
|
||||
|
||||
if (middle == std::string::npos || id[0] != 'L')
|
||||
throw std::runtime_error("Invalid LandTexture ID");
|
||||
|
||||
plugin = std::stoi(id.substr(1,middle-1));
|
||||
index = std::stoi(id.substr(middle+1));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,11 @@ namespace CSMWorld
|
|||
int mPluginIndex;
|
||||
|
||||
void load (ESM::ESMReader &esm, bool &isDeleted);
|
||||
|
||||
/// Returns a string identifier that will be unique to any LandTexture.
|
||||
static std::string createUniqueRecordId(int plugin, int index);
|
||||
/// Deconstructs a unique string identifier into plugin and index.
|
||||
static void parseUniqueRecordId(const std::string& id, int& plugin, int& index);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
106
apps/opencs/view/world/landtexturecreator.cpp
Normal file
106
apps/opencs/view/world/landtexturecreator.cpp
Normal file
|
@ -0,0 +1,106 @@
|
|||
#include "landtexturecreator.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
|
||||
#include <QIntValidator>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
|
||||
#include "../../model/world/commands.hpp"
|
||||
#include "../../model/world/idtable.hpp"
|
||||
#include "../../model/world/landtexture.hpp"
|
||||
|
||||
namespace CSVWorld
|
||||
{
|
||||
LandTextureCreator::LandTextureCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id)
|
||||
: GenericCreator(data, undoStack, id)
|
||||
{
|
||||
// One index is reserved for a default texture
|
||||
const size_t MaxIndex = std::numeric_limits<uint16_t>::max() - 1;
|
||||
|
||||
setManualEditing(false);
|
||||
|
||||
QLabel* nameLabel = new QLabel("Name");
|
||||
insertBeforeButtons(nameLabel, false);
|
||||
|
||||
mNameEdit = new QLineEdit(this);
|
||||
insertBeforeButtons(mNameEdit, true);
|
||||
|
||||
QLabel* indexLabel = new QLabel("Index");
|
||||
insertBeforeButtons(indexLabel, false);
|
||||
|
||||
QIntValidator* indexValidator = new QIntValidator(0, MaxIndex, this);
|
||||
|
||||
mIndexEdit = new QLineEdit(this);
|
||||
mIndexEdit->setValidator(indexValidator);
|
||||
insertBeforeButtons(mIndexEdit, true);
|
||||
|
||||
connect(mNameEdit, SIGNAL(textChanged(const QString&)), this, SLOT(nameChanged(const QString&)));
|
||||
connect(mIndexEdit, SIGNAL(textChanged(const QString&)), this, SLOT(indexChanged(const QString&)));
|
||||
}
|
||||
|
||||
void LandTextureCreator::cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type)
|
||||
{
|
||||
GenericCreator::cloneMode(originId, type);
|
||||
|
||||
CSMWorld::IdTable& table = dynamic_cast<CSMWorld::IdTable&>(*getData().getTableModel(getCollectionId()));
|
||||
|
||||
int column = table.findColumnIndex(CSMWorld::Columns::ColumnId_TextureHandle);
|
||||
mNameEdit->setText((table.data(table.getModelIndex(originId, column)).toString()));
|
||||
|
||||
column = table.findColumnIndex(CSMWorld::Columns::ColumnId_TextureIndex);
|
||||
mIndexEdit->setText((table.data(table.getModelIndex(originId, column)).toString()));
|
||||
}
|
||||
|
||||
void LandTextureCreator::focus()
|
||||
{
|
||||
mIndexEdit->setFocus();
|
||||
}
|
||||
|
||||
void LandTextureCreator::reset()
|
||||
{
|
||||
GenericCreator::reset();
|
||||
mNameEdit->setText("");
|
||||
mIndexEdit->setText("");
|
||||
}
|
||||
|
||||
std::string LandTextureCreator::getErrors() const
|
||||
{
|
||||
std::string id = getId();
|
||||
|
||||
// TODO empty index edit?
|
||||
if (getData().getLandTextures().searchId(getId()) >= 0)
|
||||
{
|
||||
return "Index is already in use";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
void LandTextureCreator::configureCreateCommand(CSMWorld::CreateCommand& command) const
|
||||
{
|
||||
GenericCreator::configureCreateCommand(command);
|
||||
|
||||
CSMWorld::IdTable& table = dynamic_cast<CSMWorld::IdTable&>(*getData().getTableModel(getCollectionId()));
|
||||
int column = table.findColumnIndex(CSMWorld::Columns::ColumnId_TextureHandle);
|
||||
command.addValue(column, mName.c_str());
|
||||
}
|
||||
|
||||
std::string LandTextureCreator::getId() const
|
||||
{
|
||||
return CSMWorld::LandTexture::createUniqueRecordId(0, mIndex);
|
||||
}
|
||||
|
||||
void LandTextureCreator::nameChanged(const QString& value)
|
||||
{
|
||||
mName = value.toUtf8().constData();
|
||||
update();
|
||||
}
|
||||
|
||||
void LandTextureCreator::indexChanged(const QString& value)
|
||||
{
|
||||
mIndex = value.toInt();
|
||||
update();
|
||||
}
|
||||
}
|
49
apps/opencs/view/world/landtexturecreator.hpp
Normal file
49
apps/opencs/view/world/landtexturecreator.hpp
Normal file
|
@ -0,0 +1,49 @@
|
|||
#ifndef CSV_WORLD_LANDTEXTURECREATOR_H
|
||||
#define CSV_WORLD_LANDTEXTURECREATOR_H
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "genericcreator.hpp"
|
||||
|
||||
class QLineEdit;
|
||||
|
||||
namespace CSVWorld
|
||||
{
|
||||
class LandTextureCreator : public GenericCreator
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
|
||||
LandTextureCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id);
|
||||
|
||||
void cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) override;
|
||||
|
||||
void focus() override;
|
||||
|
||||
void reset() override;
|
||||
|
||||
std::string getErrors() const override;
|
||||
|
||||
protected:
|
||||
|
||||
void configureCreateCommand(CSMWorld::CreateCommand& command) const override;
|
||||
|
||||
std::string getId() const override;
|
||||
|
||||
private slots:
|
||||
|
||||
void nameChanged(const QString& val);
|
||||
void indexChanged(const QString& val);
|
||||
|
||||
private:
|
||||
|
||||
QLineEdit* mNameEdit;
|
||||
QLineEdit* mIndexEdit;
|
||||
|
||||
std::string mName;
|
||||
int mIndex;
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
|
@ -18,6 +18,7 @@
|
|||
#include "pathgridcreator.hpp"
|
||||
#include "previewsubview.hpp"
|
||||
#include "bodypartcreator.hpp"
|
||||
#include "landtexturecreator.hpp"
|
||||
|
||||
void CSVWorld::addSubViewFactories (CSVDoc::SubViewFactoryManager& manager)
|
||||
{
|
||||
|
@ -43,8 +44,6 @@ void CSVWorld::addSubViewFactories (CSVDoc::SubViewFactoryManager& manager)
|
|||
CSMWorld::UniversalId::Type_Spells,
|
||||
CSMWorld::UniversalId::Type_Enchantments,
|
||||
CSMWorld::UniversalId::Type_SoundGens,
|
||||
CSMWorld::UniversalId::Type_Lands,
|
||||
CSMWorld::UniversalId::Type_LandTextures,
|
||||
|
||||
CSMWorld::UniversalId::Type_None // end marker
|
||||
};
|
||||
|
@ -83,6 +82,12 @@ void CSVWorld::addSubViewFactories (CSVDoc::SubViewFactoryManager& manager)
|
|||
manager.add (CSMWorld::UniversalId::Type_Pathgrids,
|
||||
new CSVDoc::SubViewFactoryWithCreator<TableSubView, PathgridCreatorFactory>);
|
||||
|
||||
manager.add (CSMWorld::UniversalId::Type_Lands,
|
||||
new CSVDoc::SubViewFactoryWithCreator<TableSubView, CreatorFactory<GenericCreator> >);
|
||||
|
||||
manager.add (CSMWorld::UniversalId::Type_LandTextures,
|
||||
new CSVDoc::SubViewFactoryWithCreator<TableSubView, CreatorFactory<LandTextureCreator> >);
|
||||
|
||||
manager.add (CSMWorld::UniversalId::Type_Globals,
|
||||
new CSVDoc::SubViewFactoryWithCreator<TableSubView, CreatorFactory<GlobalCreator> >);
|
||||
|
||||
|
@ -183,6 +188,12 @@ void CSVWorld::addSubViewFactories (CSVDoc::SubViewFactoryManager& manager)
|
|||
manager.add (CSMWorld::UniversalId::Type_Pathgrid,
|
||||
new CSVDoc::SubViewFactoryWithCreator<DialogueSubView, PathgridCreatorFactory> (false));
|
||||
|
||||
manager.add (CSMWorld::UniversalId::Type_Land,
|
||||
new CSVDoc::SubViewFactoryWithCreator<DialogueSubView, CreatorFactory<GenericCreator> >(false));
|
||||
|
||||
manager.add (CSMWorld::UniversalId::Type_LandTexture,
|
||||
new CSVDoc::SubViewFactoryWithCreator<DialogueSubView, CreatorFactory<LandTextureCreator> >(false));
|
||||
|
||||
manager.add (CSMWorld::UniversalId::Type_DebugProfile,
|
||||
new CSVDoc::SubViewFactoryWithCreator<DialogueSubView, CreatorFactory<GenericCreator, CSMWorld::Scope_Project | CSMWorld::Scope_Session> > (false));
|
||||
|
||||
|
|
|
@ -59,7 +59,7 @@ namespace ESM
|
|||
|
||||
void LandTexture::blank()
|
||||
{
|
||||
mId.clear();
|
||||
mTexture.clear();
|
||||
mIndex = -1;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,14 +31,15 @@ struct LandTexture
|
|||
/// Return a string descriptor for this record type. Currently used for debugging / error logs only.
|
||||
static std::string getRecordType() { return "LandTexture"; }
|
||||
|
||||
// mId is merely a user friendly name for the texture in the editor.
|
||||
std::string mId, mTexture;
|
||||
int mIndex;
|
||||
|
||||
void load(ESMReader &esm, bool &isDeleted);
|
||||
void save(ESMWriter &esm, bool isDeleted = false) const;
|
||||
|
||||
/// Sets the record to the default state. Does not touch the index. Does touch mID.
|
||||
void blank();
|
||||
///< Set record to default state (does not touch the ID).
|
||||
};
|
||||
}
|
||||
#endif
|
||||
|
|
Loading…
Reference in a new issue