screenshot utility
This commit is contained in:
@@ -0,0 +1 @@
|
||||
add_subdirectory(ZShell)
|
||||
@@ -0,0 +1,43 @@
|
||||
find_package(Qt6 REQUIRED COMPONENTS Core Qml Gui Quick Concurrent Sql)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
|
||||
set(QT_QML_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qml")
|
||||
qt_standard_project_setup(REQUIRES 6.9)
|
||||
|
||||
function(qml_module arg_TARGET)
|
||||
cmake_parse_arguments(PARSE_ARGV 1 arg "" "URI" "SOURCES;LIBRARIES")
|
||||
qt_add_qml_module(${arg_TARGET}
|
||||
URI ${arg_URI}
|
||||
VERSION 1.0
|
||||
SOURCES ${arg_SOURCES}
|
||||
)
|
||||
|
||||
qt_query_qml_module(${arg_TARGET}
|
||||
URI module_uri
|
||||
VERSION module_version
|
||||
PLUGIN_TARGET module_plugin_target
|
||||
TARGET_PATH module_target_path
|
||||
QMLDIR module_qmldir
|
||||
TYPEINFO module_typeinfo
|
||||
)
|
||||
set(module_dir "/usr/lib/qt6/qml/${module_target_path}")
|
||||
install(TARGETS ${arg_TARGET} LIBRARY DESTINATION "${module_dir}" RUNTIME DESTINATION "${module_dir}")
|
||||
install(TARGETS "${module_plugin_target}" LIBRARY DESTINATION "${module_dir}" RUNTIME DESTINATION "${module_dir}")
|
||||
install(FILES "${module_qmldir}" DESTINATION "${module_dir}")
|
||||
install(FILES "${module_typeinfo}" DESTINATION "${module_dir}")
|
||||
target_link_libraries(${arg_TARGET} PRIVATE Qt::Core Qt::Qml ${arg_LIBRARIES})
|
||||
endfunction()
|
||||
|
||||
qml_module(ZShell
|
||||
URI ZShell
|
||||
SOURCES
|
||||
writefile.hpp writefile.cpp
|
||||
appdb.hpp appdb.cpp
|
||||
LIBRARIES
|
||||
Qt::Gui
|
||||
Qt::Quick
|
||||
Qt::Concurrent
|
||||
Qt::Sql
|
||||
)
|
||||
|
||||
add_subdirectory(Models)
|
||||
@@ -0,0 +1,8 @@
|
||||
qml_module(ZShell-models
|
||||
URI ZShell.Models
|
||||
SOURCES
|
||||
filesystemmodel.hpp filesystemmodel.cpp
|
||||
LIBRARIES
|
||||
Qt::Gui
|
||||
Qt::Concurrent
|
||||
)
|
||||
@@ -0,0 +1,479 @@
|
||||
#include "filesystemmodel.hpp"
|
||||
|
||||
#include <qdiriterator.h>
|
||||
#include <qfuturewatcher.h>
|
||||
#include <qtconcurrentrun.h>
|
||||
|
||||
namespace ZShell::models {
|
||||
|
||||
FileSystemEntry::FileSystemEntry(const QString& path, const QString& relativePath, QObject* parent)
|
||||
: QObject(parent)
|
||||
, m_fileInfo(path)
|
||||
, m_path(path)
|
||||
, m_relativePath(relativePath)
|
||||
, m_isImageInitialised(false)
|
||||
, m_mimeTypeInitialised(false) {}
|
||||
|
||||
QString FileSystemEntry::path() const {
|
||||
return m_path;
|
||||
};
|
||||
|
||||
QString FileSystemEntry::relativePath() const {
|
||||
return m_relativePath;
|
||||
};
|
||||
|
||||
QString FileSystemEntry::name() const {
|
||||
return m_fileInfo.fileName();
|
||||
};
|
||||
|
||||
QString FileSystemEntry::baseName() const {
|
||||
return m_fileInfo.baseName();
|
||||
};
|
||||
|
||||
QString FileSystemEntry::parentDir() const {
|
||||
return m_fileInfo.absolutePath();
|
||||
};
|
||||
|
||||
QString FileSystemEntry::suffix() const {
|
||||
return m_fileInfo.completeSuffix();
|
||||
};
|
||||
|
||||
qint64 FileSystemEntry::size() const {
|
||||
return m_fileInfo.size();
|
||||
};
|
||||
|
||||
bool FileSystemEntry::isDir() const {
|
||||
return m_fileInfo.isDir();
|
||||
};
|
||||
|
||||
bool FileSystemEntry::isImage() const {
|
||||
if (!m_isImageInitialised) {
|
||||
QImageReader reader(m_path);
|
||||
m_isImage = reader.canRead();
|
||||
m_isImageInitialised = true;
|
||||
}
|
||||
return m_isImage;
|
||||
}
|
||||
|
||||
QString FileSystemEntry::mimeType() const {
|
||||
if (!m_mimeTypeInitialised) {
|
||||
const QMimeDatabase db;
|
||||
m_mimeType = db.mimeTypeForFile(m_path).name();
|
||||
m_mimeTypeInitialised = true;
|
||||
}
|
||||
return m_mimeType;
|
||||
}
|
||||
|
||||
void FileSystemEntry::updateRelativePath(const QDir& dir) {
|
||||
const auto relPath = dir.relativeFilePath(m_path);
|
||||
if (m_relativePath != relPath) {
|
||||
m_relativePath = relPath;
|
||||
emit relativePathChanged();
|
||||
}
|
||||
}
|
||||
|
||||
FileSystemModel::FileSystemModel(QObject* parent)
|
||||
: QAbstractListModel(parent)
|
||||
, m_recursive(false)
|
||||
, m_watchChanges(true)
|
||||
, m_showHidden(false)
|
||||
, m_filter(NoFilter) {
|
||||
connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &FileSystemModel::watchDirIfRecursive);
|
||||
connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &FileSystemModel::updateEntriesForDir);
|
||||
}
|
||||
|
||||
int FileSystemModel::rowCount(const QModelIndex& parent) const {
|
||||
if (parent != QModelIndex()) {
|
||||
return 0;
|
||||
}
|
||||
return static_cast<int>(m_entries.size());
|
||||
}
|
||||
|
||||
QVariant FileSystemModel::data(const QModelIndex& index, int role) const {
|
||||
if (role != Qt::UserRole || !index.isValid() || index.row() >= m_entries.size()) {
|
||||
return QVariant();
|
||||
}
|
||||
return QVariant::fromValue(m_entries.at(index.row()));
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> FileSystemModel::roleNames() const {
|
||||
return { { Qt::UserRole, "modelData" } };
|
||||
}
|
||||
|
||||
QString FileSystemModel::path() const {
|
||||
return m_path;
|
||||
}
|
||||
|
||||
void FileSystemModel::setPath(const QString& path) {
|
||||
if (m_path == path) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_path = path;
|
||||
emit pathChanged();
|
||||
|
||||
m_dir.setPath(m_path);
|
||||
|
||||
for (const auto& entry : std::as_const(m_entries)) {
|
||||
entry->updateRelativePath(m_dir);
|
||||
}
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
bool FileSystemModel::recursive() const {
|
||||
return m_recursive;
|
||||
}
|
||||
|
||||
void FileSystemModel::setRecursive(bool recursive) {
|
||||
if (m_recursive == recursive) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_recursive = recursive;
|
||||
emit recursiveChanged();
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
bool FileSystemModel::watchChanges() const {
|
||||
return m_watchChanges;
|
||||
}
|
||||
|
||||
void FileSystemModel::setWatchChanges(bool watchChanges) {
|
||||
if (m_watchChanges == watchChanges) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_watchChanges = watchChanges;
|
||||
emit watchChangesChanged();
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
bool FileSystemModel::showHidden() const {
|
||||
return m_showHidden;
|
||||
}
|
||||
|
||||
void FileSystemModel::setShowHidden(bool showHidden) {
|
||||
if (m_showHidden == showHidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_showHidden = showHidden;
|
||||
emit showHiddenChanged();
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
bool FileSystemModel::sortReverse() const {
|
||||
return m_sortReverse;
|
||||
}
|
||||
|
||||
void FileSystemModel::setSortReverse(bool sortReverse) {
|
||||
if (m_sortReverse == sortReverse) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_sortReverse = sortReverse;
|
||||
emit sortReverseChanged();
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
FileSystemModel::Filter FileSystemModel::filter() const {
|
||||
return m_filter;
|
||||
}
|
||||
|
||||
void FileSystemModel::setFilter(Filter filter) {
|
||||
if (m_filter == filter) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_filter = filter;
|
||||
emit filterChanged();
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
QStringList FileSystemModel::nameFilters() const {
|
||||
return m_nameFilters;
|
||||
}
|
||||
|
||||
void FileSystemModel::setNameFilters(const QStringList& nameFilters) {
|
||||
if (m_nameFilters == nameFilters) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_nameFilters = nameFilters;
|
||||
emit nameFiltersChanged();
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
QQmlListProperty<FileSystemEntry> FileSystemModel::entries() {
|
||||
return QQmlListProperty<FileSystemEntry>(this, &m_entries);
|
||||
}
|
||||
|
||||
void FileSystemModel::watchDirIfRecursive(const QString& path) {
|
||||
if (m_recursive && m_watchChanges) {
|
||||
const auto currentDir = m_dir;
|
||||
const bool showHidden = m_showHidden;
|
||||
const auto future = QtConcurrent::run([showHidden, path]() {
|
||||
QDir::Filters filters = QDir::Dirs | QDir::NoDotAndDotDot;
|
||||
if (showHidden) {
|
||||
filters |= QDir::Hidden;
|
||||
}
|
||||
|
||||
QDirIterator iter(path, filters, QDirIterator::Subdirectories);
|
||||
QStringList dirs;
|
||||
while (iter.hasNext()) {
|
||||
dirs << iter.next();
|
||||
}
|
||||
return dirs;
|
||||
});
|
||||
const auto watcher = new QFutureWatcher<QStringList>(this);
|
||||
connect(watcher, &QFutureWatcher<QStringList>::finished, this, [currentDir, showHidden, watcher, this]() {
|
||||
const auto paths = watcher->result();
|
||||
if (currentDir == m_dir && showHidden == m_showHidden && !paths.isEmpty()) {
|
||||
// Ignore if dir or showHidden has changed
|
||||
m_watcher.addPaths(paths);
|
||||
}
|
||||
watcher->deleteLater();
|
||||
});
|
||||
watcher->setFuture(future);
|
||||
}
|
||||
}
|
||||
|
||||
void FileSystemModel::update() {
|
||||
updateWatcher();
|
||||
updateEntries();
|
||||
}
|
||||
|
||||
void FileSystemModel::updateWatcher() {
|
||||
if (!m_watcher.directories().isEmpty()) {
|
||||
m_watcher.removePaths(m_watcher.directories());
|
||||
}
|
||||
|
||||
if (!m_watchChanges || m_path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_watcher.addPath(m_path);
|
||||
watchDirIfRecursive(m_path);
|
||||
}
|
||||
|
||||
void FileSystemModel::updateEntries() {
|
||||
if (m_path.isEmpty()) {
|
||||
if (!m_entries.isEmpty()) {
|
||||
beginResetModel();
|
||||
qDeleteAll(m_entries);
|
||||
m_entries.clear();
|
||||
endResetModel();
|
||||
emit entriesChanged();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto& future : m_futures) {
|
||||
future.cancel();
|
||||
}
|
||||
m_futures.clear();
|
||||
|
||||
updateEntriesForDir(m_path);
|
||||
}
|
||||
|
||||
void FileSystemModel::updateEntriesForDir(const QString& dir) {
|
||||
const auto recursive = m_recursive;
|
||||
const auto showHidden = m_showHidden;
|
||||
const auto filter = m_filter;
|
||||
const auto nameFilters = m_nameFilters;
|
||||
|
||||
QSet<QString> oldPaths;
|
||||
for (const auto& entry : std::as_const(m_entries)) {
|
||||
oldPaths << entry->path();
|
||||
}
|
||||
|
||||
const auto future = QtConcurrent::run([=](QPromise<QPair<QSet<QString>, QSet<QString>>>& promise) {
|
||||
const auto flags = recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags;
|
||||
|
||||
std::optional<QDirIterator> iter;
|
||||
|
||||
if (filter == Images) {
|
||||
QStringList extraNameFilters = nameFilters;
|
||||
const auto formats = QImageReader::supportedImageFormats();
|
||||
for (const auto& format : formats) {
|
||||
extraNameFilters << "*." + format;
|
||||
}
|
||||
|
||||
QDir::Filters filters = QDir::Files;
|
||||
if (showHidden) {
|
||||
filters |= QDir::Hidden;
|
||||
}
|
||||
|
||||
iter.emplace(dir, extraNameFilters, filters, flags);
|
||||
} else {
|
||||
QDir::Filters filters;
|
||||
|
||||
if (filter == Files) {
|
||||
filters = QDir::Files;
|
||||
} else if (filter == Dirs) {
|
||||
filters = QDir::Dirs | QDir::NoDotAndDotDot;
|
||||
} else {
|
||||
filters = QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot;
|
||||
}
|
||||
|
||||
if (showHidden) {
|
||||
filters |= QDir::Hidden;
|
||||
}
|
||||
|
||||
if (nameFilters.isEmpty()) {
|
||||
iter.emplace(dir, filters, flags);
|
||||
} else {
|
||||
iter.emplace(dir, nameFilters, filters, flags);
|
||||
}
|
||||
}
|
||||
|
||||
QSet<QString> newPaths;
|
||||
while (iter->hasNext()) {
|
||||
if (promise.isCanceled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString path = iter->next();
|
||||
|
||||
if (filter == Images) {
|
||||
QImageReader reader(path);
|
||||
if (!reader.canRead()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
newPaths.insert(path);
|
||||
}
|
||||
|
||||
if (promise.isCanceled() || newPaths == oldPaths) {
|
||||
return;
|
||||
}
|
||||
|
||||
promise.addResult(qMakePair(oldPaths - newPaths, newPaths - oldPaths));
|
||||
});
|
||||
|
||||
if (m_futures.contains(dir)) {
|
||||
m_futures[dir].cancel();
|
||||
}
|
||||
m_futures.insert(dir, future);
|
||||
|
||||
const auto watcher = new QFutureWatcher<QPair<QSet<QString>, QSet<QString>>>(this);
|
||||
|
||||
connect(watcher, &QFutureWatcher<QPair<QSet<QString>, QSet<QString>>>::finished, this, [dir, watcher, this]() {
|
||||
m_futures.remove(dir);
|
||||
|
||||
if (!watcher->future().isResultReadyAt(0)) {
|
||||
watcher->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
const auto result = watcher->result();
|
||||
applyChanges(result.first, result.second);
|
||||
|
||||
watcher->deleteLater();
|
||||
});
|
||||
|
||||
watcher->setFuture(future);
|
||||
}
|
||||
|
||||
void FileSystemModel::applyChanges(const QSet<QString>& removedPaths, const QSet<QString>& addedPaths) {
|
||||
QList<int> removedIndices;
|
||||
for (int i = 0; i < m_entries.size(); ++i) {
|
||||
if (removedPaths.contains(m_entries[i]->path())) {
|
||||
removedIndices << i;
|
||||
}
|
||||
}
|
||||
std::sort(removedIndices.begin(), removedIndices.end(), std::greater<int>());
|
||||
|
||||
// Batch remove old entries
|
||||
int start = -1;
|
||||
int end = -1;
|
||||
for (int idx : std::as_const(removedIndices)) {
|
||||
if (start == -1) {
|
||||
start = idx;
|
||||
end = idx;
|
||||
} else if (idx == end - 1) {
|
||||
end = idx;
|
||||
} else {
|
||||
beginRemoveRows(QModelIndex(), end, start);
|
||||
for (int i = start; i >= end; --i) {
|
||||
m_entries.takeAt(i)->deleteLater();
|
||||
}
|
||||
endRemoveRows();
|
||||
|
||||
start = idx;
|
||||
end = idx;
|
||||
}
|
||||
}
|
||||
if (start != -1) {
|
||||
beginRemoveRows(QModelIndex(), end, start);
|
||||
for (int i = start; i >= end; --i) {
|
||||
m_entries.takeAt(i)->deleteLater();
|
||||
}
|
||||
endRemoveRows();
|
||||
}
|
||||
|
||||
// Create new entries
|
||||
QList<FileSystemEntry*> newEntries;
|
||||
for (const auto& path : addedPaths) {
|
||||
newEntries << new FileSystemEntry(path, m_dir.relativeFilePath(path), this);
|
||||
}
|
||||
std::sort(newEntries.begin(), newEntries.end(), [this](const FileSystemEntry* a, const FileSystemEntry* b) {
|
||||
return compareEntries(a, b);
|
||||
});
|
||||
|
||||
// Batch insert new entries
|
||||
int insertStart = -1;
|
||||
QList<FileSystemEntry*> batchItems;
|
||||
for (const auto& entry : std::as_const(newEntries)) {
|
||||
const auto it = std::lower_bound(
|
||||
m_entries.begin(), m_entries.end(), entry, [this](const FileSystemEntry* a, const FileSystemEntry* b) {
|
||||
return compareEntries(a, b);
|
||||
});
|
||||
const auto row = static_cast<int>(it - m_entries.begin());
|
||||
|
||||
if (insertStart == -1) {
|
||||
insertStart = row;
|
||||
batchItems << entry;
|
||||
} else if (row == insertStart + batchItems.size()) {
|
||||
batchItems << entry;
|
||||
} else {
|
||||
beginInsertRows(QModelIndex(), insertStart, insertStart + static_cast<int>(batchItems.size()) - 1);
|
||||
for (int i = 0; i < batchItems.size(); ++i) {
|
||||
m_entries.insert(insertStart + i, batchItems[i]);
|
||||
}
|
||||
endInsertRows();
|
||||
|
||||
insertStart = row;
|
||||
batchItems.clear();
|
||||
batchItems << entry;
|
||||
}
|
||||
}
|
||||
if (!batchItems.isEmpty()) {
|
||||
beginInsertRows(QModelIndex(), insertStart, insertStart + static_cast<int>(batchItems.size()) - 1);
|
||||
for (int i = 0; i < batchItems.size(); ++i) {
|
||||
m_entries.insert(insertStart + i, batchItems[i]);
|
||||
}
|
||||
endInsertRows();
|
||||
}
|
||||
|
||||
emit entriesChanged();
|
||||
}
|
||||
|
||||
bool FileSystemModel::compareEntries(const FileSystemEntry* a, const FileSystemEntry* b) const {
|
||||
if (a->isDir() != b->isDir()) {
|
||||
return m_sortReverse ^ a->isDir();
|
||||
}
|
||||
const auto cmp = a->relativePath().localeAwareCompare(b->relativePath());
|
||||
return m_sortReverse ? cmp > 0 : cmp < 0;
|
||||
}
|
||||
|
||||
} // namespace ZShell::models
|
||||
@@ -0,0 +1,148 @@
|
||||
#pragma once
|
||||
|
||||
#include <qabstractitemmodel.h>
|
||||
#include <qdir.h>
|
||||
#include <qfilesystemwatcher.h>
|
||||
#include <qfuture.h>
|
||||
#include <qimagereader.h>
|
||||
#include <qmimedatabase.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qqmllist.h>
|
||||
|
||||
namespace ZShell::models {
|
||||
|
||||
class FileSystemEntry : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("FileSystemEntry instances can only be retrieved from a FileSystemModel")
|
||||
|
||||
Q_PROPERTY(QString path READ path CONSTANT)
|
||||
Q_PROPERTY(QString relativePath READ relativePath NOTIFY relativePathChanged)
|
||||
Q_PROPERTY(QString name READ name CONSTANT)
|
||||
Q_PROPERTY(QString baseName READ baseName CONSTANT)
|
||||
Q_PROPERTY(QString parentDir READ parentDir CONSTANT)
|
||||
Q_PROPERTY(QString suffix READ suffix CONSTANT)
|
||||
Q_PROPERTY(qint64 size READ size CONSTANT)
|
||||
Q_PROPERTY(bool isDir READ isDir CONSTANT)
|
||||
Q_PROPERTY(bool isImage READ isImage CONSTANT)
|
||||
Q_PROPERTY(QString mimeType READ mimeType CONSTANT)
|
||||
|
||||
public:
|
||||
explicit FileSystemEntry(const QString& path, const QString& relativePath, QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] QString path() const;
|
||||
[[nodiscard]] QString relativePath() const;
|
||||
[[nodiscard]] QString name() const;
|
||||
[[nodiscard]] QString baseName() const;
|
||||
[[nodiscard]] QString parentDir() const;
|
||||
[[nodiscard]] QString suffix() const;
|
||||
[[nodiscard]] qint64 size() const;
|
||||
[[nodiscard]] bool isDir() const;
|
||||
[[nodiscard]] bool isImage() const;
|
||||
[[nodiscard]] QString mimeType() const;
|
||||
|
||||
void updateRelativePath(const QDir& dir);
|
||||
|
||||
signals:
|
||||
void relativePathChanged();
|
||||
|
||||
private:
|
||||
const QFileInfo m_fileInfo;
|
||||
|
||||
const QString m_path;
|
||||
QString m_relativePath;
|
||||
|
||||
mutable bool m_isImage;
|
||||
mutable bool m_isImageInitialised;
|
||||
|
||||
mutable QString m_mimeType;
|
||||
mutable bool m_mimeTypeInitialised;
|
||||
};
|
||||
|
||||
class FileSystemModel : public QAbstractListModel {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged)
|
||||
Q_PROPERTY(bool recursive READ recursive WRITE setRecursive NOTIFY recursiveChanged)
|
||||
Q_PROPERTY(bool watchChanges READ watchChanges WRITE setWatchChanges NOTIFY watchChangesChanged)
|
||||
Q_PROPERTY(bool showHidden READ showHidden WRITE setShowHidden NOTIFY showHiddenChanged)
|
||||
Q_PROPERTY(bool sortReverse READ sortReverse WRITE setSortReverse NOTIFY sortReverseChanged)
|
||||
Q_PROPERTY(Filter filter READ filter WRITE setFilter NOTIFY filterChanged)
|
||||
Q_PROPERTY(QStringList nameFilters READ nameFilters WRITE setNameFilters NOTIFY nameFiltersChanged)
|
||||
|
||||
Q_PROPERTY(QQmlListProperty<ZShell::models::FileSystemEntry> entries READ entries NOTIFY entriesChanged)
|
||||
|
||||
public:
|
||||
enum Filter {
|
||||
NoFilter,
|
||||
Images,
|
||||
Files,
|
||||
Dirs
|
||||
};
|
||||
Q_ENUM(Filter)
|
||||
|
||||
explicit FileSystemModel(QObject* parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
[[nodiscard]] QString path() const;
|
||||
void setPath(const QString& path);
|
||||
|
||||
[[nodiscard]] bool recursive() const;
|
||||
void setRecursive(bool recursive);
|
||||
|
||||
[[nodiscard]] bool watchChanges() const;
|
||||
void setWatchChanges(bool watchChanges);
|
||||
|
||||
[[nodiscard]] bool showHidden() const;
|
||||
void setShowHidden(bool showHidden);
|
||||
|
||||
[[nodiscard]] bool sortReverse() const;
|
||||
void setSortReverse(bool sortReverse);
|
||||
|
||||
[[nodiscard]] Filter filter() const;
|
||||
void setFilter(Filter filter);
|
||||
|
||||
[[nodiscard]] QStringList nameFilters() const;
|
||||
void setNameFilters(const QStringList& nameFilters);
|
||||
|
||||
[[nodiscard]] QQmlListProperty<FileSystemEntry> entries();
|
||||
|
||||
signals:
|
||||
void pathChanged();
|
||||
void recursiveChanged();
|
||||
void watchChangesChanged();
|
||||
void showHiddenChanged();
|
||||
void sortReverseChanged();
|
||||
void filterChanged();
|
||||
void nameFiltersChanged();
|
||||
void entriesChanged();
|
||||
|
||||
private:
|
||||
QDir m_dir;
|
||||
QFileSystemWatcher m_watcher;
|
||||
QList<FileSystemEntry*> m_entries;
|
||||
QHash<QString, QFuture<QPair<QSet<QString>, QSet<QString>>>> m_futures;
|
||||
|
||||
QString m_path;
|
||||
bool m_recursive;
|
||||
bool m_watchChanges;
|
||||
bool m_showHidden;
|
||||
bool m_sortReverse;
|
||||
Filter m_filter;
|
||||
QStringList m_nameFilters;
|
||||
|
||||
void watchDirIfRecursive(const QString& path);
|
||||
void update();
|
||||
void updateWatcher();
|
||||
void updateEntries();
|
||||
void updateEntriesForDir(const QString& dir);
|
||||
void applyChanges(const QSet<QString>& removedPaths, const QSet<QString>& addedPaths);
|
||||
[[nodiscard]] bool compareEntries(const FileSystemEntry* a, const FileSystemEntry* b) const;
|
||||
};
|
||||
|
||||
} // namespace ZShell::models
|
||||
@@ -0,0 +1,265 @@
|
||||
#include "appdb.hpp"
|
||||
|
||||
#include <qsqldatabase.h>
|
||||
#include <qsqlquery.h>
|
||||
#include <quuid.h>
|
||||
|
||||
namespace ZShell {
|
||||
|
||||
AppEntry::AppEntry(QObject* entry, unsigned int frequency, QObject* parent)
|
||||
: QObject(parent)
|
||||
, m_entry(entry)
|
||||
, m_frequency(frequency) {
|
||||
const auto mo = m_entry->metaObject();
|
||||
const auto tmo = metaObject();
|
||||
|
||||
for (const auto& prop :
|
||||
{ "name", "comment", "execString", "startupClass", "genericName", "categories", "keywords" }) {
|
||||
const auto metaProp = mo->property(mo->indexOfProperty(prop));
|
||||
const auto thisMetaProp = tmo->property(tmo->indexOfProperty(prop));
|
||||
QObject::connect(m_entry, metaProp.notifySignal(), this, thisMetaProp.notifySignal());
|
||||
}
|
||||
|
||||
QObject::connect(m_entry, &QObject::destroyed, this, [this]() {
|
||||
m_entry = nullptr;
|
||||
deleteLater();
|
||||
});
|
||||
}
|
||||
|
||||
QObject* AppEntry::entry() const {
|
||||
return m_entry;
|
||||
}
|
||||
|
||||
quint32 AppEntry::frequency() const {
|
||||
return m_frequency;
|
||||
}
|
||||
|
||||
void AppEntry::setFrequency(unsigned int frequency) {
|
||||
if (m_frequency != frequency) {
|
||||
m_frequency = frequency;
|
||||
emit frequencyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void AppEntry::incrementFrequency() {
|
||||
m_frequency++;
|
||||
emit frequencyChanged();
|
||||
}
|
||||
|
||||
QString AppEntry::id() const {
|
||||
if (!m_entry) {
|
||||
return "";
|
||||
}
|
||||
return m_entry->property("id").toString();
|
||||
}
|
||||
|
||||
QString AppEntry::name() const {
|
||||
if (!m_entry) {
|
||||
return "";
|
||||
}
|
||||
return m_entry->property("name").toString();
|
||||
}
|
||||
|
||||
QString AppEntry::comment() const {
|
||||
if (!m_entry) {
|
||||
return "";
|
||||
}
|
||||
return m_entry->property("comment").toString();
|
||||
}
|
||||
|
||||
QString AppEntry::execString() const {
|
||||
if (!m_entry) {
|
||||
return "";
|
||||
}
|
||||
return m_entry->property("execString").toString();
|
||||
}
|
||||
|
||||
QString AppEntry::startupClass() const {
|
||||
if (!m_entry) {
|
||||
return "";
|
||||
}
|
||||
return m_entry->property("startupClass").toString();
|
||||
}
|
||||
|
||||
QString AppEntry::genericName() const {
|
||||
if (!m_entry) {
|
||||
return "";
|
||||
}
|
||||
return m_entry->property("genericName").toString();
|
||||
}
|
||||
|
||||
QString AppEntry::categories() const {
|
||||
if (!m_entry) {
|
||||
return "";
|
||||
}
|
||||
return m_entry->property("categories").toStringList().join(" ");
|
||||
}
|
||||
|
||||
QString AppEntry::keywords() const {
|
||||
if (!m_entry) {
|
||||
return "";
|
||||
}
|
||||
return m_entry->property("keywords").toStringList().join(" ");
|
||||
}
|
||||
|
||||
AppDb::AppDb(QObject* parent)
|
||||
: QObject(parent)
|
||||
, m_timer(new QTimer(this))
|
||||
, m_uuid(QUuid::createUuid().toString()) {
|
||||
m_timer->setSingleShot(true);
|
||||
m_timer->setInterval(300);
|
||||
QObject::connect(m_timer, &QTimer::timeout, this, &AppDb::updateApps);
|
||||
|
||||
auto db = QSqlDatabase::addDatabase("QSQLITE", m_uuid);
|
||||
db.setDatabaseName(":memory:");
|
||||
db.open();
|
||||
|
||||
QSqlQuery query(db);
|
||||
query.exec("CREATE TABLE IF NOT EXISTS frequencies (id TEXT PRIMARY KEY, frequency INTEGER)");
|
||||
}
|
||||
|
||||
QString AppDb::uuid() const {
|
||||
return m_uuid;
|
||||
}
|
||||
|
||||
QString AppDb::path() const {
|
||||
return m_path;
|
||||
}
|
||||
|
||||
void AppDb::setPath(const QString& path) {
|
||||
auto newPath = path.isEmpty() ? ":memory:" : path;
|
||||
|
||||
if (m_path == newPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_path = newPath;
|
||||
emit pathChanged();
|
||||
|
||||
auto db = QSqlDatabase::database(m_uuid, false);
|
||||
db.close();
|
||||
db.setDatabaseName(newPath);
|
||||
db.open();
|
||||
|
||||
QSqlQuery query(db);
|
||||
query.exec("CREATE TABLE IF NOT EXISTS frequencies (id TEXT PRIMARY KEY, frequency INTEGER)");
|
||||
|
||||
updateAppFrequencies();
|
||||
}
|
||||
|
||||
QObjectList AppDb::entries() const {
|
||||
return m_entries;
|
||||
}
|
||||
|
||||
void AppDb::setEntries(const QObjectList& entries) {
|
||||
if (m_entries == entries) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_entries = entries;
|
||||
emit entriesChanged();
|
||||
|
||||
m_timer->start();
|
||||
}
|
||||
|
||||
QQmlListProperty<AppEntry> AppDb::apps() {
|
||||
return QQmlListProperty<AppEntry>(this, &getSortedApps());
|
||||
}
|
||||
|
||||
void AppDb::incrementFrequency(const QString& id) {
|
||||
auto db = QSqlDatabase::database(m_uuid);
|
||||
QSqlQuery query(db);
|
||||
|
||||
query.prepare("INSERT INTO frequencies (id, frequency) "
|
||||
"VALUES (:id, 1) "
|
||||
"ON CONFLICT (id) DO UPDATE SET frequency = frequency + 1");
|
||||
query.bindValue(":id", id);
|
||||
query.exec();
|
||||
|
||||
auto* app = m_apps.value(id);
|
||||
if (app) {
|
||||
const auto before = getSortedApps();
|
||||
|
||||
app->incrementFrequency();
|
||||
|
||||
if (before != getSortedApps()) {
|
||||
emit appsChanged();
|
||||
}
|
||||
} else {
|
||||
qWarning() << "AppDb::incrementFrequency: could not find app with id" << id;
|
||||
}
|
||||
}
|
||||
|
||||
QList<AppEntry*>& AppDb::getSortedApps() const {
|
||||
m_sortedApps = m_apps.values();
|
||||
std::sort(m_sortedApps.begin(), m_sortedApps.end(), [](AppEntry* a, AppEntry* b) {
|
||||
if (a->frequency() != b->frequency()) {
|
||||
return a->frequency() > b->frequency();
|
||||
}
|
||||
return a->name().localeAwareCompare(b->name()) < 0;
|
||||
});
|
||||
return m_sortedApps;
|
||||
}
|
||||
|
||||
quint32 AppDb::getFrequency(const QString& id) const {
|
||||
auto db = QSqlDatabase::database(m_uuid);
|
||||
QSqlQuery query(db);
|
||||
|
||||
query.prepare("SELECT frequency FROM frequencies WHERE id = :id");
|
||||
query.bindValue(":id", id);
|
||||
|
||||
if (query.exec() && query.next()) {
|
||||
return query.value(0).toUInt();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void AppDb::updateAppFrequencies() {
|
||||
const auto before = getSortedApps();
|
||||
|
||||
for (auto* app : std::as_const(m_apps)) {
|
||||
app->setFrequency(getFrequency(app->id()));
|
||||
}
|
||||
|
||||
if (before != getSortedApps()) {
|
||||
emit appsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void AppDb::updateApps() {
|
||||
bool dirty = false;
|
||||
|
||||
for (const auto& entry : std::as_const(m_entries)) {
|
||||
const auto id = entry->property("id").toString();
|
||||
if (!m_apps.contains(id)) {
|
||||
dirty = true;
|
||||
auto* const newEntry = new AppEntry(entry, getFrequency(id), this);
|
||||
QObject::connect(newEntry, &QObject::destroyed, this, [id, this]() {
|
||||
if (m_apps.remove(id)) {
|
||||
emit appsChanged();
|
||||
}
|
||||
});
|
||||
m_apps.insert(id, newEntry);
|
||||
}
|
||||
}
|
||||
|
||||
QSet<QString> newIds;
|
||||
for (const auto& entry : std::as_const(m_entries)) {
|
||||
newIds.insert(entry->property("id").toString());
|
||||
}
|
||||
|
||||
for (auto it = m_apps.keyBegin(); it != m_apps.keyEnd(); ++it) {
|
||||
const auto& id = *it;
|
||||
if (!newIds.contains(id)) {
|
||||
dirty = true;
|
||||
m_apps.take(id)->deleteLater();
|
||||
}
|
||||
}
|
||||
|
||||
if (dirty) {
|
||||
emit appsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ZShell
|
||||
@@ -0,0 +1,106 @@
|
||||
#pragma once
|
||||
|
||||
#include <qhash.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qqmllist.h>
|
||||
#include <qtimer.h>
|
||||
|
||||
namespace ZShell {
|
||||
|
||||
class AppEntry : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("AppEntry instances can only be retrieved from an AppDb")
|
||||
|
||||
// The actual DesktopEntry, but we don't have access to the type so it's a QObject
|
||||
Q_PROPERTY(QObject* entry READ entry CONSTANT)
|
||||
|
||||
Q_PROPERTY(quint32 frequency READ frequency NOTIFY frequencyChanged)
|
||||
Q_PROPERTY(QString id READ id CONSTANT)
|
||||
Q_PROPERTY(QString name READ name NOTIFY nameChanged)
|
||||
Q_PROPERTY(QString comment READ comment NOTIFY commentChanged)
|
||||
Q_PROPERTY(QString execString READ execString NOTIFY execStringChanged)
|
||||
Q_PROPERTY(QString startupClass READ startupClass NOTIFY startupClassChanged)
|
||||
Q_PROPERTY(QString genericName READ genericName NOTIFY genericNameChanged)
|
||||
Q_PROPERTY(QString categories READ categories NOTIFY categoriesChanged)
|
||||
Q_PROPERTY(QString keywords READ keywords NOTIFY keywordsChanged)
|
||||
|
||||
public:
|
||||
explicit AppEntry(QObject* entry, quint32 frequency, QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] QObject* entry() const;
|
||||
|
||||
[[nodiscard]] quint32 frequency() const;
|
||||
void setFrequency(quint32 frequency);
|
||||
void incrementFrequency();
|
||||
|
||||
[[nodiscard]] QString id() const;
|
||||
[[nodiscard]] QString name() const;
|
||||
[[nodiscard]] QString comment() const;
|
||||
[[nodiscard]] QString execString() const;
|
||||
[[nodiscard]] QString startupClass() const;
|
||||
[[nodiscard]] QString genericName() const;
|
||||
[[nodiscard]] QString categories() const;
|
||||
[[nodiscard]] QString keywords() const;
|
||||
|
||||
signals:
|
||||
void frequencyChanged();
|
||||
void nameChanged();
|
||||
void commentChanged();
|
||||
void execStringChanged();
|
||||
void startupClassChanged();
|
||||
void genericNameChanged();
|
||||
void categoriesChanged();
|
||||
void keywordsChanged();
|
||||
|
||||
private:
|
||||
QObject* m_entry;
|
||||
quint32 m_frequency;
|
||||
};
|
||||
|
||||
class AppDb : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(QString uuid READ uuid CONSTANT)
|
||||
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged REQUIRED)
|
||||
Q_PROPERTY(QObjectList entries READ entries WRITE setEntries NOTIFY entriesChanged REQUIRED)
|
||||
Q_PROPERTY(QQmlListProperty<ZShell::AppEntry> apps READ apps NOTIFY appsChanged)
|
||||
|
||||
public:
|
||||
explicit AppDb(QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] QString uuid() const;
|
||||
|
||||
[[nodiscard]] QString path() const;
|
||||
void setPath(const QString& path);
|
||||
|
||||
[[nodiscard]] QObjectList entries() const;
|
||||
void setEntries(const QObjectList& entries);
|
||||
|
||||
[[nodiscard]] QQmlListProperty<AppEntry> apps();
|
||||
|
||||
Q_INVOKABLE void incrementFrequency(const QString& id);
|
||||
|
||||
signals:
|
||||
void pathChanged();
|
||||
void entriesChanged();
|
||||
void appsChanged();
|
||||
|
||||
private:
|
||||
QTimer* m_timer;
|
||||
|
||||
const QString m_uuid;
|
||||
QString m_path;
|
||||
QObjectList m_entries;
|
||||
QHash<QString, AppEntry*> m_apps;
|
||||
mutable QList<AppEntry*> m_sortedApps;
|
||||
|
||||
QList<AppEntry*>& getSortedApps() const;
|
||||
quint32 getFrequency(const QString& id) const;
|
||||
void updateAppFrequencies();
|
||||
void updateApps();
|
||||
};
|
||||
|
||||
} // namespace ZShell
|
||||
@@ -0,0 +1,131 @@
|
||||
#include "writefile.hpp"
|
||||
|
||||
#include <QtConcurrent/qtconcurrentrun.h>
|
||||
#include <QtQuick/qquickitemgrabresult.h>
|
||||
#include <QtQuick/qquickwindow.h>
|
||||
#include <qdir.h>
|
||||
#include <qfileinfo.h>
|
||||
#include <qfuturewatcher.h>
|
||||
#include <qqmlengine.h>
|
||||
|
||||
namespace ZShell {
|
||||
|
||||
void ZShellIo::saveItem(QQuickItem* target, const QUrl& path) {
|
||||
this->saveItem(target, path, QRect(), QJSValue(), QJSValue());
|
||||
}
|
||||
|
||||
void ZShellIo::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect) {
|
||||
this->saveItem(target, path, rect, QJSValue(), QJSValue());
|
||||
}
|
||||
|
||||
void ZShellIo::saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved) {
|
||||
this->saveItem(target, path, QRect(), onSaved, QJSValue());
|
||||
}
|
||||
|
||||
void ZShellIo::saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved, QJSValue onFailed) {
|
||||
this->saveItem(target, path, QRect(), onSaved, onFailed);
|
||||
}
|
||||
|
||||
void ZShellIo::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved) {
|
||||
this->saveItem(target, path, rect, onSaved, QJSValue());
|
||||
}
|
||||
|
||||
void ZShellIo::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved, QJSValue onFailed) {
|
||||
if (!target) {
|
||||
qWarning() << "ZShellIo::saveItem: a target is required";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!path.isLocalFile()) {
|
||||
qWarning() << "ZShellIo::saveItem:" << path << "is not a local file";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!target->window()) {
|
||||
qWarning() << "ZShellIo::saveItem: unable to save target" << target << "without a window";
|
||||
return;
|
||||
}
|
||||
|
||||
auto scaledRect = rect;
|
||||
const qreal scale = target->window()->devicePixelRatio();
|
||||
if (rect.isValid() && !qFuzzyCompare(scale + 1.0, 2.0)) {
|
||||
scaledRect =
|
||||
QRectF(rect.left() * scale, rect.top() * scale, rect.width() * scale, rect.height() * scale).toRect();
|
||||
}
|
||||
|
||||
const QSharedPointer<const QQuickItemGrabResult> grabResult = target->grabToImage();
|
||||
|
||||
QObject::connect(grabResult.data(), &QQuickItemGrabResult::ready, this,
|
||||
[grabResult, scaledRect, path, onSaved, onFailed, this]() {
|
||||
const auto future = QtConcurrent::run([=]() {
|
||||
QImage image = grabResult->image();
|
||||
|
||||
if (scaledRect.isValid()) {
|
||||
image = image.copy(scaledRect);
|
||||
}
|
||||
|
||||
const QString file = path.toLocalFile();
|
||||
const QString parent = QFileInfo(file).absolutePath();
|
||||
return QDir().mkpath(parent) && image.save(file);
|
||||
});
|
||||
|
||||
auto* watcher = new QFutureWatcher<bool>(this);
|
||||
auto* engine = qmlEngine(this);
|
||||
|
||||
QObject::connect(watcher, &QFutureWatcher<bool>::finished, this, [=]() {
|
||||
if (watcher->result()) {
|
||||
if (onSaved.isCallable()) {
|
||||
onSaved.call(
|
||||
{ QJSValue(path.toLocalFile()), engine->toScriptValue(QVariant::fromValue(path)) });
|
||||
}
|
||||
} else {
|
||||
qWarning() << "ZShellIo::saveItem: failed to save" << path;
|
||||
if (onFailed.isCallable()) {
|
||||
onFailed.call({ engine->toScriptValue(QVariant::fromValue(path)) });
|
||||
}
|
||||
}
|
||||
watcher->deleteLater();
|
||||
});
|
||||
watcher->setFuture(future);
|
||||
});
|
||||
}
|
||||
|
||||
bool ZShellIo::copyFile(const QUrl& source, const QUrl& target, bool overwrite) const {
|
||||
if (!source.isLocalFile()) {
|
||||
qWarning() << "ZShellIo::copyFile: source" << source << "is not a local file";
|
||||
return false;
|
||||
}
|
||||
if (!target.isLocalFile()) {
|
||||
qWarning() << "ZShellIo::copyFile: target" << target << "is not a local file";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (overwrite) {
|
||||
if (!QFile::remove(target.toLocalFile())) {
|
||||
qWarning() << "ZShellIo::copyFile: overwrite was specified but failed to remove" << target.toLocalFile();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return QFile::copy(source.toLocalFile(), target.toLocalFile());
|
||||
}
|
||||
|
||||
bool ZShellIo::deleteFile(const QUrl& path) const {
|
||||
if (!path.isLocalFile()) {
|
||||
qWarning() << "ZShellIo::deleteFile: path" << path << "is not a local file";
|
||||
return false;
|
||||
}
|
||||
|
||||
return QFile::remove(path.toLocalFile());
|
||||
}
|
||||
|
||||
QString ZShellIo::toLocalFile(const QUrl& url) const {
|
||||
if (!url.isLocalFile()) {
|
||||
qWarning() << "ZShellIo::toLocalFile: given url is not a local file" << url;
|
||||
return QString();
|
||||
}
|
||||
|
||||
return url.toLocalFile();
|
||||
}
|
||||
|
||||
} // namespace ZShell
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <QtQuick/qquickitem.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
|
||||
namespace ZShell {
|
||||
|
||||
class ZShellIo : public QObject {
|
||||
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_SINGLETON
|
||||
|
||||
public:
|
||||
// clang-format off
|
||||
Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path);
|
||||
Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect);
|
||||
Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved);
|
||||
Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved, QJSValue onFailed);
|
||||
Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved);
|
||||
Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved, QJSValue onFailed);
|
||||
// clang-format on
|
||||
|
||||
Q_INVOKABLE bool copyFile(const QUrl& source, const QUrl& target, bool overwrite = true) const;
|
||||
Q_INVOKABLE bool deleteFile(const QUrl& path) const;
|
||||
Q_INVOKABLE QString toLocalFile(const QUrl& url) const;
|
||||
};
|
||||
|
||||
|
||||
} // namespace ZShell
|
||||
Reference in New Issue
Block a user