#include "folder_model.h" #include "folder_item.h" #include "data_base_management.h" #include "folder.h" #include "db_helper.h" #include "qnaturalsorting.h" #include "yacreader_global.h" #include "yacreader_global_gui.h" #include #include #ifdef Y_MAC_UI #include QIcon finishedFolderIcon; void drawMacOSXFinishedFolderIcon() { QIcon ico = QFileIconProvider().icon(QFileIconProvider::Folder); QPixmap pixNormalOff = ico.pixmap(16, 16, QIcon::Normal, QIcon::Off); QPixmap pixNormalOn = ico.pixmap(16, 16, QIcon::Normal, QIcon::On); QPixmap pixSelectedOff = ico.pixmap(16, 16, QIcon::Selected, QIcon::Off); QPixmap pixSelectedOn = ico.pixmap(16, 16, QIcon::Selected, QIcon::On); QPixmap tick(":/images/folder_finished_macosx.png"); { QPainter p(&pixNormalOff); p.drawPixmap(4, 7, tick); } finishedFolderIcon.addPixmap(pixNormalOff, QIcon::Normal, QIcon::Off); { QPainter p(&pixNormalOn); p.drawPixmap(4, 7, tick); } finishedFolderIcon.addPixmap(pixNormalOn, QIcon::Normal, QIcon::On); { QPainter p(&pixSelectedOff); p.drawPixmap(4, 7, tick); } finishedFolderIcon.addPixmap(pixSelectedOff, QIcon::Selected, QIcon::Off); { QPainter p(&pixSelectedOn); p.drawPixmap(4, 7, tick); } finishedFolderIcon.addPixmap(pixSelectedOn, QIcon::Selected, QIcon::On); } #endif #define ROOT 1 struct FolderColumns { int name; int path; int finished; int completed; int id; int parentId; int numChildren; int firstChildHash; int customImage; int type; int added; int updated; FolderColumns(QSqlQuery &sqlquery) { auto record = sqlquery.record(); name = record.indexOf("name"); path = record.indexOf("path"); finished = record.indexOf("finished"); completed = record.indexOf("completed"); id = record.indexOf("id"); parentId = record.indexOf("parentId"); numChildren = record.indexOf("numChildren"); firstChildHash = record.indexOf("firstChildHash"); customImage = record.indexOf("customImage"); type = record.indexOf("type"); added = record.indexOf("added"); updated = record.indexOf("updated"); } }; QList folderDataFromQuery(QSqlQuery &query, const FolderColumns &columns) { QList data; data << query.value(columns.name); data << query.value(columns.path); data << query.value(columns.finished); data << query.value(columns.completed); data << query.value(columns.numChildren); data << query.value(columns.firstChildHash); data << query.value(columns.customImage); data << query.value(columns.type); data << query.value(columns.added); data << query.value(columns.updated); return data; } // TODO: load from DB, type is pretty much needed FolderItem *createRoot(QSqlDatabase &db) { QList data; QSqlQuery selectQuery(db); selectQuery.prepare("SELECT * FROM folder WHERE id = 1"); selectQuery.exec(); auto columns = FolderColumns(selectQuery); if (!selectQuery.next()) { return nullptr; } data = folderDataFromQuery(selectQuery, columns); data[0] = "root"; auto root = new FolderItem(data); root->id = ROOT; root->parentItem = nullptr; return root; } FolderModel::FolderModel(QObject *parent) : QAbstractItemModel(parent), isSubfolder(false), rootItem(nullptr), folderIcon(YACReader::noHighlightedIcon(":/images/sidebar/folder.svg")), folderFinishedIcon(YACReader::noHighlightedIcon(":/images/sidebar/folder_finished.svg")), showRecent(false), recentDays(1) { } FolderModel::~FolderModel() { if (rootItem != nullptr) delete rootItem; } int FolderModel::columnCount(const QModelIndex &parent) const { if (parent.isValid()) return static_cast(parent.internalPointer())->columnCount(); else { if (rootItem == nullptr) { return 0; } return 1; } } QHash FolderModel::roleNames() const { QHash roles; roles[FinishedRole] = "is_finished"; roles[CompletedRole] = "is_completed"; roles[IdRole] = "id"; roles[CoverPathRole] = "cover_path"; roles[FolderNameRole] = "name"; roles[NumChildrenRole] = "num_children"; roles[TypeRole] = "type"; roles[AddedRole] = "added"; roles[UpdatedRole] = "updated"; roles[ShowRecentRole] = "show_recent"; roles[RecentRangeRole] = "recent_range"; return roles; } void FolderModel::reload() { if (rootItem == nullptr) return; if (!isSubfolder) { auto newModelData = createModelData(_databasePath); takeUpdatedChildrenInfo(rootItem, QModelIndex(), newModelData.rootItem); // copy items from newModelData to this model that are not in this model foreach (auto key, newModelData.items.keys()) { if (!items.contains(key)) { items[key] = (newModelData.items[key]); } } delete newModelData.rootItem; } else { QString connectionName = ""; { QSqlDatabase db = DataBaseManagement::loadDatabase(_databasePath); QSqlQuery selectQuery(db); selectQuery.prepare("SELECT * FROM folder WHERE parentId = :parentId and id <> 1"); selectQuery.bindValue(":parentId", rootItem->id); selectQuery.exec(); auto tempRoot = new FolderItem(rootItem->getData(), rootItem->parentItem); tempRoot->id = rootItem->id; auto newModelData = createModelData(selectQuery, tempRoot); takeUpdatedChildrenInfo(rootItem, QModelIndex(), newModelData.rootItem); items = newModelData.items; // copy items from newModelData to this model that are not in this model foreach (auto key, newModelData.items.keys()) { if (!items.contains(key)) { items[key] = (newModelData.items[key]); } } delete newModelData.rootItem; connectionName = db.connectionName(); } QSqlDatabase::removeDatabase(connectionName); } } void FolderModel::takeUpdatedChildrenInfo(FolderItem *parent, const QModelIndex &parentModelIndex, FolderItem *updated) { auto currentChildren = parent->children(); auto updatedChildren = updated->children(); int lenght = currentChildren.size(); int lenghtUpdated = updatedChildren.size(); int m; // index that reflects modifications on the actual model for this parent int i; // index of the original children before update int j; // index of the updated children for (m = 0, i = 0, j = 0; (i < lenght) && (j < lenghtUpdated);) { auto child = currentChildren[i]; auto updatedChild = updatedChildren[j]; // same folder auto sameFolderId = child->id == updatedChild->id; // and also same name if (sameFolderId) { // 1. check if child data needs to be udpated if (child->getData() != updatedChild->getData()) { auto modelIndexToUpdate = index(m, 0, parentModelIndex); child->setData(updatedChild->getData()); emit dataChanged(modelIndexToUpdate, modelIndexToUpdate); } // 2. update children info takeUpdatedChildrenInfo(child, index(m, 0, parentModelIndex), updatedChild); m++; i++; j++; continue; } auto childName = child->data(Name).toString(); auto childUpdatedName = updatedChild->data(Name).toString(); // folder added if (!naturalSortLessThanCI(childName, childUpdatedName)) { beginInsertRows(parentModelIndex, m, m); parent->addChild(updatedChild, m); updated->removeChild(updatedChild); endInsertRows(); m++; j++; continue; } // folder removed if (naturalSortLessThanCI(childName, childUpdatedName)) { beginRemoveRows(parentModelIndex, m, m); delete parent->child(m); parent->removeChild(m); endRemoveRows(); i++; continue; } } // add remaining children for (; j < lenghtUpdated; j++) { auto updatedChild = updatedChildren[j]; beginInsertRows(parentModelIndex, m, m); parent->addChild(updatedChild, m); updated->removeChild(updatedChild); endInsertRows(); m++; } // remove remaining children for (; i < lenght; i++) { beginRemoveRows(parentModelIndex, m, m); delete parent->child(m); parent->removeChild(m); endRemoveRows(); } } void FolderModel::reload(const QModelIndex &index) { // TODO: reload just the content under index for better efficiency reload(); } QVariant FolderModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); auto item = static_cast(index.internalPointer()); if (role == Qt::ToolTipRole) { QString toolTip = item->data(FolderModel::Name).toString(); int totalNumOfChildren = item->childCount() + item->comicNames.size(); if (totalNumOfChildren > 0) { toolTip = toolTip + " - " + QString::number(totalNumOfChildren); } return toolTip; } if (role == Qt::DecorationRole) { #ifdef Y_MAC_UI if (item->data(FolderModel::Finished).toBool()) { if (finishedFolderIcon.isNull()) { drawMacOSXFinishedFolderIcon(); } return QVariant(finishedFolderIcon); } else { return QVariant(QFileIconProvider().icon(QFileIconProvider::Folder)); } #else if (item->data(FolderModel::Finished).toBool()) return QVariant(folderFinishedIcon); else return QVariant(folderIcon); #endif } if (role == FolderModel::FolderNameRole) { return item->data(FolderModel::Name); } if (role == FolderModel::CompletedRole) return item->data(FolderModel::Completed); if (role == FolderModel::FinishedRole) return item->data(FolderModel::Finished); if (role == FolderModel::IdRole) return item->id; if (role == FolderModel::CoverPathRole) return getCoverUrlPathForComicHash(item->data(FirstChildHash).toString()); if (role == FolderModel::NumChildrenRole) return item->data(NumChildren); if (role == FolderModel::TypeRole) return item->data(Type); if (role == FolderModel::AddedRole) return item->data(Added); if (role == FolderModel::UpdatedRole) return item->data(Updated); if (role == FolderModel::ShowRecentRole) return showRecent; if (role == FolderModel::RecentRangeRole) return recentDays * 86400; if (role != Qt::DisplayRole) return QVariant(); return item->data(index.column()); } Qt::ItemFlags FolderModel::flags(const QModelIndex &index) const { if (!index.isValid()) return {}; return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDropEnabled | Qt::ItemIsDragEnabled; } QVariant FolderModel::headerData(int section, Qt::Orientation orientation, int role) const { if (rootItem == nullptr) { return QVariant(); } if (orientation == Qt::Horizontal && role == Qt::DisplayRole) return rootItem->data(section); return QVariant(); } QModelIndex FolderModel::index(int row, int column, const QModelIndex &parent) const { if (!hasIndex(row, column, parent)) return QModelIndex(); FolderItem *parentItem; if (!parent.isValid()) parentItem = rootItem; else parentItem = static_cast(parent.internalPointer()); FolderItem *childItem = parentItem->child(row); if (childItem) return createIndex(row, column, childItem); else return QModelIndex(); } QModelIndex FolderModel::index(qulonglong folderId) const { QModelIndex index; YACReader::iterate(QModelIndex(), this, [&](const QModelIndex &idx) { if (index.isValid()) { return false; } auto item = static_cast(idx.internalPointer()); if (item->id == folderId) { index = idx; return false; } return true; }); return index; } QModelIndex FolderModel::parent(const QModelIndex &index) const { if (!index.isValid()) return QModelIndex(); auto childItem = static_cast(index.internalPointer()); FolderItem *parentItem = childItem->parent(); if (parentItem == rootItem) return QModelIndex(); return createIndex(parentItem->row(), 0, parentItem); } int FolderModel::rowCount(const QModelIndex &parent) const { FolderItem *parentItem; if (parent.column() > 0) return 0; if (!parent.isValid()) { if (rootItem == nullptr) { return 0; } parentItem = rootItem; } else { parentItem = static_cast(parent.internalPointer()); } return parentItem->childCount(); } void FolderModel::setupModelData(QString path) { beginResetModel(); if (rootItem != nullptr) delete rootItem; // TODO comprobar que se libera bien la memoria rootItem = nullptr; _databasePath = path; setModelData(createModelData(path)); endResetModel(); } void FolderModel::setModelData(const ModelData &modelData) { rootItem = modelData.rootItem; items = modelData.items; } FolderModel::ModelData FolderModel::createModelData(const QString &path) const { ModelData modelData; QString connectionName = ""; { QSqlDatabase db = DataBaseManagement::loadDatabase(path); QSqlQuery selectQuery("select * from folder where id <> 1 order by parentId,name", db); auto root = createRoot(db); modelData = createModelData(selectQuery, root); connectionName = db.connectionName(); } QSqlDatabase::removeDatabase(connectionName); return modelData; } FolderModel::ModelData FolderModel::createModelData(QSqlQuery &sqlquery, FolderItem *parent) const { QMap itemsLookup; // add parent to the lookup itemsLookup.insert(parent->id, parent); auto columns = FolderColumns(sqlquery); while (sqlquery.next()) { auto item = new FolderItem(folderDataFromQuery(sqlquery, columns)); item->id = sqlquery.value(columns.id).toULongLong(); // la inserci�n de hijos se hace de forma ordenada FolderItem *parent = itemsLookup.value(sqlquery.value(columns.parentId).toULongLong()); parent->appendChild(item); // se a�ade el item al map, de forma que se pueda encontrar como padre en siguientes iteraciones itemsLookup.insert(item->id, item); } return FolderModel::ModelData { parent, itemsLookup }; } QString FolderModel::getDatabase() { return _databasePath; } QString FolderModel::getFolderPath(const QModelIndex &folder) { if (!folder.isValid()) // root folder return "/"; return static_cast(folder.internalPointer())->data(FolderModel::Path).toString(); } void FolderModel::updateFolderCompletedStatus(const QModelIndexList &list, bool status) { QString connectionName = ""; { QSqlDatabase db = DataBaseManagement::loadDatabase(_databasePath); db.transaction(); foreach (QModelIndex mi, list) { auto item = static_cast(mi.internalPointer()); item->setData(FolderModel::Completed, status); if (!isSubfolder) { Folder f = DBHelper::loadFolder(item->id, db); f.completed = status; DBHelper::update(f, db); } } db.commit(); connectionName = db.connectionName(); } QSqlDatabase::removeDatabase(connectionName); emit dataChanged(index(list.first().row(), FolderModel::Name), index(list.last().row(), FolderModel::Updated)); } void FolderModel::updateFolderFinishedStatus(const QModelIndexList &list, bool status) { QString connectionName = ""; { QSqlDatabase db = DataBaseManagement::loadDatabase(_databasePath); db.transaction(); foreach (QModelIndex mi, list) { auto item = static_cast(mi.internalPointer()); item->setData(FolderModel::Finished, status); if (!isSubfolder) { Folder f = DBHelper::loadFolder(item->id, db); f.finished = status; DBHelper::update(f, db); } } db.commit(); connectionName = db.connectionName(); } QSqlDatabase::removeDatabase(connectionName); emit dataChanged(index(list.first().row(), FolderModel::Name), index(list.last().row(), FolderModel::Updated)); } void FolderModel::updateFolderType(const QModelIndexList &list, YACReader::FileType type) { QString connectionName = ""; { QSqlDatabase db = DataBaseManagement::loadDatabase(_databasePath); db.transaction(); foreach (QModelIndex mi, list) { auto item = static_cast(mi.internalPointer()); std::function setType; setType = [&setType](FolderItem *item, YACReader::FileType type) -> void { item->setData(FolderModel::Type, QVariant::fromValue(type)); for (auto child : item->children()) { setType(child, type); } }; setType(item, type); if (!isSubfolder) { DBHelper::updateFolderTreeType(item->id, db, type); } } db.commit(); connectionName = db.connectionName(); } QSqlDatabase::removeDatabase(connectionName); emit dataChanged(index(list.first().row(), FolderModel::Name), index(list.last().row(), FolderModel::Updated)); } void FolderModel::updateTreeType(YACReader::FileType type) { QString connectionName = ""; { QSqlDatabase db = DataBaseManagement::loadDatabase(_databasePath); db.transaction(); auto item = rootItem; std::function setType; setType = [&setType](FolderItem *item, YACReader::FileType type) -> void { item->setData(FolderModel::Type, QVariant::fromValue(type)); for (auto child : item->children()) { setType(child, type); } }; setType(item, type); if (!isSubfolder) { DBHelper::updateDBType(db, type); } db.commit(); connectionName = db.connectionName(); } QSqlDatabase::removeDatabase(connectionName); } QStringList FolderModel::getSubfoldersNames(const QModelIndex &mi) { QStringList result; qulonglong id = 1; if (mi.isValid()) { auto item = static_cast(mi.internalPointer()); id = item->id; } QString connectionName = ""; { QSqlDatabase db = DataBaseManagement::loadDatabase(_databasePath); db.transaction(); result = DBHelper::loadSubfoldersNames(id, db); db.commit(); connectionName = db.connectionName(); } QSqlDatabase::removeDatabase(connectionName); std::sort(result.begin(), result.end(), naturalSortLessThanCI); return result; } FolderModel *FolderModel::getSubfoldersModel(const QModelIndex &mi) { qulonglong id = 1; FolderItem *parent = nullptr; if (mi.isValid()) { auto item = static_cast(mi.internalPointer()); parent = new FolderItem(item->getData(), item->parent()); id = parent->id = item->id; } if (id == 1) { if (parent != nullptr) { delete parent; } return this; } auto model = new FolderModel(); QString connectionName = ""; { QSqlDatabase db = DataBaseManagement::loadDatabase(_databasePath); QSqlQuery selectQuery(db); // TODO check selectQuery.prepare("SELECT * FROM folder WHERE parentId = :parentId and id <> 1"); selectQuery.bindValue(":parentId", id); selectQuery.exec(); if (parent != nullptr) { model->setModelData(createModelData(selectQuery, parent)); } connectionName = db.connectionName(); } QSqlDatabase::removeDatabase(connectionName); model->_databasePath = _databasePath; model->isSubfolder = true; return model; } Folder FolderModel::getFolder(const QModelIndex &mi) { auto folderItem = static_cast(mi.internalPointer()); auto name = folderItem->data(FolderModel::Name).toString(); auto parentItem = folderItem->parent(); auto folder = Folder(folderItem->id, parentItem->id, name, folderItem->parent()->data(Columns::Path).toString() + "/" + name, folderItem->data(Columns::Completed).toBool(), folderItem->data(Columns::Finished).toBool(), folderItem->data(Columns::NumChildren).toInt(), folderItem->data(Columns::FirstChildHash).toString(), folderItem->data(Columns::CustomImage).toString(), folderItem->data(Columns::Type).value(), folderItem->data(Columns::Added).toLongLong(), folderItem->data(Columns::Updated).toLongLong()); return folder; } QModelIndex FolderModel::getIndexFromFolderId(qulonglong folderId, const QModelIndex &parent) { if (rootItem == nullptr) { return QModelIndex(); } auto numRows = rowCount(parent); for (auto i = 0; i < numRows; i++) { auto modelIndex = index(i, 0, parent); if (modelIndex.isValid()) { auto folderItem = static_cast(modelIndex.internalPointer()); if (folderItem->id == folderId) { return modelIndex; } auto childModelIndex = getIndexFromFolderId(folderId, modelIndex); if (childModelIndex.isValid()) { auto folderItem = static_cast(childModelIndex.internalPointer()); if (folderItem->id == folderId) { return childModelIndex; } } } } return QModelIndex(); } QModelIndex FolderModel::getIndexFromFolder(const Folder &folder, const QModelIndex &parent) { return getIndexFromFolderId(folder.id, parent); } QModelIndex FolderModel::addFolderAtParent(const QString &folderName, const QModelIndex &parent) { FolderItem *parentItem; if (parent.isValid()) parentItem = static_cast(parent.internalPointer()); else parentItem = rootItem; Folder newFolder; newFolder.name = folderName; newFolder.parentId = parentItem->id; newFolder.path = parentItem->data(Columns::Path).toString() + "/" + folderName; newFolder.type = parentItem->data(Columns::Type).value(); QString connectionName = ""; { QSqlDatabase db = DataBaseManagement::loadDatabase(_databasePath); newFolder.id = DBHelper::insert(&newFolder, db); DBHelper::updateChildrenInfo(parentItem->id, db); connectionName = db.connectionName(); } QSqlDatabase::removeDatabase(connectionName); int destRow = 0; QList data; data << newFolder.name; data << newFolder.path; data << false; // finished data << true; // completed data << 0; // numChildren data << QVariant(); // first child hash, new folder is empty data << QVariant(); // custom cover data << QVariant::fromValue(newFolder.type); data << newFolder.added; data << newFolder.updated; auto item = new FolderItem(data); item->id = newFolder.id; beginInsertRows(parent, 0, 0); // TODO calculate the destRow before inserting the new child parentItem->appendChild(item); destRow = parentItem->children().indexOf(item); // TODO optimize this, appendChild should return the index of the new item items.insert(item->id, item); endInsertRows(); return index(destRow, 0, parent); } QUrl FolderModel::getCoverUrlPathForComicHash(const QString &hash) const { auto coverPath = LibraryPaths::coverPathFromLibraryDataPath(_databasePath, hash); return QUrl::fromLocalFile(coverPath); } void FolderModel::setShowRecent(bool showRecent) { if (this->showRecent == showRecent) return; this->showRecent = showRecent; emit dataChanged(index(0, 0), index(rowCount() - 1, 0), { FolderModel::ShowRecentRole }); } void FolderModel::setRecentRange(int days) { if (this->recentDays == days) return; this->recentDays = days; emit dataChanged(index(0, 0), index(rowCount() - 1, 0), { FolderModel::RecentRangeRole }); } void FolderModel::deleteFolder(const QModelIndex &mi) { beginRemoveRows(mi.parent(), mi.row(), mi.row()); auto item = static_cast(mi.internalPointer()); FolderItem *parent = item->parent(); parent->removeChild(mi.row()); Folder f; f.setId(item->id); QString connectionName = ""; { QSqlDatabase db = DataBaseManagement::loadDatabase(_databasePath); DBHelper::removeFromDB(&f, db); auto folder = DBHelper::updateChildrenInfo(item->parent()->id, db); DBHelper::propagateFolderUpdatesToParent(folder, db); connectionName = db.connectionName(); } QSqlDatabase::removeDatabase(connectionName); endRemoveRows(); } void FolderModel::updateFolderChildrenInfo(qulonglong folderId) { QString connectionName = ""; { QSqlDatabase db = DataBaseManagement::loadDatabase(_databasePath); auto folder = DBHelper::updateChildrenInfo(folderId, db); DBHelper::propagateFolderUpdatesToParent(folder, db); connectionName = db.connectionName(); } QSqlDatabase::removeDatabase(connectionName); } // PROXY FolderModelProxy::FolderModelProxy(QObject *parent) : QSortFilterProxyModel(parent), rootItem(0), filterEnabled(false) { } FolderModelProxy::~FolderModelProxy() { } bool FolderModelProxy::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const { if (!filterEnabled) return true; auto parent = static_cast(source_parent.internalPointer()); if (parent == 0) parent = static_cast(sourceModel())->rootItem; FolderItem *item = parent->children().at(source_row); return filteredItems.contains(item->id); } void FolderModelProxy::setFilterData(QMap *filteredItems, FolderItem *root) { clear(); filterEnabled = true; beginResetModel(); if (rootItem != 0) delete rootItem; // TODO comprobar que se libera bien la memoria rootItem = root; this->filteredItems.insert(*filteredItems); endResetModel(); delete filteredItems; } void FolderModelProxy::clear() { filterEnabled = false; filteredItems.clear(); QSortFilterProxyModel::invalidate(); }