screenshot utility

This commit is contained in:
Zacharias-Brohn
2025-11-18 13:47:12 +01:00
parent 77800d1779
commit 5b069bf4c2
19 changed files with 1610 additions and 8 deletions
+1
View File
@@ -1 +1,2 @@
.qmlls.ini .qmlls.ini
build/
+21
View File
@@ -0,0 +1,21 @@
cmake_minimum_required(VERSION 3.19)
project(ZShell)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
set(INSTALL_LIBDIR "usr/lib/ZShell" CACHE STRING "Library install dir")
set(INSTALL_QMLDIR "usr/lib/qt6/qml" CACHE STRING "QML install dir")
add_compile_options(
-Wall -Wextra -Wpedantic -Wshadow -Wconversion
-Wold-style-cast -Wnull-dereference -Wdouble-promotion
-Wformat=2 -Wfloat-equal -Woverloaded-virtual
-Wsign-conversion -Wredundant-decls -Wswitch
-Wunreachable-code
)
add_subdirectory(Plugins)
+69
View File
@@ -0,0 +1,69 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Wayland
import Quickshell.Io
import Quickshell.Hyprland
import ZShell
Scope {
LazyLoader {
id: root
property bool freeze
property bool closing
Variants {
model: Quickshell.screens
PanelWindow {
id: win
color: "transparent"
required property ShellScreen modelData
screen: modelData
WlrLayershell.namespace: "areapicker"
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: root.closing ? WlrKeyboardFocus.None : WlrKeyboardFocus.Exclusive
mask: root.closing ? empty : null
anchors {
top: true
bottom: true
left: true
right: true
}
Region {
id: empty
}
Picker {
loader: root
screen: win.modelData
}
}
}
}
GlobalShortcut {
name: "screenshot"
appid: "ZShell"
onPressed: {
root.freeze = false;
root.closing = false;
root.activeAsync = true;
}
}
GlobalShortcut {
name: "screenshotFreeze"
appid: "ZShell"
onPressed: {
root.freeze = true;
root.closing = false;
root.activeAsync = true;
}
}
}
+292
View File
@@ -0,0 +1,292 @@
pragma ComponentBehavior: Bound
import ZShell
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import QtQuick
import QtQuick.Effects
import qs.Modules
import qs.Config
MouseArea {
id: root
required property LazyLoader loader
required property ShellScreen screen
property bool onClient
property real realRounding: 6
property real realBorderWidth: 1
property real ssx
property real ssy
property real sx: 0
property real sy: 0
property real ex: screen.width
property real ey: screen.height
property real rsx: Math.min(sx, ex)
property real rsy: Math.min(sy, ey)
property real sw: Math.abs(sx - ex)
property real sh: Math.abs(sy - ey)
property list<var> clients: {
const mon = Hyprland.monitorFor(screen);
if (!mon)
return [];
const special = mon.lastIpcObject.specialWorkspace;
const wsId = special.name ? special.id : mon.activeWorkspace.id;
return Hyprland.toplevels.values.filter(c => c.workspace?.id === wsId).sort((a, b) => {
const ac = a.lastIpcObject;
const bc = b.lastIpcObject;
return (bc.pinned - ac.pinned) || ((bc.fullscreen !== 0) - (ac.fullscreen !== 0)) || (bc.floating - ac.floating);
});
}
function checkClientRects(x: real, y: real): void {
for (const client of clients) {
if (!client)
continue;
let {
at: [cx, cy],
size: [cw, ch]
} = client.lastIpcObject;
cx -= screen.x;
cy -= screen.y;
if (cx <= x && cy <= y && cx + cw >= x && cy + ch >= y) {
onClient = true;
sx = cx;
sy = cy;
ex = cx + cw;
ey = cy + ch;
break;
}
}
}
function save(): void {
const tmpfile = Qt.resolvedUrl(`/tmp/zshell-picker-${Quickshell.processId}-${Date.now()}.png`);
ZShellIo.saveItem(screencopy, tmpfile, Qt.rect(Math.ceil(rsx), Math.ceil(rsy), Math.floor(sw), Math.floor(sh)), path => Quickshell.execDetached(["swappy", "-f", path]));
closeAnim.start();
}
onClientsChanged: checkClientRects(mouseX, mouseY)
anchors.fill: parent
opacity: 0
hoverEnabled: true
cursorShape: Qt.CrossCursor
Component.onCompleted: {
if (loader.freeze)
clients = clients;
opacity = 1;
const c = clients[0];
if (c) {
const cx = c.lastIpcObject.at[0] - screen.x;
const cy = c.lastIpcObject.at[1] - screen.y;
onClient = true;
sx = cx;
sy = cy;
ex = cx + c.lastIpcObject.size[0];
ey = cy + c.lastIpcObject.size[1];
} else {
sx = screen.width / 2 - 100;
sy = screen.height / 2 - 100;
ex = screen.width / 2 + 100;
ey = screen.height / 2 + 100;
}
}
onPressed: event => {
ssx = event.x;
ssy = event.y;
}
onReleased: {
if (closeAnim.running)
return;
if (root.loader.freeze) {
save();
} else {
overlay.visible = border.visible = false;
screencopy.visible = false;
screencopy.active = true;
}
}
onPositionChanged: event => {
const x = event.x;
const y = event.y;
if (pressed) {
onClient = false;
sx = ssx;
sy = ssy;
ex = x;
ey = y;
} else {
checkClientRects(x, y);
}
}
focus: true
Keys.onEscapePressed: closeAnim.start()
SequentialAnimation {
id: closeAnim
PropertyAction {
target: root.loader
property: "closing"
value: true
}
ParallelAnimation {
Anim {
target: root
property: "opacity"
to: 0
duration: 300
}
ExAnim {
target: root
properties: "rsx,rsy"
to: 0
}
ExAnim {
target: root
property: "sw"
to: root.screen.width
}
ExAnim {
target: root
property: "sh"
to: root.screen.height
}
}
PropertyAction {
target: root.loader
property: "activeAsync"
value: false
}
}
Loader {
id: screencopy
anchors.fill: parent
active: root.loader.freeze
asynchronous: true
sourceComponent: ScreencopyView {
captureSource: root.screen
paintCursor: false
onHasContentChanged: {
if (hasContent && !root.loader.freeze) {
overlay.visible = border.visible = true;
root.save();
}
}
}
}
Rectangle {
id: overlay
anchors.fill: parent
color: "white"
opacity: 0.3
layer.enabled: true
layer.effect: MultiEffect {
maskSource: selectionWrapper
maskEnabled: true
maskInverted: true
maskSpreadAtMin: 1
maskThresholdMin: 0.5
}
}
Item {
id: selectionWrapper
anchors.fill: parent
layer.enabled: true
visible: false
Rectangle {
id: selectionRect
radius: root.realRounding
x: root.rsx
y: root.rsy
implicitWidth: root.sw
implicitHeight: root.sh
}
}
Rectangle {
id: border
color: "transparent"
radius: root.realRounding > 0 ? root.realRounding + root.realBorderWidth : 0
border.width: root.realBorderWidth
border.color: Config.accentColor.accents.primary
x: selectionRect.x - root.realBorderWidth
y: selectionRect.y - root.realBorderWidth
implicitWidth: selectionRect.implicitWidth + root.realBorderWidth * 2
implicitHeight: selectionRect.implicitHeight + root.realBorderWidth * 2
Behavior on border.color {
Anim {}
}
}
Behavior on opacity {
Anim {
duration: 300
}
}
Behavior on rsx {
enabled: !root.pressed
ExAnim {}
}
Behavior on rsy {
enabled: !root.pressed
ExAnim {}
}
Behavior on sw {
enabled: !root.pressed
ExAnim {}
}
Behavior on sh {
enabled: !root.pressed
ExAnim {}
}
component ExAnim: Anim {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
}
}
+1 -1
View File
@@ -4,7 +4,7 @@ import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Config import qs.Config
import qs.Modules import qs.Modules
import Caelestia.Models import ZShell.Models
Searcher { Searcher {
id: root id: root
+6 -4
View File
@@ -104,15 +104,17 @@ Repeater {
Layout.fillHeight: true Layout.fillHeight: true
Layout.preferredWidth: 30 Layout.preferredWidth: 30
color: collapseArea.containsMouse ? "#15FFFFFF" : "transparent" color: collapseArea.containsMouse ? "#15FFFFFF" : "transparent"
radius: 4 radius: groupColumn.isExpanded ? 4 : Layout.preferredHeight / 2
visible: groupColumn.isExpanded visible: true
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: "\ue944" text: groupColumn.isExpanded ? "\ue944" : groupColumn.notifications.length
font.family: "Material Symbols Rounded" font.family: groupColumn.isExpanded ? "Material Symbols Rounded" : "Rubik"
font.pointSize: 18 font.pointSize: 18
color: "white" color: "white"
} }
MouseArea { MouseArea {
id: collapseArea id: collapseArea
anchors.fill: parent anchors.fill: parent
+4
View File
@@ -127,6 +127,10 @@ Item {
duration: MaterialEasing.expressiveEffectsTime duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects easing.bezierCurve: MaterialEasing.expressiveEffects
} }
onFinished: {
if ( !mouseArea.containsMouse )
closeAnim.start();
}
} }
ParallelAnimation { ParallelAnimation {
+1 -1
View File
@@ -1,6 +1,6 @@
pragma Singleton pragma Singleton
import Caelestia import ZShell
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
+1 -1
View File
@@ -1,7 +1,7 @@
import Quickshell import Quickshell
import Quickshell.Widgets import Quickshell.Widgets
import QtQuick import QtQuick
import Caelestia.Models import ZShell.Models
Item { Item {
id: root id: root
+1
View File
@@ -0,0 +1 @@
add_subdirectory(ZShell)
+43
View File
@@ -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)
+8
View File
@@ -0,0 +1,8 @@
qml_module(ZShell-models
URI ZShell.Models
SOURCES
filesystemmodel.hpp filesystemmodel.cpp
LIBRARIES
Qt::Gui
Qt::Concurrent
)
+479
View File
@@ -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
+148
View File
@@ -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
+265
View File
@@ -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
+106
View File
@@ -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
+131
View File
@@ -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
+31
View File
@@ -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
+2 -1
View File
@@ -2,10 +2,11 @@
//@ pragma Env QSG_RENDER_LOOP=threaded //@ pragma Env QSG_RENDER_LOOP=threaded
import Quickshell import Quickshell
import qs.Modules import qs.Modules
import qs.Helpers
Scope { Scope {
Bar {} Bar {}
Wallpaper {} Wallpaper {}
// NotificationCenter {}
Launcher {} Launcher {}
AreaPicker {}
} }