harbour-books/app/src/BooksShelf.cpp
2018-05-16 18:27:12 +03:00

1242 lines
35 KiB
C++

/*
* Copyright (C) 2015-2018 Jolla Ltd.
* Copyright (C) 2015-2018 Slava Monich <slava.monich@jolla.com>
*
* You may use this file under the terms of the BSD license as follows:
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Jolla Ltd nor the names of its contributors
* may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "BooksShelf.h"
#include "BooksDefs.h"
#include "BooksBook.h"
#include "BooksUtil.h"
#include "HarbourDebug.h"
#include "HarbourJson.h"
#include "HarbourTask.h"
#include <errno.h>
enum BooksItemRole {
BooksItemName = Qt::UserRole,
BooksItemBook,
BooksItemShelf,
BooksItemAccessible,
BooksItemCopyingOut,
BooksItemCopyingIn,
BooksItemCopyProgress,
BooksItemDummy,
BooksItemDeleteRequested
};
#define SHELF_STATE_FILE BOOKS_STATE_FILE_SUFFIX
#define SHELF_STATE_ORDER "order"
class BooksShelf::CopyTask : public HarbourTask, BooksItem::CopyOperation {
Q_OBJECT
public:
CopyTask(QThreadPool* aPool, BooksShelf::Data* aDestData,
BooksItem* aSrcItem);
~CopyTask();
void performTask();
QString srcPath() const;
QString destPath() const;
// BooksItem::CopyOperation
virtual bool isCanceled() const;
virtual void copyProgressChanged(int aProgress);
Q_SIGNALS:
void copyProgressChanged();
public:
BooksShelf::Data* iDestData;
BooksStorage iDestStorage;
QString iDestRelPath;
QString iDestAbsPath;
BooksItem* iSrcItem;
BooksItem* iDestItem;
int iCopyProgress;
};
// ==========================================================================
// BooksShelf::LoadTask
// ==========================================================================
class BooksShelf::LoadTask : public HarbourTask {
public:
LoadTask(QThreadPool* aPool, BooksStorage aStorage, QString aRelPath,
QString aStateFile) : HarbourTask(aPool),
iStorage(aStorage), iRelativePath(aRelPath),
iStateFilePath(aStateFile) {}
~LoadTask();
void performTask();
int findBook(QString aFileName) const;
static int find(QFileInfoList aList, QString aFileName, int aStart);
public:
BooksStorage iStorage;
QString iRelativePath;
QString iStateFilePath;
QList<BooksItem*> iItems;
};
BooksShelf::LoadTask::~LoadTask()
{
const int n = iItems.count();
for (int i=0; i<n; i++) iItems.at(i)->release();
}
int BooksShelf::LoadTask::find(QFileInfoList aList, QString aName, int aStart)
{
if (!aName.isEmpty()) {
const int n = aList.count();
for (int i=aStart; i<n; i++) {
if (aList.at(i).fileName() == aName) {
return i;
}
}
}
return -1;
}
int BooksShelf::LoadTask::findBook(QString aFileName) const
{
if (!aFileName.isEmpty()) {
const int n = iItems.count();
for (int i=0; i<n; i++) {
BooksItem* item = iItems.at(i);
if (item->book() && item->fileName() == aFileName) {
return i;
}
}
}
return -1;
}
void BooksShelf::LoadTask::performTask()
{
if (!isCanceled()) {
QString path(iStorage.fullPath(iRelativePath));
HDEBUG("checking" << path);
QDir dir(path);
QFileInfoList list = dir.entryInfoList(QDir::Files |
QDir::Dirs | QDir::NoDotAndDotDot, QDir::Time);
// Restore the order
QVariantMap state;
if (HarbourJson::load(iStateFilePath, state)) {
QVariantList order = state.value(SHELF_STATE_ORDER).toList();
const int n = order.count();
for (int i=0, dest=0; i<n; i++) {
int index = find(list, order.at(i).toString(), dest);
if (index >= 0) {
if (index != dest) {
HDEBUG(order.at(i).toString() << index << "->" << dest);
list.move(index, dest);
} else {
HDEBUG(order.at(i).toString() << index);
}
dest++;
} else {
HDEBUG(order.at(i).toString());
}
}
}
const int n = list.count();
for (int i=0; i<n && !isCanceled(); i++) {
const QFileInfo& info = list.at(i);
QString path(info.filePath());
if (info.isDir()) {
HDEBUG("directory:" << qPrintable(path));
QString folderPath(iRelativePath);
if (!folderPath.isEmpty() && !folderPath.endsWith('/')) {
folderPath += '/';
}
folderPath += info.fileName();
BooksShelf* newShelf = new BooksShelf(iStorage, folderPath);
iItems.append(newShelf);
} else {
shared_ptr<Book> book = BooksUtil::bookFromFile(path);
if (!book.isNull()) {
BooksBook* newBook = new BooksBook(iStorage,
iRelativePath, book);
iItems.append(newBook);
HDEBUG("[" << iItems.size() << "]" <<
qPrintable(newBook->fileName()) <<
newBook->title());
} else {
HDEBUG("not a book:" << qPrintable(path));
}
}
}
}
// Cleanup the state files
if (!isCanceled()) {
QStringList deleteMe;
const QString suffix(BOOKS_STATE_FILE_SUFFIX);
QDirIterator configIt(iStorage.configDir());
while (configIt.hasNext() && !isCanceled()) {
QString path(configIt.next());
if (path.endsWith(suffix)) {
QString fileName(configIt.fileName());
QString name(fileName.left(fileName.length() - suffix.length()));
if (!name.isEmpty() && findBook(name) < 0) {
deleteMe.append(path);
}
}
}
while (!deleteMe.isEmpty() && !isCanceled()) {
QString path(deleteMe.takeLast());
if (QFile::remove(path)) {
HDEBUG("removed" << qPrintable(path));
} else {
HWARN("failed to remove" << qPrintable(path));
}
}
}
}
// ==========================================================================
// BooksShelf::Data
// ==========================================================================
class BooksShelf::Data {
public:
Data(BooksShelf* aShelf, BooksItem* aItem, bool aExternal);
~Data();
QString name() { return iItem ? iItem->name() : QString(); }
QString fileName() { return iItem ? iItem->fileName() : QString(); }
QString path() { return iItem ? iItem->path() : QString(); }
QObject* object() { return iItem ? iItem->object() : NULL; }
BooksBook* book() { return iItem ? iItem->book() : NULL; }
BooksShelf* shelf() { return iItem ? iItem->shelf() : NULL; }
bool accessible() const { return !iCopyTask && iItem && iItem->accessible(); }
bool isBook() const { return iItem && iItem->book(); }
bool isShelf() const { return iItem && iItem->shelf(); }
bool copyingOut();
bool copyingIn() { return iCopyTask != NULL; }
double copyProgress() { return iCopyTask ? (iCopyTask->iCopyProgress/
((double)PROGRESS_PRECISION)) : 0.0; }
void setBook(BooksBook* aBook, bool aExternal);
void connectSignals(BooksBook* aBook);
public:
BooksShelf* iShelf;
BooksItem* iItem;
BooksShelf::CopyTask* iCopyTask;
bool iDeleteRequested;
};
BooksShelf::Data::Data(BooksShelf* aShelf, BooksItem* aItem, bool aExternal) :
iShelf(aShelf),
iItem(aItem),
iCopyTask(NULL),
iDeleteRequested(false)
{
if (iItem && !aExternal) {
connectSignals(iItem->book());
}
}
BooksShelf::Data::~Data()
{
if (iItem) {
iItem->object()->disconnect(iShelf);
iItem->release();
}
if (iCopyTask) {
iCopyTask->release(iShelf);
}
}
void BooksShelf::Data::connectSignals(BooksBook* aBook)
{
if (aBook) {
iShelf->connect(aBook,
SIGNAL(accessibleChanged()),
SLOT(onBookAccessibleChanged()));
iShelf->connect(aBook,
SIGNAL(copyingOutChanged()),
SLOT(onBookCopyingOutChanged()));
iShelf->connect(aBook,
SIGNAL(movedAway()),
SLOT(onBookMovedAway()));
}
}
void BooksShelf::Data::setBook(BooksBook* aBook, bool aExternal)
{
if (iItem != aBook) {
if (iItem) {
iItem->object()->disconnect(iShelf);
iItem->release();
}
iItem = aBook;
if (aBook) {
aBook->retain();
if (!aExternal) connectSignals(aBook);
}
}
}
inline bool BooksShelf::Data::copyingOut()
{
BooksBook* bookItem = book();
return bookItem && bookItem->copyingOut();
}
// ==========================================================================
// BooksShelf::CopyTask
// ==========================================================================
BooksShelf::CopyTask::CopyTask(QThreadPool* aPool, BooksShelf::Data* aDestData,
BooksItem* aSrcItem) :
HarbourTask(aPool),
iDestData(aDestData),
iDestStorage(aDestData->iShelf->storage()),
iDestRelPath(aDestData->iShelf->relativePath()),
iDestAbsPath(iDestStorage.fullPath(iDestRelPath + "/" + aSrcItem->fileName())),
iSrcItem(aSrcItem->retain()),
iDestItem(NULL),
iCopyProgress(0)
{
if (iDestData->iCopyTask) {
iDestData->iCopyTask->release(iDestData->iShelf);
}
iDestData->iCopyTask = this;
iDestData->iShelf->connect(this, SIGNAL(done()), SLOT(onCopyTaskDone()));
iDestData->iShelf->connect(this, SIGNAL(copyProgressChanged()),
SLOT(onCopyTaskProgressChanged()), Qt::QueuedConnection);
HDEBUG(qPrintable(aSrcItem->path()) << "->" << destPath());
}
BooksShelf::CopyTask::~CopyTask()
{
HASSERT(!iDestData);
iSrcItem->release();
if (iDestItem) iDestItem->release();
}
inline QString BooksShelf::CopyTask::srcPath() const
{
return iSrcItem->path();
}
inline QString BooksShelf::CopyTask::destPath() const
{
return iDestAbsPath;
}
void BooksShelf::CopyTask::performTask()
{
iDestItem = iSrcItem->copyTo(iDestStorage, iDestRelPath, this);
}
bool BooksShelf::CopyTask::isCanceled() const
{
return HarbourTask::isCanceled();
}
void BooksShelf::CopyTask::copyProgressChanged(int aProgress)
{
iCopyProgress = aProgress;
Q_EMIT copyProgressChanged();
}
// ==========================================================================
// BooksShelf::DeleteTask
// ==========================================================================
class BooksShelf::DeleteTask : public HarbourTask {
Q_OBJECT
public:
DeleteTask(QThreadPool* aPool, BooksItem* aItem);
~DeleteTask();
void performTask();
public:
BooksItem* iItem;
};
BooksShelf::DeleteTask::DeleteTask(QThreadPool* aPool, BooksItem* aItem) :
HarbourTask(aPool),
iItem(aItem)
{
iItem->retain();
}
BooksShelf::DeleteTask::~DeleteTask()
{
iItem->release();
}
void BooksShelf::DeleteTask::performTask()
{
if (isCanceled()) {
HDEBUG("cancelled" << iItem->fileName());
} else {
iItem->deleteFiles();
}
}
// ==========================================================================
// BooksShelf::Counts
// ==========================================================================
class BooksShelf::Counts {
public:
Counts(BooksShelf* aShelf);
void count(BooksShelf* aShelf);
void emitSignals(BooksShelf* aShelf);
int iTotalCount;
int iBookCount;
int iShelfCount;
};
BooksShelf::Counts::Counts(BooksShelf* aShelf)
{
count(aShelf);
}
void BooksShelf::Counts::count(BooksShelf* aShelf)
{
iTotalCount = aShelf->iList.count(),
iBookCount = 0;
iShelfCount = 0;
for (int i=0; i<iTotalCount; i++) {
const Data* data = aShelf->iList.at(i);
if (data->isBook()) {
iBookCount++;
} else if (data->isShelf()) {
iShelfCount++;
}
}
}
void BooksShelf::Counts::emitSignals(BooksShelf* aShelf)
{
const int oldTotalCount = iTotalCount;
const int oldBookCount = iBookCount;
const int oldShelfCount = iShelfCount;
count(aShelf);
if (oldBookCount != iBookCount) {
Q_EMIT aShelf->bookCountChanged();
}
if (oldShelfCount != iShelfCount) {
Q_EMIT aShelf->shelfCountChanged();
}
if (oldTotalCount != iTotalCount) {
Q_EMIT aShelf->countChanged();
}
}
// ==========================================================================
// BooksShelf
// ==========================================================================
BooksShelf::BooksShelf(QObject* aParent) :
QAbstractListModel(aParent),
iLoadBookList(true),
iLoadTask(NULL),
iDummyItemIndex(-1),
iEditMode(false),
iRef(-1),
iSaveTimer(new BooksSaveTimer(this)),
iTaskQueue(BooksTaskQueue::defaultQueue())
{
init();
connect(iSaveTimer, SIGNAL(save()), SLOT(saveState()));
}
BooksShelf::BooksShelf(BooksStorage aStorage, QString aRelativePath) :
iLoadBookList(false),
iLoadTask(NULL),
iRelativePath(aRelativePath),
iStorage(aStorage),
iDummyItemIndex(-1),
iEditMode(false),
iRef(1),
iSaveTimer(NULL),
iTaskQueue(BooksTaskQueue::defaultQueue())
{
init();
// Refcounted BooksShelf objects are managed by C++ code
// They also don't need to read the content of the directory -
// only the objects allocated by QML do that.
QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership);
updatePath();
}
BooksShelf::~BooksShelf()
{
const int n = iDeleteTasks.count();
for (int i=0; i<n; i++) iDeleteTasks.at(i)->release(this);
if (iLoadTask) iLoadTask->release(this);
if (iSaveTimer && iSaveTimer->saveRequested()) saveState();
qDeleteAll(iList);
HDEBUG("destroyed");
}
void BooksShelf::init()
{
#if QT_VERSION < 0x050000
setRoleNames(roleNames());
#endif
moveToThread(qApp->thread());
connect(BooksStorageManager::instance(),
SIGNAL(storageReplaced(BooksStorage,BooksStorage)),
SLOT(onStorageReplaced(BooksStorage,BooksStorage)));
}
void BooksShelf::setRelativePath(QString aPath)
{
if (iRelativePath != aPath) {
iRelativePath = aPath;
updatePath();
Q_EMIT relativePathChanged();
}
}
void BooksShelf::setDevice(QString aDevice)
{
if (device() != aDevice) {
iStorage = BooksStorageManager::instance()->storageForDevice(aDevice);
updatePath();
Q_EMIT deviceChanged();
}
}
void BooksShelf::onStorageReplaced(BooksStorage aOld, BooksStorage aNew)
{
if (iStorage == aOld) {
iStorage = aNew;
updatePath();
}
}
void BooksShelf::setName(QString aName)
{
if (iStorage.isValid() &&
BooksUtil::isValidFileName(aName) &&
iFileName != aName) {
QString parentDir;
const int lastSlash = iRelativePath.lastIndexOf('/');
if (lastSlash > 0) {
parentDir = iRelativePath.left(lastSlash);
}
QString newRelativePath;
if (!parentDir.isEmpty()) {
newRelativePath = parentDir;
newRelativePath += '/';
}
newRelativePath += aName;
const QString oldPath = iStorage.fullPath(iRelativePath);
const QString newPath = iStorage.fullPath(newRelativePath);
HDEBUG("renaming" << qPrintable(oldPath) << "->" << qPrintable(newPath));
if (rename(qPrintable(oldPath), qPrintable(newPath)) == 0) {
// Rename the config/cache directory too
const QString oldConfPath = iStorage.fullConfigPath(iRelativePath);
const QString newConfPath = iStorage.fullConfigPath(newRelativePath);
HDEBUG(qPrintable(oldConfPath) << "->" << qPrintable(newConfPath));
if (rename(qPrintable(oldConfPath), qPrintable(newConfPath))) {
HWARN(strerror(errno));
}
const QString oldFileName(iFileName);
iRelativePath = newRelativePath;
iPath = iStorage.fullPath(iRelativePath);
updateFileName();
Q_EMIT pathChanged();
Q_EMIT relativePathChanged();
// Since this directiry has been renamed, we need to update the
// order of objects in the parent directory.
QVariantMap state;
const QString stateFile = stateFileName(parentDir);
if (HarbourJson::load(stateFile, state)) {
QVariantList order = state.value(SHELF_STATE_ORDER).toList();
int i, n = order.count();
for (i=0; i<n; i++) {
if (order.at(i).toString() == oldFileName) {
order[i] = iFileName;
state.insert(SHELF_STATE_ORDER, order);
if (HarbourJson::save(stateFile, state)) {
HDEBUG("wrote" << qPrintable(stateFile));
}
break;
}
}
}
} else {
HWARN(strerror(errno));
}
}
}
void BooksShelf::updatePath()
{
BooksLoadingSignalBlocker block(this);
const QString oldPath = iPath;
iPath.clear();
if (iStorage.isValid()) {
iPath = iStorage.fullPath(iRelativePath);
}
updateFileName();
if (oldPath != iPath) {
const int oldDummyItemIndex = iDummyItemIndex;
Counts counts(this);
HDEBUG(iPath);
// Clear the model
if (!iList.isEmpty()) {
beginRemoveRows(QModelIndex(), 0, iList.size()-1);
while (!iList.isEmpty()) {
Data* data = iList.takeLast();
BooksBook* book = data->book();
if (book) {
Q_EMIT bookRemoved(book);
}
delete data;
}
endRemoveRows();
}
iDummyItemIndex = -1;
if (!iPath.isEmpty() && iLoadBookList) loadBookList();
Q_EMIT pathChanged();
if (oldDummyItemIndex != iDummyItemIndex) {
Q_EMIT dummyItemIndexChanged();
if (oldDummyItemIndex <0 || iDummyItemIndex < 0) {
Q_EMIT hasDummyItemChanged();
}
}
counts.emitSignals(this);
}
}
void BooksShelf::updateFileName()
{
const int slashPos = iRelativePath.lastIndexOf('/');
const QString fileName = (slashPos >= 0) ?
iRelativePath.right(iRelativePath.length() - slashPos - 1) :
iRelativePath;
if (iFileName != fileName) {
iFileName = fileName;
Q_EMIT nameChanged();
}
}
void BooksShelf::onLoadTaskDone()
{
HASSERT(iLoadTask);
HASSERT(iLoadTask == sender());
if (iLoadTask && iLoadTask == sender()) {
BooksLoadingSignalBlocker block(this);
const int oldSize = iList.size();
const int newSize = iLoadTask->iItems.size();
HASSERT(iList.isEmpty());
if (newSize > 0) {
Counts counts(this);
beginInsertRows(QModelIndex(), oldSize, oldSize + newSize - 1);
for (int i=0; i<newSize; i++) {
BooksItem* item = iLoadTask->iItems.at(i);
BooksBook* book = item->book();
if (book) {
Q_EMIT bookAdded(book);
}
iList.append(new Data(this, item->retain(), false));
}
endInsertRows();
counts.emitSignals(this);
}
iLoadTask->release(this);
iLoadTask = NULL;
}
}
void BooksShelf::loadBookList()
{
BooksLoadingSignalBlocker block(this);
if (iLoadTask) iLoadTask->release(this);
if (iPath.isEmpty()) {
iLoadTask = NULL;
} else {
HDEBUG(iPath);
(iLoadTask = new LoadTask(iTaskQueue->pool(), iStorage, iRelativePath,
stateFileName()))->submit(this, SLOT(onLoadTaskDone()));
}
}
void BooksShelf::saveState()
{
QStringList order;
const int n = iList.count();
for (int i=0; i<n; i++) {
order.append(iList.at(i)->fileName());
}
QVariantMap state;
state.insert(SHELF_STATE_ORDER, order);
if (HarbourJson::save(stateFileName(), state)) {
HDEBUG("wrote" << qPrintable(stateFileName()));
}
}
void BooksShelf::queueStateSave()
{
if (iEditMode && iSaveTimer) {
iSaveTimer->requestSave();
}
}
QString BooksShelf::stateFileName(QString aRelativePath) const
{
if (iStorage.isValid()) {
QString path(iStorage.configDir().path());
if (!aRelativePath.isEmpty()) {
path += "/";
path += aRelativePath;
}
path += "/" SHELF_STATE_FILE;
return path;
} else {
return QString();
}
}
int BooksShelf::bookIndex(BooksBook* aBook) const
{
if (aBook) {
const int n = iList.count();
for (int i=0; i<n; i++) {
if (iList.at(i)->book() == aBook) {
return i;
}
}
}
return -1;
}
int BooksShelf::itemIndex(QString aFileName, int aStartIndex) const
{
if (!aFileName.isEmpty()) {
const int n = iList.count();
for (int i=aStartIndex; i<n; i++) {
if (iList.at(i)->fileName() == aFileName) {
return i;
}
}
}
return -1;
}
void BooksShelf::setHasDummyItem(bool aHasDummyItem)
{
if (aHasDummyItem && !hasDummyItem()) {
iDummyItemIndex = iList.count();
beginInsertRows(QModelIndex(), iDummyItemIndex, iDummyItemIndex);
iList.append(new Data(this, NULL, false));
endInsertRows();
Q_EMIT countChanged();
Q_EMIT hasDummyItemChanged();
Q_EMIT dummyItemIndexChanged();
} else if (!aHasDummyItem && hasDummyItem()) {
remove(iDummyItemIndex);
}
}
void BooksShelf::setEditMode(bool aEditMode)
{
if (iEditMode != aEditMode) {
iEditMode = aEditMode;
HDEBUG(iEditMode);
if (iSaveTimer && iSaveTimer->saveRequested()) {
iSaveTimer->saveNow();
}
setHasDummyItem(false);
Q_EMIT editModeChanged();
}
}
void BooksShelf::setDummyItemIndex(int aIndex)
{
if (validIndex(aIndex) && hasDummyItem() && iDummyItemIndex != aIndex) {
const int oldDummyItemIndex = iDummyItemIndex;
iDummyItemIndex = aIndex;
move(oldDummyItemIndex, aIndex);
Q_EMIT dummyItemIndexChanged();
}
}
BooksItem* BooksShelf::retain()
{
if (iRef.load() >= 0) {
iRef.ref();
}
return this;
}
void BooksShelf::release()
{
if (iRef.load() >= 0 && !iRef.deref()) {
delete this;
}
}
QObject* BooksShelf::object()
{
return this;
}
BooksShelf* BooksShelf::shelf()
{
return this;
}
BooksBook* BooksShelf::book()
{
return NULL;
}
QString BooksShelf::name() const
{
return iFileName;
}
QString BooksShelf::fileName() const
{
return iFileName;
}
QString BooksShelf::path() const
{
return iPath;
}
bool BooksShelf::accessible() const
{
return true;
}
int BooksShelf::count() const
{
return iList.count();
}
int BooksShelf::bookCount() const
{
int n=0, total = iList.count();
for(int i=0; i<total; i++) {
if (iList.at(i)->book()) n++;
}
return n;
}
int BooksShelf::shelfCount() const
{
int n=0, total = iList.count();
for(int i=0; i<total; i++) {
if (iList.at(i)->shelf()) n++;
}
return n;
}
QObject* BooksShelf::get(int aIndex) const
{
if (validIndex(aIndex)) {
return iList.at(aIndex)->object();
}
HWARN("invalid index" << aIndex);
return NULL;
}
BooksBook* BooksShelf::bookAt(int aIndex) const
{
if (validIndex(aIndex)) {
return iList.at(aIndex)->book();
}
HWARN("invalid index" << aIndex);
return NULL;
}
bool BooksShelf::drop(QObject* aItem)
{
if (iDummyItemIndex >= 0) {
BooksBook* book = qobject_cast<BooksBook*>(aItem);
if (!book) {
HWARN("unexpected drop object");
} else if (itemIndex(book->fileName()) >= 0) {
HWARN("duplicate file name");
setHasDummyItem(false);
} else {
HDEBUG("copying" << book->name() << "to" << qPrintable(path()));
book->setCopyingOut(true);
// Dropped object replaces the dummy placeholder object
QModelIndex index(createIndex(iDummyItemIndex, 0));
Data* data = iList.at(iDummyItemIndex);
HASSERT(!data->iItem);
iDummyItemIndex = -1;
// Don't connect signals since it's not our item
data->setBook(book, true);
// Start copying the data
(new CopyTask(iTaskQueue->pool(), data, book))->submit();
Q_EMIT hasDummyItemChanged();
Q_EMIT dummyItemIndexChanged();
Q_EMIT dataChanged(index, index);
return true;
}
} else {
HWARN("unexpected drop");
}
return false;
}
void BooksShelf::move(int aFrom, int aTo)
{
if (aFrom != aTo) {
if (validIndex(aFrom) && validIndex(aTo)) {
HDEBUG(iList.at(aFrom)->name() << "from" << aFrom << "to" << aTo);
int dest = (aTo < aFrom) ? aTo : (aTo+1);
beginMoveRows(QModelIndex(), aFrom, aFrom, QModelIndex(), dest);
iList.move(aFrom, aTo);
queueStateSave();
endMoveRows();
} else {
HWARN("invalid move" << aFrom << "->" << aTo);
}
}
}
void BooksShelf::submitDeleteTask(int aIndex)
{
BooksItem* item = iList.at(aIndex)->iItem;
if (item) {
DeleteTask* task = new DeleteTask(iTaskQueue->pool(), item);
iDeleteTasks.append(task);
task->submit();
BooksBook* book = item->book();
if (book) {
book->cancelCoverRequest();
Q_EMIT bookRemoved(book);
}
}
}
void BooksShelf::remove(int aIndex)
{
if (validIndex(aIndex)) {
Counts counts(this);
HDEBUG(iList.at(aIndex)->name());
beginRemoveRows(QModelIndex(), aIndex, aIndex);
submitDeleteTask(aIndex);
if (iDummyItemIndex == aIndex) {
iDummyItemIndex = -1;
Q_EMIT hasDummyItemChanged();
Q_EMIT dummyItemIndexChanged();
}
delete iList.takeAt(aIndex);
queueStateSave();
counts.emitSignals(this);
endRemoveRows();
}
}
void BooksShelf::removeAll()
{
if (!iList.isEmpty()) {
Counts counts(this);
beginRemoveRows(QModelIndex(), 0, iList.count()-1);
const int n = iList.count();
for (int i=0; i<n; i++) {
submitDeleteTask(i);
}
if (iDummyItemIndex >= 0) {
iDummyItemIndex = -1;
Q_EMIT hasDummyItemChanged();
Q_EMIT dummyItemIndexChanged();
}
qDeleteAll(iList);
iList.clear();
queueStateSave();
counts.emitSignals(this);
endRemoveRows();
}
}
bool BooksShelf::deleteRequested(int aIndex) const
{
if (validIndex(aIndex)) {
return iList.at(aIndex)->iDeleteRequested;
} else {
return false;
}
}
void BooksShelf::setDeleteRequested(int aIndex, bool aValue)
{
if (validIndex(aIndex)) {
Data* data = iList.at(aIndex);
if (data->iDeleteRequested != aValue) {
if (aValue) {
if (!data->copyingIn() && !data->copyingOut()) {
data->iDeleteRequested = true;
HDEBUG(aValue << data->name());
emitDataChangedSignal(aIndex, BooksItemDeleteRequested);
}
} else {
data->iDeleteRequested = false;
HDEBUG(aValue << data->name());
emitDataChangedSignal(aIndex, BooksItemDeleteRequested);
}
}
}
}
void BooksShelf::cancelAllDeleteRequests()
{
for (int i=iList.count()-1; i>=0; i--) {
setDeleteRequested(i, false);
}
}
void BooksShelf::importBook(QObject* aBook)
{
BooksBook* book = qobject_cast<BooksBook*>(aBook);
if (!book) {
HWARN("unexpected import object");
} else if (itemIndex(book->fileName()) >= 0) {
HWARN("duplicate file name" << book->fileName());
} else {
HDEBUG(qPrintable(book->path()) << "->" << qPrintable(iPath));
beginInsertRows(QModelIndex(), 0, 0);
Counts counts(this);
Data* data = new Data(this, book->retain(), true);
iList.insert(0, data);
(new CopyTask(iTaskQueue->pool(), data, book))->submit();
counts.emitSignals(this);
endInsertRows();
saveState();
}
}
void BooksShelf::emitDataChangedSignal(int aRow, int aRole)
{
if (aRow >= 0) {
QModelIndex index(createIndex(aRow, 0));
QVector<int> roles;
roles.append(aRole);
Q_EMIT dataChanged(index, index, roles);
}
}
void BooksShelf::onBookAccessibleChanged()
{
int row = bookIndex(qobject_cast<BooksBook*>(sender()));
if (row >= 0) {
HDEBUG(iList.at(row)->name() << iList.at(row)->accessible());
emitDataChangedSignal(row, BooksItemAccessible);
}
}
void BooksShelf::onBookCopyingOutChanged()
{
int row = bookIndex(qobject_cast<BooksBook*>(sender()));
if (row >= 0) {
HDEBUG(iList.at(row)->name() << iList.at(row)->copyingOut());
emitDataChangedSignal(row, BooksItemCopyingOut);
}
}
void BooksShelf::onBookMovedAway()
{
BooksBook* book = qobject_cast<BooksBook*>(sender());
HASSERT(book);
if (book) {
const int row = bookIndex(book);
HDEBUG(book->title() << row);
if (row >= 0) {
remove(row);
}
}
}
void BooksShelf::onCopyTaskProgressChanged()
{
CopyTask* task = qobject_cast<CopyTask*>(sender());
HASSERT(task);
if (task) {
HDEBUG(task->destPath() << task->iCopyProgress);
const int row = iList.indexOf(task->iDestData);
emitDataChangedSignal(row, BooksItemCopyProgress);
}
}
void BooksShelf::onCopyTaskDone()
{
CopyTask* task = qobject_cast<CopyTask*>(sender());
HASSERT(task);
if (task) {
QString dest = task->destPath();
HDEBUG(qPrintable(task->srcPath()) << "->" << qPrintable(dest) <<
"copy" << (task->iDestItem ? "done" : "FAILED"));
Data* data = task->iDestData;
const int row = iList.indexOf(data);
HASSERT(row >= 0);
BooksBook* copy = NULL;
BooksBook* src = data->book();
HASSERT(src);
if (src) {
src->retain();
if (task->iDestItem) {
copy = task->iDestItem->book();
if (copy) {
copy->retain();
copy->setPageStack(src->pageStack(), src->pageStackPos());
copy->setCoverImage(src->coverImage());
copy->requestCoverImage();
} else {
HWARN("not a book:" << qPrintable(dest));
}
}
}
// Disassociate book data from the copy task
data->iCopyTask = NULL;
task->iDestData = NULL;
task->release(this);
// Notify the source shelf. This will actually remove the source file.
if (copy) {
Q_EMIT src->movedAway();
}
if (src) {
src->setCopyingOut(false);
src->release();
}
if (copy) {
// We own this item now, connect the signals
data->setBook(copy, false);
Q_EMIT bookAdded(copy);
// The entire row has changed
QModelIndex index(createIndex(row, 0));
Q_EMIT dataChanged(index, index);
} else {
remove(row);
}
}
}
void BooksShelf::onDeleteTaskDone()
{
DeleteTask* task = qobject_cast<DeleteTask*>(sender());
HASSERT(task);
if (task) {
task->release(this);
HVERIFY(iDeleteTasks.removeOne(task));
}
}
void BooksShelf::deleteFiles()
{
if (iStorage.isValid()) {
QString path(iStorage.fullPath(iRelativePath));
HDEBUG("removing" << path);
if (!QDir(path).removeRecursively()) {
HWARN("some content couldn't be deleted under" << path);
}
path = iStorage.configDir().path() + "/" + iRelativePath;
HDEBUG("removing" << path);
if (!QDir(path).removeRecursively()) {
HWARN("some content couldn't be deleted under" << path);
}
}
}
BooksItem* BooksShelf::copyTo(const BooksStorage& aStorage, QString aRelPath,
CopyOperation* aObserver)
{
HWARN("copying folders is not implemented!!");
return NULL;
}
QHash<int,QByteArray> BooksShelf::roleNames() const
{
QHash<int, QByteArray> roles;
roles.insert(BooksItemName, "name");
roles.insert(BooksItemBook, "book");
roles.insert(BooksItemShelf, "shelf");
roles.insert(BooksItemAccessible, "accessible");
roles.insert(BooksItemCopyingOut, "copyingOut");
roles.insert(BooksItemCopyingIn, "copyingIn");
roles.insert(BooksItemCopyProgress, "copyProgress");
roles.insert(BooksItemDummy, "dummy");
roles.insert(BooksItemDeleteRequested, "deleteRequested");
return roles;
}
int BooksShelf::rowCount(const QModelIndex&) const
{
return iList.count();
}
QVariant BooksShelf::data(const QModelIndex& aIndex, int aRole) const
{
const int i = aIndex.row();
if (validIndex(i)) {
Data* data = iList.at(i);
switch (aRole) {
case BooksItemName: return data->name();
case BooksItemBook: return QVariant::fromValue(data->book());
case BooksItemShelf: return QVariant::fromValue(data->shelf());
case BooksItemAccessible: return data->accessible();
case BooksItemCopyingOut: return data->copyingOut();
case BooksItemCopyingIn: return data->copyingIn();
case BooksItemCopyProgress: return data->copyProgress();
case BooksItemDummy: return QVariant::fromValue(!data->iItem);
case BooksItemDeleteRequested: return data->iDeleteRequested;
}
}
return QVariant();
}
#include "BooksShelf.moc"