Lots of fixes, VFS now seems to work reliably. Still a lot of cleanup and shoring to do.

Decided to leave the VFS tab for now. Filtering likely doesn't work, except Show Only Open. Sorting works.
Rewrites to replace dynamicRoles done for Mods and Plugins tables.
Database.qml now reads "moddir", though installing extracted is still unimplemented so far.
Launch still doesn't work.
Ignore the "file -> test extract". It's a test for file extraction.
This commit is contained in:
Daniel O'Neill 2025-11-10 13:15:26 -08:00
parent 4bd0e980f3
commit 191cd5362a
21 changed files with 853 additions and 461 deletions

View file

@ -144,11 +144,10 @@ QList< QPair< int, QString > > FuseAccessorArchive::activeArchives(QSqlDatabase
int FuseAccessorArchive::makeWritable(struct fuse_file_info *fi, const QString &origsource, const QString &path, const QByteArray &fullData, QIODeviceBase::OpenMode mode, const QByteArray &data, off_t offset)
{
FuseFHBase *fh = m_parent->makeWritableFromData(fullData, origsource, path, mode);
FuseFHBase *fh = m_parent->makeWritableFromData(fi, fullData, origsource, path, mode);
if( !fh )
return -1;
fi->fh = reinterpret_cast<quint64>(fh);
return fh->write(data, offset, fi);
}
@ -223,7 +222,7 @@ int FuseAccessorArchive::getattr(const QString &path, struct stat *stbuf)
}
}
//qDebug() << "Not found.";
//qDebug() << "Not found:" << path;
m_statCache[path] = NULL;
return -ENOENT;
}
@ -248,6 +247,12 @@ QStringList FuseAccessorArchive::readdir(const QString &path)
if( !mapping.m_path.startsWith(lpath) )
continue;
/* Avoid cases where a dir and filename share a starting:
* eg: Data/f4ee, Data/f4ee.ini, etc.
*/
if( QDir::separator() != mapping.m_path.at(lpath.length()) )
continue;
QStringList parts = mapping.m_path.mid(lplen).toLower().split( QDir::separator(), Qt::SkipEmptyParts );
QString nent = parts.at(0);
if( results.contains(nent) )
@ -353,7 +358,7 @@ bool FuseAccessorArchive::slurpData(const QString &path, const QString &origpath
m_mutex.unlock();
qDebug() << "Caching" << path << "which is" << data.length() << "bytes.";
m_parent->cacheInsert(origpath, data);
m_parent->cacheInsert(this, origpath, data);
return true;
}
@ -362,7 +367,7 @@ QSharedPointer<QByteArray> FuseAccessorArchive::cacheGrab(const QString &path)
{
if(QSharedPointer<QByteArray> sptr = m_parent->cacheGrab(path))
return sptr;
return QSharedPointer<QByteArray>();
return {};
}
FuseFHArchive::FuseFHArchive(FuseAccessorBase *parent, const QString &path, const QString &newpath)
@ -436,6 +441,7 @@ int FuseFHArchive::write(const QByteArray &data, off_t offset, struct fuse_file_
QByteArray dcopy(entry->constData(), entry->length());
int rval = accessor->makeWritable(fi, m_path, m_newpath, dcopy, m_mode, data, offset);
release();
qDebug() << "FuseFHArchive::write: is now writable. So long, pardner!" << rval;
deleteLater();
return rval;
}

View file

@ -60,6 +60,7 @@ public:
virtual ~FuseFHBase() {}
FuseAccessorBase *getAccessor() { return m_parent; }
virtual const QString getResource() { return m_parent->getResource(); }
const QString &getPath() { return m_path; }
virtual bool open(QIODeviceBase::OpenMode mode) = 0;

View file

@ -8,6 +8,9 @@
#include <string.h>
#include <sys/mount.h>
#include <QSqlError>
#include <QSqlQuery>
#define CACHE_ENABLED true
//#define FUSE_DEBUG 1
@ -125,6 +128,8 @@ int FuseInterface::pump()
return ret;
fuse_session_process_buf(m_fuse_session, &fbuf);
free(fbuf.mem);
} while(0 == ret);
return ret;
}
@ -186,6 +191,8 @@ void FuseInterface::init()
qDebug() << "Adding game data dir path:" << m_gameDataDir;
append(vanilla);
readDeleted();
m_mutex.unlock();
}
@ -197,6 +204,7 @@ void FuseInterface::clear()
fab->deleteLater();
}
m_accessors.clear();
m_deleted.clear();
m_dircache.clear();
for( struct stat *st : std::as_const(m_cache_stat) )
free(st);
@ -241,7 +249,22 @@ int FuseInterface::utilQPermsToMode(QFileDevice::Permissions mode)
return smode;
}
*/
FuseFHBase *FuseInterface::makeWritableFromSource(const QString &source, const QString &origsource, const QString &dest, QIODeviceBase::OpenMode mode)
/*
* In FUSE3, the kernel treats fuse_file_info.fh as opaque state owned by the userspace daemon, but its
* copied once at open time into the kernels internal struct file. After that, every subsequent operation
* (read/write/release/etc.) gets a copy of that fh value from the kernel, not a live reference back into
* userspace memory.
*/
FuseFHBase *FuseInterface::getFH(struct fuse_file_info *fi)
{
if( m_cow_redirects.contains(fi->fh) )
return m_cow_redirects.value(fi->fh);
return reinterpret_cast<FuseFHBase *>(fi->fh);
}
FuseFHBase *FuseInterface::makeWritableFromSource(fuse_file_info *fi, const QString &source, const QString &origsource, const QString &dest, QIODeviceBase::OpenMode mode)
{
m_mutex.lock();
for( FuseAccessorBase *fab : std::as_const(m_accessors) )
@ -251,6 +274,8 @@ FuseFHBase *FuseInterface::makeWritableFromSource(const QString &source, const Q
FuseAccessorSandbox *sandbox = reinterpret_cast<FuseAccessorSandbox *>(fab);
FuseFHBase *fh = sandbox->makeWritableFromSource(source, origsource, dest, mode);
m_cow_redirects.insert(fi->fh, fh);
cacheInvalidate(dest);
m_mutex.unlock();
return fh;
}
@ -258,7 +283,7 @@ FuseFHBase *FuseInterface::makeWritableFromSource(const QString &source, const Q
return NULL;
}
FuseFHBase *FuseInterface::makeWritableFromData(const QByteArray &source, const QString &origsource, const QString &dest, QIODeviceBase::OpenMode mode)
FuseFHBase *FuseInterface::makeWritableFromData(struct fuse_file_info *fi, const QByteArray &source, const QString &origsource, const QString &dest, QIODeviceBase::OpenMode mode)
{
m_mutex.lock();
for( FuseAccessorBase *fab : std::as_const(m_accessors) )
@ -268,11 +293,13 @@ FuseFHBase *FuseInterface::makeWritableFromData(const QByteArray &source, const
FuseAccessorSandbox *sandbox = reinterpret_cast<FuseAccessorSandbox *>(fab);
FuseFHBase *fh = sandbox->makeWritableFromData(source, origsource, dest, mode);
m_cow_redirects.insert(fi->fh, fh);
cacheInvalidate(dest);
m_mutex.unlock();
return fh;
}
m_mutex.unlock();
qDebug() << "FuseInterface::makeWritableFromData: Couldn't do it.";
return NULL;
}
@ -289,25 +316,101 @@ static QString humanSize(qint64 bytes)
return QString::number(size, 'f', (i == 0 ? 0 : 2)) + ' ' + suffixes[i];
}
void FuseInterface::cacheInsert(const QString &path, QByteArray &data)
void FuseInterface::cacheInsert(FuseAccessorBase *fab, const QString &path, const QByteArray &data)
{
if( data.isEmpty() )
if (data.isEmpty())
return;
QSharedPointer<QByteArray> sharedData = QSharedPointer<QByteArray>::create(std::move(data));
QSharedPointer<QByteArray> *val = new QSharedPointer<QByteArray>(std::move(sharedData));
m_cache_data.insert(path, val, val->data()->size());
QString qpath(path.toLower());
auto val = QSharedPointer<QByteArray>::create(data);
m_cache_data.insert(qpath, new QSharedPointer<QByteArray>(std::move(val)), val->size());
qDebug() << " - m_cache_data now contains" << m_cache_data.size() << "entries, totalling" << humanSize(m_cache_data.totalCost()) << "bytes";
emit entryUpdated(path, "Archive", 1, 1);
emit entryUpdated(qpath, fab->getType(), fab->getResource(), 1, 1);
}
QSharedPointer<QByteArray> FuseInterface::cacheGrab(const QString &path)
{
if( !m_cache_data.contains(path) )
if (auto ptr = m_cache_data.object(path.toLower()))
return *ptr;
return {};
}
/*
void FuseInterface::cacheInsert(FuseAccessorBase *fab, const QString &path, const QByteArray &data)
{
if( data.isEmpty() )
return;
QString qpath(path.toLower());
//QSharedPointer<QByteArray> val = QSharedPointer<QByteArray>::create(data);
//m_cache_data.insert(qpath, new QSharedPointer<QByteArray>(val), val->size());
m_cache_data.insert(qpath, new QByteArray(data), data.size());
qDebug() << " - m_cache_data now contains" << m_cache_data.size() << "entries, totalling" << humanSize(m_cache_data.totalCost()) << "bytes";
emit entryUpdated(qpath, fab->getType(), fab->getResource(), 1, 1);
}
QByteArray FuseInterface::cacheGrab(const QString &path)
{
QString qpath(path.toLower());
if( !m_cache_data.contains(qpath) )
return {};
auto *p = m_cache_data.object(path);
return *p;
auto *data = m_cache_data.object(qpath);
return data ? *data : QByteArray();
}
*/
void FuseInterface::cacheInvalidate(const QString &path)
{
QString qpath = path.toLower();
//qDebug() << "FuseInterface::cacheInvalidate:" << path;
if( m_cache_data.contains(qpath) )
m_cache_data.remove(qpath);
if( m_dircache.contains(qpath) )
m_dircache.remove(qpath);
if( m_cache_stat.contains(qpath) ) {
struct stat *st = m_cache_stat.take(qpath);
free(st);
}
if( m_cache_accessor.contains(qpath) )
m_cache_accessor.remove(qpath);
}
bool FuseInterface::markDeleted(const QString &path, bool isdeleted)
{
if( isdeleted )
m_deleted.append(path.toLower());
else
m_deleted.removeAll(path.toLower());
QString qstr = QString("REPLACE INTO proxyOverrides(path, deleted)VALUES(?, %1)").arg(isdeleted ? 1 : 0);
qDebug() << qstr << path;
QSqlQuery q = QSqlQuery(qstr, m_database);
q.bindValue(0, path);
bool ret = q.exec();
if( !ret )
qDebug() << m_database.lastError();
return ret;
}
void FuseInterface::readDeleted()
{
m_deleted.clear();
QString qstr = QString("SELECT path, deleted FROM proxyOverrides WHERE deleted=1");
qDebug() << qstr;
QSqlQuery q = QSqlQuery(qstr, m_database);
while( q.next() )
{
QString path = q.value(0).toString();
m_deleted.append(path.toLower());
}
}
int FuseInterface::getattr(const char *path, struct stat *stbuf, struct fuse_file_info *fi)
@ -316,13 +419,17 @@ int FuseInterface::getattr(const char *path, struct stat *stbuf, struct fuse_fil
int ret;
QString qpath(path);
QString lpath = qpath.toLower();
if( CACHE_ENABLED && m_cache_stat.contains(qpath) )
if( m_deleted.contains(lpath) )
return -ENOENT;
if( CACHE_ENABLED && m_cache_stat.contains(lpath) )
{
if( !m_cache_stat[qpath] )
if( !m_cache_stat[lpath] )
return -ENOENT;
memcpy( stbuf, m_cache_stat[qpath], sizeof(struct stat) );
memcpy( stbuf, m_cache_stat[lpath], sizeof(struct stat) );
return 0;
}
@ -332,13 +439,13 @@ int FuseInterface::getattr(const char *path, struct stat *stbuf, struct fuse_fil
if( 0 == ret ) {
struct stat *scache = (struct stat *)malloc(sizeof(struct stat));
memcpy( scache, stbuf, sizeof(struct stat) );
m_cache_stat.insert(qpath, scache);
m_cache_accessor.insert(qpath, ent);
m_cache_stat.insert(lpath, scache);
//m_cache_accessor.insert(qpath, ent);
return 0;
}
}
m_cache_stat.insert(qpath, NULL);
m_cache_stat.insert(lpath, NULL);
return -ENOENT;
}
@ -362,16 +469,16 @@ int FuseInterface::create(const char *path, mode_t mode, struct fuse_file_info *
break;
}
if( CACHE_ENABLED && m_cache_stat.contains(qpath) )
{
struct stat *st = m_cache_stat.take(qpath);
if( st )
free(st);
}
if( CACHE_ENABLED )
cacheInvalidate(qpath);
m_mutex.unlock();
if( 0 == ret && sb )
if( 0 == ret && sb ) {
if( m_deleted.contains(qpath.toLower()) )
markDeleted(qpath, false);
return open(path, fi);
}
return ret;
}
@ -393,16 +500,11 @@ int FuseInterface::mkdir(const char *path, mode_t mode)
break;
}
if( CACHE_ENABLED ) {
if( m_cache_stat.contains(qpath) )
{
struct stat *st = m_cache_stat.take(qpath);
if( st )
free(st);
}
if( m_dircache.contains(qpath) )
m_dircache.remove(qpath);
}
if( CACHE_ENABLED )
cacheInvalidate(qpath);
if( 0 == ret && m_deleted.contains(qpath.toLower()) )
markDeleted(qpath, false);
m_mutex.unlock();
return ret;
@ -410,12 +512,21 @@ int FuseInterface::mkdir(const char *path, mode_t mode)
int FuseInterface::unlink(const char *path)
{
int ret = 1;
bool isFound = false;
int ret = -ENOENT;
QString qpath(path);
struct stat st;
m_mutex.lock();
for( FuseAccessorBase *fab : std::as_const(m_accessors) )
{
int sret = fab->getattr(qpath, &st);
if( 0 == sret )
isFound = true;
else
continue;
if( FuseAccessorBase::FH_SANDBOX != fab->getType() )
continue;
@ -424,15 +535,12 @@ int FuseInterface::unlink(const char *path)
break;
}
if( CACHE_ENABLED ) {
if( m_cache_stat.contains(qpath) )
{
struct stat *st = m_cache_stat.take(qpath);
if( st )
free(st);
}
if( m_dircache.contains(qpath) )
m_dircache.remove(qpath);
if( CACHE_ENABLED )
cacheInvalidate(qpath);
if( isFound ) {
markDeleted(qpath, true);
ret = 0;
}
m_mutex.unlock();
@ -455,16 +563,11 @@ int FuseInterface::rmdir(const char *path)
break;
}
if( CACHE_ENABLED ) {
if( m_cache_stat.contains(qpath) )
{
struct stat *st = m_cache_stat.take(qpath);
if( st )
free(st);
}
if( m_dircache.contains(qpath) )
m_dircache.remove(qpath);
}
if( CACHE_ENABLED )
cacheInvalidate(qpath);
if( 0 == ret )
markDeleted(qpath, true);
m_mutex.unlock();
return ret;
@ -495,12 +598,11 @@ int FuseInterface::truncate(const char *path, off_t offset, struct fuse_file_inf
break;
}
if( CACHE_ENABLED && m_cache_stat.contains(qpath) )
{
struct stat *st = m_cache_stat.take(qpath);
if( st )
free(st);
}
if( CACHE_ENABLED )
cacheInvalidate(qpath);
if( 0 == ret && m_deleted.contains(qpath.toLower()) )
markDeleted(qpath, false);
m_mutex.unlock();
return ret;
@ -516,11 +618,17 @@ int FuseInterface::readdir(const char *path, void *buf, fuse_fill_dir_t filler,
Q_UNUSED(flags)
QString qpath(path);
if( CACHE_ENABLED && m_dircache.contains(qpath) )
QString lpath = qpath.toLower();
if( CACHE_ENABLED && m_dircache.contains(lpath) )
{
QStringList ents = m_dircache[qpath];
for( const QString &ent : std::as_const(ents) )
QStringList ents = m_dircache[lpath];
for( const QString &ent : std::as_const(ents) ) {
QString qipath = QString("%1/%2").arg(qpath).arg(ent);
if( m_deleted.contains(qipath.toLower()) )
continue;
filler(buf, ent.toStdString().c_str(), NULL, 0, FUSE_FILL_DIR_DEFAULTS);
}
return 0;
}
@ -530,6 +638,10 @@ int FuseInterface::readdir(const char *path, void *buf, fuse_fill_dir_t filler,
QStringList dir = fab->readdir(QString(path));
for( const QStringView nent : std::as_const(dir) )
{
QString qipath = QString("%1/%2").arg(qpath).arg(nent);
if( m_deleted.contains(qipath.toLower()) )
continue;
QString blorp = nent.toString();
QString lc = blorp.toLower();
if( lents.contains(lc) )
@ -542,16 +654,16 @@ int FuseInterface::readdir(const char *path, void *buf, fuse_fill_dir_t filler,
}
if( CACHE_ENABLED )
m_dircache[qpath] = ents;
m_dircache[lpath] = ents;
return 0;
}
int FuseInterface::write(const char *path, const char *buf, size_t bufsz, off_t offset, struct fuse_file_info *fi)
{
Q_UNUSED(path)
FuseFHBase *fhbase = reinterpret_cast<FuseFHBase *>(fi->fh);
qDebug() << "FuseFHBase:" << fhbase;
QString qpath(path);
FuseFHBase *fhbase = getFH(fi);
//qDebug() << "FuseInterface::write:" << fi << fhbase << path;
//FuseAccessorBase *fabase = fhbase->getAccessor();
/*
@ -561,11 +673,14 @@ int FuseInterface::write(const char *path, const char *buf, size_t bufsz, off_t
FuseFHBase *sfh = qobject_cast<FuseFHBase *>(fhbase);
QByteArray data(buf, bufsz);
if( CACHE_ENABLED )
cacheInvalidate(qpath);
return sfh->write(data, offset, fi);
}
int FuseInterface::open(const char *path, struct fuse_file_info *fi)
{
bool checkDeleted = false;
int fam = (fi->flags & O_ACCMODE);
QString qpath(path);
@ -586,13 +701,19 @@ int FuseInterface::open(const char *path, struct fuse_file_info *fi)
break;
default:
qmode = QIODeviceBase::ReadOnly;
checkDeleted = true;
}
if( !( O_CREAT & fi->flags ) )
if( !( O_CREAT & fi->flags ) ) {
qmode |= QIODeviceBase::ExistingOnly;
checkDeleted = true;
}
if( checkDeleted && m_deleted.contains(qpath.toLower()) )
return -ENOENT;
// STATS:
VFSStatEntry se { qpath, QDateTime::currentSecsSinceEpoch(), 0, 0 };
VFSStatEntry se { qpath, QDateTime::currentSecsSinceEpoch(), 0, 0, 1 };
if( CACHE_ENABLED
&& QIODeviceBase::ReadOnly & qmode
@ -610,7 +731,7 @@ int FuseInterface::open(const char *path, struct fuse_file_info *fi)
}
m_stats[qpath].openCount++;
m_stats[qpath].refCount++;
emit entryUpdated(qpath, fab->getResource(), m_stats[qpath].openCount, m_stats[qpath].refCount);
emit entryUpdated(qpath.toLower(), fab->getType(), fh->getResource(), m_stats[qpath].openCount, m_stats[qpath].refCount);
return 0;
}
@ -632,12 +753,12 @@ int FuseInterface::open(const char *path, struct fuse_file_info *fi)
m_open_handles.prepend(fh);
if( !m_stats.contains(qpath) ) {
qDebug() << "Adding2" << qpath;
qDebug() << "Adding2" << qpath << fh << qmode;
m_stats.insert(qpath, se);
}
m_stats[qpath].openCount++;
m_stats[qpath].refCount++;
emit entryUpdated(qpath, fab->getResource(), m_stats[qpath].openCount, m_stats[qpath].refCount);
emit entryUpdated(qpath.toLower(), fab->getType(), fh->getResource(), m_stats[qpath].openCount, m_stats[qpath].refCount);
return 0;
}
@ -647,23 +768,31 @@ int FuseInterface::open(const char *path, struct fuse_file_info *fi)
int FuseInterface::release(const char *path, struct fuse_file_info *fi)
{
Q_UNUSED(path)
//Q_UNUSED(path)
FuseFHBase *fhbase = reinterpret_cast<FuseFHBase *>(fi->fh);
m_mutex.lock();
FuseFHBase *fhbase = getFH(fi);
qDebug() << "FuseInterface::release:" << fi << fhbase << path;
QString qpath = fhbase->getPath();
VFSStatEntry se { qpath, QDateTime::currentSecsSinceEpoch(), 0, 0 };
VFSStatEntry se { qpath, QDateTime::currentSecsSinceEpoch(), 0, 0, 1 };
if( !m_stats.contains(qpath) )
qDebug() << "Warning:" << qpath << "release, wasn't opened. Name was changed?";
else {
m_stats[qpath].refCount--;
emit entryUpdated(qpath, fhbase->getAccessor()->getResource(), m_stats[qpath].openCount, m_stats[qpath].refCount);
emit entryUpdated(qpath.toLower(), fhbase->getAccessor()->getType(), fhbase->getResource(), m_stats[qpath].openCount, m_stats[qpath].refCount);
}
m_open_handles.removeOne(fhbase);
fhbase->release();
fhbase->deleteLater();
if( m_cow_redirects.contains(fi->fh))
m_cow_redirects.remove(fi->fh);
m_mutex.unlock();
return 0;
}
@ -671,7 +800,7 @@ int FuseInterface::read(const char *path, char *buf, size_t size, off_t offset,
{
Q_UNUSED(path)
FuseFHBase *fhbase = reinterpret_cast<FuseFHBase *>(fi->fh);
FuseFHBase *fhbase = getFH(fi);
QByteArray data = fhbase->read(size, offset);
if( data.isEmpty() )
return 0;
@ -684,7 +813,7 @@ off_t FuseInterface::lseek(const char *path, off_t off, int whence, struct fuse_
{
Q_UNUSED(path)
FuseFHBase *fhbase = reinterpret_cast<FuseFHBase *>(fi->fh);
FuseFHBase *fhbase = getFH(fi);
//FuseAccessorBase *fabase = fhbase->getAccessor();
return fhbase->lseek(off, whence);
}

View file

@ -29,6 +29,7 @@ struct VFSStatEntry {
time_t lastAccess;
quint32 openCount;
quint32 refCount;
qint8 type;
};
class FuseAccessorBase;
@ -52,13 +53,16 @@ class FuseInterface : public QObject
QList< FuseAccessorBase * > m_accessors;
QList< FuseFHBase * > m_open_handles;
QStringList m_deleted;
QMap< QString, QStringList > m_dircache;
QMap<QString, struct stat *> m_cache_stat;
QMap<QString, FuseAccessorBase *> m_cache_accessor;
QCache< QString, QSharedPointer<QByteArray> > m_cache_data;
QCache<QString, QSharedPointer<QByteArray>> m_cache_data;
QFileSystemWatcher m_watcher;
QHash< QString, VFSStatEntry > m_stats;
QMap< qint64, FuseFHBase * > m_cow_redirects;
public:
explicit FuseInterface(QSqlDatabase &database, int profileId, const QString &gameDataDir, const QString &sandboxDir, const QString &mountpoint, QObject *parent=nullptr);
~FuseInterface();
@ -76,11 +80,17 @@ public:
int getProfileId() { return m_profileId; }
//int utilQPermsToMode(QFileDevice::Permissions perms);
FuseFHBase *makeWritableFromSource(const QString &source, const QString &origsource, const QString &dest, QIODeviceBase::OpenMode mode);
FuseFHBase *makeWritableFromData(const QByteArray &source, const QString &origsource, const QString &dest, QIODeviceBase::OpenMode mode);
FuseFHBase *getFH(struct fuse_file_info *fi);
FuseFHBase *makeWritableFromSource(struct fuse_file_info *fi, const QString &source, const QString &origsource, const QString &dest, QIODeviceBase::OpenMode mode);
FuseFHBase *makeWritableFromData(struct fuse_file_info *fi, const QByteArray &source, const QString &origsource, const QString &dest, QIODeviceBase::OpenMode mode);
void cacheInsert(const QString &path, QByteArray &data);
void cacheInsert(FuseAccessorBase *fab, const QString &path, const QByteArray &data);
//QByteArray cacheGrab(const QString &path);
QSharedPointer<QByteArray> cacheGrab(const QString &path);
void cacheInvalidate(const QString &path);
bool markDeleted(const QString &path, bool isdeleted=true);
void readDeleted();
protected:
void init();
@ -121,7 +131,7 @@ protected:
static off_t f_lseek(const char *path, off_t off, int whence, struct fuse_file_info *fi);
signals:
void entryUpdated(const QString &path, const QString &facility, quint32 count, quint8 refcount);
void entryUpdated(const QString &path, qint8 type, const QString &facility, quint32 count, quint8 refcount);
};
Q_DECLARE_METATYPE(FuseInterface *)

View file

@ -114,21 +114,9 @@ QList<FuseProxyMapping> FuseAccessorProxy::proxyList(QSqlDatabase &database, int
}
int FuseAccessorProxy::makeWritable(struct fuse_file_info *fi, const QString &path, QIODeviceBase::OpenMode mode, const QByteArray &data, off_t offset)
{
QString qpath = m_resource + QDir::separator() + resolvePath( m_resource, path );
FuseFHBase *fh = m_parent->makeWritableFromSource(qpath, path, path, mode);
if( !fh )
return -1;
fi->fh = reinterpret_cast<quint64>(fh);
return fh->write(data, offset, fi);
}
/***********/
int FuseAccessorProxy::getattr(const QString &path, struct stat *stbuf)
{
if( !m_entries.isEmpty() ) {
qDebug() << "FuseAccessorProxy::makeWritable: Looking for" << path;
QString lpath = path.toLower();
QStringList lparts = lpath.split(QDir::separator());
for( const FuseProxyMapping &ent : std::as_const(m_entries) )
@ -139,8 +127,55 @@ int FuseAccessorProxy::getattr(const QString &path, struct stat *stbuf)
nomatch = true;
}
if( nomatch )
if( nomatch ) {
//qDebug() << "FuseAccessorProxy::getattr: skipping" << ent.mapped;
continue;
}
if( lparts.length() < ent.lparts.length() )
{
qDebug() << "FuseAccessorProxy::getattr: Found dir" << lparts.join(QDir::separator()) << "in" << ent.lparts.join(QDir::separator());
return -EACCES;
}
QString qpath = ent.basedir + QDir::separator() + ent.actual;
FuseFHBase *fh = m_parent->makeWritableFromSource(fi, qpath, path, path, mode);
if( !fh )
return -1;
return fh->write(data, offset, fi);
}
}
QString qpath = m_resource + QDir::separator() + resolvePath( m_resource, path );
qDebug() << "FuseAccessorProxy::makeWritable:" << qpath;
FuseFHBase *fh = m_parent->makeWritableFromSource(fi, qpath, path, path, mode);
if( !fh )
return -1;
return fh->write(data, offset, fi);
}
/***********/
int FuseAccessorProxy::getattr(const QString &path, struct stat *stbuf)
{
if( !m_entries.isEmpty() ) {
qDebug() << "FuseAccessorProxy::getattr: Looking for" << path;
QString lpath = path.toLower();
QStringList lparts = lpath.split(QDir::separator());
for( const FuseProxyMapping &ent : std::as_const(m_entries) )
{
bool nomatch = false;
for( int x=1; !nomatch && x < lparts.length() && x < ent.lparts.length(); x++ ) {
if( lparts[x] != ent.lparts[x] )
nomatch = true;
}
if( nomatch ) {
//qDebug() << "FuseAccessorProxy::getattr: skipping" << ent.mapped;
continue;
}
if( lparts.length() < ent.lparts.length() )
{
@ -179,13 +214,15 @@ QStringList FuseAccessorProxy::readdir(const QString &path)
if( !ent.lmapped.startsWith(lpath) )
continue;
qDebug() << "readdir:" << lpath << ent.lmapped << ent.actual;
//qDebug() << "readdir:" << lpath << ent.lmapped << ent.actual;
int trim = lpath.length();
QStringList sent = ent.mapped.mid(trim).split(QDir::separator());
// Eg: lpath="/data" & ent.mapped="/data/meshes/..." -> sent[0] = "", sent[1] = "meshes"
if( sent.at(0).isEmpty() )
sent.removeFirst();
if( !sent.at(0).isEmpty() )
continue;
sent.removeFirst();
//qDebug() << "sent:" << sent;
//results.append(m_entries[ent]);
@ -225,7 +262,7 @@ FuseFHBase *FuseAccessorProxy::open(const QString &path, QIODeviceBase::OpenMode
if( nomatch )
continue;
if( lparts.length() < ent.lparts.length() )
if( lparts.length() < ent.lparts.length() && QDir::separator() == ent.lparts.at(lparts.length()) )
{
qDebug() << "FuseAccessorArchive::open: Found dir" << lparts.join(QDir::separator()) << "in" << ent.lparts.join(QDir::separator());
return NULL;
@ -238,15 +275,17 @@ FuseFHBase *FuseAccessorProxy::open(const QString &path, QIODeviceBase::OpenMode
}
QFileInfo fi( qpath );
if( !fi.exists() && QIODeviceBase::ReadOnly & mode ) {
if( !fi.exists() && !( mode & QIODeviceBase::ReadWrite ) && !( mode & QIODeviceBase::WriteOnly ) ) {
//qDebug() << "FuseAccessorProxy::open:"<<qpath<<"doesn't exist";
return NULL;
}
/*
else if( fi.exists() && !(QIODeviceBase::ReadOnly & mode) )
{
qDebug() << "FuseAccessorProxy::open: ** Make writable:" << qpath << "--==>" << path << "--" << m_resource;
return m_parent->makeWritableFromSource(qpath, path, path, mode);
return m_parent->makeWritableFromSource(fi, qpath, path, path, mode);
}
*/
FuseFHProxy *f = new FuseFHProxy(this, path, qpath);
if( !f->open(mode) )
@ -272,6 +311,39 @@ FuseFHProxy::~FuseFHProxy()
FuseFHProxy::release();
}
const QString FuseFHProxy::getResource()
{
return m_file.fileName();
/*
QList< FuseProxyMapping > entries = m_parent->entries();
if( !m_entries.isEmpty() ) {
qDebug() << "FuseAccessorProxy::getattr: Looking for" << path;
QString lpath = path.toLower();
QStringList lparts = lpath.split(QDir::separator());
for( const FuseProxyMapping &ent : std::as_const(m_entries) )
{
bool nomatch = false;
for( int x=1; !nomatch && x < lparts.length() && x < ent.lparts.length(); x++ ) {
if( lparts[x] != ent.lparts[x] )
nomatch = true;
}
if( nomatch ) {
//qDebug() << "FuseAccessorProxy::getattr: skipping" << ent.mapped;
continue;
}
QString qpath = ent.basedir + QDir::separator() + ent.actual;
qDebug() << "FuseAccessorProxy::getattr:" << qpath;
int ret = ::stat(qpath.toStdString().c_str(), stbuf);
return ret;
}
}
*/
return m_parent->getResource();
}
bool FuseFHProxy::open(QIODeviceBase::OpenMode mode)
{
m_mode = mode;
@ -280,7 +352,7 @@ bool FuseFHProxy::open(QIODeviceBase::OpenMode mode)
QByteArray FuseFHProxy::read(size_t size, off_t offset)
{
if( offset >= 0 && !m_file.seek(offset) )
if( !m_file.seek(offset) )
return QByteArray();
return m_file.read(size);
@ -296,8 +368,10 @@ off_t FuseFHProxy::lseek(off_t off, int whence)
int FuseFHProxy::write(const QByteArray &data, off_t offset, struct fuse_file_info *fi)
{
if( m_mode & QIODeviceBase::ReadOnly )
if( !( m_mode & QIODeviceBase::ReadWrite ) && !( m_mode & QIODeviceBase::WriteOnly ) ) {
qDebug() << "FuseFHProxy::write: Write attempted on ReadOnly handle:" << m_path;
return -1;
}
//FuseArchiveEntry &entry = m_entries[lpath];
qDebug() << "FuseFHArchive::write: Upgrading" << m_path << "to writable...";

View file

@ -42,6 +42,8 @@ public:
// This expects the new entry to be created, then return "write" on the opened handle, otherwise return the appropriate "write" error code.
int makeWritable(struct fuse_file_info *fi, const QString &path, QIODeviceBase::OpenMode mode, const QByteArray &data, off_t offset);
QList< FuseProxyMapping > &entries() { return m_entries; }
};
@ -53,6 +55,8 @@ public:
explicit FuseFHProxy(FuseAccessorProxy *parent, const QString &path, const QString &newpath);
virtual ~FuseFHProxy();
const QString getResource() override;
bool open(QIODeviceBase::OpenMode mode) override;
QByteArray read(size_t size, off_t offset) override;
off_t lseek(off_t off, int whence) override;

View file

@ -138,10 +138,11 @@ FuseFHBase *FuseAccessorSandbox::makeWritableFromSource(const QString &source, c
return NULL;
}
QStringList parts = dest.toLower().split(QDir::separator(), Qt::SkipEmptyParts);
QString dfname = parts.takeLast();
//QStringList parts = dest.toLower().split(QDir::separator(), Qt::SkipEmptyParts);
//QString dfname = parts.takeLast();
//ddir.mkpath( parts.join(QDir::separator()) );
QDir ddir(m_resource);
ddir.mkpath( parts.join(QDir::separator()) );
QFile::copy(source, qdest);
qDebug() << "FuseAccessorSandbox::makeWritableFromSource: cp" << source << qdest;
@ -176,6 +177,7 @@ FuseFHBase *FuseAccessorSandbox::makeWritableFromData(const QByteArray &source,
FuseFHSandbox *ent = new FuseFHSandbox(this, origpath, qdest);
if( !ent->open(mode) )
{
qDebug() << "FuseAccessorSandbox::makeWritableFromData: FuseFHSandbox open failed";
ent->deleteLater();
return NULL;
}

View file

@ -128,14 +128,24 @@ Item {
if( !profileId )
profileId = 1;
let q = conn.query("SELECT m.modId, m.name, m.nexusGameCode, m.nexusId, m.nexusFileId, m.author, m.version, m.website, m.description, m.groups, (SELECT COUNT(ps.modId) FROM profile_selections ps WHERE ps.profileId=? AND ps.modId=m.modId) AS enabled, m.installed, m.filename FROM mods m", [profileId]);
let q = conn.query("SELECT m.modId, m.name, m.nexusGameCode, m.nexusId, m.nexusFileId, m.author, m.version, m.website, m.description, m.groups, (SELECT COUNT(ps.modId) FROM profile_selections ps WHERE ps.profileId=? AND ps.modId=m.modId) AS enabled, m.installed, m.filename, m.moddir FROM mods m", [profileId]);
const results = q.toArray();
q.destroy();
results.map( function(m) {
if( m['groups'].length > 0 )
m['groups'] = JSON.parse(m['groups']);
else m['groups'] = [];
try {
m["modId"] = parseInt(""+m["modId"]);
m["nexusId"] = parseInt(""+m["nexusId"]);
m["nexusFileId"] = parseInt(""+m["nexusFileId"]);
/*
if( m['groups'].length > 0 ) {
m['groups'] = JSON.parse(m['groups']);
} else m['groups'] = "";
*/
} catch(e) {
console.log(`getMods: Mod ${m['modId']} contains unparseable 'groups' data: `+m['groups']);
m['groups'] = "";
}
} );
return results;
@ -193,8 +203,8 @@ Item {
const tgroups = JSON.stringify(modinfo['groups']);
conn.transaction();
let q = conn.query("INSERT INTO mods (nexusGameCode, nexusId, nexusFileId, name, author, version, website, description, groups, installed, enabled, filename)VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
[modinfo['nexusGameCode'], modinfo['nexusId'], modinfo['nexusFileId'], modinfo['name'], modinfo['author'], modinfo['version'], modinfo['website'], modinfo['description'], tgroups, modinfo['installed'], modinfo['enabled'], modinfo['filename']]);
let q = conn.query("INSERT INTO mods (nexusGameCode, nexusId, nexusFileId, name, author, version, website, description, groups, installed, enabled, filename, moddir)VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
[modinfo['nexusGameCode'], modinfo['nexusId'], modinfo['nexusFileId'], modinfo['name'], modinfo['author'], modinfo['version'], modinfo['website'], modinfo['description'], tgroups, modinfo['installed'], modinfo['enabled'], modinfo['filename'], modinfo['moddir']]);
modinfo['modId'] = q.lastInsertId();
conn.commit();
@ -219,13 +229,7 @@ Item {
let q = conn.query("SELECT _rowId_ AS id, name FROM profiles");
const results = q.toArray();
q.destroy();
/*
results.map( function(m) {
if( m['groups'].length > 0 )
m['groups'] = JSON.parse(m['groups']);
else m['groups'] = [];
} );
*/
return results;
}
@ -267,7 +271,7 @@ Item {
{
if( !checkConnection() ) return;
let qstr = "SELECT f.fileId, f.modId, f.relative, f.source, f.dest, f.priority, m.enabled FROM files f JOIN mods m ON m.modId=f.modId";
let qstr = "SELECT f.fileId, f.modId, f.relative, f.source, f.dest, f.priority, m.enabled, m.name, m.description FROM files f JOIN mods m ON m.modId=f.modId";
let token = ' WHERE ';
let args = [];

View file

@ -18,6 +18,9 @@ Item {
signal deleteMod(variant mod)
signal writeRequested()
function forceLayout() {
modsList.forceLayout();
}
SplitView {
id: header
@ -87,8 +90,13 @@ Item {
height: row.height
width: row.width
readonly property var modent: modsModel.get(index)
readonly property int rowIndex: index
required property int index
required property var modelData
//readonly property var modent: dragArea.modelData
//readonly property var modent: modsModel.get(index)
//readonly property int rowIndex: index
readonly property int rowIndex: dmodel.modelIndex(index).row
//readonly property int modelIndex: dmodel.items.get(rowIndex).model.index
property bool held: false
@ -160,7 +168,7 @@ Item {
clip: true
color: (dragArea.rowIndex % 2) === 0 ? Material.background : Qt.darker(Material.background, 1.20)
color: (dragArea.index % 2) === 0 ? Material.background : Qt.darker(Material.background, 1.20)
Drag.active: dragArea.held
Drag.source: dragArea
@ -175,19 +183,19 @@ Item {
id: switchEnabled
y: height * 0.5
width: header.widths ? header.widths[0] + 4 : 50
enabled: dragArea.modent['installed']
checked: dragArea.modent['enabled']
enabled: modelData && modelData['installed']
checked: modelData && modelData['enabled']
onToggled: {
if( !dragArea.modent['enabled'] )
enableMod(dragArea.modent);
if( !modelData['enabled'] )
enableMod(modelData);
else
disableMod(dragArea.modent);
disableMod(modelData);
}
}
Label {
text: dragArea.modent ? dragArea.modent['modId'] : '???'
enabled: dragArea.modent['installed']
text: modelData ? modelData['modId'] : '???'
enabled: modelData && modelData['installed']
verticalAlignment: Text.AlignVCenter
leftPadding: 5
width: header.widths ? header.widths[1] + 4 : 100
@ -196,8 +204,8 @@ Item {
maximumLineCount: 1
}
Label {
text: dragArea.modent ? dragArea.modent['name'] : '???'
enabled: dragArea.modent['installed']
text: modelData ? modelData['name'] : '???'
enabled: modelData && modelData['installed']
verticalAlignment: Text.AlignVCenter
leftPadding: 5
width: header.widths ? header.widths[2] + 4 : 100
@ -207,8 +215,8 @@ Item {
}
Label {
id: cellText
text: dragArea.modent ? dragArea.modent['author'] : '???'
enabled: dragArea.modent['installed']
text: modelData ? modelData['author'] : '???'
enabled: modelData && modelData['installed']
verticalAlignment: Text.AlignVCenter
leftPadding: 5
height: parent.height
@ -217,8 +225,8 @@ Item {
maximumLineCount: 1
}
Label {
text: dragArea.modent ? dragArea.modent['version'] : '???'
enabled: dragArea.modent['installed']
text: modelData ? modelData['version'] : '???'
enabled: modelData && modelData['installed']
verticalAlignment: Text.AlignVCenter
leftPadding: 5
width: header.widths ? header.widths[4] + 4 : 100
@ -227,8 +235,8 @@ Item {
maximumLineCount: 1
}
Label {
text: dragArea.modent ? dragArea.modent['description'] : '???'
enabled: dragArea.modent['installed']
text: modelData ? modelData['description'] : '???'
enabled: modelData && modelData['installed']
verticalAlignment: Text.AlignVCenter
leftPadding: 5
height: parent.height
@ -237,8 +245,8 @@ Item {
maximumLineCount: 1
}
Label {
text: dragArea.modent ? dragArea.modent['website'] : '???'
enabled: dragArea.modent['installed']
text: modelData ? modelData['website'] : '???'
enabled: modelData && modelData['installed']
verticalAlignment: Text.AlignVCenter
leftPadding: 5
width: header.widths ? header.widths[6] + 4 : 100
@ -250,33 +258,45 @@ Item {
Menu {
id: cellMenu
MenuItem {
text: dragArea.modent && dragArea.modent['installed'] ? qsTr('Uninstall') : qsTr('Install')
onTriggered: {
if( dragArea.modent && dragArea.modent['installed'] )
uninstallMod(dragArea.modent);
else
installMod(dragArea.modent);
Menu {
enabled: !actionUninstall.enabled
title: qsTr('Install')
Action {
text: qsTr('Install Compressed')
onTriggered: installMod(modsModel.get(dragArea.rowIndex));
}
Action {
text: qsTr('Install Uncompressed')
onTriggered: installMod(modsModel.get(dragArea.rowIndex));
}
}
MenuItem {
text: dragArea.modent && dragArea.modent['enabled'] ? qsTr('Disable') : qsTr('Enable')
enabled: dragArea.modent && dragArea.modent['installed'] ? true:false
id: actionUninstall
text: qsTr('Uninstall')
enabled: modelData && modelData['installed']
onTriggered: uninstallMod(modsModel.get(dragArea.rowIndex));
}
MenuItem {
text: modelData && modelData['enabled'] ? qsTr('Disable') : qsTr('Enable')
enabled: modelData && modelData['installed'] ? true:false
onTriggered: {
if( !dragArea.modent['enabled'] )
enableMod(dragArea.modent);
if( !modelData['enabled'] )
enableMod(modsModel.get(dragArea.rowIndex));
else
disableMod(dragArea.modent);
disableMod(modsModel.get(dragArea.rowIndex));
}
}
MenuItem {
text: qsTr('Delete')
onTriggered: deleteMod(dragArea.modent);
onTriggered: deleteMod(modsModel.get(dragArea.rowIndex));
}
MenuItem {
text: qsTr('Re-configure')
enabled: dragArea.modent && dragArea.modent['installed'] ? true:false
onTriggered: reinstallMod(dragArea.modent);
enabled: modelData && modelData['installed'] ? true:false
onTriggered: reinstallMod(modsModel.get(dragArea.rowIndex));
}
}
@ -296,7 +316,7 @@ Item {
//enabled: !dragArea.held
onEntered: function(drag) {
console.log(`Drag: ${drag.source.DelegateModel.itemsIndex} -> ${dragArea.rowIndex}`);
console.log(`Drag: ${drag.source.DelegateModel.itemsIndex} -> ${dragArea.index}`);
dragArea.targeted = true;
dmodel.items.move(drag.source.DelegateModel.itemsIndex, dragArea.DelegateModel.itemsIndex);
}
@ -313,7 +333,27 @@ Item {
ListModel {
id: modsModel
//dynamicRoles: true
ListElement {
modId: 0
nexusId: 0
nexusFileId: 0
//groups: []
groups: ""
enabled: 0
installed: 0
name: ""
nexusGameCode: ""
author: ""
version: ""
website: ""
description: ""
filename: ""
moddir: ""
}
function some( scb )
{
for( let a=0; a < count; a++ )
@ -322,7 +362,7 @@ Item {
if( scb(ment) )
return a;
}
return false;
return -1;
}
}
@ -335,6 +375,10 @@ Item {
bottom: parent.bottom
}
contentWidth: header.width
ScrollBar.vertical.policy: ScrollBar.AlwaysOn
ScrollBar.horizontal.policy: ScrollBar.AlwaysOn
ListView {
id: modsList

View file

@ -14,7 +14,18 @@ Item {
ListModel {
id: pluginsModel
dynamicRoles: true
//dynamicRoles: true
ListElement {
modId: 0
name: ""
description: ""
enabled: false
filename: ""
missing: false
notfound: false
}
function some( scb )
{
for( let a=0; a < count; a++ )
@ -23,10 +34,39 @@ Item {
if( scb(ment) )
return a;
}
return false;
return -1;
}
}
function setModel(newModel) {
console.log("Mod master listed updated, rebuilding list!");
const model = pluginsModel;
const newLen = newModel.length;
const oldLen = model.count;
for( let a=0; a < newLen; a++ )
{
let nent = newModel[a];
if( a >= oldLen ) {
model.append(nent);
continue;
}
const mtents = model.get(a);
for( const k in nent ) {
if( nent[k] === mtents[k] )
continue;
model.setProperty(a, k, nent[k]);
}
//model.set(a, nent);
}
if( newLen < oldLen )
model.remove(newLen, oldLen-newLen);
}
/*
function setModel(newModel)
{
// Remove old:
@ -75,7 +115,7 @@ Item {
//console.log(` +++ ${JSON.stringify(ne)}`);
}
}
*/
SplitView {
id: header
x: 0-pluginsList.contentX
@ -131,6 +171,10 @@ Item {
bottom: parent.bottom
}
contentWidth: header.width
ScrollBar.vertical.policy: ScrollBar.AlwaysOn
ScrollBar.horizontal.policy: ScrollBar.AlwaysOn
ListView {
id: pluginsList
spacing: 0
@ -154,9 +198,11 @@ Item {
height: implicitHeight
width: implicitWidth
readonly property int rowIndex: index
readonly property int modelIndex: dmodel.items.get(rowIndex).model.index
property variant modent: pluginsModel.get(modelIndex)
required property int index
required property var modelData
readonly property int rowIndex: dmodel.modelIndex(index).row
property bool held: false
property bool targeted: false
property int previousIndex: -1
@ -247,13 +293,13 @@ Item {
Drag.hotSpot.y: height / 2
property color rowColour: (dragArea.rowIndex % 2) === 0 ? Material.background : Qt.darker(Material.background, 1.20)
property color validColour: dragArea.modent && dragArea.modent['notfound'] ? '#888800' : rowColour
property color validColour: modelData['notfound'] ? '#888800' : rowColour
color: dragArea.modent && dragArea.modent['missing'] ? '#880000' : validColour
color: modelData['missing'] ? '#880000' : validColour
ToolTip.visible: dragArea.containsMouse && dragArea.modent && (dragArea.modent['missing'] || dragArea.modent['notfound']) ? true : false
ToolTip.text: dragArea.modent && dragArea.modent['missing'] && dragArea.modent['missing'].length ? qsTr('Missing (or loaded out of order) masters will prevent this plugin from loading:\n\n%1').arg(dragArea.modent['missing'].join('\n'))
: dragArea.modent && dragArea.modent['notfound'] ? qsTr("The file for this entry can't be found, so it won't be loaded.") : ''
ToolTip.visible: dragArea.containsMouse && (modelData['missing'] || modelData['notfound']) ? true : false
ToolTip.text: modelData['missing'] && modelData['missing'].length ? qsTr('Missing (or loaded out of order) masters will prevent this plugin from loading:\n\n%1').arg(modelData['missing'].join('\n'))
: modelData && modelData['notfound'] ? qsTr("The file for this entry can't be found, so it won't be loaded.") : ''
Row {
id: realRow
@ -264,13 +310,13 @@ Item {
Switch {
id: cellEnabled
checked: dragArea.modent && dragArea.modent['enabled'] ? true : false
checked: modelData['enabled'] ? true : false
anchors.centerIn: parent
onClicked: {
if( !dragArea.modent['enabled'] )
enableMod(dragArea.modent);
if( !modelData['enabled'] )
enableMod(pluginsModel.get(dragArea.rowIndex));
else
disableMod(dragArea.modent);
disableMod(pluginsModel.get(dragArea.rowIndex));
}
indicator: Rectangle {
@ -304,21 +350,21 @@ Item {
Loader {
sourceComponent: textCell
property string modelText: dragArea.modent ? dragArea.modent['filename'] : '???'
property string modelText: modelData['filename'] || '???'
width: header.widths ? header.widths[ 1 ] + 5 : 24
height: 32
}
Loader {
sourceComponent: textCell
property string modelText: dragArea.modent ? dragArea.modent['name'] : '???'
property string modelText: modelData['name'] || '???'
width: header.widths ? header.widths[ 2 ] + 5 : 24
height: 32
}
Loader {
sourceComponent: textCell
property string modelText: dragArea.modent ? dragArea.modent['description'] : '???'
property string modelText: modelData['description'] || '???'
width: header.widths ? header.widths[ 3 ] + 5 : 24
height: 32
}
@ -327,12 +373,12 @@ Item {
Menu {
id: cellMenu
MenuItem {
text: dragArea.modent && dragArea.modent['enabled'] ? qsTr('Disable') : qsTr('Enable')
text: modelData['enabled'] ? qsTr('Disable') : qsTr('Enable')
onTriggered: {
if( !dragArea.modent['enabled'] )
enableMod(dragArea.modent);
if( !modelData['enabled'] )
enableMod(pluginsModel.get(dragArea.rowIndex));
else
disableMod(dragArea.modent);
disableMod(pluginsModel.get(dragArea.rowIndex));
}
}
}

View file

@ -1,68 +1,80 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Controls.Material
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Controls.Material 2.15
ScrollView {
id: root
clip: true
import Qt.labs.qmlmodels 1.0
// these are bound by the parent page
property variant model
property variant proxy
Item {
id: vfsTable
property int sortColumn: -1
property bool sortAscending: true
required property variant model
required property variant proxy
ColumnLayout {
anchors.fill: parent
spacing: 0
property int sortColumn: 0
property bool sortAscending: false
// ---------- HEADER ----------
Rectangle {
Layout.fillWidth: true
height: 28
color: Qt.lighter(Material.background, 1.05)
border.color: "#ccc"
readonly property var originName: [ 'Undefined', 'Unknown', 'Archive', 'Proxy', 'Sandbox' ]
readonly property var rowColours: [ '#808080','#ff0000',"#6ec8fa","#f59b14","#358611" ]
RowLayout {
anchors.fill: parent
spacing: 1
SplitView {
id: header
x: 0-logEntryList.contentX
height: 32
width: implicitWidth > parent.width ? implicitWidth * 2 : parent.width * 2
Repeater {
model: root.model ? root.model.columns : []
readonly property variant preferredWidths: [ 65, header.width*0.33, 64, header.width*0.50, 32, header.width*0.40, 32, 16 ]
property variant widths
delegate: Rectangle {
color: "transparent"
border.color: "#ccc"
Layout.fillHeight: true
Layout.preferredWidth: fontMetrics.boundingRect(modelData.title).width + 60
Repeater {
id: headerRepeater
model: [ qsTr('Open'), qsTr('Name'), qsTr('Origin'), qsTr('Facility'), qsTr('TotalOpen'), qsTr('Last Access'), qsTr('Open'), qsTr('') ]
readonly property variant sortRoleId: [ 3, 0, 4, 1, 2, 5, 3, 3 ]
property int sortColumn: 0
Label {
id: thisLabel
required property int index
required property var modelData
Row {
anchors.centerIn: parent
spacing: 4
Label { text: modelData.title; font.bold: true }
Label {
text: (root.sortColumn === index)
? (root.sortAscending ? "▲" : "▼")
: ""
font.pixelSize: 10
}
}
SplitView.minimumWidth: 48
text: modelData
verticalAlignment: Text.AlignVCenter
onWidthChanged: header.updateSizes();
font.pointSize: 10
font.bold: true
leftPadding: 5
rightPadding: 48
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.sortColumn === index)
root.sortAscending = !root.sortAscending
else
root.sortAscending = true
root.sortColumn = index
if (root.proxy)
root.proxy.sort(index, root.sortAscending
? Qt.AscendingOrder
: Qt.DescendingOrder)
Rectangle {
visible: thisLabel.index < headerRepeater.model.length-1
enabled: visible
color: 'transparent'
border.width: 1
border.color: thisLabel.index === sortColumn ? Material.accent : Material.frameColor
radius: 180
width: parent.height * 0.5
height: width
anchors {
rightMargin: 5
right: parent.right
verticalCenter: parent.verticalCenter
}
Text {
anchors.centerIn: parent
font.pixelSize: parent.height * 0.5
text: vfsTable.sortAscending ? '↑' : '↓'
visible: thisLabel.index === headerRepeater.sortColumn
color: Material.foreground
}
MouseArea {
anchors.fill: parent
onClicked: {
if( headerRepeater.sortColumn === thisLabel.index )
vfsTable.sortAscending = !vfsTable.sortAscending;
else {
headerRepeater.sortColumn = thisLabel.index;
vfsTable.sortColumn = headerRepeater.sortRoleId[ thisLabel.index ];
}
}
}
@ -70,59 +82,155 @@ ScrollView {
}
}
// ---------- TABLE ----------
TableView {
id: table
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
columnSpacing: 1
rowSpacing: 1
reuseItems: true
boundsBehavior: Flickable.StopAtBounds
model: root.proxy
function updateSizes() {
var newWidths = [];
for( let sidx=0; sidx < headerRepeater.count; sidx++ )
{
const hri = headerRepeater.itemAt(sidx);
if( !hri )
return;
newWidths.push( hri.width );
}
delegate: Rectangle {
implicitHeight: 26
color: styleData.selected
? Material.color(Material.Blue, Material.Shade100)
: "transparent"
header.widths = newWidths;
logEntryList.forceLayout();
}
Label {
text: modelData
anchors.left: parent.left
anchors.leftMargin: 8
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
width: parent.width - 8
Component.onCompleted: {
try {
if( settings.logEntryListColumnSizes )
{
header.restoreState( settings.logEntryListColumnSizes );
logEntryList.forceLayout();
return;
}
} catch(err) {
console.log("Couldn't restore column widths. Oh well.");
}
columnWidthProvider: function(col) {
const columns = root.model ? root.model.columns : []
if (!columns.length) return 100
const headerText = columns[col].title
const headerWidth = fontMetrics.boundingRect(headerText).width + 20
let maxCellWidth = headerWidth
const rowsToCheck = Math.min(100, table.rows)
for (let i = 0; i < rowsToCheck; ++i) {
const value = table.proxy.data(table.model.index(i, col))
if (value)
maxCellWidth = Math.max(
maxCellWidth,
fontMetrics.boundingRect(value.toString()).width + 20
)
}
return maxCellWidth
}
Component.onCompleted: {
const cols = root.model ? root.model.columns : []
for (let i = 0; i < cols.length; ++i)
table.addColumn({ role: "display" }) // DisplayRole matches modelData
}
FontMetrics { id: fontMetrics }
for( let sidx=0; sidx < headerRepeater.count; sidx++ )
headerRepeater.itemAt(sidx).width = preferredWidths[sidx] || 32;
logEntryList.forceLayout();
}
Component.onDestruction: {
settings.logEntryListColumnSizes = header.saveState();
}
}
Component {
id: rowDelegate
Rectangle {
id: row
required property int index
required property var modelData
//readonly property int rowIndex: vfsTable.proxy.modelIndex(index).row
width: labelRow.implicitWidth
height: labelRow.height
clip: true
color: (index % 2) === 0 ? Material.background : Qt.darker(Material.background, 1.20)
Row {
id: labelRow
height: childrenRect.height
spacing: 10
Item {
width: header.widths ? header.widths[0] : 50
height: parent.height
Rectangle {
anchors.centerIn: parent
radius: 180
color: row.modelData['refcount'] > 0 ? '#358611' : '#808080'
width: 16
height: 16
}
}
Label {
text: row.modelData['path']
verticalAlignment: Text.AlignVCenter
leftPadding: 5
width: header.widths ? header.widths[1] : 100
//height: parent.height
elide: Text.ElideRight
maximumLineCount: 1
}
Label {
text: vfsTable.originName[ row.modelData['type'] ]
verticalAlignment: Text.AlignVCenter
leftPadding: 5
width: header.widths ? header.widths[2] : 100
//height: parent.height
elide: Text.ElideRight
maximumLineCount: 1
color: vfsTable.rowColours[ row.modelData["type"] ]
}
Label {
text: row.modelData['facility']
verticalAlignment: Text.AlignVCenter
leftPadding: 5
width: header.widths ? header.widths[3] : 100
//height: parent.height
elide: Text.ElideRight
maximumLineCount: 1
color: vfsTable.rowColours[ row.modelData["type"] ]
}
Label {
text: row.modelData['count']
verticalAlignment: Text.AlignVCenter
leftPadding: 5
//height: parent.height
width: header.widths ? header.widths[4] : 100
elide: Text.ElideRight
maximumLineCount: 1
}
Label {
text: row.modelData['lastAccess']
verticalAlignment: Text.AlignVCenter
leftPadding: 5
//height: parent.height
width: header.widths ? header.widths[5] : 100
elide: Text.ElideRight
maximumLineCount: 1
}
Label {
text: row.modelData['refcount']
verticalAlignment: Text.AlignVCenter
leftPadding: 5
width: header.widths ? header.widths[6] : 100
//height: parent.height
elide: Text.ElideRight
maximumLineCount: 1
}
}
} // Rectangle
}
ScrollView {
id: scrollView
anchors {
top: header.bottom
left: parent.left
right: parent.right
bottom: parent.bottom
}
contentWidth: header.width
ScrollBar.vertical.policy: ScrollBar.AlwaysOn
ScrollBar.horizontal.policy: ScrollBar.AlwaysOn
ListView {
id: logEntryList
clip: true
delegate: rowDelegate
model: vfsTable.proxy
//model: vfsTable.model
move: Transition { SmoothedAnimation {} }
} // TableView
} // ScrollView
}

View file

@ -4,7 +4,7 @@ import QtQuick.Controls.Material
import QtQuick.Layouts
ColumnLayout {
anchors.fill: parent
//anchors.fill: parent
spacing: 8
// --- Filter bar ---
@ -41,11 +41,7 @@ ColumnLayout {
model: VFSModel
proxy: VFSProxy
onSortColumnChanged: resort();
onSortAscendingChanged: resort();
function resort() {
VFSProxy.sort(sortColumn, sortAscending ? Qt.AscendingOrder : Qt.DescendingOrder);
}
onSortColumnChanged: VFSProxy.setSort(sortColumn);
onSortAscendingChanged: VFSProxy.setSortDir(sortAscending ? Qt.AscendingOrder : Qt.DescendingOrder);
}
}

View file

@ -14,7 +14,6 @@ function loadForGame()
const dbpath = sobj.dbPath;
db.open(dbpath);
//modMasterList = db.getMods();
Mods.rereadMods();
statusBar.text = qsTr('Now managing "%1"').arg(currentGame);

View file

@ -184,6 +184,9 @@ ApplicationWindow {
TabButton {
text: qsTr('VFS')
}
TabButton {
text: qsTr('Launch')
}
}
ComboBox {
id: profilesMenu
@ -278,11 +281,9 @@ ApplicationWindow {
id: vfsStats
}
/*
LaunchPage {
id: launchPage
}
*/
}
Settings {
@ -293,6 +294,7 @@ ApplicationWindow {
property var modListColumnSizes
property var pluginsListColumnSizes
property var loadorderListColumnSizes
property var logEntryListColumnSizes
property string nexusApiKey
property string nexusUuid
@ -453,31 +455,34 @@ ApplicationWindow {
property var modMasterList: []
onModMasterListChanged: {
/*
modTable.model.clear();
for( let ent of modMasterList ) {
modTable.model.append(ent);
}
*/
Qt.callLater( function() { updateMasterList(); } );
}
function updateMasterList() {
console.log("Mod master listed updated, rebuilding list!");
const newLen = modMasterList.length;
const oldLen = modTable.model.count;
modTable.model.clear();
for( let a=0; a < modMasterList.length; a++ )
for( let a=0; a < newLen; a++ )
{
let nent = modMasterList[a];
modTable.model.set(a, nent);
/*
let oent = modTable.model.get(a);
for( k of Object.keys(nent) )
{
if( oent[k] != nent[k] )
modTable.model.set(a, nent);
if( a >= oldLen ) {
modTable.model.append(nent);
continue;
}
*/
const mtents = modTable.model.get(a);
for( const k in nent ) {
if( nent[k] === mtents[k] )
continue;
modTable.model.setProperty(a, k, nent[k]);
}
//modTable.model.set(a, nent);
}
if( modMasterList.length < modTable.model.count )
modTable.model.remove(modMasterList.length, modTable.model.count-modMasterList.length);
if( newLen < oldLen )
modTable.model.remove(newLen, oldLen-newLen);
}
readonly property variant gameDefinitions: [

View file

@ -65,11 +65,13 @@ function manifestFromFomod(filepath, cb)
console.log("FomodDir: "+fomodDir);
let fomodParts = fomodDir.split(/\//g);
/*
if( fomodParts.length > 1 )
{
fomodParts.pop();
rel = fomodParts.join('/');
}
*/
pathInfo = filelist.find( e => e['pathname'].toLowerCase() === fomodLow + '/info.xml' );
pathConfig = filelist.find( e => e['pathname'].toLowerCase() === fomodLow + '/moduleconfig.xml' );
@ -179,7 +181,7 @@ function installFromFilesystem(filepath, cb, gamecode, nexusModId, nexusFileId)
});
} catch(err) { console.log("ERROR: "+err); }
}
/*
function checkOverwrites(fileMap)
{
let overwrites = [];
@ -198,7 +200,7 @@ function checkOverwrites(fileMap)
} );
return overwrites;
}
*/
function enableMod(mod)
{
let sobj = gameSettings.objFor(currentGame);
@ -679,11 +681,12 @@ function installFancyMod(mod, files, folders, modinfo, flags)
// HACK: Don't -actually- extract anything:
return finished();
/*
if( Object.keys(fileMap).length > 0 )
extractFileMap(mod, fileMap, finished);
else
finished();
*/
} );
}

View file

@ -301,13 +301,6 @@ function enableMod(mod)
function updatePluginsTable(plugins)
{
// Let's get hairy...
let mods = db.getMods();
let files = {};
mods.forEach( function(mod) {
files[mod['modId']] = db.getFiles(mod['modId']);
} );
let model = [];
plugins.forEach( function(ent) {
let nent = { 'enabled':ent['enabled'], 'filename':ent['filename'], 'name':'???', 'description':'???' };
@ -315,35 +308,19 @@ function updatePluginsTable(plugins)
{
nent['name'] = currentGameEntry['name'];
nent['description'] = qsTr('Built-In');
}
} else {
if( ent['description'] )
nent['description'] = ent['description'];
if( ent['description'] )
nent['description'] = ent['description'];
if( ent['missing'] )
nent['missing'] = ent['missing'];
if( ent['missing'] )
nent['missing'] = ent['missing'];
/**
* HACK We don't care, we're using a VFS now:
**
if( !ent['plugin'] )
nent['notfound'] = true;
*/
let done = false;
for( let a=0; a < mods.length && !done; a++ )
{
const mod = mods[a];
const fent = files[mod['modId']];
for( let b=0; b < fent.length && !done; b++ )
const results = db.getFilesEndingWith([ent['filename']]);
if( results.length > 0 )
{
const f = fent[b];
if( f['dest'].endsWith(ent['filename']) )
{
nent['name'] = mod['name'];
nent['description'] = mod['description'];
done = true;
}
const fent = results[0];
nent['name'] = fent['name'];
nent['description'] = fent['description'];
}
}

View file

@ -11,6 +11,8 @@ QMAKE_LDFLAGS += -fPIE -g
LIBS += -larchive
#CONFIG+=sanitizer sanitize_address sanitize_undefined
debug {
#CONFIG+=sanitizer sanitize_address sanitize_undefined
#CONFIG+=sanitize_thread sanitize_memory
@ -54,6 +56,8 @@ CONFIG += c++17 cmdline link_pkgconfig
PKGCONFIG += fuse3 libunarr
#QMAKE_CXXFLAGS += -D_FILE_OFFSET_BITS=64 -O3
QMAKE_CXXFLAGS += -D_FILE_OFFSET_BITS=64 -g -O0
QMAKE_CXXFLAGS_RELEASE -= -O2
QMAKE_CXXFLAGS_DEBUG -= -O2
# You can make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.

View file

@ -20,7 +20,7 @@ public:
Q_INVOKABLE FuseInterface *create(const QVariant &dbhandle, const QString &gameName, int profileId);
signals:
void entryUpdated(const QString &path, const QString &facility, quint32 count, quint8 refcount);
void entryUpdated(const QString &path, qint8 type, const QString &facility, quint32 count, quint8 refcount);
};
#endif // FUSEMANAGER_H

View file

@ -108,29 +108,18 @@ int main(int argc, char *argv[])
engine.rootContext()->setContextProperty("FUSEManager", fuse);
VFSTableModel *vfsModel = new VFSTableModel();
QSortFilterProxyModel *vfsProxy = new QSortFilterProxyModel();
VFSSortProxy *vfsProxy = new VFSSortProxy();
vfsProxy->setSourceModel(vfsModel);
vfsProxy->setDynamicSortFilter(true);
vfsProxy->setSortCaseSensitivity(Qt::CaseInsensitive);
vfsProxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
vfsProxy->setFilterKeyColumn(-1);
vfsProxy->setFilterKeyColumn(0);
//vfsProxy->setFilterRole(Qt::DisplayRole);
QObject::connect( fuse, &FUSEManager::entryUpdated, vfsModel, &VFSTableModel::upsertEntry );
engine.rootContext()->setContextProperty("VFSModel", vfsModel);
engine.rootContext()->setContextProperty("VFSProxy", vfsProxy);
QObject::connect(fuse, &FUSEManager::entryUpdated,
[vfsModel, vfsProxy](const QString &path, const QString &facility, int count, int refcount)
{
qDebug().noquote() << "entryUpdated:" << path
<< facility << count << refcount
<< "rows(model)=" << vfsModel->rowCount()
<< "rows(proxy)=" << vfsProxy->rowCount();
qDebug() << "vfsModel:" << vfsModel->roleNames();
qDebug() << "vfsProxy:" << vfsProxy->roleNames();
});
QObject::connect(&engine, &QQmlEngine::destroyed, vfsModel, &QObject::deleteLater);
QObject::connect(&engine, &QQmlEngine::destroyed, vfsProxy, &QObject::deleteLater);
#endif

View file

@ -2,103 +2,76 @@
VFSTableModel::VFSTableModel(QObject *parent)
: QAbstractTableModel(parent)
{
// column definitions (DisplayRole = header, UserRole = data role)
m_columns = {
QMap<int, QVariant>{ {Qt::DisplayRole, "Path"}, {Qt::UserRole, "path"} },
QMap<int, QVariant>{ {Qt::DisplayRole, "Facility"}, {Qt::UserRole, "facility"} },
QMap<int, QVariant>{ {Qt::DisplayRole, "Count"}, {Qt::UserRole, "count"} },
QMap<int, QVariant>{ {Qt::DisplayRole, "RefCount"}, {Qt::UserRole, "refcount"} }
};
{}
// build roleNames map
int base = Qt::UserRole;
for (int i = 0; i < m_columns.size(); ++i) {
const QByteArray role = m_columns[i].value(Qt::UserRole).toByteArray();
m_roleNames[base + i] = role;
}
emit columnsChanged();
}
int VFSTableModel::rowCount(const QModelIndex &) const
{
return m_rows.size();
}
int VFSTableModel::columnCount(const QModelIndex &) const
{
return m_columns.size();
}
int VFSTableModel::rowCount(const QModelIndex &) const { return m_rows.size(); }
int VFSTableModel::columnCount(const QModelIndex &) const { return 1; }
QVariant VFSTableModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() ||
index.row() >= m_rows.size() ||
index.column() >= m_columns.size())
if (!index.isValid() || role < Path || role >= ColumnCount )
return {};
const VFSEntry &e = m_rows[index.row()];
if( Qt::DisplayRole != role )
return {};
// Allow both DisplayRole and specific UserRole access
switch( index.column() )
{
case 0:
return e.path;
case 1:
return e.facility;
case 2:
return e.count;
case 3:
return e.refcount;
const auto &row = m_rows[index.row()];
switch (role) {
case Path: return row.path;
case Facility: return row.facility;
case Count: return row.count;
case RefCount: return row.refcount;
case Type: return row.type;
case LastAccess:return row.lastAccess;
}
return {};
}
QVariant VFSTableModel::headerData(int section, Qt::Orientation orientation, int role) const
QVariant VFSTableModel::headerData(int section, Qt::Orientation o, int role) const
{
if (orientation == Qt::Horizontal &&
section >= 0 && section < m_columns.size())
return m_columns[section].value(role);
if (o == Qt::Horizontal && role == Qt::DisplayRole) {
switch (section) {
case Path: return "Path";
case Facility: return "Facility";
case Count: return "Count";
case RefCount: return "RefCount";
case Type: return "Type";
case LastAccess:return "LastAccess";
}
}
return {};
}
QHash<int, QByteArray> VFSTableModel::roleNames() const
{
return m_roleNames;
return {
{ Path, "path" },
{ Facility, "facility" },
{ Count, "count" },
{ RefCount, "refcount" },
{ Type, "type" },
{ LastAccess,"lastAccess" }
};
}
QVariantList VFSTableModel::columns()
{
QVariantList out;
for (const auto &col : m_columns) {
QVariantMap map;
map.insert("title", col.value(Qt::DisplayRole).toString());
map.insert("role", col.value(Qt::UserRole).toString());
out.append(map);
}
return out;
}
void VFSTableModel::upsertEntry(const QString &path,
const QString &facility,
int count,
int refcount)
void VFSTableModel::upsertEntry(const QString &path, qint8 type, const QString &facility, int count, int refcount)
{
auto it = m_index.find(path);
if (it == m_index.end()) {
const int row = m_rows.size();
beginInsertRows({}, row, row);
m_rows.append({path, facility, count, refcount});
m_index[path] = row;
// new row
int newRow = m_rows.size();
beginInsertRows({}, newRow, newRow);
m_rows.append({ path, facility, count, refcount, type, QDateTime::currentDateTime() });
m_index.insert(path, newRow);
endInsertRows();
emit dataChanged(index(newRow, 0), index(newRow, 0));
} else {
const int row = it.value();
m_rows[row] = {path, facility, count, refcount};
emit dataChanged(index(row, 0), index(row, columnCount() - 1));
// update existing
int row = it.value();
auto &entry = m_rows[row];
entry.facility = facility;
entry.count = count;
entry.refcount = refcount;
entry.type = type;
entry.lastAccess = QDateTime::currentDateTime();
emit dataChanged(index(row, 0), index(row, 0));
}
}

View file

@ -2,55 +2,54 @@
#define VFSTABLEMODEL_H
#include <QAbstractTableModel>
#include <QSortFilterProxyModel>
#include <QDateTime>
#include <QHash>
#include <QMap>
#include <QSortFilterProxyModel>
#include <QString>
#include <QVariant>
#include <QVector>
#include <QString>
struct VFSEntry {
QString path;
QString facility;
int count = 0;
int refcount = 0;
qint8 type = 0;
QDateTime lastAccess;
};
class VFSTableModel : public QAbstractTableModel
{
Q_OBJECT
Q_PROPERTY(QVariantList columns READ columns NOTIFY columnsChanged)
public:
enum Columns { Path = Qt::UserRole, Facility, Count, RefCount, Type, LastAccess, ColumnCount };
Q_ENUM(Columns)
explicit VFSTableModel(QObject *parent = nullptr);
// core model interface
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
QHash<int, QByteArray> roleNames() const override;
// QML integration
QVariantList columns();
public slots:
void upsertEntry(const QString &path, const QString &facility, int count, int refcount);
void upsertEntry(const QString &path, qint8 type, const QString &facility, int count, int refcount);
void clear();
signals:
void columnsChanged();
private:
QVector<VFSEntry> m_rows;
QHash<QString, int> m_index; // path → row index
QVector<QMap<int, QVariant>> m_columns;
QHash<int, QByteArray> m_roleNames;
QList<VFSEntry> m_rows;
QHash<QString, int> m_index;
};
class VFSSortProxy : public QSortFilterProxyModel {
Q_OBJECT
Q_PROPERTY(bool onlyActive READ onlyActive WRITE setOnlyActive NOTIFY onlyActiveChanged)
Q_PROPERTY(Qt::SortOrder sortDir READ sortDir WRITE setSortDir NOTIFY sortDirChanged)
public:
using QSortFilterProxyModel::QSortFilterProxyModel;
@ -62,22 +61,41 @@ public:
invalidateFilter();
emit onlyActiveChanged();
}
Q_INVOKABLE void setSort(int roleId) {
QSortFilterProxyModel::setDynamicSortFilter(false);
QSortFilterProxyModel::setSortRole(Qt::UserRole + roleId);
QSortFilterProxyModel::sort(0, m_sortDir);
QSortFilterProxyModel::setDynamicSortFilter(true);
}
Q_INVOKABLE Qt::SortOrder sortDir() { return m_sortDir; }
Q_INVOKABLE void setSortDir(Qt::SortOrder dir) {
if( m_sortDir == dir )
return;
m_sortDir = dir;
QSortFilterProxyModel::setDynamicSortFilter(false);
QSortFilterProxyModel::sort(0, m_sortDir);
QSortFilterProxyModel::setDynamicSortFilter(true);
emit sortDirChanged();
}
protected:
bool filterAcceptsRow(int sourceRow, const QModelIndex &srcParent) const override {
if (!m_onlyActive)
return true;
const QModelIndex idx = sourceModel()->index(sourceRow, 3, srcParent); // refcount column
int refcount = sourceModel()->data(idx, Qt::DisplayRole).toInt();
const QModelIndex idx = sourceModel()->index(sourceRow, 0, srcParent);
int refcount = sourceModel()->data(idx, VFSTableModel::RefCount).toInt();
return refcount > 0;
}
signals:
void onlyActiveChanged();
void sortDirChanged();
private:
bool m_onlyActive = false;
Qt::SortOrder m_sortDir = Qt::DescendingOrder;
};