Fix doc path
[opentx.git] / companion / src / modelslist.cpp
blobecc7dcec7cb15b31c26d9966c4b3301e27a26911
1 /*
2 * Copyright (C) OpenTX
4 * Based on code named
5 * th9x - http://code.google.com/p/th9x
6 * er9x - http://code.google.com/p/er9x
7 * gruvin9x - http://code.google.com/p/gruvin9x
9 * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
11 * This program is free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License version 2 as
13 * published by the Free Software Foundation.
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU General Public License for more details.
21 #include "modelslist.h"
23 TreeItem::TreeItem(const QVector<QVariant> & itemData):
24 itemData(itemData),
25 parentItem(NULL),
26 categoryIndex(-1),
27 modelIndex(-1),
28 flags(0)
32 TreeItem::TreeItem(TreeItem * parent, int categoryIndex, int modelIndex):
33 TreeItem(QVector<QVariant>(parent->columnCount()))
35 setParent(parent);
36 setCategoryIndex(categoryIndex);
37 setModelIndex(modelIndex);
40 TreeItem::~TreeItem()
42 qDeleteAll(childItems);
45 TreeItem * TreeItem::child(int number)
47 return childItems.value(number);
50 int TreeItem::childCount() const
52 return childItems.count();
55 int TreeItem::childNumber() const
57 if (parentItem)
58 return parentItem->childItems.indexOf(const_cast<TreeItem*>(this));
60 return 0;
63 int TreeItem::columnCount() const
65 return itemData.count();
68 QVariant TreeItem::data(int column) const
70 return itemData.value(column);
73 TreeItem *TreeItem::insertChild(const int row, int categoryIndex, int modelIndex)
75 TreeItem * item = new TreeItem(this, categoryIndex, modelIndex);
76 childItems.insert(row, item);
77 return item;
80 TreeItem * TreeItem::appendChild(int categoryIndex, int modelIndex)
82 return insertChild(childItems.size(), categoryIndex, modelIndex);
85 bool TreeItem::removeChildren(int position, int count)
87 if (position < 0 || position + count > childItems.size())
88 return false;
90 for (int row = 0; row < count; ++row)
91 delete childItems.takeAt(position);
93 return true;
96 bool TreeItem::insertChildren(int row, int count)
98 for (int i=0; i < count; ++i) {
99 insertChild(row + i, -1, -1);
101 return true;
104 bool TreeItem::setData(int column, const QVariant & value)
106 if (column < 0 || column >= itemData.size())
107 return false;
109 itemData[column] = value;
110 return true;
113 void TreeItem::setFlag(const quint16 & flag, const bool on)
115 if (on)
116 flags |= flag;
117 else
118 flags &= ~flag;
121 bool TreeItem::isCategory() const
123 return (modelIndex < 0 && categoryIndex > -1);
126 bool TreeItem::isModel() const
128 return (modelIndex > -1);
133 * TreeModel
136 TreeModel::TreeModel(RadioData * radioData, QObject * parent):
137 QAbstractItemModel(parent),
138 radioData(radioData),
139 availableEEpromSize(-1)
141 Board::Type board = getCurrentBoard();
142 QVector<QVariant> labels;
143 if (!getCurrentFirmware()->getCapability(Capability::HasModelCategories))
144 labels << tr("Index");
145 labels << tr("Name");
146 if (!(IS_HORUS(board) || IS_SKY9X(board))) {
147 labels << tr("Size");
149 rootItem = new TreeItem(labels);
150 // uniqueId and version for drag/drop operations (see encodeHeaderData())
151 mimeHeaderData.instanceId = QUuid::createUuid();
152 mimeHeaderData.dataVersion = 1;
154 refresh();
155 //connect(this, &QAbstractItemModel::rowsAboutToBeRemoved, this, &TreeModel::onRowsAboutToBeRemoved);
156 connect(this, &QAbstractItemModel::rowsRemoved, this, &TreeModel::onRowsRemoved);
159 TreeModel::~TreeModel()
161 delete rootItem;
164 int TreeModel::columnCount(const QModelIndex & /* parent */) const
166 return rootItem->columnCount();
169 QVariant TreeModel::data(const QModelIndex & index, int role) const
171 if (!index.isValid())
172 return QVariant();
174 TreeItem * item = getItem(index);
176 if (role == Qt::DisplayRole || role == Qt::EditRole) {
177 return item->data(index.column());
180 if (role == Qt::FontRole && item->isModel() && item->getModelIndex() == (int)radioData->generalSettings.currModelIndex) {
181 QFont font;
182 font.setBold(true);
183 return font;
186 if (role == Qt::ForegroundRole && (item->getFlags() & TreeItem::MarkedForCut)) {
187 return QPalette().brush(QPalette::Disabled, QPalette::Text);
190 return QVariant();
193 Qt::ItemFlags TreeModel::flags(const QModelIndex &index) const
195 Qt::ItemFlags f = Qt::ItemIsSelectable | Qt::ItemIsEnabled;
197 if (index.isValid()) {
198 if (getItem(index)->isCategory())
199 f |= Qt::ItemIsEditable;
200 else
201 f |= Qt::ItemIsDragEnabled; // TODO drag/drop categories
203 f |= Qt::ItemIsDropEnabled;
205 //qDebug() << f;
206 return f;
209 QVariant TreeModel::headerData(int section, Qt::Orientation orientation, int role) const
211 if (orientation == Qt::Horizontal && role == Qt::DisplayRole)
212 return rootItem->data(section);
214 return QVariant();
217 QModelIndex TreeModel::index(int row, int column, const QModelIndex & parent) const
219 if (parent.isValid() && parent.column() != 0)
220 return QModelIndex();
222 TreeItem * parentItem = getItem(parent);
223 TreeItem * childItem = parentItem->child(row);
224 if (childItem)
225 return createIndex(row, column, childItem);
226 else
227 return QModelIndex();
230 QModelIndex TreeModel::parent(const QModelIndex & index) const
232 if (!index.isValid())
233 return QModelIndex();
235 TreeItem * childItem = getItem(index);
236 TreeItem * parentItem = childItem->parent();
238 if (parentItem == rootItem)
239 return QModelIndex();
241 return createIndex(parentItem->childNumber(), 0, parentItem);
244 bool TreeModel::removeRows(int position, int rows, const QModelIndex & parent)
246 TreeItem * parentItem = getItem(parent);
247 if (!parentItem)
248 return false;
250 bool success = true;
251 if (position >= 0 && rows > 0) {
252 beginRemoveRows(parent, position, position + rows - 1);
253 success = parentItem->removeChildren(position, rows);
254 endRemoveRows();
257 return success;
260 /* unused but possibly useful in future
261 bool TreeModel::insertRows(int row, int count, const QModelIndex & parent)
263 TreeItem * parentItem = getItem(parent);
264 if (!parentItem)
265 return false;
267 bool success = true;
268 if (row >= 0 && count > 0) {
269 beginInsertRows(parent, row, row + count - 1);
270 success = parentItem->insertChildren(row, count);
271 endInsertRows();
274 return success;
275 } */
277 int TreeModel::rowCount(const QModelIndex &parent) const
279 TreeItem * parentItem = getItem(parent);
280 return parentItem->childCount();
283 bool TreeModel::setData(const QModelIndex & index, const QVariant & value, int role)
285 if (role != Qt::EditRole)
286 return false;
288 if (!index.isValid())
289 return false;
291 TreeItem * item = getItem(index);
292 bool result = item->setData(index.column(), value);
294 if (result) {
295 emit dataChanged(index, index);
298 return result;
301 QStringList TreeModel::mimeTypes() const
303 QStringList types;
304 types << "application/x-companion-modeldata";
305 types << "application/x-companion-generaldata";
306 //type << "application/x-companion-radiodata-header"; // supported but not advertised, must be in conjunction with one of the above
307 return types;
310 Qt::DropActions TreeModel::supportedDropActions() const
312 return Qt::CopyAction | Qt::MoveAction;
315 Qt::DropActions TreeModel::supportedDragActions() const
317 return Qt::CopyAction | Qt::MoveAction;
320 // This method encodes all the data on default drag operation, including general radio settings. This is useful for eg. Compare dialog/model printer.
321 QMimeData * TreeModel::mimeData(const QModelIndexList & indexes) const
323 QMimeData * mimeData = new QMimeData();
324 getModelsMimeData(indexes, mimeData);
325 getGeneralMimeData(mimeData);
326 getHeaderMimeData(mimeData);
327 return mimeData;
330 bool TreeModel::canDropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) const
332 Q_UNUSED(action);
333 //qDebug() << action << row << column << parent.row();
335 // we do not accept dropped general settings right now (user must copy/paste those)
336 if (hasHeaderMimeData(data) && hasModelsMimeData(data) && (row > -1 || parent.isValid()))
337 return true;
339 return false;
342 bool TreeModel::dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent)
344 if (!canDropMimeData(data, action, row, column, parent))
345 return false;
347 if (action == Qt::IgnoreAction)
348 return true;
350 QModelIndex idx;
351 bool isInsert = false;
352 if (row > -1) {
353 // dropped between rows (insert)
354 isInsert = true;
355 idx = index(row, column, parent);
357 else if (parent.isValid()) {
358 // was dropped on a row (overwrite)
359 idx = parent;
361 else {
362 // dropped who knows where, (shouldn't be here though due check in canDropMimeData())
363 return false;
365 //qDebug() << action << row << column << parent.row() << idx << idx.row() << hasOwnMimeData(data);
367 // Force drops from other file windows to be copy actions because we don't want to delete our models.
368 if (action == Qt::MoveAction && !hasOwnMimeData(data))
369 action = Qt::CopyAction;
371 // canDropMimeData() only accepts models
372 emit modelsDropped(data, idx, isInsert, (action == Qt::MoveAction));
374 return true;
377 QMimeData *TreeModel::getModelsMimeData(const QModelIndexList & indexes, QMimeData * mimeData) const
379 if (!mimeData)
380 mimeData = new QMimeData();
381 QByteArray mData;
382 encodeModelsData(indexes, &mData);
383 mimeData->setData("application/x-companion-modeldata", mData);
384 return mimeData;
387 QMimeData *TreeModel::getGeneralMimeData(QMimeData * mimeData) const
389 if (!mimeData)
390 mimeData = new QMimeData();
391 QByteArray mData;
392 encodeGeneralData(&mData);
393 mimeData->setData("application/x-companion-generaldata", mData);
394 return mimeData;
397 QMimeData *TreeModel::getHeaderMimeData(QMimeData * mimeData) const
399 if (!mimeData)
400 mimeData = new QMimeData();
401 QByteArray mData;
402 encodeHeaderData(&mData);
403 mimeData->setData("application/x-companion-radiodata-header", mData);
404 return mimeData;
407 QUuid TreeModel::getMimeDataSourceId(const QMimeData * mimeData) const
409 MimeHeaderData header;
410 decodeHeaderData(mimeData, &header);
411 return header.instanceId;
414 bool TreeModel::hasSupportedMimeData(const QMimeData * mimeData) const
416 foreach (const QString & mtype, mimeTypes()) {
417 if (mimeData->hasFormat(mtype))
418 return true;
420 return false;
423 bool TreeModel::hasModelsMimeData(const QMimeData * mimeData) const
425 return mimeData->hasFormat("application/x-companion-modeldata");
428 bool TreeModel::hasGenralMimeData(const QMimeData * mimeData) const
430 return mimeData->hasFormat("application/x-companion-generaldata");
433 bool TreeModel::hasHeaderMimeData(const QMimeData * mimeData) const
435 return mimeData->hasFormat("application/x-companion-radiodata-header");
438 // returns true if mime data origin was this data model (vs. from another file window)
439 bool TreeModel::hasOwnMimeData(const QMimeData * mimeData) const
441 return (getMimeDataSourceId(mimeData) == mimeHeaderData.instanceId);
444 void TreeModel::encodeModelsData(const QModelIndexList & indexes, QByteArray * data) const
446 foreach (const QModelIndex &index, indexes) {
447 if (index.isValid() && index.column() == 0) {
448 if (!getItem(index)->isCategory()) { // TODO: encode categoreis also
449 data->append('M');
450 data->append((char *)&radioData->models[getModelIndex(index)], sizeof(ModelData));
456 void TreeModel::encodeGeneralData(QByteArray * data) const
458 data->append('G');
459 data->append((char *)&radioData->generalSettings, sizeof(GeneralSettings));
462 void TreeModel::encodeHeaderData(QByteArray * data) const
464 // We use a unique ID representing this TreeModel instance (a unique file).
465 // This can be used eg. to detect cross-file drop operations.
466 QDataStream stream(data, QIODevice::WriteOnly);
467 stream << mimeHeaderData.dataVersion;
468 stream << mimeHeaderData.instanceId;
471 // static
472 bool TreeModel::decodeHeaderData(const QMimeData * mimeData, MimeHeaderData * header)
474 if (header && mimeData->hasFormat("application/x-companion-radiodata-header")) {
475 QByteArray data = mimeData->data("application/x-companion-radiodata-header");
476 QDataStream stream(&data, QIODevice::ReadOnly);
477 stream >> header->dataVersion >> header->instanceId;
478 return true;
480 return false;
483 // static
484 bool TreeModel::decodeMimeData(const QMimeData * mimeData, QVector<ModelData> * models, GeneralSettings * gs, bool * hasGenSet)
486 bool ret = false;
487 char * gData;
489 if (hasGenSet)
490 *hasGenSet = false;
492 if (models && mimeData->hasFormat("application/x-companion-modeldata")) {
493 QByteArray mdlData = mimeData->data("application/x-companion-modeldata");
494 gData = mdlData.data();
495 int size = 0;
496 while (size < mdlData.size()) {
497 char c = *gData++;
498 if (c != 'M')
499 break;
500 ModelData model(*((ModelData *)gData));
501 models->append(model);
502 gData += sizeof(ModelData);
503 size += sizeof(ModelData) + 1;
504 ret = true;
508 // General settings
509 if (gs && mimeData->hasFormat("application/x-companion-generaldata")) {
510 QByteArray genData = mimeData->data("application/x-companion-generaldata");
511 gData = genData.data();
512 char c = *gData++;
513 if (c == 'G') {
514 *gs = *((GeneralSettings *)gData);
515 ret = true;
516 if (hasGenSet)
517 *hasGenSet = true;
521 return ret;
524 // static
525 int TreeModel::countModelsInMimeData(const QMimeData * mimeData)
527 int ret = 0;
528 if (mimeData->hasFormat("application/x-companion-modeldata")) {
529 QByteArray mdlData = mimeData->data("application/x-companion-modeldata");
530 ret = mdlData.size() / (sizeof(ModelData) + 1);
532 return ret;
536 TreeItem * TreeModel::getItem(const QModelIndex & index) const
538 if (index.isValid()) {
539 TreeItem * item = static_cast<TreeItem *>(index.internalPointer());
540 if (item) {
541 return item;
544 return rootItem;
547 // recursive
548 QModelIndex TreeModel::getIndexForModel(const int modelIndex, QModelIndex parent)
550 for (int i=0; i < rowCount(parent); ++i) {
551 QModelIndex idx = index(i, 0, parent);
552 if (hasChildren(idx) && (idx = getIndexForModel(modelIndex, idx)).isValid())
553 return idx;
554 if (getItem(idx)->getModelIndex() == modelIndex)
555 return idx;
557 return QModelIndex();
560 QModelIndex TreeModel::getIndexForCategory(const int categoryIndex)
562 for (int i=0; i < rowCount(); ++i) {
563 if (getItem(index(i, 0))->getCategoryIndex() == categoryIndex)
564 return index(i, 0);
566 return QModelIndex();
569 int TreeModel::getAvailableEEpromSize()
571 return availableEEpromSize;
574 int TreeModel::getModelIndex(const QModelIndex & index) const
576 return getItem(index)->getModelIndex();
579 int TreeModel::getCategoryIndex(const QModelIndex & index) const
581 return getItem(index)->getCategoryIndex();
584 int TreeModel::rowNumber(const QModelIndex & index) const
586 return getItem(index)->childNumber();
589 bool TreeModel::isCategoryType(const QModelIndex & index) const
591 return index.isValid() && getItem(index)->isCategory();
594 bool TreeModel::isModelType(const QModelIndex & index) const
596 return index.isValid() && getItem(index)->isModel();
599 void TreeModel::markItemForCut(const QModelIndex & index, bool on)
601 if (index.isValid() && index.column() == 0)
602 getItem(index)->setFlag(TreeItem::MarkedForCut, on);
605 void TreeModel::markItemsForCut(const QModelIndexList & indexes, bool on)
607 foreach (const QModelIndex &index, indexes)
608 markItemForCut(index, on);
611 // onRowsAboutToBeRemoved could be a way to deal with models being drag-drop moved to another window/file.
612 // TreeModel detects these as removals and runs removeRows(), which deletes the Model indexes
613 // but not the actual models from the RadioData::models array.
614 // BUT this also runs when moving rows within our own tree, and if there is an error during the move,
615 // or the user cancels the operation, removeRows() is still called automatically somewhere inside QAbstractItemModel().
616 // If a solution could be found to this problem then we could enable DnD-moving models between file windows.
618 void TreeModel::onRowsAboutToBeRemoved(const QModelIndex & parent, int first, int last)
620 qDebug() << parent << first << last;
621 QVector<int> modelIndices;
622 for (int i=first; i <= last; ++i) {
623 modelIndices << getItem(index(i, 0, parent))->getModelIndex();
625 if (modelIndices.size())
626 emit modelsRemoved(modelIndices);
630 void TreeModel::onRowsRemoved(const QModelIndex & parent, int first, int last)
632 // This is a workaround to deal with models being DnD moved to another window/file or if user cancels a DnD move within our own.
633 // TreeModel detects these as removals and runs removeRows(), which deletes the Model indexes but not our actual models. See notes above.
634 //qDebug() << parent << first << last;
635 emit refreshRequested(); // request refresh from View because it may have it's own ideas
638 void TreeModel::refresh()
640 EEPROMInterface * eepromInterface = getCurrentEEpromInterface();
641 Board::Type board = eepromInterface->getBoard();
642 TreeItem * defaultCategoryItem = NULL;
643 bool hasCategories = getCurrentFirmware()->getCapability(Capability::HasModelCategories);
644 bool hasEepromSizeData = (rootItem->columnCount() > 2);
646 if (hasEepromSizeData) {
647 availableEEpromSize = Boards::getEEpromSize(board) - 64; // let's consider fat
648 availableEEpromSize -= 16 * ((eepromInterface->getSize(radioData->generalSettings) + 14) / 15);
651 this->blockSignals(true); // make sure onRowsRemoved is not triggered
652 removeRows(0, rowCount());
653 this->blockSignals(false);
655 if (hasCategories) {
656 for (unsigned i = 0; i < radioData->categories.size(); i++) {
657 TreeItem * current = rootItem->appendChild(i, -1);
658 current->setData(0, QString(radioData->categories[i].name));
662 for (unsigned i=0; i<radioData->models.size(); i++) {
663 ModelData & model = radioData->models[i];
664 int currentColumn = 0;
665 TreeItem * current = NULL;
667 model.modelIndex = i;
669 if (hasCategories) {
670 if (!model.isEmpty()) {
671 TreeItem * categoryItem;
672 // TODO category should be set to -1 if not Horus
673 if (model.category >= 0 && model.category < rootItem->childCount()) {
674 categoryItem = rootItem->child(model.category);
676 else {
677 model.category = 0;
678 if (!defaultCategoryItem) {
679 defaultCategoryItem = rootItem->appendChild(0, -1);
680 /*: Translators do NOT use accent for this, this is the default category name on Horus. */
681 defaultCategoryItem->setData(0, tr("Models"));
683 categoryItem = defaultCategoryItem;
685 current = categoryItem->appendChild(model.category, i);
688 else {
689 current = rootItem->appendChild(0, i);
690 current->setData(currentColumn++, QString().sprintf("%02d", i + 1));
693 if (!model.isEmpty() && current) {
694 QString modelName;
695 if (strlen(model.name) > 0) {
696 modelName = model.name;
698 else {
699 /*: Translators: do NOT use accents here, this is a default model name. */
700 modelName = tr("Model %1").arg(uint(i+1), 2, 10, QChar('0'));
702 current->setData(currentColumn++, modelName);
703 if (hasEepromSizeData) {
704 int size = eepromInterface->getSize(model);
705 current->setData(currentColumn++, QString().sprintf("%5d", size));
706 size = 16 * ((size + 14) / 15);
707 availableEEpromSize -= size;
708 if (i == radioData->generalSettings.currModelIndex) {
709 // Because we need this space for a TEMP model each time we have to write it again
710 availableEEpromSize -= size;
716 if (hasEepromSizeData) {
717 availableEEpromSize = (availableEEpromSize / 16) * 15;