diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index d8ca6c409..66c446bae 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -18,7 +18,7 @@ opencs_hdrs_noqt (model/doc opencs_units (model/world - idtable idtableproxymodel + idtable idtableproxymodel regionmap ) @@ -57,7 +57,7 @@ opencs_hdrs_noqt (view/doc opencs_units (view/world - table tablesubview scriptsubview util + table tablesubview scriptsubview util regionmapsubview ) opencs_units_noqt (view/world diff --git a/apps/opencs/model/world/data.cpp b/apps/opencs/model/world/data.cpp index 392175442..d54b3ac16 100644 --- a/apps/opencs/model/world/data.cpp +++ b/apps/opencs/model/world/data.cpp @@ -11,6 +11,7 @@ #include "idtable.hpp" #include "columns.hpp" +#include "regionmap.hpp" void CSMWorld::Data::addModel (QAbstractItemModel *model, UniversalId::Type type1, UniversalId::Type type2) @@ -304,7 +305,20 @@ QAbstractItemModel *CSMWorld::Data::getTableModel (const UniversalId& id) std::map::iterator iter = mModelIndex.find (id.getType()); if (iter==mModelIndex.end()) + { + // try creating missing (secondary) tables on the fly + // + // Note: We create these tables here so we don't have to deal with them during load/initial + // construction of the ESX data where no update signals are available. + if (id.getType()==UniversalId::Type_RegionMap) + { + RegionMap *table = 0; + addModel (table = new RegionMap (*this), UniversalId::Type_RegionMap, + UniversalId::Type_None); + return table; + } throw std::logic_error ("No table model available for " + id.toString()); + } return iter->second; } diff --git a/apps/opencs/model/world/regionmap.cpp b/apps/opencs/model/world/regionmap.cpp new file mode 100644 index 000000000..697c5f450 --- /dev/null +++ b/apps/opencs/model/world/regionmap.cpp @@ -0,0 +1,513 @@ + +#include "regionmap.hpp" + +#include + +#include + +#include + +#include "data.hpp" +#include "universalid.hpp" + +CSMWorld::RegionMap::CellDescription::CellDescription() : mDeleted (false) {} + +CSMWorld::RegionMap::CellDescription::CellDescription (const Record& cell) +{ + const Cell& cell2 = cell.get(); + + if (!cell2.isExterior()) + throw std::logic_error ("Interior cell in region map"); + + mDeleted = cell.isDeleted(); + + mRegion = cell2.mRegion; + mName = cell2.mName; +} + +CSMWorld::RegionMap::CellIndex CSMWorld::RegionMap::getIndex (const QModelIndex& index) const +{ + return CellIndex (index.column()+mMin.first, + (mMax.second-mMin.second - index.row()-1)+mMin.second); +} + +QModelIndex CSMWorld::RegionMap::getIndex (const CellIndex& index) const +{ + // I hate you, Qt API naming scheme! + return QAbstractTableModel::index (mMax.second-mMin.second - (index.second-mMin.second)-1, + index.first-mMin.first); +} + +void CSMWorld::RegionMap::buildRegions() +{ + const IdCollection& regions = mData.getRegions(); + + int size = regions.getSize(); + + for (int i=0; i& region = regions.getRecord (i); + + if (!region.isDeleted()) + mColours.insert (std::make_pair (Misc::StringUtils::lowerCase (region.get().mId), + region.get().mMapColor)); + } +} + +void CSMWorld::RegionMap::buildMap() +{ + const IdCollection& cells = mData.getCells(); + + int size = cells.getSize(); + + for (int i=0; i& cell = cells.getRecord (i); + + const Cell& cell2 = cell.get(); + + if (cell2.isExterior()) + { + CellDescription description (cell); + + CellIndex index (cell2.mData.mX, cell2.mData.mY); + + mMap.insert (std::make_pair (index, description)); + } + } + + std::pair mapSize = getSize(); + + mMin = mapSize.first; + mMax = mapSize.second; +} + +void CSMWorld::RegionMap::addCell (const CellIndex& index, const CellDescription& description) +{ + std::map::iterator cell = mMap.find (index); + + if (cell!=mMap.end()) + { + cell->second = description; + } + else + { + updateSize(); + + mMap.insert (std::make_pair (index, description)); + } + + QModelIndex index2 = getIndex (index); + + dataChanged (index2, index2); +} + +void CSMWorld::RegionMap::addCells (int start, int end) +{ + const IdCollection& cells = mData.getCells(); + + for (int i=start; i<=end; ++i) + { + const Record& cell = cells.getRecord (i); + + const Cell& cell2 = cell.get(); + + if (cell2.isExterior()) + { + CellIndex index (cell2.mData.mX, cell2.mData.mY); + + CellDescription description (cell); + + addCell (index, description); + } + } +} + +void CSMWorld::RegionMap::removeCell (const CellIndex& index) +{ + std::map::iterator cell = mMap.find (index); + + if (cell!=mMap.end()) + { + mMap.erase (cell); + + QModelIndex index2 = getIndex (index); + + dataChanged (index2, index2); + + updateSize(); + } +} + +void CSMWorld::RegionMap::addRegion (const std::string& region, unsigned int colour) +{ + mColours[Misc::StringUtils::lowerCase (region)] = colour; +} + +void CSMWorld::RegionMap::removeRegion (const std::string& region) +{ + std::map::iterator iter ( + mColours.find (Misc::StringUtils::lowerCase (region))); + + if (iter!=mColours.end()) + mColours.erase (iter); +} + +void CSMWorld::RegionMap::updateRegions (const std::vector& regions) +{ + std::vector regions2 (regions); + + std::for_each (regions2.begin(), regions2.end(), &Misc::StringUtils::lowerCase); + std::sort (regions2.begin(), regions2.end()); + + for (std::map::const_iterator iter (mMap.begin()); + iter!=mMap.end(); ++iter) + { + if (!iter->second.mRegion.empty() && + std::find (regions2.begin(), regions2.end(), + Misc::StringUtils::lowerCase (iter->second.mRegion))!=regions2.end()) + { + QModelIndex index = getIndex (iter->first); + + dataChanged (index, index); + } + } +} + +void CSMWorld::RegionMap::updateSize() +{ + std::pair size = getSize(); + + { + int diff = size.first.first - mMin.first; + + if (diff<0) + { + beginInsertColumns (QModelIndex(), 0, -diff-1); + mMin.first = size.first.first; + endInsertColumns(); + } + else if (diff>0) + { + beginRemoveColumns (QModelIndex(), 0, diff-1); + mMin.first = size.first.first; + endRemoveColumns(); + } + } + + { + int diff = size.first.second - mMin.second; + + if (diff<0) + { + beginInsertRows (QModelIndex(), 0, -diff-1); + mMin.second = size.first.second; + endInsertRows(); + } + else if (diff>0) + { + beginRemoveRows (QModelIndex(), 0, diff-1); + mMin.second = size.first.second; + endRemoveRows(); + } + } + + { + int diff = size.second.first - mMax.first; + + if (diff>0) + { + int columns = columnCount(); + beginInsertColumns (QModelIndex(), columns, columns+diff-1); + mMax.first = size.second.first; + endInsertColumns(); + } + else if (diff<0) + { + int columns = columnCount(); + beginRemoveColumns (QModelIndex(), columns+diff, columns-1); + mMax.first = size.second.first; + endRemoveColumns(); + } + } + + { + int diff = size.second.second - mMax.second; + + if (diff>0) + { + int rows = rowCount(); + beginInsertRows (QModelIndex(), rows, rows+diff-1); + mMax.second = size.second.second; + endInsertRows(); + } + else if (diff<0) + { + int rows = rowCount(); + beginRemoveRows (QModelIndex(), rows+diff, rows-1); + mMax.second = size.second.second; + endRemoveRows(); + } + } +} + +std::pair CSMWorld::RegionMap::getSize() + const +{ + const IdCollection& cells = mData.getCells(); + + int size = cells.getSize(); + + CellIndex min (0, 0); + CellIndex max (0, 0); + + for (int i=0; i& cell = cells.getRecord (i); + + const Cell& cell2 = cell.get(); + + if (cell2.isExterior()) + { + CellIndex index (cell2.mData.mX, cell2.mData.mY); + + if (min==max) + { + min = index; + max = std::make_pair (min.first+1, min.second+1); + } + else + { + if (index.first=max.first) + max.first = index.first + 1; + + if (index.second=max.second) + max.second = index.second + 1; + } + } + } + + return std::make_pair (min, max); +} + +CSMWorld::RegionMap::RegionMap (Data& data) : mData (data) +{ + buildRegions(); + buildMap(); + + QAbstractItemModel *regions = data.getTableModel (UniversalId (UniversalId::Type_Regions)); + + connect (regions, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), + this, SLOT (regionsAboutToBeRemoved (const QModelIndex&, int, int))); + connect (regions, SIGNAL (rowsInserted (const QModelIndex&, int, int)), + this, SLOT (regionsInserted (const QModelIndex&, int, int))); + connect (regions, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), + this, SLOT (regionsChanged (const QModelIndex&, const QModelIndex&))); + + QAbstractItemModel *cells = data.getTableModel (UniversalId (UniversalId::Type_Cells)); + + connect (cells, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), + this, SLOT (cellsAboutToBeRemoved (const QModelIndex&, int, int))); + connect (cells, SIGNAL (rowsInserted (const QModelIndex&, int, int)), + this, SLOT (cellsInserted (const QModelIndex&, int, int))); + connect (cells, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), + this, SLOT (cellsChanged (const QModelIndex&, const QModelIndex&))); +} + +int CSMWorld::RegionMap::rowCount (const QModelIndex& parent) const +{ + if (parent.isValid()) + return 0; + + return mMax.second-mMin.second; +} + +int CSMWorld::RegionMap::columnCount (const QModelIndex& parent) const +{ + if (parent.isValid()) + return 0; + + return mMax.first-mMin.first; +} + +QVariant CSMWorld::RegionMap::data (const QModelIndex& index, int role) const +{ + if (role==Qt::SizeHintRole) + return QSize (16, 16); + + if (role==Qt::BackgroundRole) + { + /// \todo GUI class in non-GUI code. Needs to be addressed eventually. + + std::map::const_iterator cell = + mMap.find (getIndex (index)); + + if (cell!=mMap.end()) + { + if (cell->second.mDeleted) + return QBrush (Qt::red, Qt::DiagCrossPattern); + + std::map::const_iterator iter = + mColours.find (Misc::StringUtils::lowerCase (cell->second.mRegion)); + + if (iter!=mColours.end()) + return QBrush ( + QColor (iter->second>>24, (iter->second>>16) & 255, (iter->second>>8) & 255, + iter->second & 255)); + + if (cell->second.mRegion.empty()) + return QBrush (Qt::Dense6Pattern); // no region + + return QBrush (Qt::red, Qt::Dense6Pattern); // invalid region + } + + return QBrush (Qt::DiagCrossPattern); + } + + if (role==Qt::ToolTipRole) + { + CellIndex cellIndex = getIndex (index); + + std::ostringstream stream; + + stream << cellIndex.first << ", " << cellIndex.second; + + std::map::const_iterator cell = + mMap.find (cellIndex); + + if (cell!=mMap.end()) + { + if (!cell->second.mName.empty()) + stream << " " << cell->second.mName; + + if (cell->second.mDeleted) + stream << " (deleted)"; + + if (!cell->second.mRegion.empty()) + { + stream << "
"; + + std::map::const_iterator iter = + mColours.find (Misc::StringUtils::lowerCase (cell->second.mRegion)); + + if (iter!=mColours.end()) + stream << cell->second.mRegion; + else + stream << "" << cell->second.mRegion << ""; + } + } + else + stream << " (no cell)"; + + return QString::fromUtf8 (stream.str().c_str()); + } + + return QVariant(); +} + +Qt::ItemFlags CSMWorld::RegionMap::flags (const QModelIndex& index) const +{ + if (mMap.find (getIndex (index))!=mMap.end()) + return Qt::ItemIsSelectable | Qt::ItemIsEnabled; + + return 0; +} + +void CSMWorld::RegionMap::regionsAboutToBeRemoved (const QModelIndex& parent, int start, int end) +{ + std::vector update; + + const IdCollection& regions = mData.getRegions(); + + for (int i=start; i<=end; ++i) + { + const Record& region = regions.getRecord (i); + + update.push_back (region.get().mId); + + removeRegion (region.get().mId); + } + + updateRegions (update); +} + +void CSMWorld::RegionMap::regionsInserted (const QModelIndex& parent, int start, int end) +{ + std::vector update; + + const IdCollection& regions = mData.getRegions(); + + for (int i=start; i<=end; ++i) + { + const Record& region = regions.getRecord (i); + + if (!region.isDeleted()) + { + update.push_back (region.get().mId); + + addRegion (region.get().mId, region.get().mMapColor); + } + } + + updateRegions (update); +} + +void CSMWorld::RegionMap::regionsChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) +{ + // Note: At this point an additional check could be inserted to see if there is any change to the + // columns we are interested in. If not we can exit the function here and avoid all updating. + + std::vector update; + + const IdCollection& regions = mData.getRegions(); + + for (int i=topLeft.row(); i<=bottomRight.column(); ++i) + { + const Record& region = regions.getRecord (i); + + update.push_back (region.get().mId); + + if (!region.isDeleted()) + addRegion (region.get().mId, region.get().mMapColor); + else + removeRegion (region.get().mId); + } + + updateRegions (update); +} + +void CSMWorld::RegionMap::cellsAboutToBeRemoved (const QModelIndex& parent, int start, int end) +{ + const IdCollection& cells = mData.getCells(); + + for (int i=start; i<=end; ++i) + { + const Record& cell = cells.getRecord (i); + + const Cell& cell2 = cell.get(); + + if (cell2.isExterior()) + { + CellIndex index (cell2.mData.mX, cell2.mData.mY); + + removeCell (index); + } + } +} + +void CSMWorld::RegionMap::cellsInserted (const QModelIndex& parent, int start, int end) +{ + addCells (start, end); +} + +void CSMWorld::RegionMap::cellsChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) +{ + // Note: At this point an additional check could be inserted to see if there is any change to the + // columns we are interested in. If not we can exit the function here and avoid all updating. + + addCells (topLeft.row(), bottomRight.row()); +} \ No newline at end of file diff --git a/apps/opencs/model/world/regionmap.hpp b/apps/opencs/model/world/regionmap.hpp new file mode 100644 index 000000000..7fb89f20a --- /dev/null +++ b/apps/opencs/model/world/regionmap.hpp @@ -0,0 +1,111 @@ +#ifndef CSM_WOLRD_REGIONMAP_H +#define CSM_WOLRD_REGIONMAP_H + +#include +#include +#include + +#include + +#include "record.hpp" +#include "cell.hpp" + +namespace CSMWorld +{ + class Data; + + /// \brief Model for the region map + /// + /// This class does not holds any record data (other than for the purpose of buffering). + class RegionMap : public QAbstractTableModel + { + Q_OBJECT + + public: + + typedef std::pair CellIndex; + + private: + + struct CellDescription + { + bool mDeleted; + std::string mRegion; + std::string mName; + + CellDescription(); + + CellDescription (const Record& cell); + }; + + Data& mData; + std::map mMap; + CellIndex mMin; ///< inclusive + CellIndex mMax; ///< exclusive + std::map mColours; ///< region ID, colour (RGBA) + + CellIndex getIndex (const QModelIndex& index) const; + ///< Translates a Qt model index into a cell index (which can contain negative components) + + QModelIndex getIndex (const CellIndex& index) const; + + void buildRegions(); + + void buildMap(); + + void addCell (const CellIndex& index, const CellDescription& description); + ///< May be called on a cell that is already in the map (in which case an update is + // performed) + + void addCells (int start, int end); + + void removeCell (const CellIndex& index); + ///< May be called on a cell that is not in the map (in which case the call is ignored) + + void addRegion (const std::string& region, unsigned int colour); + ///< May be called on a region that is already listed (in which case an update is + /// performed) + /// + /// \note This function does not update the region map. + + void removeRegion (const std::string& region); + ///< May be called on a region that is not listed (in which case the call is ignored) + /// + /// \note This function does not update the region map. + + void updateRegions (const std::vector& regions); + ///< Update cells affected by the listed regions + + void updateSize(); + + std::pair getSize() const; + + public: + + RegionMap (Data& data); + + virtual int rowCount (const QModelIndex& parent = QModelIndex()) const; + + virtual int columnCount (const QModelIndex& parent = QModelIndex()) const; + + virtual QVariant data (const QModelIndex& index, int role = Qt::DisplayRole) const; + + virtual Qt::ItemFlags flags (const QModelIndex& index) const; + + private slots: + + void regionsAboutToBeRemoved (const QModelIndex& parent, int start, int end); + + void regionsInserted (const QModelIndex& parent, int start, int end); + + void regionsChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); + + void cellsAboutToBeRemoved (const QModelIndex& parent, int start, int end); + + void cellsInserted (const QModelIndex& parent, int start, int end); + + void cellsChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); + }; +} + +#endif diff --git a/apps/opencs/model/world/universalid.cpp b/apps/opencs/model/world/universalid.cpp index ef32681a0..c66101950 100644 --- a/apps/opencs/model/world/universalid.cpp +++ b/apps/opencs/model/world/universalid.cpp @@ -33,6 +33,8 @@ namespace "Referenceables" }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_References, "References" }, + { CSMWorld::UniversalId::Class_NonRecord, CSMWorld::UniversalId::Type_RegionMap, + "Region Map" }, { CSMWorld::UniversalId::Class_None, CSMWorld::UniversalId::Type_None, 0 } // end marker }; diff --git a/apps/opencs/model/world/universalid.hpp b/apps/opencs/model/world/universalid.hpp index 9fae0a9f4..453da3ef3 100644 --- a/apps/opencs/model/world/universalid.hpp +++ b/apps/opencs/model/world/universalid.hpp @@ -81,7 +81,8 @@ namespace CSMWorld Type_Static, Type_Weapon, Type_References, - Type_Reference + Type_Reference, + Type_RegionMap }; private: diff --git a/apps/opencs/view/doc/view.cpp b/apps/opencs/view/doc/view.cpp index 43f068042..6425797a1 100644 --- a/apps/opencs/view/doc/view.cpp +++ b/apps/opencs/view/doc/view.cpp @@ -139,6 +139,10 @@ void CSVDoc::View::setupWorldMenu() QAction *references = new QAction (tr ("References"), this); connect (references, SIGNAL (triggered()), this, SLOT (addReferencesSubView())); world->addAction (references); + + QAction *regionMap = new QAction (tr ("Region Map"), this); + connect (regionMap, SIGNAL (triggered()), this, SLOT (addRegionMapSubView())); + world->addAction (regionMap); } void CSVDoc::View::setupUi() @@ -362,6 +366,11 @@ void CSVDoc::View::addReferencesSubView() addSubView (CSMWorld::UniversalId::Type_References); } +void CSVDoc::View::addRegionMapSubView() +{ + addSubView (CSMWorld::UniversalId::Type_RegionMap); +} + void CSVDoc::View::abortOperation (int type) { mDocument->abortOperation (type); diff --git a/apps/opencs/view/doc/view.hpp b/apps/opencs/view/doc/view.hpp index b3c4ae22a..ff81f8223 100644 --- a/apps/opencs/view/doc/view.hpp +++ b/apps/opencs/view/doc/view.hpp @@ -151,6 +151,8 @@ namespace CSVDoc void addReferencesSubView(); + void addRegionMapSubView(); + void showUserSettings(); }; } diff --git a/apps/opencs/view/world/regionmapsubview.cpp b/apps/opencs/view/world/regionmapsubview.cpp new file mode 100644 index 000000000..b82c1afb5 --- /dev/null +++ b/apps/opencs/view/world/regionmapsubview.cpp @@ -0,0 +1,29 @@ + +#include "regionmapsubview.hpp" + +#include +#include + +CSVWorld::RegionMapSubView::RegionMapSubView (CSMWorld::UniversalId universalId, + CSMDoc::Document& document) +: CSVDoc::SubView (universalId) +{ + mTable = new QTableView (this); + + mTable->verticalHeader()->hide(); + mTable->horizontalHeader()->hide(); + + mTable->setSelectionMode (QAbstractItemView::ExtendedSelection); + + mTable->setModel (document.getData().getTableModel (universalId)); + + mTable->resizeColumnsToContents(); + mTable->resizeRowsToContents(); + + setWidget (mTable); +} + +void CSVWorld::RegionMapSubView::setEditLock (bool locked) +{ + +} \ No newline at end of file diff --git a/apps/opencs/view/world/regionmapsubview.hpp b/apps/opencs/view/world/regionmapsubview.hpp new file mode 100644 index 000000000..1655107af --- /dev/null +++ b/apps/opencs/view/world/regionmapsubview.hpp @@ -0,0 +1,27 @@ +#ifndef CSV_WORLD_REGIONMAPSUBVIEW_H +#define CSV_WORLD_REGIONMAPSUBVIEW_H + +#include "../doc/subview.hpp" + +class QTableView; + +namespace CSMDoc +{ + class Document; +} + +namespace CSVWorld +{ + class RegionMapSubView : public CSVDoc::SubView + { + QTableView *mTable; + + public: + + RegionMapSubView (CSMWorld::UniversalId universalId, CSMDoc::Document& document); + + virtual void setEditLock (bool locked); + }; +} + +#endif diff --git a/apps/opencs/view/world/subviews.cpp b/apps/opencs/view/world/subviews.cpp index cd98ed4e0..1c06ea2f6 100644 --- a/apps/opencs/view/world/subviews.cpp +++ b/apps/opencs/view/world/subviews.cpp @@ -6,6 +6,7 @@ #include "tablesubview.hpp" #include "dialoguesubview.hpp" #include "scriptsubview.hpp" +#include "regionmapsubview.hpp" void CSVWorld::addSubViewFactories (CSVDoc::SubViewFactoryManager& manager) { @@ -38,6 +39,8 @@ void CSVWorld::addSubViewFactories (CSVDoc::SubViewFactoryManager& manager) manager.add (CSMWorld::UniversalId::Type_Script, new CSVDoc::SubViewFactory); + manager.add (CSMWorld::UniversalId::Type_RegionMap, new CSVDoc::SubViewFactory); + // manager.add (CSMWorld::UniversalId::Type_Global, // new CSVDoc::SubViewFactoryWithCreateFlag (true)); } \ No newline at end of file