refactor(wayland): seperate cmake files for modularity

This commit is contained in:
outfoxxed 2024-02-19 00:52:03 -08:00
parent 5bbd0333ef
commit c6dde9ca9d
Signed by untrusted user: outfoxxed
GPG key ID: 4C88A185FB89301E
28 changed files with 32 additions and 33 deletions

31
src/core/CMakeLists.txt Normal file
View file

@ -0,0 +1,31 @@
qt_add_executable(quickshell
main.cpp
shell.cpp
variants.cpp
rootwrapper.cpp
proxywindow.cpp
reload.cpp
rootwrapper.cpp
qmlglobal.cpp
qmlscreen.cpp
watcher.cpp
region.cpp
persistentprops.cpp
shellwindow.cpp
)
qt_add_qml_module(quickshell URI QuickShell)
# qml type registration requires this
target_include_directories(quickshell PRIVATE src/core)
target_link_libraries(quickshell PRIVATE Qt6::Gui Qt6::Qml Qt6::Quick Qt6::QuickControls2)
if (LAYERSHELL)
find_package(LayerShellQt REQUIRED)
target_link_libraries(quickshell PRIVATE LayerShellQtInterface)
target_compile_definitions(quickshell PRIVATE CONF_LAYERSHELL)
target_sources(quickshell PRIVATE layershell.cpp)
endif()

300
src/core/layershell.cpp Normal file
View file

@ -0,0 +1,300 @@
#include "layershell.hpp"
#include <LayerShellQt/window.h>
#include <qmargins.h>
#include <qobject.h>
#include <qqmllist.h>
#include <qquickitem.h>
#include <qquickwindow.h>
#include <qscreen.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qwindow.h>
#include "proxywindow.hpp"
#include "shellwindow.hpp"
WaylandShellWindow::WaylandShellWindow(QObject* parent):
ProxyShellWindow(parent), mWayland(new WaylandShellWindowExtensions(this)) {}
void WaylandShellWindow::setupWindow() {
this->shellWindow = LayerShellQt::Window::get(this->window);
this->ProxyShellWindow::setupWindow();
// clang-format off
QObject::connect(this->shellWindow, &LayerShellQt::Window::anchorsChanged, this, &ProxyShellWindow::anchorsChanged);
QObject::connect(this->shellWindow, &LayerShellQt::Window::marginsChanged, this, &ProxyShellWindow::marginsChanged);
QObject::connect(
this->shellWindow, &LayerShellQt::Window::layerChanged,
this->mWayland, &WaylandShellWindowExtensions::layerChanged
);
QObject::connect(
this->shellWindow, &LayerShellQt::Window::keyboardInteractivityChanged,
this->mWayland, &WaylandShellWindowExtensions::keyboardFocusChanged
);
QObject::connect(this->window, &QWindow::widthChanged, this, &WaylandShellWindow::updateExclusionZone);
QObject::connect(this->window, &QWindow::heightChanged, this, &WaylandShellWindow::updateExclusionZone);
QObject::connect(this, &ProxyShellWindow::anchorsChanged, this, &WaylandShellWindow::updateExclusionZone);
QObject::connect(this, &ProxyShellWindow::marginsChanged, this, &WaylandShellWindow::updateExclusionZone);
// clang-format on
this->setAnchors(this->mAnchors);
this->setMargins(this->mMargins);
this->setExclusionMode(this->mExclusionMode); // also sets exclusion zone
this->mWayland->setLayer(this->mLayer);
this->shellWindow->setScope(this->mScope);
this->mWayland->setKeyboardFocus(this->mKeyboardFocus);
this->connected = true;
}
QQuickWindow* WaylandShellWindow::disownWindow() {
QObject::disconnect(this->shellWindow, nullptr, this, nullptr);
return this->ProxyWindowBase::disownWindow();
}
void WaylandShellWindow::setWidth(qint32 width) {
this->mWidth = width;
// only update the actual size if not blocked by anchors
auto anchors = this->anchors();
if (!anchors.mLeft || !anchors.mRight) this->ProxyShellWindow::setWidth(width);
}
void WaylandShellWindow::setHeight(qint32 height) {
this->mHeight = height;
// only update the actual size if not blocked by anchors
auto anchors = this->anchors();
if (!anchors.mTop || !anchors.mBottom) this->ProxyShellWindow::setHeight(height);
}
void WaylandShellWindow::setAnchors(Anchors anchors) {
if (this->window == nullptr) {
this->mAnchors = anchors;
return;
}
auto lsAnchors = LayerShellQt::Window::Anchors();
if (anchors.mLeft) lsAnchors |= LayerShellQt::Window::AnchorLeft;
if (anchors.mRight) lsAnchors |= LayerShellQt::Window::AnchorRight;
if (anchors.mTop) lsAnchors |= LayerShellQt::Window::AnchorTop;
if (anchors.mBottom) lsAnchors |= LayerShellQt::Window::AnchorBottom;
if (!anchors.mLeft || !anchors.mRight) this->ProxyWindowBase::setWidth(this->mWidth);
if (!anchors.mTop || !anchors.mBottom) this->ProxyWindowBase::setHeight(this->mHeight);
this->shellWindow->setAnchors(lsAnchors);
}
Anchors WaylandShellWindow::anchors() const {
if (this->window == nullptr) return this->mAnchors;
auto lsAnchors = this->shellWindow->anchors();
Anchors anchors;
anchors.mLeft = lsAnchors.testFlag(LayerShellQt::Window::AnchorLeft);
anchors.mRight = lsAnchors.testFlag(LayerShellQt::Window::AnchorRight);
anchors.mTop = lsAnchors.testFlag(LayerShellQt::Window::AnchorTop);
anchors.mBottom = lsAnchors.testFlag(LayerShellQt::Window::AnchorBottom);
return anchors;
}
void WaylandShellWindow::setExclusiveZone(qint32 zone) {
if (zone < 0) zone = 0;
if (this->connected && zone == this->mExclusionZone) return;
this->mExclusionZone = zone;
if (this->window != nullptr && this->exclusionMode() == ExclusionMode::Normal) {
this->shellWindow->setExclusiveZone(zone);
emit this->exclusionZoneChanged();
}
}
qint32 WaylandShellWindow::exclusiveZone() const {
if (this->window == nullptr) return this->mExclusionZone;
else return this->shellWindow->exclusionZone();
}
ExclusionMode::Enum WaylandShellWindow::exclusionMode() const { return this->mExclusionMode; }
void WaylandShellWindow::setExclusionMode(ExclusionMode::Enum exclusionMode) {
if (this->connected && exclusionMode == this->mExclusionMode) return;
this->mExclusionMode = exclusionMode;
if (this->window != nullptr) {
if (exclusionMode == ExclusionMode::Normal) {
this->shellWindow->setExclusiveZone(this->mExclusionZone);
emit this->exclusionZoneChanged();
} else if (exclusionMode == ExclusionMode::Ignore) {
this->shellWindow->setExclusiveZone(-1);
emit this->exclusionZoneChanged();
} else {
this->updateExclusionZone();
}
}
}
void WaylandShellWindow::setMargins(Margins margins) {
if (this->window == nullptr) this->mMargins = margins;
else {
auto lsMargins = QMargins(margins.mLeft, margins.mTop, margins.mRight, margins.mBottom);
this->shellWindow->setMargins(lsMargins);
}
}
Margins WaylandShellWindow::margins() const {
if (this->window == nullptr) return this->mMargins;
auto lsMargins = this->shellWindow->margins();
auto margins = Margins();
margins.mLeft = lsMargins.left();
margins.mRight = lsMargins.right();
margins.mTop = lsMargins.top();
margins.mBottom = lsMargins.bottom();
return margins;
}
void WaylandShellWindowExtensions::setLayer(Layer::Enum layer) {
if (this->window->window == nullptr) {
this->window->mLayer = layer;
return;
}
auto lsLayer = LayerShellQt::Window::LayerBackground;
// clang-format off
switch (layer) {
case Layer::Background: lsLayer = LayerShellQt::Window::LayerBackground; break;
case Layer::Bottom: lsLayer = LayerShellQt::Window::LayerBottom; break;
case Layer::Top: lsLayer = LayerShellQt::Window::LayerTop; break;
case Layer::Overlay: lsLayer = LayerShellQt::Window::LayerOverlay; break;
}
// clang-format on
this->window->shellWindow->setLayer(lsLayer);
}
Layer::Enum WaylandShellWindowExtensions::layer() const {
if (this->window->window == nullptr) return this->window->mLayer;
auto layer = Layer::Top;
auto lsLayer = this->window->shellWindow->layer();
// clang-format off
switch (lsLayer) {
case LayerShellQt::Window::LayerBackground: layer = Layer::Background; break;
case LayerShellQt::Window::LayerBottom: layer = Layer::Bottom; break;
case LayerShellQt::Window::LayerTop: layer = Layer::Top; break;
case LayerShellQt::Window::LayerOverlay: layer = Layer::Overlay; break;
}
// clang-format on
return layer;
}
void WaylandShellWindowExtensions::setScope(const QString& scope) {
if (this->window->window == nullptr) this->window->mScope = scope;
else this->window->shellWindow->setScope(scope);
}
QString WaylandShellWindowExtensions::scope() const {
if (this->window->window == nullptr) return this->window->mScope;
else return this->window->shellWindow->scope();
}
void WaylandShellWindowExtensions::setKeyboardFocus(KeyboardFocus::Enum focus) {
if (this->window->window == nullptr) {
this->window->mKeyboardFocus = focus;
return;
}
auto lsFocus = LayerShellQt::Window::KeyboardInteractivityNone;
// clang-format off
switch (focus) {
case KeyboardFocus::None: lsFocus = LayerShellQt::Window::KeyboardInteractivityNone; break;
case KeyboardFocus::Exclusive: lsFocus = LayerShellQt::Window::KeyboardInteractivityExclusive; break;
case KeyboardFocus::OnDemand: lsFocus = LayerShellQt::Window::KeyboardInteractivityOnDemand; break;
}
// clang-format on
this->window->shellWindow->setKeyboardInteractivity(lsFocus);
}
KeyboardFocus::Enum WaylandShellWindowExtensions::keyboardFocus() const {
if (this->window->window == nullptr) return this->window->mKeyboardFocus;
auto focus = KeyboardFocus::None;
auto lsFocus = this->window->shellWindow->keyboardInteractivity();
// clang-format off
switch (lsFocus) {
case LayerShellQt::Window::KeyboardInteractivityNone: focus = KeyboardFocus::None; break;
case LayerShellQt::Window::KeyboardInteractivityExclusive: focus = KeyboardFocus::Exclusive; break;
case LayerShellQt::Window::KeyboardInteractivityOnDemand: focus = KeyboardFocus::OnDemand; break;
}
// clang-format on
return focus;
}
void WaylandShellWindowExtensions::setScreenConfiguration(ScreenConfiguration::Enum configuration) {
if (this->window->window == nullptr) {
this->window->mScreenConfiguration = configuration;
return;
}
auto lsConfiguration = LayerShellQt::Window::ScreenFromQWindow;
// clang-format off
switch (configuration) {
case ScreenConfiguration::Window: lsConfiguration = LayerShellQt::Window::ScreenFromQWindow; break;
case ScreenConfiguration::Compositor: lsConfiguration = LayerShellQt::Window::ScreenFromCompositor; break;
}
// clang-format on
this->window->shellWindow->setScreenConfiguration(lsConfiguration);
}
ScreenConfiguration::Enum WaylandShellWindowExtensions::screenConfiguration() const {
if (this->window->window == nullptr) return this->window->mScreenConfiguration;
auto configuration = ScreenConfiguration::Window;
auto lsConfiguration = this->window->shellWindow->screenConfiguration();
// clang-format off
switch (lsConfiguration) {
case LayerShellQt::Window::ScreenFromQWindow: configuration = ScreenConfiguration::Window; break;
case LayerShellQt::Window::ScreenFromCompositor: configuration = ScreenConfiguration::Compositor; break;
}
// clang-format on
return configuration;
}
void WaylandShellWindow::updateExclusionZone() {
if (this->window != nullptr && this->exclusionMode() == ExclusionMode::Auto) {
auto anchors = this->anchors();
auto zone = -1;
if (anchors.mTop && anchors.mBottom) {
if (anchors.mLeft) zone = this->width() + this->margins().mLeft;
else if (anchors.mRight) zone = this->width() + this->margins().mRight;
} else if (anchors.mLeft && anchors.mRight) {
if (anchors.mTop) zone = this->height() + this->margins().mTop;
else if (anchors.mBottom) zone = this->height() + this->margins().mBottom;
}
if (zone != -1) {
this->shellWindow->setExclusiveZone(zone);
emit this->exclusionZoneChanged();
}
}
}

144
src/core/layershell.hpp Normal file
View file

@ -0,0 +1,144 @@
#pragma once
#include <LayerShellQt/window.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "shellwindow.hpp"
namespace Layer { // NOLINT
Q_NAMESPACE;
QML_ELEMENT;
enum Enum {
Background = 0,
Bottom = 1,
Top = 2,
Overlay = 3,
};
Q_ENUM_NS(Enum);
} // namespace Layer
/// Type of keyboard focus that will be accepted by a [ShellWindow]
///
/// [ShellWindow]: ../shellwindow
namespace KeyboardFocus { // NOLINT
Q_NAMESPACE;
QML_ELEMENT;
enum Enum {
/// No keyboard input will be accepted.
None = 0,
/// Exclusive access to the keyboard, locking out all other windows.
Exclusive = 1,
/// Access to the keyboard as determined by the operating system.
///
/// > [!WARNING] On some systems, `OnDemand` may cause the shell window to
/// > retain focus over another window unexpectedly.
/// > You should try `None` if you experience issues.
OnDemand = 2,
};
Q_ENUM_NS(Enum);
} // namespace KeyboardFocus
namespace ScreenConfiguration { // NOLINT
Q_NAMESPACE;
QML_ELEMENT;
enum Enum {
Window = 0,
Compositor = 1,
};
Q_ENUM_NS(Enum);
} // namespace ScreenConfiguration
class WaylandShellWindowExtensions;
class WaylandShellWindow: public ProxyShellWindow {
Q_OBJECT;
Q_PROPERTY(WaylandShellWindowExtensions* wayland MEMBER mWayland CONSTANT);
QML_NAMED_ELEMENT(ShellWindow);
public:
explicit WaylandShellWindow(QObject* parent = nullptr);
WaylandShellWindowExtensions* wayland();
void setupWindow() override;
QQuickWindow* disownWindow() override;
void setWidth(qint32 width) override;
void setHeight(qint32 height) override;
void setAnchors(Anchors anchors) override;
[[nodiscard]] Anchors anchors() const override;
void setExclusiveZone(qint32 zone) override;
[[nodiscard]] qint32 exclusiveZone() const override;
void setExclusionMode(ExclusionMode::Enum exclusionMode) override;
[[nodiscard]] ExclusionMode::Enum exclusionMode() const override;
void setMargins(Margins margins) override;
[[nodiscard]] Margins margins() const override;
protected slots:
void updateExclusionZone();
private:
WaylandShellWindowExtensions* mWayland = nullptr;
LayerShellQt::Window* shellWindow = nullptr;
Layer::Enum mLayer = Layer::Top;
QString mScope;
KeyboardFocus::Enum mKeyboardFocus = KeyboardFocus::None;
ScreenConfiguration::Enum mScreenConfiguration = ScreenConfiguration::Window;
bool connected = false;
friend class WaylandShellWindowExtensions;
};
class WaylandShellWindowExtensions: public QObject {
Q_OBJECT;
/// The shell layer the window sits in. Defaults to `Layer.Top`.
Q_PROPERTY(Layer::Enum layer READ layer WRITE setLayer NOTIFY layerChanged);
Q_PROPERTY(QString scope READ scope WRITE setScope);
/// The degree of keyboard focus taken. Defaults to `KeyboardFocus.None`.
Q_PROPERTY(KeyboardFocus::Enum keyboardFocus READ keyboardFocus WRITE setKeyboardFocus NOTIFY
keyboardFocusChanged);
Q_PROPERTY(ScreenConfiguration::Enum screenConfiguration READ screenConfiguration WRITE
setScreenConfiguration);
QML_ELEMENT;
QML_UNCREATABLE("WaylandShellWindowExtensions cannot be created");
public:
explicit WaylandShellWindowExtensions(WaylandShellWindow* window):
QObject(window), window(window) {}
void setLayer(Layer::Enum layer);
[[nodiscard]] Layer::Enum layer() const;
void setScope(const QString& scope);
[[nodiscard]] QString scope() const;
void setKeyboardFocus(KeyboardFocus::Enum focus);
[[nodiscard]] KeyboardFocus::Enum keyboardFocus() const;
void setScreenConfiguration(ScreenConfiguration::Enum configuration);
[[nodiscard]] ScreenConfiguration::Enum screenConfiguration() const;
signals:
void layerChanged();
void keyboardFocusChanged();
private:
WaylandShellWindow* window;
friend class WaylandShellWindow;
};

56
src/core/main.cpp Normal file
View file

@ -0,0 +1,56 @@
#include <qcommandlineoption.h>
#include <qcommandlineparser.h>
#include <qdir.h>
#include <qguiapplication.h>
#include <qlogging.h>
#include <qobject.h>
#include <qquickwindow.h>
#include <qstandardpaths.h>
#include <qstring.h>
#include "rootwrapper.hpp"
#ifdef CONF_LAYERSHELL
#include <LayerShellQt/shell.h>
#endif
int main(int argc, char** argv) {
const auto app = QGuiApplication(argc, argv);
QGuiApplication::setApplicationName("quickshell");
QGuiApplication::setApplicationVersion("0.0.1");
QCommandLineParser parser;
parser.addHelpOption();
parser.addVersionOption();
auto configOption = QCommandLineOption({"c", "config"}, "Path to configuration file.", "path");
parser.addOption(configOption);
parser.process(app);
QString configPath;
if (parser.isSet(configOption)) {
configPath = parser.value(configOption);
} else {
configPath = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation);
configPath = QDir(QDir(configPath).filePath("quickshell")).filePath("shell.qml");
}
qInfo() << "config file path:" << configPath;
if (!QFile(configPath).exists()) {
qCritical() << "config file does not exist";
return -1;
}
#if CONF_LAYERSHELL
LayerShellQt::Shell::useLayerShell();
#endif
// Base window transparency appears to be additive.
// Use a fully transparent window with a colored rect.
QQuickWindow::setDefaultAlphaBuffer(true);
auto root = RootWrapper(configPath);
return QGuiApplication::exec();
}

15
src/core/module.md Normal file
View file

@ -0,0 +1,15 @@
name = "QuickShell"
description = "Core QuickShell types"
headers = [
"qmlglobal.hpp",
"qmlscreen.hpp",
"reload.hpp",
"shell.hpp",
"variants.hpp",
"proxywindow.hpp",
"layershell.hpp",
"region.hpp",
"persistentprops.hpp",
]
-----
The core types provided by QuickShell

View file

@ -0,0 +1,24 @@
#include "persistentprops.hpp"
#include <qobject.h>
#include <qtmetamacros.h>
void PersistentProperties::onReload(QObject* oldInstance) {
if (qobject_cast<PersistentProperties*>(oldInstance) == nullptr) {
emit this->loaded();
return;
}
const auto* metaObject = this->metaObject();
for (auto i = metaObject->propertyOffset(); i < metaObject->propertyCount(); i++) {
const auto prop = metaObject->property(i);
auto oldProp = oldInstance->property(prop.name());
if (oldProp.isValid()) {
this->setProperty(prop.name(), oldProp);
}
}
emit this->loaded();
emit this->reloaded();
}

View file

@ -0,0 +1,57 @@
#pragma once
#include <qobject.h>
#include <qqmlintegration.h>
#include "reload.hpp"
///! Object that holds properties that can persist across a config reload.
/// PersistentProperties holds properties declated in it across a reload, which is
/// often useful for things like keeping expandable popups open and styling them.
///
/// Below is an example of using `PersistentProperties` to keep track of the state
/// of an expandable panel. When the configuration is reloaded, the `expanderOpen` property
/// will be saved and the expandable panel will stay in the open/closed state.
///
/// ```qml
/// PersistentProperties {
/// id: persist
/// reloadableId: "persistedStates"
///
/// property bool expanderOpen: false
/// }
///
/// Button {
/// id: expanderButton
/// anchors.centerIn: parent
/// text: "toggle expander"
/// onClicked: persist.expanderOpen = !persist.expanderOpen
/// }
///
/// Rectangle {
/// anchors.top: expanderButton.bottom
/// anchors.left: expanderButton.left
/// anchors.right: expanderButton.right
/// height: 100
///
/// color: "lightblue"
/// visible: persist.expanderOpen
/// }
/// ```
class PersistentProperties: public Reloadable {
Q_OBJECT;
QML_ELEMENT;
public:
PersistentProperties(QObject* parent = nullptr): Reloadable(parent) {}
void onReload(QObject* oldInstance) override;
signals:
/// Called every time the reload stage completes.
/// Will be called every time, including when nothing was loaded from an old instance.
void loaded();
/// Called every time the properties are reloaded.
/// Will not be called if no old instance was loaded.
void reloaded();
};

216
src/core/proxywindow.cpp Normal file
View file

@ -0,0 +1,216 @@
#include "proxywindow.hpp"
#include <qobject.h>
#include <qqmllist.h>
#include <qquickitem.h>
#include <qquickwindow.h>
#include <qregion.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qwindow.h>
#include "qmlscreen.hpp"
#include "region.hpp"
#include "reload.hpp"
ProxyWindowBase::ProxyWindowBase(QObject* parent): Reloadable(parent) {
this->mContentItem = new QQuickItem(); // NOLINT
this->mContentItem->setParent(this);
QObject::connect(this, &ProxyWindowBase::widthChanged, this, &ProxyWindowBase::onWidthChanged);
QObject::connect(this, &ProxyWindowBase::heightChanged, this, &ProxyWindowBase::onHeightChanged);
}
ProxyWindowBase::~ProxyWindowBase() {
if (this->window != nullptr) {
this->window->deleteLater();
}
}
void ProxyWindowBase::onReload(QObject* oldInstance) {
auto* old = qobject_cast<ProxyWindowBase*>(oldInstance);
if (old == nullptr || old->window == nullptr) {
this->window = new QQuickWindow();
} else {
this->window = old->disownWindow();
}
this->setupWindow();
Reloadable::reloadRecursive(this->mContentItem, oldInstance);
this->mContentItem->setParentItem(this->window->contentItem());
this->mContentItem->setWidth(this->width());
this->mContentItem->setHeight(this->height());
// without this the dangling screen pointer wont be updated to a real screen
emit this->screenChanged();
emit this->windowConnected();
this->window->setVisible(this->mVisible);
}
void ProxyWindowBase::setupWindow() {
// clang-format off
QObject::connect(this->window, &QWindow::visibilityChanged, this, &ProxyWindowBase::visibleChanged);
QObject::connect(this->window, &QWindow::widthChanged, this, &ProxyWindowBase::widthChanged);
QObject::connect(this->window, &QWindow::heightChanged, this, &ProxyWindowBase::heightChanged);
QObject::connect(this->window, &QWindow::screenChanged, this, &ProxyWindowBase::screenChanged);
QObject::connect(this->window, &QQuickWindow::colorChanged, this, &ProxyWindowBase::colorChanged);
QObject::connect(this, &ProxyWindowBase::maskChanged, this, &ProxyWindowBase::onMaskChanged);
QObject::connect(this, &ProxyWindowBase::widthChanged, this, &ProxyWindowBase::onMaskChanged);
QObject::connect(this, &ProxyWindowBase::heightChanged, this, &ProxyWindowBase::onMaskChanged);
// clang-format on
this->window->setScreen(this->mScreen);
this->setWidth(this->mWidth);
this->setHeight(this->mHeight);
this->setColor(this->mColor);
this->updateMask();
}
QQuickWindow* ProxyWindowBase::disownWindow() {
QObject::disconnect(this->window, nullptr, this, nullptr);
this->mContentItem->setParentItem(nullptr);
auto* window = this->window;
this->window = nullptr;
return window;
}
QQuickWindow* ProxyWindowBase::backingWindow() const { return this->window; }
QQuickItem* ProxyWindowBase::contentItem() const { return this->mContentItem; }
bool ProxyWindowBase::isVisible() const {
if (this->window == nullptr) return this->mVisible;
else return this->window->isVisible();
}
void ProxyWindowBase::setVisible(bool visible) {
if (this->window == nullptr) {
this->mVisible = visible;
emit this->visibleChanged();
} else this->window->setVisible(visible);
}
qint32 ProxyWindowBase::width() const {
if (this->window == nullptr) return this->mWidth;
else return this->window->width();
}
void ProxyWindowBase::setWidth(qint32 width) {
if (this->window == nullptr) {
this->mWidth = width;
emit this->widthChanged();
} else this->window->setWidth(width);
}
qint32 ProxyWindowBase::height() const {
if (this->window == nullptr) return this->mHeight;
else return this->window->height();
}
void ProxyWindowBase::setHeight(qint32 height) {
if (this->window == nullptr) {
this->mHeight = height;
emit this->heightChanged();
} else this->window->setHeight(height);
}
void ProxyWindowBase::setScreen(QuickShellScreenInfo* screen) {
if (this->mScreen != nullptr) {
QObject::disconnect(this->mScreen, nullptr, this, nullptr);
}
auto* qscreen = screen == nullptr ? nullptr : screen->screen;
if (qscreen != nullptr) {
QObject::connect(qscreen, &QObject::destroyed, this, &ProxyWindowBase::onScreenDestroyed);
}
if (this->window == nullptr) this->mScreen = qscreen;
else this->window->setScreen(qscreen);
}
void ProxyWindowBase::onScreenDestroyed() { this->mScreen = nullptr; }
QuickShellScreenInfo* ProxyWindowBase::screen() const {
QScreen* qscreen = nullptr;
if (this->window == nullptr) {
if (this->mScreen != nullptr) qscreen = this->mScreen;
} else {
qscreen = this->window->screen();
}
return new QuickShellScreenInfo(
const_cast<ProxyWindowBase*>(this), // NOLINT
qscreen
);
}
QColor ProxyWindowBase::color() const {
if (this->window == nullptr) return this->mColor;
else return this->window->color();
}
void ProxyWindowBase::setColor(QColor color) {
if (this->window == nullptr) {
this->mColor = color;
emit this->colorChanged();
} else this->window->setColor(color);
}
PendingRegion* ProxyWindowBase::mask() const { return this->mMask; }
void ProxyWindowBase::setMask(PendingRegion* mask) {
if (this->mMask != nullptr) {
this->mMask->deleteLater();
}
if (mask != nullptr) {
mask->setParent(this);
this->mMask = mask;
QObject::connect(mask, &PendingRegion::changed, this, &ProxyWindowBase::maskChanged);
emit this->maskChanged();
}
}
void ProxyWindowBase::onMaskChanged() {
if (this->window != nullptr) this->updateMask();
}
void ProxyWindowBase::updateMask() {
QRegion mask;
if (this->mMask != nullptr) {
// if left as the default, dont combine it with the whole window area, leave it as is.
if (this->mMask->mIntersection == Intersection::Combine) {
mask = this->mMask->build();
} else {
auto windowRegion = QRegion(QRect(0, 0, this->width(), this->height()));
mask = this->mMask->applyTo(windowRegion);
}
}
this->window->setMask(mask);
}
QQmlListProperty<QObject> ProxyWindowBase::data() {
return this->mContentItem->property("data").value<QQmlListProperty<QObject>>();
}
void ProxyWindowBase::onWidthChanged() { this->mContentItem->setWidth(this->width()); }
void ProxyWindowBase::onHeightChanged() { this->mContentItem->setHeight(this->height()); }
void ProxyFloatingWindow::setWidth(qint32 width) {
if (this->window == nullptr || !this->window->isVisible()) this->ProxyWindowBase::setWidth(width);
}
void ProxyFloatingWindow::setHeight(qint32 height) {
if (this->window == nullptr || !this->window->isVisible())
this->ProxyWindowBase::setHeight(height);
}

190
src/core/proxywindow.hpp Normal file
View file

@ -0,0 +1,190 @@
#pragma once
#include <qcolor.h>
#include <qcontainerfwd.h>
#include <qevent.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qqmllist.h>
#include <qqmlparserstatus.h>
#include <qquickitem.h>
#include <qquickwindow.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "qmlscreen.hpp"
#include "region.hpp"
#include "reload.hpp"
// Proxy to an actual window exposing a limited property set with the ability to
// transfer it to a new window.
///! Base class for reloadable windows
/// Base class for reloadable windows. See [ShellWindow] and [FloatingWindow]
///
/// [ShellWindow]: ../shellwindow
/// [FloatingWindow]: ../floatingwindow
class ProxyWindowBase: public Reloadable {
Q_OBJECT;
/// The QtQuick window backing this window.
///
/// > [!WARNING] Do not expect values set via this property to work correctly.
/// > Values set this way will almost certainly misbehave across a reload, possibly
/// > even without one.
/// >
/// > Use **only** if you know what you are doing.
Q_PROPERTY(QQuickWindow* _backingWindow READ backingWindow);
Q_PROPERTY(QQuickItem* contentItem READ contentItem);
/// If the window is shown or hidden. Defaults to true.
Q_PROPERTY(bool visible READ isVisible WRITE setVisible NOTIFY visibleChanged);
Q_PROPERTY(qint32 width READ width WRITE setWidth NOTIFY widthChanged);
Q_PROPERTY(qint32 height READ height WRITE setHeight NOTIFY heightChanged);
/// The screen that the window currently occupies.
///
/// > [!INFO] This cannot be changed while the window is visible.
Q_PROPERTY(QuickShellScreenInfo* screen READ screen WRITE setScreen NOTIFY screenChanged);
/// The background color of the window. Defaults to white.
///
/// > [!WARNING] This seems to behave weirdly when using transparent colors on some systems.
/// > Using a colored content item over a transparent window is the recommended way to work around this:
/// > ```qml
/// > ProxyWindow {
/// > Rectangle {
/// > anchors.fill: parent
/// > color: "#20ffffff"
/// >
/// > // your content here
/// > }
/// > }
/// > ```
Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged);
/// The clickthrough mask. Defaults to null.
///
/// If non null then the clickable areas of the window will be determined by the provided region.
///
/// ```qml
/// ShellWindow {
/// // The mask region is set to `rect`, meaning only `rect` is clickable.
/// // All other clicks pass through the window to ones behind it.
/// mask: Region { item: rect }
///
/// Rectangle {
/// id: rect
///
/// anchors.centerIn: parent
/// width: 100
/// height: 100
/// }
/// }
/// ```
///
/// If the provided region's intersection mode is `Combine` (the default),
/// then the region will be used as is. Otherwise it will be applied on top of the window region.
///
/// For example, setting the intersection mode to `Xor` will invert the mask and make everything in
/// the mask region not clickable and pass through clicks inside it through the window.
///
/// ```qml
/// ShellWindow {
/// // The mask region is set to `rect`, but the intersection mode is set to `Xor`.
/// // This inverts the mask causing all clicks inside `rect` to be passed to the window
/// // behind this one.
/// mask: Region { item: rect; intersection: Intersection.Xor }
///
/// Rectangle {
/// id: rect
///
/// anchors.centerIn: parent
/// width: 100
/// height: 100
/// }
/// }
/// ```
Q_PROPERTY(PendingRegion* mask READ mask WRITE setMask NOTIFY maskChanged);
Q_PROPERTY(QQmlListProperty<QObject> data READ data);
Q_CLASSINFO("DefaultProperty", "data");
QML_ELEMENT;
QML_UNCREATABLE("use ShellWindow or FloatingWindow");
public:
explicit ProxyWindowBase(QObject* parent = nullptr);
~ProxyWindowBase() override;
ProxyWindowBase(ProxyWindowBase&) = delete;
ProxyWindowBase(ProxyWindowBase&&) = delete;
void operator=(ProxyWindowBase&) = delete;
void operator=(ProxyWindowBase&&) = delete;
void onReload(QObject* oldInstance) override;
virtual void setupWindow();
// Disown the backing window and delete all its children.
virtual QQuickWindow* disownWindow();
[[nodiscard]] QQuickWindow* backingWindow() const;
[[nodiscard]] QQuickItem* contentItem() const;
[[nodiscard]] virtual bool isVisible() const;
virtual void setVisible(bool visible);
[[nodiscard]] virtual qint32 width() const;
virtual void setWidth(qint32 width);
[[nodiscard]] virtual qint32 height() const;
virtual void setHeight(qint32 height);
virtual void setScreen(QuickShellScreenInfo* screen);
[[nodiscard]] QuickShellScreenInfo* screen() const;
[[nodiscard]] QColor color() const;
void setColor(QColor color);
[[nodiscard]] PendingRegion* mask() const;
void setMask(PendingRegion* mask);
QQmlListProperty<QObject> data();
signals:
void windowConnected();
void visibleChanged();
void widthChanged();
void heightChanged();
void screenChanged();
void colorChanged();
void maskChanged();
private slots:
void onMaskChanged();
void onWidthChanged();
void onHeightChanged();
void onScreenDestroyed();
protected:
bool mVisible = true;
qint32 mWidth = 100;
qint32 mHeight = 100;
QScreen* mScreen = nullptr;
QColor mColor = Qt::white;
PendingRegion* mMask = nullptr;
QQuickWindow* window = nullptr;
QQuickItem* mContentItem = nullptr;
private:
void updateMask();
};
// qt attempts to resize the window but fails because wayland
// and only resizes the graphics context which looks terrible.
///! Standard floating window.
class ProxyFloatingWindow: public ProxyWindowBase {
Q_OBJECT;
QML_NAMED_ELEMENT(FloatingWindow);
public:
// Setting geometry while the window is visible makes the content item shrink but not the window
// which is awful so we disable it for floating windows.
void setWidth(qint32 width) override;
void setHeight(qint32 height) override;
};

76
src/core/qmlglobal.cpp Normal file
View file

@ -0,0 +1,76 @@
#include "qmlglobal.hpp"
#include <qcontainerfwd.h>
#include <qcoreapplication.h>
#include <qguiapplication.h>
#include <qlogging.h>
#include <qobject.h>
#include <qqmlcontext.h>
#include <qqmlengine.h>
#include <qqmllist.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "qmlscreen.hpp"
#include "rootwrapper.hpp"
QuickShellGlobal::QuickShellGlobal(QObject* parent): QObject(parent) {
auto* app = QCoreApplication::instance();
auto* guiApp = qobject_cast<QGuiApplication*>(app);
if (guiApp != nullptr) {
// clang-format off
QObject::connect(guiApp, &QGuiApplication::primaryScreenChanged, this, &QuickShellGlobal::updateScreens);
QObject::connect(guiApp, &QGuiApplication::screenAdded, this, &QuickShellGlobal::updateScreens);
QObject::connect(guiApp, &QGuiApplication::screenRemoved, this, &QuickShellGlobal::updateScreens);
// clang-format on
this->updateScreens();
}
}
qsizetype QuickShellGlobal::screensCount(QQmlListProperty<QuickShellScreenInfo>* prop) {
return static_cast<QuickShellGlobal*>(prop->object)->mScreens.size(); // NOLINT
}
QuickShellScreenInfo*
QuickShellGlobal::screenAt(QQmlListProperty<QuickShellScreenInfo>* prop, qsizetype i) {
return static_cast<QuickShellGlobal*>(prop->object)->mScreens.at(i); // NOLINT
}
QQmlListProperty<QuickShellScreenInfo> QuickShellGlobal::screens() {
return QQmlListProperty<QuickShellScreenInfo>(
this,
nullptr,
&QuickShellGlobal::screensCount,
&QuickShellGlobal::screenAt
);
}
void QuickShellGlobal::reload(bool hard) {
auto* rootobj = QQmlEngine::contextForObject(this)->engine()->parent();
auto* root = qobject_cast<RootWrapper*>(rootobj);
if (root == nullptr) {
qWarning() << "cannot find RootWrapper for reload, ignoring request";
return;
}
root->reloadGraph(hard);
}
void QuickShellGlobal::updateScreens() {
auto screens = QGuiApplication::screens();
this->mScreens.resize(screens.size());
for (auto i = 0; i < screens.size(); i++) {
if (this->mScreens[i] != nullptr) {
this->mScreens[i]->screen = nullptr;
this->mScreens[i]->setParent(nullptr); // delete if not owned by the js engine
}
this->mScreens[i] = new QuickShellScreenInfo(this, screens[i]);
}
emit this->screensChanged();
}

64
src/core/qmlglobal.hpp Normal file
View file

@ -0,0 +1,64 @@
#pragma once
#include <qcontainerfwd.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qqmllist.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "qmlscreen.hpp"
class QuickShellGlobal: public QObject {
Q_OBJECT;
/// All currently connected screens.
///
/// This property updates as connected screens change.
///
/// #### Reusing a window on every screen
/// ```qml
/// ShellRoot {
/// Variants {
/// ShellWindow {
/// // ...
/// }
///
/// // see Variants for details
/// variants: QuickShell.screens.map(screen => ({ screen }))
/// }
/// }
/// ```
///
/// This creates an instance of your window once on every screen.
/// As screens are added or removed your window will be created or destroyed on those screens.
Q_PROPERTY(QQmlListProperty<QuickShellScreenInfo> screens READ screens NOTIFY screensChanged);
QML_SINGLETON;
QML_NAMED_ELEMENT(QuickShell);
public:
QuickShellGlobal(QObject* parent = nullptr);
QQmlListProperty<QuickShellScreenInfo> screens();
/// Reload the shell from the [ShellRoot].
///
/// `hard` - perform a hard reload. If this is false, QuickShell will attempt to reuse windows
/// that already exist. If true windows will be recreated.
///
/// See [Reloadable] for more information on what can be reloaded and how.
///
/// [Reloadable]: ../reloadable
Q_INVOKABLE void reload(bool hard);
signals:
void screensChanged();
public slots:
void updateScreens();
private:
static qsizetype screensCount(QQmlListProperty<QuickShellScreenInfo>* prop);
static QuickShellScreenInfo* screenAt(QQmlListProperty<QuickShellScreenInfo>* prop, qsizetype i);
QVector<QuickShellScreenInfo*> mScreens;
};

110
src/core/qmlscreen.cpp Normal file
View file

@ -0,0 +1,110 @@
#include "qmlscreen.hpp"
#include <qdebug.h>
#include <qlogging.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qscreen.h>
#include <qtypes.h>
QuickShellScreenInfo::QuickShellScreenInfo(QObject* parent, QScreen* screen):
QObject(parent), screen(screen) {
if (this->screen != nullptr) {
// clang-format off
QObject::connect(this->screen, &QScreen::geometryChanged, this, &QuickShellScreenInfo::geometryChanged);
QObject::connect(this->screen, &QScreen::physicalDotsPerInchChanged, this, &QuickShellScreenInfo::physicalPixelDensityChanged);
QObject::connect(this->screen, &QScreen::logicalDotsPerInchChanged, this, &QuickShellScreenInfo::logicalPixelDensityChanged);
QObject::connect(this->screen, &QScreen::orientationChanged, this, &QuickShellScreenInfo::orientationChanged);
QObject::connect(this->screen, &QScreen::primaryOrientationChanged, this, &QuickShellScreenInfo::primaryOrientationChanged);
QObject::connect(this->screen, &QObject::destroyed, this, &QuickShellScreenInfo::screenDestroyed);
// clang-format on
}
}
bool QuickShellScreenInfo::operator==(QuickShellScreenInfo& other) const {
return this->screen == other.screen;
}
void QuickShellScreenInfo::warnDangling() const {
if (this->dangling) {
qWarning() << "attempted to use dangling screen object";
}
}
QString QuickShellScreenInfo::name() const {
if (this->screen == nullptr) {
this->warnDangling();
return "{ NULL SCREEN }";
}
return this->screen->name();
}
qint32 QuickShellScreenInfo::width() const {
if (this->screen == nullptr) {
this->warnDangling();
return 0;
}
return this->screen->size().width();
}
qint32 QuickShellScreenInfo::height() const {
if (this->screen == nullptr) {
this->warnDangling();
return 0;
}
return this->screen->size().height();
}
qreal QuickShellScreenInfo::physicalPixelDensity() const {
if (this->screen == nullptr) {
this->warnDangling();
return 0.0;
}
return this->screen->physicalDotsPerInch() / 25.4;
}
qreal QuickShellScreenInfo::logicalPixelDensity() const {
if (this->screen == nullptr) {
this->warnDangling();
return 0.0;
}
return this->screen->logicalDotsPerInch() / 25.4;
}
qreal QuickShellScreenInfo::devicePixelRatio() const {
if (this->screen == nullptr) {
this->warnDangling();
return 0.0;
}
return this->screen->devicePixelRatio();
}
Qt::ScreenOrientation QuickShellScreenInfo::orientation() const {
if (this->screen == nullptr) {
this->warnDangling();
return Qt::PrimaryOrientation;
}
return this->screen->orientation();
}
Qt::ScreenOrientation QuickShellScreenInfo::primaryOrientation() const {
if (this->screen == nullptr) {
this->warnDangling();
return Qt::PrimaryOrientation;
}
return this->screen->primaryOrientation();
}
void QuickShellScreenInfo::screenDestroyed() {
this->screen = nullptr;
this->dangling = true;
}

73
src/core/qmlscreen.hpp Normal file
View file

@ -0,0 +1,73 @@
#pragma once
#include <qnamespace.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qscreen.h>
#include <qtmetamacros.h>
#include <qtypes.h>
// unfortunately QQuickScreenInfo is private.
/// Monitor object useful for setting the monitor for a [ShellWindow]
/// or querying information about the monitor.
///
/// > [!WARNING] If the monitor is disconnected than any stored copies of its ShellMonitor will
/// > be marked as dangling and all properties will return default values.
/// > Reconnecting the monitor will not reconnect it to the ShellMonitor object.
///
/// Due to some technical limitations, it was not possible to reuse the native qml [Screen] type.
///
/// [ShellWindow]: ../shellwindow
/// [Screen]: https://doc.qt.io/qt-6/qml-qtquick-screen.html
class QuickShellScreenInfo: public QObject {
Q_OBJECT;
QML_NAMED_ELEMENT(ShellScreen);
QML_UNCREATABLE("ShellScreen can only be obtained via QuickShell.screens");
// clang-format off
/// The name of the screen as seen by the operating system.
///
/// Usually something like `DP-1`, `HDMI-1`, `eDP-1`.
Q_PROPERTY(QString name READ name CONSTANT);
Q_PROPERTY(qint32 width READ width NOTIFY geometryChanged);
Q_PROPERTY(qint32 height READ height NOTIFY geometryChanged);
/// The number of physical pixels per millimeter.
Q_PROPERTY(qreal physicalPixelDensity READ physicalPixelDensity NOTIFY physicalPixelDensityChanged);
/// The number of device-independent (scaled) pixels per millimeter.
Q_PROPERTY(qreal logicalPixelDensity READ logicalPixelDensity NOTIFY logicalPixelDensityChanged);
/// The ratio between physical pixels and device-independent (scaled) pixels.
Q_PROPERTY(qreal devicePixelRatio READ devicePixelRatio NOTIFY physicalPixelDensityChanged);
Q_PROPERTY(Qt::ScreenOrientation orientation READ orientation NOTIFY orientationChanged);
Q_PROPERTY(Qt::ScreenOrientation primatyOrientation READ primaryOrientation NOTIFY primaryOrientationChanged);
// clang-format on
public:
QuickShellScreenInfo(QObject* parent, QScreen* screen); //: QObject(parent), screen(screen) {}
bool operator==(QuickShellScreenInfo& other) const;
[[nodiscard]] QString name() const;
[[nodiscard]] qint32 width() const;
[[nodiscard]] qint32 height() const;
[[nodiscard]] qreal physicalPixelDensity() const;
[[nodiscard]] qreal logicalPixelDensity() const;
[[nodiscard]] qreal devicePixelRatio() const;
[[nodiscard]] Qt::ScreenOrientation orientation() const;
[[nodiscard]] Qt::ScreenOrientation primaryOrientation() const;
QScreen* screen;
private:
void warnDangling() const;
bool dangling = false;
signals:
void geometryChanged();
void physicalPixelDensityChanged();
void logicalPixelDensityChanged();
void orientationChanged();
void primaryOrientationChanged();
private slots:
void screenDestroyed();
};

107
src/core/region.cpp Normal file
View file

@ -0,0 +1,107 @@
#include "region.hpp"
#include <cmath>
#include <qobject.h>
#include <qpoint.h>
#include <qqmllist.h>
#include <qquickitem.h>
#include <qregion.h>
#include <qtmetamacros.h>
PendingRegion::PendingRegion(QObject* parent): QObject(parent) {
QObject::connect(this, &PendingRegion::shapeChanged, this, &PendingRegion::changed);
QObject::connect(this, &PendingRegion::intersectionChanged, this, &PendingRegion::changed);
QObject::connect(this, &PendingRegion::itemChanged, this, &PendingRegion::changed);
QObject::connect(this, &PendingRegion::xChanged, this, &PendingRegion::changed);
QObject::connect(this, &PendingRegion::yChanged, this, &PendingRegion::changed);
QObject::connect(this, &PendingRegion::widthChanged, this, &PendingRegion::changed);
QObject::connect(this, &PendingRegion::heightChanged, this, &PendingRegion::changed);
QObject::connect(this, &PendingRegion::childrenChanged, this, &PendingRegion::changed);
}
void PendingRegion::setItem(QQuickItem* item) {
if (this->mItem != nullptr) {
QObject::disconnect(this->mItem, nullptr, this, nullptr);
}
this->mItem = item;
QObject::connect(this->mItem, &QQuickItem::xChanged, this, &PendingRegion::itemChanged);
QObject::connect(this->mItem, &QQuickItem::yChanged, this, &PendingRegion::itemChanged);
QObject::connect(this->mItem, &QQuickItem::widthChanged, this, &PendingRegion::itemChanged);
QObject::connect(this->mItem, &QQuickItem::heightChanged, this, &PendingRegion::itemChanged);
}
void PendingRegion::onItemDestroyed() { this->mItem = nullptr; }
QQmlListProperty<PendingRegion> PendingRegion::regions() {
return QQmlListProperty<PendingRegion>(
this,
nullptr,
PendingRegion::regionsAppend,
nullptr,
nullptr,
nullptr,
nullptr,
nullptr
);
}
bool PendingRegion::empty() const {
return this->mItem == nullptr && this->mX == 0 && this->mY == 0 && this->mWidth == 0
&& this->mHeight == 0;
}
QRegion PendingRegion::build() const {
auto type = QRegion::Rectangle;
switch (this->mShape) {
case RegionShape::Rect: type = QRegion::Rectangle; break;
case RegionShape::Ellipse: type = QRegion::Ellipse; break;
}
QRegion region;
if (this->empty()) {
region = QRegion();
} else if (this->mItem != nullptr) {
auto origin = this->mItem->mapToScene(QPointF(0, 0));
auto extent = this->mItem->mapToScene(QPointF(this->mItem->width(), this->mItem->height()));
auto size = extent - origin;
region = QRegion(
static_cast<int>(origin.x()),
static_cast<int>(origin.y()),
static_cast<int>(std::ceil(size.x())),
static_cast<int>(std::ceil(size.y())),
type
);
} else {
region = QRegion(this->mX, this->mY, this->mWidth, this->mHeight, type);
}
for (const auto& childRegion: this->mRegions) {
region = childRegion->applyTo(region);
}
return region;
}
QRegion PendingRegion::applyTo(QRegion& region) const {
switch (this->mIntersection) {
case Intersection::Combine: region = region.united(this->build()); break;
case Intersection::Subtract: region = region.subtracted(this->build()); break;
case Intersection::Intersect: region = region.intersected(this->build()); break;
case Intersection::Xor: region = region.xored(this->build()); break;
}
return region;
}
void PendingRegion::regionsAppend(QQmlListProperty<PendingRegion>* prop, PendingRegion* region) {
auto* self = static_cast<PendingRegion*>(prop->object); // NOLINT
region->setParent(self);
self->mRegions.append(region);
QObject::connect(region, &PendingRegion::changed, self, &PendingRegion::childrenChanged);
emit self->childrenChanged();
}

125
src/core/region.hpp Normal file
View file

@ -0,0 +1,125 @@
#pragma once
#include <qobject.h>
#include <qqmlengine.h>
#include <qqmlintegration.h>
#include <qqmllist.h>
#include <qquickitem.h>
#include <qregion.h>
#include <qtmetamacros.h>
#include <qtypes.h>
/// Shape of a Region.
namespace RegionShape { // NOLINT
Q_NAMESPACE;
QML_ELEMENT;
enum Enum {
Rect = 0,
Ellipse = 1,
};
Q_ENUM_NS(Enum);
} // namespace RegionShape
///! Intersection strategy for Regions.
namespace Intersection { // NOLINT
Q_NAMESPACE;
QML_ELEMENT;
enum Enum {
/// Combine this region, leaving a union of this and the other region. (opposite of `Subtract`)
Combine = 0,
/// Subtract this region, cutting this region out of the other. (opposite of `Combine`)
Subtract = 1,
/// Create an intersection of this region and the other, leaving only
/// the area covered by both. (opposite of `Xor`)
Intersect = 2,
/// Create an intersection of this region and the other, leaving only
/// the area not covered by both. (opposite of `Intersect`)
Xor = 3,
};
Q_ENUM_NS(Enum);
} // namespace Intersection
///! A composable region used as a mask.
class PendingRegion: public QObject {
Q_OBJECT;
/// Defaults to `Rect`.
Q_PROPERTY(RegionShape::Enum shape MEMBER mShape NOTIFY shapeChanged);
/// The way this region interacts with its parent region. Defaults to `Combine`.
Q_PROPERTY(Intersection::Enum intersection MEMBER mIntersection NOTIFY intersectionChanged);
/// The item that determines the geometry of the region.
/// `item` overrides `x`, `y`, `width` and `height`.
Q_PROPERTY(QQuickItem* item MEMBER mItem WRITE setItem NOTIFY itemChanged);
/// Defaults to 0. Does nothing if `item` is set.
Q_PROPERTY(qint32 x MEMBER mX NOTIFY xChanged);
/// Defaults to 0. Does nothing if `item` is set.
Q_PROPERTY(qint32 y MEMBER mY NOTIFY yChanged);
/// Defaults to 0. Does nothing if `item` is set.
Q_PROPERTY(qint32 width MEMBER mWidth NOTIFY widthChanged);
/// Defaults to 0. Does nothing if `item` is set.
Q_PROPERTY(qint32 height MEMBER mHeight NOTIFY heightChanged);
/// Regions to apply on top of this region.
///
/// Regions can be nested to create a more complex region.
/// For example this will create a square region with a cutout in the middle.
/// ```qml
/// Region {
/// width: 100; height: 100;
///
/// Region {
/// x: 50; y: 50;
/// width: 50; height: 50;
/// intersection: Intersection.Subtract
/// }
/// }
/// ```
Q_PROPERTY(QQmlListProperty<PendingRegion> regions READ regions);
Q_CLASSINFO("DefaultProperty", "regions");
QML_NAMED_ELEMENT(Region);
public:
explicit PendingRegion(QObject* parent = nullptr);
void setItem(QQuickItem* item);
QQmlListProperty<PendingRegion> regions();
[[nodiscard]] bool empty() const;
[[nodiscard]] QRegion build() const;
[[nodiscard]] QRegion applyTo(QRegion& region) const;
RegionShape::Enum mShape = RegionShape::Rect;
Intersection::Enum mIntersection = Intersection::Combine;
signals:
void shapeChanged();
void intersectionChanged();
void itemChanged();
void xChanged();
void yChanged();
void widthChanged();
void heightChanged();
void childrenChanged();
void changed();
private slots:
void onItemDestroyed();
private:
static void regionsAppend(QQmlListProperty<PendingRegion>* prop, PendingRegion* region);
QQuickItem* mItem = nullptr;
qint32 mX = 0;
qint32 mY = 0;
qint32 mWidth = 0;
qint32 mHeight = 0;
QList<PendingRegion*> mRegions;
};

77
src/core/reload.cpp Normal file
View file

@ -0,0 +1,77 @@
#include "reload.hpp"
#include <qcontainerfwd.h>
#include <qobject.h>
#include <qqmllist.h>
void ReloadPropagator::onReload(QObject* oldInstance) {
auto* old = qobject_cast<ReloadPropagator*>(oldInstance);
for (auto i = 0; i < this->mChildren.length(); i++) {
auto* newChild = qobject_cast<Reloadable*>(this->mChildren.at(i));
if (newChild != nullptr) {
auto* oldChild = old == nullptr || old->mChildren.length() <= i
? nullptr
: qobject_cast<Reloadable*>(old->mChildren.at(i));
newChild->onReload(oldChild);
} else {
Reloadable::reloadRecursive(newChild, oldInstance);
}
}
}
QQmlListProperty<QObject> ReloadPropagator::data() {
return QQmlListProperty<QObject>(
this,
nullptr,
&ReloadPropagator::appendComponent,
nullptr,
nullptr,
nullptr,
nullptr,
nullptr
);
}
void ReloadPropagator::appendComponent(QQmlListProperty<QObject>* list, QObject* obj) {
auto* self = static_cast<ReloadPropagator*>(list->object); // NOLINT
obj->setParent(self);
self->mChildren.append(obj);
}
void Reloadable::reloadRecursive(QObject* newObj, QObject* oldRoot) {
auto* reloadable = qobject_cast<Reloadable*>(newObj);
if (reloadable != nullptr) {
QObject* oldInstance = nullptr;
if (oldRoot != nullptr && !reloadable->mReloadableId.isEmpty()) {
oldInstance = Reloadable::getChildByReloadId(oldRoot, reloadable->mReloadableId);
}
// pass handling to the child's onReload, which should call back into reloadRecursive,
// with its oldInstance becoming the new oldRoot.
reloadable->onReload(oldInstance);
} else if (newObj != nullptr) {
Reloadable::reloadChildrenRecursive(newObj, oldRoot);
}
}
void Reloadable::reloadChildrenRecursive(QObject* newRoot, QObject* oldRoot) {
for (auto* child: newRoot->children()) {
Reloadable::reloadRecursive(child, oldRoot);
}
}
QObject* Reloadable::getChildByReloadId(QObject* parent, const QString& reloadId) {
for (auto* child: parent->children()) {
auto* reloadable = qobject_cast<Reloadable*>(child);
if (reloadable != nullptr) {
if (reloadable->mReloadableId == reloadId) return reloadable;
// if not then don't check its children as thats a seperate reload scope.
} else {
auto* reloadable = Reloadable::getChildByReloadId(child, reloadId);
if (reloadable != nullptr) return reloadable;
}
}
return nullptr;
}

111
src/core/reload.hpp Normal file
View file

@ -0,0 +1,111 @@
#pragma once
#include <qobject.h>
#include <qqmlcomponent.h>
#include <qqmlintegration.h>
#include <qqmllist.h>
#include <qqmlparserstatus.h>
#include <qtmetamacros.h>
///! The base class of all types that can be reloaded.
/// Reloadables will attempt to take specific state from previous config revisions if possible.
/// Some examples are [ProxyWindowBase] and [PersistentProperties]
///
/// [ProxyWindowBase]: ../proxywindowbase
/// [PersistentProperties]: ../persistentproperties
class Reloadable: public QObject, public QQmlParserStatus {
Q_OBJECT;
Q_INTERFACES(QQmlParserStatus);
/// An additional identifier that can be used to try to match a reloadable object to its
/// previous state.
///
/// Simply keeping a stable identifier across config versions (saves) is
/// enough to help the reloader figure out which object in the old revision corrosponds to
/// this object in the current revision, and facilitate smoother reloading.
///
/// Note that identifiers are scoped, and will try to do the right thing in context.
/// For example if you have a `Variants` wrapping an object with an identified element inside,
/// a scope is created at the variant level.
///
/// ```qml
/// Variants {
/// // multiple variants of the same object tree
/// variants: [ { foo: 1 }, { foo: 2 } ]
///
/// // any non `Reloadable` object
/// QtObject {
/// FloatingWindow {
/// // this FloatingWindow will now be matched to the same one in the previous
/// // widget tree for its variant. "myFloatingWindow" refers to both the variant in
/// // `foo: 1` and `foo: 2` for each tree.
/// reloadableId: "myFloatingWindow"
///
/// // ...
/// }
/// }
/// }
/// ```
Q_PROPERTY(QString reloadableId MEMBER mReloadableId);
QML_ELEMENT;
QML_UNCREATABLE(
"Reloadable is the base class of reloadable types and cannot be created on its own."
);
public:
explicit Reloadable(QObject* parent = nullptr): QObject(parent) {}
// Called unconditionally in the reload phase, with nullptr if no source could be determined.
// If non null the old instance may or may not be of the same type, and should be checked
// by `onReload`.
virtual void onReload(QObject* oldInstance) = 0;
// TODO: onReload runs after initialization for reloadable objects created late
void classBegin() override {}
void componentComplete() override {}
// Reload objects in the parent->child graph recursively.
static void reloadRecursive(QObject* newObj, QObject* oldRoot);
// Same as above but does not reload the passed object, only its children.
static void reloadChildrenRecursive(QObject* newRoot, QObject* oldRoot);
QString mReloadableId;
private:
static QObject* getChildByReloadId(QObject* parent, const QString& reloadId);
};
///! Basic type that propagates reloads to child items in order.
/// Convenience type equivalent to setting `reloadableId` on properties in a
/// QtObject instance.
///
/// Note that this does not work for visible `Item`s (all widgets).
///
/// ```qml
/// ShellRoot {
/// Variants {
/// variants: ...
///
/// ReloadPropagator {
/// // everything in here behaves the same as if it was defined
/// // directly in `Variants` reload-wise.
/// }
/// }
/// }
class ReloadPropagator: public Reloadable {
Q_OBJECT;
Q_PROPERTY(QQmlListProperty<QObject> children READ data);
Q_CLASSINFO("DefaultProperty", "children");
QML_ELEMENT;
public:
explicit ReloadPropagator(QObject* parent = nullptr): Reloadable(parent) {}
void onReload(QObject* oldInstance) override;
QQmlListProperty<QObject> data();
private:
static void appendComponent(QQmlListProperty<QObject>* list, QObject* obj);
QList<QObject*> mChildren;
};

81
src/core/rootwrapper.cpp Normal file
View file

@ -0,0 +1,81 @@
#include "rootwrapper.hpp"
#include <cstdlib>
#include <utility>
#include <qfileinfo.h>
#include <qlogging.h>
#include <qobject.h>
#include <qqmlcomponent.h>
#include <qqmlengine.h>
#include <qurl.h>
#include "shell.hpp"
#include "watcher.hpp"
RootWrapper::RootWrapper(QString rootPath):
QObject(nullptr), rootPath(std::move(rootPath)), engine(this) {
this->reloadGraph(true);
if (this->root == nullptr) {
qCritical() << "could not create scene graph, exiting";
exit(-1); // NOLINT
}
}
void RootWrapper::reloadGraph(bool hard) {
if (this->root != nullptr) {
this->engine.clearComponentCache();
}
auto component = QQmlComponent(&this->engine, QUrl::fromLocalFile(this->rootPath));
auto* obj = component.beginCreate(this->engine.rootContext());
if (obj == nullptr) {
qWarning() << component.errorString().toStdString().c_str();
qWarning() << "failed to create root component";
return;
}
auto* newRoot = qobject_cast<ShellRoot*>(obj);
if (newRoot == nullptr) {
qWarning() << "root component was not a QuickShell.ShellRoot";
delete obj;
return;
}
component.completeCreate();
newRoot->onReload(hard ? nullptr : this->root);
if (this->root != nullptr) {
this->root->deleteLater();
this->root = nullptr;
}
this->root = newRoot;
this->onConfigChanged();
}
void RootWrapper::onConfigChanged() {
auto config = this->root->config();
if (config.mWatchFiles && this->configWatcher == nullptr) {
this->configWatcher = new FiletreeWatcher();
this->configWatcher->addPath(QFileInfo(this->rootPath).dir().path());
QObject::connect(this->root, &ShellRoot::configChanged, this, &RootWrapper::onConfigChanged);
QObject::connect(
this->configWatcher,
&FiletreeWatcher::fileChanged,
this,
&RootWrapper::onWatchedFilesChanged
);
} else if (!config.mWatchFiles && this->configWatcher != nullptr) {
this->configWatcher->deleteLater();
this->configWatcher = nullptr;
}
}
void RootWrapper::onWatchedFilesChanged() { this->reloadGraph(false); }

28
src/core/rootwrapper.hpp Normal file
View file

@ -0,0 +1,28 @@
#pragma once
#include <qobject.h>
#include <qqmlengine.h>
#include <qtmetamacros.h>
#include <qurl.h>
#include "shell.hpp"
#include "watcher.hpp"
class RootWrapper: public QObject {
Q_OBJECT;
public:
explicit RootWrapper(QString rootPath);
void reloadGraph(bool hard);
private slots:
void onConfigChanged();
void onWatchedFilesChanged();
private:
QString rootPath;
QQmlEngine engine;
ShellRoot* root = nullptr;
FiletreeWatcher* configWatcher = nullptr;
};

11
src/core/shell.cpp Normal file
View file

@ -0,0 +1,11 @@
#include "shell.hpp"
#include <qtmetamacros.h>
void ShellRoot::setConfig(ShellConfig config) {
this->mConfig = config;
emit this->configChanged();
}
ShellConfig ShellRoot::config() const { return this->mConfig; }

37
src/core/shell.hpp Normal file
View file

@ -0,0 +1,37 @@
#pragma once
#include <qcontainerfwd.h>
#include <qlist.h>
#include <qqmlengine.h>
#include <qtmetamacros.h>
#include "reload.hpp"
class ShellConfig {
Q_GADGET;
Q_PROPERTY(bool watchFiles MEMBER mWatchFiles);
public:
bool mWatchFiles = true;
};
///! Root config element
class ShellRoot: public ReloadPropagator {
Q_OBJECT;
/// If `config.watchFiles` is true the configuration will be reloaded whenever it changes.
/// Defaults to true.
Q_PROPERTY(ShellConfig config READ config WRITE setConfig);
QML_ELEMENT;
public:
explicit ShellRoot(QObject* parent = nullptr): ReloadPropagator(parent) {}
void setConfig(ShellConfig config);
[[nodiscard]] ShellConfig config() const;
signals:
void configChanged();
private:
ShellConfig mConfig;
};

1
src/core/shellwindow.cpp Normal file
View file

@ -0,0 +1 @@
#include "shellwindow.hpp" // NOLINT

134
src/core/shellwindow.hpp Normal file
View file

@ -0,0 +1,134 @@
#pragma once
#include <qobject.h>
#include <qqmlintegration.h>
#include <qqmllist.h>
#include <qquickwindow.h>
#include <qscreen.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qvariant.h>
#include <qwindow.h>
#include "proxywindow.hpp"
class Anchors {
Q_GADGET;
Q_PROPERTY(bool left MEMBER mLeft);
Q_PROPERTY(bool right MEMBER mRight);
Q_PROPERTY(bool top MEMBER mTop);
Q_PROPERTY(bool bottom MEMBER mBottom);
public:
bool mLeft = false;
bool mRight = false;
bool mTop = false;
bool mBottom = false;
};
class Margins {
Q_GADGET;
Q_PROPERTY(qint32 left MEMBER mLeft);
Q_PROPERTY(qint32 right MEMBER mRight);
Q_PROPERTY(qint32 top MEMBER mTop);
Q_PROPERTY(qint32 bottom MEMBER mBottom);
public:
qint32 mLeft = 0;
qint32 mRight = 0;
qint32 mTop = 0;
qint32 mBottom = 0;
};
namespace ExclusionMode { // NOLINT
Q_NAMESPACE;
QML_ELEMENT;
enum Enum {
/// Respect the exclusion zone of other shell layers and optionally set one
Normal = 0,
/// Ignore exclusion zones of other shell layers. You cannot set an exclusion zone in this mode.
Ignore = 1,
/// Decide the exclusion zone based on the window dimensions and anchors.
///
/// Will attempt to reseve exactly enough space for the window and its margins if
/// exactly 3 anchors are connected.
Auto = 2,
};
Q_ENUM_NS(Enum);
} // namespace ExclusionMode
///! Decorationless window attached to screen edges by anchors.
/// Decorationless window attached to screen edges by anchors.
///
/// #### Example
/// The following snippet creates a white bar attached to the bottom of the screen.
///
/// ```qml
/// ShellWindow {
/// anchors {
/// left: true
/// bottom: true
/// right: true
/// }
///
/// Text {
/// anchors.horizontalCenter: parent.horizontalCenter
/// anchors.verticalCenter: parent.verticalCenter
/// text: "Hello!"
/// }
/// }
/// ```
class ProxyShellWindow: public ProxyWindowBase {
// clang-format off
Q_OBJECT;
/// Anchors attach a shell window to the sides of the screen.
/// By default all anchors are disabled to avoid blocking the entire screen due to a misconfiguration.
///
/// > [!INFO] When two opposite anchors are attached at the same time, the corrosponding dimension
/// > (width or height) will be forced to equal the screen width/height.
/// > Margins can be used to create anchored windows that are also disconnected from the monitor sides.
Q_PROPERTY(Anchors anchors READ anchors WRITE setAnchors NOTIFY anchorsChanged);
/// The amount of space reserved for the shell layer relative to its anchors.
///
/// > [!INFO] Some systems will require exactly 3 anchors to be attached for the exclusion zone to take
/// > effect.
Q_PROPERTY(qint32 exclusionZone READ exclusiveZone WRITE setExclusiveZone NOTIFY exclusionZoneChanged);
/// Defaults to `ExclusionMode.Normal`.
Q_PROPERTY(ExclusionMode::Enum exclusionMode READ exclusionMode WRITE setExclusionMode NOTIFY exclusionModeChanged);
/// Offsets from the sides of the screen.
///
/// > [!INFO] Only applies to edges with anchors
Q_PROPERTY(Margins margins READ margins WRITE setMargins NOTIFY marginsChanged);
// clang-format on
public:
explicit ProxyShellWindow(QObject* parent = nullptr): ProxyWindowBase(parent) {}
QQmlListProperty<QObject> data();
virtual void setAnchors(Anchors anchors) = 0;
[[nodiscard]] virtual Anchors anchors() const = 0;
virtual void setExclusiveZone(qint32 zone) = 0;
[[nodiscard]] virtual qint32 exclusiveZone() const = 0;
virtual void setExclusionMode(ExclusionMode::Enum exclusionMode) = 0;
[[nodiscard]] virtual ExclusionMode::Enum exclusionMode() const = 0;
virtual void setMargins(Margins margins) = 0;
[[nodiscard]] virtual Margins margins() const = 0;
signals:
void anchorsChanged();
void marginsChanged();
void exclusionZoneChanged();
void exclusionModeChanged();
protected:
ExclusionMode::Enum mExclusionMode = ExclusionMode::Normal;
qint32 mExclusionZone = 0;
Anchors mAnchors;
Margins mMargins;
};

149
src/core/variants.cpp Normal file
View file

@ -0,0 +1,149 @@
#include "variants.hpp"
#include <algorithm>
#include <utility>
#include <qcontainerfwd.h>
#include <qlogging.h>
#include <qobject.h>
#include <qqmlengine.h>
#include "reload.hpp"
void Variants::onReload(QObject* oldInstance) {
auto* old = qobject_cast<Variants*>(oldInstance);
for (auto& [variant, instanceObj]: this->instances.values) {
QObject* oldInstance = nullptr;
if (old != nullptr) {
auto& values = old->instances.values;
int matchcount = 0;
int matchi = 0;
int i = 0;
for (auto& [valueSet, _]: values) {
int count = 0;
for (auto& [k, v]: variant.toStdMap()) {
if (valueSet.contains(k) && valueSet.value(k) == v) {
count++;
}
}
if (count > matchcount) {
matchcount = count;
matchi = i;
}
i++;
}
if (matchcount > 0) {
oldInstance = values.takeAt(matchi).second;
}
}
auto* instance = qobject_cast<Reloadable*>(instanceObj);
if (instance != nullptr) instance->onReload(oldInstance);
else Reloadable::reloadChildrenRecursive(instanceObj, oldInstance);
}
}
void Variants::setVariants(QVariantList variants) {
this->mVariants = std::move(variants);
this->updateVariants();
}
void Variants::componentComplete() {
this->Reloadable::componentComplete();
this->updateVariants();
}
void Variants::updateVariants() {
if (this->mComponent == nullptr) {
qWarning() << "Variants instance does not have a component specified";
return;
}
// clean up removed entries
for (auto iter = this->instances.values.begin(); iter < this->instances.values.end();) {
if (this->mVariants.contains(iter->first)) {
iter++;
} else {
iter->second->deleteLater();
iter = this->instances.values.erase(iter);
}
}
for (auto iter = this->mVariants.begin(); iter < this->mVariants.end(); iter++) {
auto& variantObj = *iter;
if (!variantObj.canConvert<QVariantMap>()) {
qWarning() << "value passed to Variants is not an object and will be ignored:" << variantObj;
} else {
auto variant = variantObj.value<QVariantMap>();
for (auto iter2 = this->mVariants.begin(); iter2 < iter; iter2++) {
if (*iter2 == variantObj) {
qWarning() << "same value specified twice in Variants, duplicates will be ignored:"
<< variantObj;
goto outer;
}
}
if (this->instances.contains(variant)) {
continue; // we dont need to recreate this one
}
auto* instance = this->mComponent->createWithInitialProperties(
variant,
QQmlEngine::contextForObject(this)
);
if (instance == nullptr) {
qWarning() << this->mComponent->errorString().toStdString().c_str();
qWarning() << "failed to create variant with object" << variant;
continue;
}
instance->setParent(this);
this->instances.insert(variant, instance);
}
outer:;
}
}
template <typename K, typename V>
bool AwfulMap<K, V>::contains(const K& key) const {
return std::ranges::any_of(this->values, [&](const QPair<K, V>& pair) {
return pair.first == key;
});
}
template <typename K, typename V>
V* AwfulMap<K, V>::get(const K& key) {
for (auto& [k, v]: this->values) {
if (key == k) {
return &v;
}
}
return nullptr;
}
template <typename K, typename V>
void AwfulMap<K, V>::insert(K key, V value) {
this->values.push_back(QPair<K, V>(key, value));
}
template <typename K, typename V>
bool AwfulMap<K, V>::remove(const K& key) {
for (auto iter = this->values.begin(); iter < this->values.end(); iter++) {
if (iter->first == key) {
this->values.erase(iter);
return true;
}
}
return false;
}

56
src/core/variants.hpp Normal file
View file

@ -0,0 +1,56 @@
#pragma once
#include <qcontainerfwd.h>
#include <qlist.h>
#include <qlogging.h>
#include <qmap.h>
#include <qobject.h>
#include <qqmlcomponent.h>
#include <qqmlparserstatus.h>
#include <qtmetamacros.h>
#include "reload.hpp"
// extremely inefficient map
template <typename K, typename V>
class AwfulMap {
public:
[[nodiscard]] bool contains(const K& key) const;
[[nodiscard]] V* get(const K& key);
void insert(K key, V value); // assumes no duplicates
bool remove(const K& key); // returns true if anything was removed
QList<QPair<K, V>> values;
};
///! Creates instances of a component based on a given set of variants.
/// Creates and destroys instances of the given component when the given property changes.
///
/// See [QuickShell.screens] for an example of using `Variants` to create copies of a window per
/// screen.
///
/// [QuickShell.screens]: ../quickshell#prop.screens
class Variants: public Reloadable {
Q_OBJECT;
/// The component to create instances of
Q_PROPERTY(QQmlComponent* component MEMBER mComponent);
/// The list of sets of properties to create instances with.
/// Each set creates an instance of the component, which are updated when the input sets update.
Q_PROPERTY(QList<QVariant> variants MEMBER mVariants WRITE setVariants);
Q_CLASSINFO("DefaultProperty", "component");
QML_ELEMENT;
public:
explicit Variants(QObject* parent = nullptr): Reloadable(parent) {}
void onReload(QObject* oldInstance) override;
void componentComplete() override;
private:
void setVariants(QVariantList variants);
void updateVariants();
QQmlComponent* mComponent = nullptr;
QVariantList mVariants;
AwfulMap<QVariantMap, QObject*> instances;
};

38
src/core/watcher.cpp Normal file
View file

@ -0,0 +1,38 @@
#include "watcher.hpp"
#include <qdir.h>
#include <qfileinfo.h>
#include <qfilesystemwatcher.h>
#include <qobject.h>
#include <qtmetamacros.h>
FiletreeWatcher::FiletreeWatcher(QObject* parent): QObject(parent) {
QObject::connect(
&this->watcher,
&QFileSystemWatcher::fileChanged,
this,
&FiletreeWatcher::onFileChanged
);
QObject::connect(
&this->watcher,
&QFileSystemWatcher::directoryChanged,
this,
&FiletreeWatcher::onDirectoryChanged
);
}
void FiletreeWatcher::addPath(const QString& path) {
this->watcher.addPath(path);
if (QFileInfo(path).isDir()) {
auto dir = QDir(path);
for (auto& entry: dir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot)) {
this->addPath(dir.filePath(entry));
}
}
}
void FiletreeWatcher::onDirectoryChanged(const QString& path) { this->addPath(path); }
void FiletreeWatcher::onFileChanged(const QString& path) { emit this->fileChanged(path); }

24
src/core/watcher.hpp Normal file
View file

@ -0,0 +1,24 @@
#pragma once
#include <qdir.h>
#include <qfilesystemwatcher.h>
#include <qobject.h>
class FiletreeWatcher: public QObject {
Q_OBJECT;
public:
explicit FiletreeWatcher(QObject* parent = nullptr);
void addPath(const QString& path);
signals:
void fileChanged(const QString& path);
private slots:
void onDirectoryChanged(const QString& path);
void onFileChanged(const QString& path);
private:
QFileSystemWatcher watcher;
};