forked from quickshell/quickshell
Compare commits
3 commits
56ef2e135b
...
4ee9ac7f7c
Author | SHA1 | Date | |
---|---|---|---|
outfoxxed | 4ee9ac7f7c | ||
kossLAN | 3b6d1c3bd8 | ||
outfoxxed | 73cfeba61b |
|
@ -14,11 +14,13 @@ option(SOCKETS "Enable unix socket support" ON)
|
||||||
option(WAYLAND "Enable wayland support" ON)
|
option(WAYLAND "Enable wayland support" ON)
|
||||||
option(WAYLAND_WLR_LAYERSHELL "Support the zwlr_layer_shell_v1 wayland protocol" ON)
|
option(WAYLAND_WLR_LAYERSHELL "Support the zwlr_layer_shell_v1 wayland protocol" ON)
|
||||||
option(WAYLAND_SESSION_LOCK "Support the ext_session_lock_v1 wayland protocol" ON)
|
option(WAYLAND_SESSION_LOCK "Support the ext_session_lock_v1 wayland protocol" ON)
|
||||||
|
option(X11 "Enable X11 support" ON)
|
||||||
option(HYPRLAND "Support hyprland specific features" ON)
|
option(HYPRLAND "Support hyprland specific features" ON)
|
||||||
option(HYPRLAND_GLOBAL_SHORTCUTS "Hyprland Global Shortcuts" ON)
|
option(HYPRLAND_GLOBAL_SHORTCUTS "Hyprland Global Shortcuts" ON)
|
||||||
option(HYPRLAND_FOCUS_GRAB "Hyprland Focus Grabbing" ON)
|
option(HYPRLAND_FOCUS_GRAB "Hyprland Focus Grabbing" ON)
|
||||||
option(SERVICE_STATUS_NOTIFIER "StatusNotifierItem service" ON)
|
option(SERVICE_STATUS_NOTIFIER "StatusNotifierItem service" ON)
|
||||||
option(SERVICE_PIPEWIRE "PipeWire service" ON)
|
option(SERVICE_PIPEWIRE "PipeWire service" ON)
|
||||||
|
option(SERVICE_MPRIS "Mpris service" ON)
|
||||||
|
|
||||||
message(STATUS "Quickshell configuration")
|
message(STATUS "Quickshell configuration")
|
||||||
message(STATUS " NVIDIA workarounds: ${NVIDIA_COMPAT}")
|
message(STATUS " NVIDIA workarounds: ${NVIDIA_COMPAT}")
|
||||||
|
@ -29,9 +31,11 @@ if (WAYLAND)
|
||||||
message(STATUS " Wlroots Layershell: ${WAYLAND_WLR_LAYERSHELL}")
|
message(STATUS " Wlroots Layershell: ${WAYLAND_WLR_LAYERSHELL}")
|
||||||
message(STATUS " Session Lock: ${WAYLAND_SESSION_LOCK}")
|
message(STATUS " Session Lock: ${WAYLAND_SESSION_LOCK}")
|
||||||
endif ()
|
endif ()
|
||||||
|
message(STATUS " X11: ${X11}")
|
||||||
message(STATUS " Services")
|
message(STATUS " Services")
|
||||||
message(STATUS " StatusNotifier: ${SERVICE_STATUS_NOTIFIER}")
|
message(STATUS " StatusNotifier: ${SERVICE_STATUS_NOTIFIER}")
|
||||||
message(STATUS " PipeWire: ${SERVICE_PIPEWIRE}")
|
message(STATUS " PipeWire: ${SERVICE_PIPEWIRE}")
|
||||||
|
message(STATUS " Mpris: ${SERVICE_MPRIS}")
|
||||||
message(STATUS " Hyprland: ${HYPRLAND}")
|
message(STATUS " Hyprland: ${HYPRLAND}")
|
||||||
if (HYPRLAND)
|
if (HYPRLAND)
|
||||||
message(STATUS " Focus Grabbing: ${HYPRLAND_FOCUS_GRAB}")
|
message(STATUS " Focus Grabbing: ${HYPRLAND_FOCUS_GRAB}")
|
||||||
|
@ -87,7 +91,7 @@ if (WAYLAND)
|
||||||
list(APPEND QT_FPDEPS WaylandClient)
|
list(APPEND QT_FPDEPS WaylandClient)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if (SERVICE_STATUS_NOTIFIER)
|
if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS)
|
||||||
set(DBUS ON)
|
set(DBUS ON)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|
|
@ -62,16 +62,22 @@ To build quickshell at all, you will need the following packages (names may vary
|
||||||
|
|
||||||
- just
|
- just
|
||||||
- cmake
|
- cmake
|
||||||
- pkg-config
|
|
||||||
- ninja
|
- ninja
|
||||||
- Qt6 [ QtBase, QtDeclarative ]
|
- Qt6 [ QtBase, QtDeclarative ]
|
||||||
|
|
||||||
To build with wayland support you will additionally need:
|
To build with wayland support you will additionally need:
|
||||||
|
- pkg-config
|
||||||
- wayland
|
- wayland
|
||||||
- wayland-scanner (may be part of wayland on some distros)
|
- wayland-scanner (may be part of wayland on some distros)
|
||||||
- wayland-protocols
|
- wayland-protocols
|
||||||
- Qt6 [ QtWayland ]
|
- Qt6 [ QtWayland ]
|
||||||
|
|
||||||
|
To build with x11 support you will additionally need:
|
||||||
|
- libxcb
|
||||||
|
|
||||||
|
To build with pipewire support you will additionally need:
|
||||||
|
- libpipewire
|
||||||
|
|
||||||
### Building
|
### Building
|
||||||
|
|
||||||
To make a release build of quickshell run:
|
To make a release build of quickshell run:
|
||||||
|
|
|
@ -10,6 +10,8 @@
|
||||||
qt6,
|
qt6,
|
||||||
wayland,
|
wayland,
|
||||||
wayland-protocols,
|
wayland-protocols,
|
||||||
|
xorg,
|
||||||
|
pipewire,
|
||||||
|
|
||||||
gitRev ? (let
|
gitRev ? (let
|
||||||
headExists = builtins.pathExists ./.git/HEAD;
|
headExists = builtins.pathExists ./.git/HEAD;
|
||||||
|
@ -24,6 +26,7 @@
|
||||||
|
|
||||||
debug ? false,
|
debug ? false,
|
||||||
enableWayland ? true,
|
enableWayland ? true,
|
||||||
|
enableX11 ? true,
|
||||||
enablePipewire ? true,
|
enablePipewire ? true,
|
||||||
nvidiaCompat ? false,
|
nvidiaCompat ? false,
|
||||||
svgSupport ? true, # you almost always want this
|
svgSupport ? true, # you almost always want this
|
||||||
|
@ -42,11 +45,12 @@
|
||||||
wayland-scanner
|
wayland-scanner
|
||||||
]);
|
]);
|
||||||
|
|
||||||
buildInputs = with pkgs; [
|
buildInputs = [
|
||||||
qt6.qtbase
|
qt6.qtbase
|
||||||
qt6.qtdeclarative
|
qt6.qtdeclarative
|
||||||
]
|
]
|
||||||
++ (lib.optionals enableWayland [ qt6.qtwayland wayland ])
|
++ (lib.optionals enableWayland [ qt6.qtwayland wayland ])
|
||||||
|
++ (lib.optionals enableX11 [ xorg.libxcb ])
|
||||||
++ (lib.optionals svgSupport [ qt6.qtsvg ])
|
++ (lib.optionals svgSupport [ qt6.qtsvg ])
|
||||||
++ (lib.optionals enablePipewire [ pipewire ]);
|
++ (lib.optionals enablePipewire [ pipewire ]);
|
||||||
|
|
||||||
|
|
|
@ -13,4 +13,8 @@ if (WAYLAND)
|
||||||
add_subdirectory(wayland)
|
add_subdirectory(wayland)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if (X11)
|
||||||
|
add_subdirectory(x11)
|
||||||
|
endif()
|
||||||
|
|
||||||
add_subdirectory(services)
|
add_subdirectory(services)
|
||||||
|
|
|
@ -117,6 +117,10 @@ class PanelWindowInterface: public WindowInterface {
|
||||||
Q_PROPERTY(qint32 exclusiveZone READ exclusiveZone WRITE setExclusiveZone NOTIFY exclusiveZoneChanged);
|
Q_PROPERTY(qint32 exclusiveZone READ exclusiveZone WRITE setExclusiveZone NOTIFY exclusiveZoneChanged);
|
||||||
/// Defaults to `ExclusionMode.Auto`.
|
/// Defaults to `ExclusionMode.Auto`.
|
||||||
Q_PROPERTY(ExclusionMode::Enum exclusionMode READ exclusionMode WRITE setExclusionMode NOTIFY exclusionModeChanged);
|
Q_PROPERTY(ExclusionMode::Enum exclusionMode READ exclusionMode WRITE setExclusionMode NOTIFY exclusionModeChanged);
|
||||||
|
/// If the panel should render above standard windows. Defaults to true.
|
||||||
|
QSDOC_HIDE Q_PROPERTY(bool aboveWindows READ aboveWindows WRITE setAboveWindows NOTIFY aboveWindowsChanged);
|
||||||
|
/// Defaults to false.
|
||||||
|
QSDOC_HIDE Q_PROPERTY(bool focusable READ focusable WRITE setFocusable NOTIFY focusableChanged);
|
||||||
// clang-format on
|
// clang-format on
|
||||||
QSDOC_NAMED_ELEMENT(PanelWindow);
|
QSDOC_NAMED_ELEMENT(PanelWindow);
|
||||||
|
|
||||||
|
@ -135,9 +139,17 @@ public:
|
||||||
[[nodiscard]] virtual ExclusionMode::Enum exclusionMode() const = 0;
|
[[nodiscard]] virtual ExclusionMode::Enum exclusionMode() const = 0;
|
||||||
virtual void setExclusionMode(ExclusionMode::Enum exclusionMode) = 0;
|
virtual void setExclusionMode(ExclusionMode::Enum exclusionMode) = 0;
|
||||||
|
|
||||||
|
[[nodiscard]] virtual bool aboveWindows() const = 0;
|
||||||
|
virtual void setAboveWindows(bool aboveWindows) = 0;
|
||||||
|
|
||||||
|
[[nodiscard]] virtual bool focusable() const = 0;
|
||||||
|
virtual void setFocusable(bool focusable) = 0;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void anchorsChanged();
|
void anchorsChanged();
|
||||||
void marginsChanged();
|
void marginsChanged();
|
||||||
void exclusiveZoneChanged();
|
void exclusiveZoneChanged();
|
||||||
void exclusionModeChanged();
|
void exclusionModeChanged();
|
||||||
|
void aboveWindowsChanged();
|
||||||
|
void focusableChanged();
|
||||||
};
|
};
|
||||||
|
|
|
@ -46,7 +46,7 @@ ProxyWindowBase::~ProxyWindowBase() { this->deleteWindow(); }
|
||||||
void ProxyWindowBase::onReload(QObject* oldInstance) {
|
void ProxyWindowBase::onReload(QObject* oldInstance) {
|
||||||
this->window = this->retrieveWindow(oldInstance);
|
this->window = this->retrieveWindow(oldInstance);
|
||||||
auto wasVisible = this->window != nullptr && this->window->isVisible();
|
auto wasVisible = this->window != nullptr && this->window->isVisible();
|
||||||
if (this->window == nullptr) this->window = new QQuickWindow();
|
if (this->window == nullptr) this->window = this->createQQuickWindow();
|
||||||
|
|
||||||
// The qml engine will leave the WindowInterface as owner of everything
|
// The qml engine will leave the WindowInterface as owner of everything
|
||||||
// nested in an item, so we have to make sure the interface's children
|
// nested in an item, so we have to make sure the interface's children
|
||||||
|
|
|
@ -188,9 +188,9 @@ public:
|
||||||
|
|
||||||
dbus::DBusPropertyGroup properties;
|
dbus::DBusPropertyGroup properties;
|
||||||
dbus::DBusProperty<quint32> version {this->properties, "Version"};
|
dbus::DBusProperty<quint32> version {this->properties, "Version"};
|
||||||
dbus::DBusProperty<QString> textDirection {this->properties, "TextDirection"};
|
dbus::DBusProperty<QString> textDirection {this->properties, "TextDirection", "", false};
|
||||||
dbus::DBusProperty<QString> status {this->properties, "Status"};
|
dbus::DBusProperty<QString> status {this->properties, "Status"};
|
||||||
dbus::DBusProperty<QStringList> iconThemePath {this->properties, "IconThemePath"};
|
dbus::DBusProperty<QStringList> iconThemePath {this->properties, "IconThemePath", {}, false};
|
||||||
|
|
||||||
void prepareToShow(qint32 item, bool sendOpened);
|
void prepareToShow(qint32 item, bool sendOpened);
|
||||||
void updateLayout(qint32 parent, qint32 depth);
|
void updateLayout(qint32 parent, qint32 depth);
|
||||||
|
|
|
@ -112,6 +112,8 @@ void asyncReadPropertyInternal(
|
||||||
}
|
}
|
||||||
|
|
||||||
void AbstractDBusProperty::tryUpdate(const QVariant& variant) {
|
void AbstractDBusProperty::tryUpdate(const QVariant& variant) {
|
||||||
|
this->mExists = true;
|
||||||
|
|
||||||
auto error = this->read(variant);
|
auto error = this->read(variant);
|
||||||
if (error.isValid()) {
|
if (error.isValid()) {
|
||||||
qCWarning(logDbusProperties).noquote()
|
qCWarning(logDbusProperties).noquote()
|
||||||
|
@ -159,6 +161,44 @@ void AbstractDBusProperty::update() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AbstractDBusProperty::write() {
|
||||||
|
if (this->group == nullptr) {
|
||||||
|
qFatal(logDbusProperties) << "Tried to write dbus property" << this->name
|
||||||
|
<< "which is not attached to a group";
|
||||||
|
} else {
|
||||||
|
const QString propStr = this->toString();
|
||||||
|
|
||||||
|
if (this->group->interface == nullptr) {
|
||||||
|
qFatal(logDbusProperties).noquote()
|
||||||
|
<< "Tried to write property" << propStr << "of a disconnected interface";
|
||||||
|
}
|
||||||
|
|
||||||
|
qCDebug(logDbusProperties).noquote() << "Writing property" << propStr;
|
||||||
|
|
||||||
|
auto pendingCall = this->group->propertyInterface->Set(
|
||||||
|
this->group->interface->interface(),
|
||||||
|
this->name,
|
||||||
|
QDBusVariant(this->serialize())
|
||||||
|
);
|
||||||
|
|
||||||
|
auto* call = new QDBusPendingCallWatcher(pendingCall, this);
|
||||||
|
|
||||||
|
auto responseCallback = [propStr](QDBusPendingCallWatcher* call) {
|
||||||
|
const QDBusPendingReply<> reply = *call;
|
||||||
|
|
||||||
|
if (reply.isError()) {
|
||||||
|
qCWarning(logDbusProperties).noquote() << "Error writing property" << propStr;
|
||||||
|
qCWarning(logDbusProperties) << reply.error();
|
||||||
|
}
|
||||||
|
delete call;
|
||||||
|
};
|
||||||
|
|
||||||
|
QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AbstractDBusProperty::exists() const { return this->mExists; }
|
||||||
|
|
||||||
QString AbstractDBusProperty::toString() const {
|
QString AbstractDBusProperty::toString() const {
|
||||||
const QString group = this->group == nullptr ? "{ NO GROUP }" : this->group->toString();
|
const QString group = this->group == nullptr ? "{ NO GROUP }" : this->group->toString();
|
||||||
return group + ':' + this->name;
|
return group + ':' + this->name;
|
||||||
|
@ -232,7 +272,7 @@ void DBusPropertyGroup::updateAllViaGetAll() {
|
||||||
} else {
|
} else {
|
||||||
qCDebug(logDbusProperties).noquote()
|
qCDebug(logDbusProperties).noquote()
|
||||||
<< "Received GetAll property set for" << this->toString();
|
<< "Received GetAll property set for" << this->toString();
|
||||||
this->updatePropertySet(reply.value());
|
this->updatePropertySet(reply.value(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
delete call;
|
delete call;
|
||||||
|
@ -242,7 +282,7 @@ void DBusPropertyGroup::updateAllViaGetAll() {
|
||||||
QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback);
|
QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DBusPropertyGroup::updatePropertySet(const QVariantMap& properties) {
|
void DBusPropertyGroup::updatePropertySet(const QVariantMap& properties, bool complainMissing) {
|
||||||
for (const auto [name, value]: properties.asKeyValueRange()) {
|
for (const auto [name, value]: properties.asKeyValueRange()) {
|
||||||
auto prop = std::find_if(
|
auto prop = std::find_if(
|
||||||
this->properties.begin(),
|
this->properties.begin(),
|
||||||
|
@ -251,11 +291,21 @@ void DBusPropertyGroup::updatePropertySet(const QVariantMap& properties) {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (prop == this->properties.end()) {
|
if (prop == this->properties.end()) {
|
||||||
qCDebug(logDbusProperties) << "Ignoring untracked property update" << name << "for" << this;
|
qCDebug(logDbusProperties) << "Ignoring untracked property update" << name << "for"
|
||||||
|
<< this->toString();
|
||||||
} else {
|
} else {
|
||||||
(*prop)->tryUpdate(value);
|
(*prop)->tryUpdate(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (complainMissing) {
|
||||||
|
for (const auto* prop: this->properties) {
|
||||||
|
if (prop->required && !properties.contains(prop->name)) {
|
||||||
|
qCWarning(logDbusProperties)
|
||||||
|
<< prop->name << "missing from property set for" << this->toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QString DBusPropertyGroup::toString() const {
|
QString DBusPropertyGroup::toString() const {
|
||||||
|
@ -291,7 +341,7 @@ void DBusPropertyGroup::onPropertiesChanged(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this->updatePropertySet(changedProperties);
|
this->updatePropertySet(changedProperties, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace qs::dbus
|
} // namespace qs::dbus
|
||||||
|
|
|
@ -79,22 +79,31 @@ class AbstractDBusProperty: public QObject {
|
||||||
Q_OBJECT;
|
Q_OBJECT;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit AbstractDBusProperty(QString name, const QMetaType& type, QObject* parent = nullptr)
|
explicit AbstractDBusProperty(
|
||||||
|
QString name,
|
||||||
|
const QMetaType& type,
|
||||||
|
bool required,
|
||||||
|
QObject* parent = nullptr
|
||||||
|
)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, name(std::move(name))
|
, name(std::move(name))
|
||||||
, type(type) {}
|
, type(type)
|
||||||
|
, required(required) {}
|
||||||
|
|
||||||
|
[[nodiscard]] bool exists() const;
|
||||||
[[nodiscard]] QString toString() const;
|
[[nodiscard]] QString toString() const;
|
||||||
[[nodiscard]] virtual QString valueString() = 0;
|
[[nodiscard]] virtual QString valueString() = 0;
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void update();
|
void update();
|
||||||
|
void write();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void changed();
|
void changed();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual QDBusError read(const QVariant& variant) = 0;
|
virtual QDBusError read(const QVariant& variant) = 0;
|
||||||
|
virtual QVariant serialize() = 0;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void tryUpdate(const QVariant& variant);
|
void tryUpdate(const QVariant& variant);
|
||||||
|
@ -103,6 +112,8 @@ private:
|
||||||
|
|
||||||
QString name;
|
QString name;
|
||||||
QMetaType type;
|
QMetaType type;
|
||||||
|
bool required;
|
||||||
|
bool mExists = false;
|
||||||
|
|
||||||
friend class DBusPropertyGroup;
|
friend class DBusPropertyGroup;
|
||||||
};
|
};
|
||||||
|
@ -133,7 +144,7 @@ private slots:
|
||||||
);
|
);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void updatePropertySet(const QVariantMap& properties);
|
void updatePropertySet(const QVariantMap& properties, bool complainMissing);
|
||||||
|
|
||||||
DBusPropertiesInterface* propertyInterface = nullptr;
|
DBusPropertiesInterface* propertyInterface = nullptr;
|
||||||
QDBusAbstractInterface* interface = nullptr;
|
QDBusAbstractInterface* interface = nullptr;
|
||||||
|
@ -145,17 +156,23 @@ private:
|
||||||
template <typename T>
|
template <typename T>
|
||||||
class DBusProperty: public AbstractDBusProperty {
|
class DBusProperty: public AbstractDBusProperty {
|
||||||
public:
|
public:
|
||||||
explicit DBusProperty(QString name, QObject* parent = nullptr, T value = T())
|
explicit DBusProperty(
|
||||||
: AbstractDBusProperty(std::move(name), QMetaType::fromType<T>(), parent)
|
QString name,
|
||||||
|
T value = T(),
|
||||||
|
bool required = true,
|
||||||
|
QObject* parent = nullptr
|
||||||
|
)
|
||||||
|
: AbstractDBusProperty(std::move(name), QMetaType::fromType<T>(), required, parent)
|
||||||
, value(std::move(value)) {}
|
, value(std::move(value)) {}
|
||||||
|
|
||||||
explicit DBusProperty(
|
explicit DBusProperty(
|
||||||
DBusPropertyGroup& group,
|
DBusPropertyGroup& group,
|
||||||
QString name,
|
QString name,
|
||||||
QObject* parent = nullptr,
|
T value = T(),
|
||||||
T value = T()
|
bool required = true,
|
||||||
|
QObject* parent = nullptr
|
||||||
)
|
)
|
||||||
: DBusProperty(std::move(name), parent, std::move(value)) {
|
: DBusProperty(std::move(name), std::move(value), required, parent) {
|
||||||
group.attachProperty(this);
|
group.attachProperty(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,7 +182,7 @@ public:
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] T get() const { return this->value; }
|
[[nodiscard]] const T& get() const { return this->value; }
|
||||||
|
|
||||||
void set(T value) {
|
void set(T value) {
|
||||||
this->value = std::move(value);
|
this->value = std::move(value);
|
||||||
|
@ -183,6 +200,8 @@ protected:
|
||||||
return result.error;
|
return result.error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QVariant serialize() override { return QVariant::fromValue(this->value); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
T value;
|
T value;
|
||||||
|
|
||||||
|
|
|
@ -5,3 +5,7 @@ endif()
|
||||||
if (SERVICE_PIPEWIRE)
|
if (SERVICE_PIPEWIRE)
|
||||||
add_subdirectory(pipewire)
|
add_subdirectory(pipewire)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if (SERVICE_MPRIS)
|
||||||
|
add_subdirectory(mpris)
|
||||||
|
endif()
|
||||||
|
|
39
src/services/mpris/CMakeLists.txt
Normal file
39
src/services/mpris/CMakeLists.txt
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
set_source_files_properties(org.mpris.MediaPlayer2.Player.xml PROPERTIES
|
||||||
|
CLASSNAME DBusMprisPlayer
|
||||||
|
NO_NAMESPACE TRUE
|
||||||
|
)
|
||||||
|
|
||||||
|
qt_add_dbus_interface(DBUS_INTERFACES
|
||||||
|
org.mpris.MediaPlayer2.Player.xml
|
||||||
|
dbus_player
|
||||||
|
)
|
||||||
|
|
||||||
|
set_source_files_properties(org.mpris.MediaPlayer2.xml PROPERTIES
|
||||||
|
CLASSNAME DBusMprisPlayerApp
|
||||||
|
NO_NAMESPACE TRUE
|
||||||
|
)
|
||||||
|
|
||||||
|
qt_add_dbus_interface(DBUS_INTERFACES
|
||||||
|
org.mpris.MediaPlayer2.xml
|
||||||
|
dbus_player_app
|
||||||
|
)
|
||||||
|
|
||||||
|
qt_add_library(quickshell-service-mpris STATIC
|
||||||
|
player.cpp
|
||||||
|
watcher.cpp
|
||||||
|
${DBUS_INTERFACES}
|
||||||
|
)
|
||||||
|
|
||||||
|
# dbus headers
|
||||||
|
target_include_directories(quickshell-service-mpris PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
|
||||||
|
|
||||||
|
qt_add_qml_module(quickshell-service-mpris
|
||||||
|
URI Quickshell.Services.Mpris
|
||||||
|
VERSION 0.1
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(quickshell-service-mpris PRIVATE ${QT_DEPS} quickshell-dbus)
|
||||||
|
target_link_libraries(quickshell PRIVATE quickshell-service-mprisplugin)
|
||||||
|
|
||||||
|
qs_pch(quickshell-service-mpris)
|
||||||
|
qs_pch(quickshell-service-mprisplugin)
|
7
src/services/mpris/module.md
Normal file
7
src/services/mpris/module.md
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
name = "Quickshell.Services.Mpris"
|
||||||
|
description = "Mpris Service"
|
||||||
|
headers = [
|
||||||
|
"player.hpp",
|
||||||
|
"watcher.hpp",
|
||||||
|
]
|
||||||
|
-----
|
24
src/services/mpris/org.mpris.MediaPlayer2.Player.xml
Normal file
24
src/services/mpris/org.mpris.MediaPlayer2.Player.xml
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<node>
|
||||||
|
<interface name="org.mpris.MediaPlayer2.Player">
|
||||||
|
<method name="OpenUri">
|
||||||
|
<arg direction="in" type="s" name="Uri"/>
|
||||||
|
</method>
|
||||||
|
<method name="SetPosition">
|
||||||
|
<arg direction="in" type="o" name="TrackId"/>
|
||||||
|
<arg direction="in" type="x" name="Position"/>
|
||||||
|
</method>
|
||||||
|
<method name="Seek">
|
||||||
|
<arg direction="in" type="x" name="Offset"/>
|
||||||
|
</method>
|
||||||
|
<method name="PlayPause"/>
|
||||||
|
<method name="Next"/>
|
||||||
|
<method name="Previous"/>
|
||||||
|
<method name="Stop"/>
|
||||||
|
<method name="Play"/>
|
||||||
|
<method name="Pause"/>
|
||||||
|
|
||||||
|
<signal name="Seeked">
|
||||||
|
<arg type="x" name="Position"/>
|
||||||
|
</signal>
|
||||||
|
</interface>
|
||||||
|
</node>
|
6
src/services/mpris/org.mpris.MediaPlayer2.xml
Normal file
6
src/services/mpris/org.mpris.MediaPlayer2.xml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<node>
|
||||||
|
<interface name="org.mpris.MediaPlayer2">
|
||||||
|
<method name="Raise"/>
|
||||||
|
<method name="Quit"/>
|
||||||
|
</interface>
|
||||||
|
</node>
|
427
src/services/mpris/player.cpp
Normal file
427
src/services/mpris/player.cpp
Normal file
|
@ -0,0 +1,427 @@
|
||||||
|
#include "player.hpp"
|
||||||
|
|
||||||
|
#include <qcontainerfwd.h>
|
||||||
|
#include <qdatetime.h>
|
||||||
|
#include <qdbusconnection.h>
|
||||||
|
#include <qdbusextratypes.h>
|
||||||
|
#include <qlogging.h>
|
||||||
|
#include <qloggingcategory.h>
|
||||||
|
#include <qobject.h>
|
||||||
|
#include <qstring.h>
|
||||||
|
#include <qtmetamacros.h>
|
||||||
|
#include <qtypes.h>
|
||||||
|
|
||||||
|
#include "../../dbus/properties.hpp"
|
||||||
|
#include "dbus_player.h"
|
||||||
|
#include "dbus_player_app.h"
|
||||||
|
|
||||||
|
using namespace qs::dbus;
|
||||||
|
|
||||||
|
namespace qs::service::mpris {
|
||||||
|
|
||||||
|
Q_LOGGING_CATEGORY(logMprisPlayer, "quickshell.service.mp.player", QtWarningMsg);
|
||||||
|
|
||||||
|
QString MprisPlaybackState::toString(MprisPlaybackState::Enum status) {
|
||||||
|
switch (status) {
|
||||||
|
case MprisPlaybackState::Stopped: return "Stopped";
|
||||||
|
case MprisPlaybackState::Playing: return "Playing";
|
||||||
|
case MprisPlaybackState::Paused: return "Paused";
|
||||||
|
default: return "Unknown Status";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString MprisLoopState::toString(MprisLoopState::Enum status) {
|
||||||
|
switch (status) {
|
||||||
|
case MprisLoopState::None: return "None";
|
||||||
|
case MprisLoopState::Track: return "Track";
|
||||||
|
case MprisLoopState::Playlist: return "Playlist";
|
||||||
|
default: return "Unknown Status";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MprisPlayer::MprisPlayer(const QString& address, QObject* parent): QObject(parent) {
|
||||||
|
this->app = new DBusMprisPlayerApp(
|
||||||
|
address,
|
||||||
|
"/org/mpris/MediaPlayer2",
|
||||||
|
QDBusConnection::sessionBus(),
|
||||||
|
this
|
||||||
|
);
|
||||||
|
|
||||||
|
this->player =
|
||||||
|
new DBusMprisPlayer(address, "/org/mpris/MediaPlayer2", QDBusConnection::sessionBus(), this);
|
||||||
|
|
||||||
|
if (!this->player->isValid() || !this->app->isValid()) {
|
||||||
|
qCWarning(logMprisPlayer) << "Cannot create MprisPlayer for" << address;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
QObject::connect(&this->pCanQuit, &AbstractDBusProperty::changed, this, &MprisPlayer::canQuitChanged);
|
||||||
|
QObject::connect(&this->pCanRaise, &AbstractDBusProperty::changed, this, &MprisPlayer::canRaiseChanged);
|
||||||
|
QObject::connect(&this->pCanSetFullscreen, &AbstractDBusProperty::changed, this, &MprisPlayer::canSetFullscreenChanged);
|
||||||
|
QObject::connect(&this->pIdentity, &AbstractDBusProperty::changed, this, &MprisPlayer::identityChanged);
|
||||||
|
QObject::connect(&this->pFullscreen, &AbstractDBusProperty::changed, this, &MprisPlayer::fullscreenChanged);
|
||||||
|
QObject::connect(&this->pSupportedUriSchemes, &AbstractDBusProperty::changed, this, &MprisPlayer::supportedUriSchemesChanged);
|
||||||
|
QObject::connect(&this->pSupportedMimeTypes, &AbstractDBusProperty::changed, this, &MprisPlayer::supportedMimeTypesChanged);
|
||||||
|
|
||||||
|
QObject::connect(&this->pCanControl, &AbstractDBusProperty::changed, this, &MprisPlayer::canControlChanged);
|
||||||
|
QObject::connect(&this->pCanSeek, &AbstractDBusProperty::changed, this, &MprisPlayer::canSeekChanged);
|
||||||
|
QObject::connect(&this->pCanGoNext, &AbstractDBusProperty::changed, this, &MprisPlayer::canGoNextChanged);
|
||||||
|
QObject::connect(&this->pCanGoPrevious, &AbstractDBusProperty::changed, this, &MprisPlayer::canGoPreviousChanged);
|
||||||
|
QObject::connect(&this->pCanPlay, &AbstractDBusProperty::changed, this, &MprisPlayer::canPlayChanged);
|
||||||
|
QObject::connect(&this->pCanPause, &AbstractDBusProperty::changed, this, &MprisPlayer::canPauseChanged);
|
||||||
|
QObject::connect(&this->pPosition, &AbstractDBusProperty::changed, this, &MprisPlayer::onPositionChanged);
|
||||||
|
QObject::connect(this->player, &DBusMprisPlayer::Seeked, this, &MprisPlayer::onSeek);
|
||||||
|
QObject::connect(&this->pVolume, &AbstractDBusProperty::changed, this, &MprisPlayer::volumeChanged);
|
||||||
|
QObject::connect(&this->pMetadata, &AbstractDBusProperty::changed, this, &MprisPlayer::onMetadataChanged);
|
||||||
|
QObject::connect(&this->pPlaybackStatus, &AbstractDBusProperty::changed, this, &MprisPlayer::onPlaybackStatusChanged);
|
||||||
|
QObject::connect(&this->pLoopStatus, &AbstractDBusProperty::changed, this, &MprisPlayer::onLoopStatusChanged);
|
||||||
|
QObject::connect(&this->pRate, &AbstractDBusProperty::changed, this, &MprisPlayer::rateChanged);
|
||||||
|
QObject::connect(&this->pMinRate, &AbstractDBusProperty::changed, this, &MprisPlayer::minRateChanged);
|
||||||
|
QObject::connect(&this->pMaxRate, &AbstractDBusProperty::changed, this, &MprisPlayer::maxRateChanged);
|
||||||
|
QObject::connect(&this->pShuffle, &AbstractDBusProperty::changed, this, &MprisPlayer::shuffleChanged);
|
||||||
|
|
||||||
|
QObject::connect(&this->playerProperties, &DBusPropertyGroup::getAllFinished, this, &MprisPlayer::onGetAllFinished);
|
||||||
|
|
||||||
|
// Ensure user triggered position updates can update length.
|
||||||
|
QObject::connect(this, &MprisPlayer::positionChanged, this, &MprisPlayer::onExportedPositionChanged);
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
|
this->appProperties.setInterface(this->app);
|
||||||
|
this->playerProperties.setInterface(this->player);
|
||||||
|
this->appProperties.updateAllViaGetAll();
|
||||||
|
this->playerProperties.updateAllViaGetAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MprisPlayer::raise() {
|
||||||
|
if (!this->canRaise()) {
|
||||||
|
qWarning() << "Cannot call raise() on" << this << "because canRaise is false.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->app->Raise();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MprisPlayer::quit() {
|
||||||
|
if (!this->canQuit()) {
|
||||||
|
qWarning() << "Cannot call quit() on" << this << "because canQuit is false.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->app->Quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MprisPlayer::openUri(const QString& uri) { this->player->OpenUri(uri); }
|
||||||
|
|
||||||
|
void MprisPlayer::next() {
|
||||||
|
if (!this->canGoNext()) {
|
||||||
|
qWarning() << "Cannot call next() on" << this << "because canGoNext is false.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->player->Next();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MprisPlayer::previous() {
|
||||||
|
if (!this->canGoPrevious()) {
|
||||||
|
qWarning() << "Cannot call previous() on" << this << "because canGoPrevious is false.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->player->Previous();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MprisPlayer::seek(qreal offset) {
|
||||||
|
if (!this->canSeek()) {
|
||||||
|
qWarning() << "Cannot call seek() on" << this << "because canSeek is false.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto target = static_cast<qlonglong>(offset * 1000) * 1000;
|
||||||
|
this->player->Seek(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MprisPlayer::isValid() const { return this->player->isValid(); }
|
||||||
|
QString MprisPlayer::address() const { return this->player->service(); }
|
||||||
|
|
||||||
|
bool MprisPlayer::canControl() const { return this->pCanControl.get(); }
|
||||||
|
bool MprisPlayer::canPlay() const { return this->canControl() && this->pCanPlay.get(); }
|
||||||
|
bool MprisPlayer::canPause() const { return this->canControl() && this->pCanPause.get(); }
|
||||||
|
bool MprisPlayer::canSeek() const { return this->canControl() && this->pCanSeek.get(); }
|
||||||
|
bool MprisPlayer::canGoNext() const { return this->canControl() && this->pCanGoNext.get(); }
|
||||||
|
bool MprisPlayer::canGoPrevious() const { return this->canControl() && this->pCanGoPrevious.get(); }
|
||||||
|
bool MprisPlayer::canQuit() const { return this->pCanQuit.get(); }
|
||||||
|
bool MprisPlayer::canRaise() const { return this->pCanRaise.get(); }
|
||||||
|
bool MprisPlayer::canSetFullscreen() const { return this->pCanSetFullscreen.get(); }
|
||||||
|
|
||||||
|
QString MprisPlayer::identity() const { return this->pIdentity.get(); }
|
||||||
|
|
||||||
|
qlonglong MprisPlayer::positionMs() const {
|
||||||
|
if (!this->positionSupported()) return 0; // unsupported
|
||||||
|
if (this->mPlaybackState == MprisPlaybackState::Stopped) return 0;
|
||||||
|
|
||||||
|
auto paused = this->mPlaybackState == MprisPlaybackState::Paused;
|
||||||
|
auto time = paused ? this->pausedTime : QDateTime::currentDateTime();
|
||||||
|
auto offset = time - this->lastPositionTimestamp;
|
||||||
|
auto rateMul = static_cast<qlonglong>(this->pRate.get() * 1000);
|
||||||
|
offset = (offset * rateMul) / 1000;
|
||||||
|
|
||||||
|
return (this->pPosition.get() / 1000) + offset.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal MprisPlayer::position() const {
|
||||||
|
if (!this->positionSupported()) return 0; // unsupported
|
||||||
|
if (this->mPlaybackState == MprisPlaybackState::Stopped) return 0;
|
||||||
|
|
||||||
|
return static_cast<qreal>(this->positionMs()) / 1000.0; // NOLINT
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MprisPlayer::positionSupported() const { return this->pPosition.exists(); }
|
||||||
|
|
||||||
|
void MprisPlayer::setPosition(qreal position) {
|
||||||
|
if (this->pPosition.get() == -1) {
|
||||||
|
qWarning() << "Cannot set position of" << this << "because position is not supported.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this->canSeek()) {
|
||||||
|
qWarning() << "Cannot set position of" << this << "because canSeek is false.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto target = static_cast<qlonglong>(position * 1000) * 1000;
|
||||||
|
this->pPosition.set(target);
|
||||||
|
|
||||||
|
if (!this->mTrackId.isEmpty()) {
|
||||||
|
this->player->SetPosition(QDBusObjectPath(this->mTrackId), target);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
auto pos = this->positionMs() * 1000;
|
||||||
|
this->player->Seek(target - pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MprisPlayer::onPositionChanged() {
|
||||||
|
const bool firstChange = !this->lastPositionTimestamp.isValid();
|
||||||
|
this->lastPositionTimestamp = QDateTime::currentDateTimeUtc();
|
||||||
|
emit this->positionChanged();
|
||||||
|
if (firstChange) emit this->positionSupportedChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MprisPlayer::onExportedPositionChanged() {
|
||||||
|
if (!this->lengthSupported()) emit this->lengthChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MprisPlayer::onSeek(qlonglong time) { this->pPosition.set(time); }
|
||||||
|
|
||||||
|
qreal MprisPlayer::length() const {
|
||||||
|
if (this->mLength == -1) {
|
||||||
|
return this->position(); // unsupported
|
||||||
|
} else {
|
||||||
|
return static_cast<qreal>(this->mLength / 1000) / 1000; // NOLINT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MprisPlayer::lengthSupported() const { return this->mLength != -1; }
|
||||||
|
|
||||||
|
qreal MprisPlayer::volume() const { return this->pVolume.get(); }
|
||||||
|
bool MprisPlayer::volumeSupported() const { return this->pVolume.exists(); }
|
||||||
|
|
||||||
|
void MprisPlayer::setVolume(qreal volume) {
|
||||||
|
if (!this->canControl()) {
|
||||||
|
qWarning() << "Cannot set volume of" << this << "because canControl is false.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this->volumeSupported()) {
|
||||||
|
qWarning() << "Cannot set volume of" << this << "because volume is not supported.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->pVolume.set(volume);
|
||||||
|
this->pVolume.write();
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap MprisPlayer::metadata() const { return this->pMetadata.get(); }
|
||||||
|
|
||||||
|
void MprisPlayer::onMetadataChanged() {
|
||||||
|
auto lengthVariant = this->pMetadata.get().value("mpris:length");
|
||||||
|
qlonglong length = -1;
|
||||||
|
if (lengthVariant.isValid() && lengthVariant.canConvert<qlonglong>()) {
|
||||||
|
length = lengthVariant.value<qlonglong>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (length != this->mLength) {
|
||||||
|
this->mLength = length;
|
||||||
|
emit this->lengthChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto trackidVariant = this->pMetadata.get().value("mpris:trackid");
|
||||||
|
if (trackidVariant.isValid() && trackidVariant.canConvert<QString>()) {
|
||||||
|
this->mTrackId = trackidVariant.value<QString>();
|
||||||
|
this->onSeek(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit this->metadataChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
MprisPlaybackState::Enum MprisPlayer::playbackState() const { return this->mPlaybackState; }
|
||||||
|
|
||||||
|
void MprisPlayer::setPlaybackState(MprisPlaybackState::Enum playbackState) {
|
||||||
|
if (playbackState == this->mPlaybackState) return;
|
||||||
|
|
||||||
|
switch (playbackState) {
|
||||||
|
case MprisPlaybackState::Stopped:
|
||||||
|
if (!this->canControl()) {
|
||||||
|
qWarning() << "Cannot set playbackState of" << this
|
||||||
|
<< "to Stopped because canControl is false.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->player->Stop();
|
||||||
|
break;
|
||||||
|
case MprisPlaybackState::Playing:
|
||||||
|
if (!this->canPlay()) {
|
||||||
|
qWarning() << "Cannot set playbackState of" << this << "to Playing because canPlay is false.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->player->Play();
|
||||||
|
break;
|
||||||
|
case MprisPlaybackState::Paused:
|
||||||
|
if (!this->canPause()) {
|
||||||
|
qWarning() << "Cannot set playbackState of" << this << "to Paused because canPause is false.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->player->Pause();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
qWarning() << "Cannot set playbackState of" << this << "to unknown value" << playbackState;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MprisPlayer::onPlaybackStatusChanged() {
|
||||||
|
const auto& status = this->pPlaybackStatus.get();
|
||||||
|
|
||||||
|
if (status == "Playing") {
|
||||||
|
this->mPlaybackState = MprisPlaybackState::Playing;
|
||||||
|
} else if (status == "Paused") {
|
||||||
|
this->mPlaybackState = MprisPlaybackState::Paused;
|
||||||
|
} else if (status == "Stopped") {
|
||||||
|
this->mPlaybackState = MprisPlaybackState::Stopped;
|
||||||
|
} else {
|
||||||
|
this->mPlaybackState = MprisPlaybackState::Stopped;
|
||||||
|
qWarning() << "Received unexpected PlaybackStatus for" << this << status;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit this->playbackStateChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
MprisLoopState::Enum MprisPlayer::loopState() const { return this->mLoopState; }
|
||||||
|
bool MprisPlayer::loopSupported() const { return this->pLoopStatus.exists(); }
|
||||||
|
|
||||||
|
void MprisPlayer::setLoopState(MprisLoopState::Enum loopState) {
|
||||||
|
if (!this->canControl()) {
|
||||||
|
qWarning() << "Cannot set loopState of" << this << "because canControl is false.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this->loopSupported()) {
|
||||||
|
qWarning() << "Cannot set loopState of" << this << "because loop state is not supported.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loopState == this->mLoopState) return;
|
||||||
|
|
||||||
|
QString loopStatusStr;
|
||||||
|
switch (loopState) {
|
||||||
|
case MprisLoopState::None: loopStatusStr = "None"; break;
|
||||||
|
case MprisLoopState::Track: loopStatusStr = "Track"; break;
|
||||||
|
case MprisLoopState::Playlist: loopStatusStr = "Playlist"; break;
|
||||||
|
default:
|
||||||
|
qWarning() << "Cannot set loopState of" << this << "to unknown value" << loopState;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->pLoopStatus.set(loopStatusStr);
|
||||||
|
this->pLoopStatus.write();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MprisPlayer::onLoopStatusChanged() {
|
||||||
|
const auto& status = this->pLoopStatus.get();
|
||||||
|
|
||||||
|
if (status == "None") {
|
||||||
|
this->mLoopState = MprisLoopState::None;
|
||||||
|
} else if (status == "Track") {
|
||||||
|
this->mLoopState = MprisLoopState::Track;
|
||||||
|
} else if (status == "Playlist") {
|
||||||
|
this->mLoopState = MprisLoopState::Playlist;
|
||||||
|
} else {
|
||||||
|
this->mLoopState = MprisLoopState::None;
|
||||||
|
qWarning() << "Received unexpected LoopStatus for" << this << status;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit this->loopStateChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal MprisPlayer::rate() const { return this->pRate.get(); }
|
||||||
|
qreal MprisPlayer::minRate() const { return this->pMinRate.get(); }
|
||||||
|
qreal MprisPlayer::maxRate() const { return this->pMaxRate.get(); }
|
||||||
|
|
||||||
|
void MprisPlayer::setRate(qreal rate) {
|
||||||
|
if (rate == this->pRate.get()) return;
|
||||||
|
|
||||||
|
if (rate < this->pMinRate.get() || rate > this->pMaxRate.get()) {
|
||||||
|
qWarning() << "Cannot set rate for" << this << "to" << rate
|
||||||
|
<< "which is outside of minRate and maxRate" << this->pMinRate.get()
|
||||||
|
<< this->pMaxRate.get();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->pRate.set(rate);
|
||||||
|
this->pRate.write();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MprisPlayer::shuffle() const { return this->pShuffle.get(); }
|
||||||
|
bool MprisPlayer::shuffleSupported() const { return this->pShuffle.exists(); }
|
||||||
|
|
||||||
|
void MprisPlayer::setShuffle(bool shuffle) {
|
||||||
|
if (!this->shuffleSupported()) {
|
||||||
|
qWarning() << "Cannot set shuffle for" << this << "because shuffle is not supported.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this->canControl()) {
|
||||||
|
qWarning() << "Cannot set shuffle state of" << this << "because canControl is false.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->pShuffle.set(shuffle);
|
||||||
|
this->pShuffle.write();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MprisPlayer::fullscreen() const { return this->pFullscreen.get(); }
|
||||||
|
|
||||||
|
void MprisPlayer::setFullscreen(bool fullscreen) {
|
||||||
|
if (!this->canSetFullscreen()) {
|
||||||
|
qWarning() << "Cannot set fullscreen for" << this << "because canSetFullscreen is false.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->pFullscreen.set(fullscreen);
|
||||||
|
this->pFullscreen.write();
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<QString> MprisPlayer::supportedUriSchemes() const { return this->pSupportedUriSchemes.get(); }
|
||||||
|
QList<QString> MprisPlayer::supportedMimeTypes() const { return this->pSupportedMimeTypes.get(); }
|
||||||
|
|
||||||
|
void MprisPlayer::onGetAllFinished() {
|
||||||
|
if (this->volumeSupported()) emit this->volumeSupportedChanged();
|
||||||
|
if (this->loopSupported()) emit this->loopSupportedChanged();
|
||||||
|
if (this->shuffleSupported()) emit this->shuffleSupportedChanged();
|
||||||
|
emit this->ready();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace qs::service::mpris
|
324
src/services/mpris/player.hpp
Normal file
324
src/services/mpris/player.hpp
Normal file
|
@ -0,0 +1,324 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <qcontainerfwd.h>
|
||||||
|
#include <qobject.h>
|
||||||
|
#include <qqmlintegration.h>
|
||||||
|
#include <qtmetamacros.h>
|
||||||
|
#include <qtypes.h>
|
||||||
|
|
||||||
|
#include "../../core/doc.hpp"
|
||||||
|
#include "../../dbus/properties.hpp"
|
||||||
|
#include "dbus_player.h"
|
||||||
|
#include "dbus_player_app.h"
|
||||||
|
|
||||||
|
namespace qs::service::mpris {
|
||||||
|
|
||||||
|
class MprisPlaybackState: public QObject {
|
||||||
|
Q_OBJECT;
|
||||||
|
QML_ELEMENT;
|
||||||
|
QML_SINGLETON;
|
||||||
|
|
||||||
|
public:
|
||||||
|
enum Enum {
|
||||||
|
Stopped = 0,
|
||||||
|
Playing = 1,
|
||||||
|
Paused = 2,
|
||||||
|
};
|
||||||
|
Q_ENUM(Enum);
|
||||||
|
|
||||||
|
Q_INVOKABLE static QString toString(MprisPlaybackState::Enum status);
|
||||||
|
};
|
||||||
|
|
||||||
|
class MprisLoopState: public QObject {
|
||||||
|
Q_OBJECT;
|
||||||
|
QML_ELEMENT;
|
||||||
|
QML_SINGLETON;
|
||||||
|
|
||||||
|
public:
|
||||||
|
enum Enum {
|
||||||
|
None = 0,
|
||||||
|
Track = 1,
|
||||||
|
Playlist = 2,
|
||||||
|
};
|
||||||
|
Q_ENUM(Enum);
|
||||||
|
|
||||||
|
Q_INVOKABLE static QString toString(MprisLoopState::Enum status);
|
||||||
|
};
|
||||||
|
|
||||||
|
///! A media player exposed over MPRIS.
|
||||||
|
/// A media player exposed over MPRIS.
|
||||||
|
///
|
||||||
|
/// > [!WARNING] Support for various functionality and general compliance to
|
||||||
|
/// > the MPRIS specification varies wildly by player.
|
||||||
|
/// > Always check the associated `canXyz` and `xyzSupported` properties if available.
|
||||||
|
///
|
||||||
|
/// > [!INFO] The TrackList and Playlist interfaces were not implemented as we could not
|
||||||
|
/// > find any media players using them to test against.
|
||||||
|
class MprisPlayer: public QObject {
|
||||||
|
Q_OBJECT;
|
||||||
|
// clang-format off
|
||||||
|
Q_PROPERTY(bool canControl READ canControl NOTIFY canControlChanged);
|
||||||
|
Q_PROPERTY(bool canPlay READ canPlay NOTIFY canPlayChanged);
|
||||||
|
Q_PROPERTY(bool canPause READ canPause NOTIFY canPauseChanged);
|
||||||
|
Q_PROPERTY(bool canSeek READ canSeek NOTIFY canSeekChanged);
|
||||||
|
Q_PROPERTY(bool canGoNext READ canGoNext NOTIFY canGoNextChanged);
|
||||||
|
Q_PROPERTY(bool canGoPrevious READ canGoPrevious NOTIFY canGoPreviousChanged);
|
||||||
|
Q_PROPERTY(bool canQuit READ canQuit NOTIFY canQuitChanged);
|
||||||
|
Q_PROPERTY(bool canRaise READ canRaise NOTIFY canRaiseChanged);
|
||||||
|
Q_PROPERTY(bool canSetFullscreen READ canSetFullscreen NOTIFY canSetFullscreenChanged);
|
||||||
|
/// The human readable name of the media player.
|
||||||
|
Q_PROPERTY(QString identity READ identity NOTIFY identityChanged);
|
||||||
|
/// The current position in the playing track, as seconds, with millisecond precision,
|
||||||
|
/// or `0` if `positionSupported` is false.
|
||||||
|
///
|
||||||
|
/// May only be written to if `canSeek` and `positionSupported` are true.
|
||||||
|
///
|
||||||
|
/// > [!WARNING] To avoid excessive property updates wasting CPU while `position` is not
|
||||||
|
/// > actively monitored, `position` usually will not update reactively, unless a nonlinear
|
||||||
|
/// > change in position occurs, however reading it will always return the current position.
|
||||||
|
/// >
|
||||||
|
/// > If you want to actively monitor the position, the simplest way it to emit the `positionChanged`
|
||||||
|
/// > signal manually for the duration you are monitoring it, Using a [FrameAnimation] if you need
|
||||||
|
/// > the value to update smoothly, such as on a slider, or a [Timer] if not, as shown below.
|
||||||
|
/// >
|
||||||
|
/// > ```qml {filename="Using a FrameAnimation"}
|
||||||
|
/// > FrameAnimation {
|
||||||
|
/// > // only emit the signal when the position is actually changing.
|
||||||
|
/// > running: player.playbackState == MprisPlaybackState.Playing
|
||||||
|
/// > // emit the positionChanged signal every frame.
|
||||||
|
/// > onTriggered: player.positionChanged()
|
||||||
|
/// > }
|
||||||
|
/// > ```
|
||||||
|
/// >
|
||||||
|
/// > ```qml {filename="Using a Timer"}
|
||||||
|
/// > Timer {
|
||||||
|
/// > // only emit the signal when the position is actually changing.
|
||||||
|
/// > running: player.playbackState == MprisPlaybackState.Playing
|
||||||
|
/// > // Make sure the position updates at least once per second.
|
||||||
|
/// > interval: 1000
|
||||||
|
/// > repeat: true
|
||||||
|
/// > // emit the positionChanged signal every second.
|
||||||
|
/// > onTriggered: player.positionChanged()
|
||||||
|
/// > }
|
||||||
|
/// > ```
|
||||||
|
///
|
||||||
|
/// [FrameAnimation]: https://doc.qt.io/qt-6/qml-qtquick-frameanimation.html
|
||||||
|
/// [Timer]: https://doc.qt.io/qt-6/qml-qtqml-timer.html
|
||||||
|
Q_PROPERTY(qreal position READ position WRITE setPosition NOTIFY positionChanged);
|
||||||
|
Q_PROPERTY(bool positionSupported READ positionSupported NOTIFY positionSupportedChanged);
|
||||||
|
/// The length of the playing track, as seconds, with millisecond precision,
|
||||||
|
/// or the value of `position` if `lengthSupported` is false.
|
||||||
|
Q_PROPERTY(qreal length READ length NOTIFY lengthChanged);
|
||||||
|
Q_PROPERTY(bool lengthSupported READ lengthSupported NOTIFY lengthSupportedChanged);
|
||||||
|
/// The volume of the playing track from 0.0 to 1.0, or 1.0 if `volumeSupported` is false.
|
||||||
|
///
|
||||||
|
/// May only be written to if `canControl` and `volumeSupported` are true.
|
||||||
|
Q_PROPERTY(qreal volume READ volume WRITE setVolume NOTIFY volumeChanged);
|
||||||
|
Q_PROPERTY(bool volumeSupported READ volumeSupported NOTIFY volumeSupportedChanged);
|
||||||
|
/// Metadata of the current track.
|
||||||
|
///
|
||||||
|
/// A map of common properties is available [here](https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata).
|
||||||
|
/// Do not count on any of them actually being present.
|
||||||
|
Q_PROPERTY(QVariantMap metadata READ metadata NOTIFY metadataChanged);
|
||||||
|
/// The playback state of the media player.
|
||||||
|
///
|
||||||
|
/// - If `canPlay` is false, you cannot assign the `Playing` state.
|
||||||
|
/// - If `canPause` is false, you cannot assign the `Paused` state.
|
||||||
|
/// - If `canControl` is false, you cannot assign the `Stopped` state.
|
||||||
|
/// (or any of the others, though their repsective properties will also be false)
|
||||||
|
Q_PROPERTY(MprisPlaybackState::Enum playbackState READ playbackState WRITE setPlaybackState NOTIFY playbackStateChanged);
|
||||||
|
/// The loop state of the media player, or `None` if `loopSupported` is false.
|
||||||
|
///
|
||||||
|
/// May only be written to if `canControl` and `loopSupported` are true.
|
||||||
|
Q_PROPERTY(MprisLoopState::Enum loopState READ loopState WRITE setLoopState NOTIFY loopStateChanged);
|
||||||
|
Q_PROPERTY(bool loopSupported READ loopSupported NOTIFY loopSupportedChanged);
|
||||||
|
/// The speed the song is playing at, as a multiplier.
|
||||||
|
///
|
||||||
|
/// Only values between `minRate` and `maxRate` (inclusive) may be written to the property.
|
||||||
|
/// Additionally, It is recommended that you only write common values such as `0.25`, `0.5`, `1.0`, `2.0`
|
||||||
|
/// to the property, as media players are free to ignore the value, and are more likely to
|
||||||
|
/// accept common ones.
|
||||||
|
Q_PROPERTY(qreal rate READ rate WRITE setRate NOTIFY rateChanged);
|
||||||
|
Q_PROPERTY(qreal minRate READ minRate NOTIFY minRateChanged);
|
||||||
|
Q_PROPERTY(qreal maxRate READ maxRate NOTIFY maxRateChanged);
|
||||||
|
/// If the play queue is currently being shuffled, or false if `shuffleSupported` is false.
|
||||||
|
///
|
||||||
|
/// May only be written if `canControl` and `shuffleSupported` are true.
|
||||||
|
Q_PROPERTY(bool shuffle READ shuffle WRITE setShuffle NOTIFY shuffleChanged);
|
||||||
|
Q_PROPERTY(bool shuffleSupported READ shuffleSupported NOTIFY shuffleSupportedChanged);
|
||||||
|
/// If the player is currently shown in fullscreen.
|
||||||
|
///
|
||||||
|
/// May only be written to if `canSetFullscreen` is true.
|
||||||
|
Q_PROPERTY(bool fullscreen READ fullscreen WRITE setFullscreen NOTIFY fullscreenChanged);
|
||||||
|
/// Uri schemes supported by `openUri`.
|
||||||
|
Q_PROPERTY(QList<QString> supportedUriSchemes READ supportedUriSchemes NOTIFY supportedUriSchemesChanged);
|
||||||
|
/// Mime types supported by `openUri`.
|
||||||
|
Q_PROPERTY(QList<QString> supportedMimeTypes READ supportedMimeTypes NOTIFY supportedMimeTypesChanged);
|
||||||
|
// clang-format on
|
||||||
|
QML_ELEMENT;
|
||||||
|
QML_UNCREATABLE("MprisPlayers can only be acquired from Mpris");
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit MprisPlayer(const QString& address, QObject* parent = nullptr);
|
||||||
|
|
||||||
|
/// Bring the media player to the front of the window stack.
|
||||||
|
///
|
||||||
|
/// May only be called if `canRaise` is true.
|
||||||
|
Q_INVOKABLE void raise();
|
||||||
|
/// Quit the media player.
|
||||||
|
///
|
||||||
|
/// May only be called if `canQuit` is true.
|
||||||
|
Q_INVOKABLE void quit();
|
||||||
|
/// Open the given URI in the media player.
|
||||||
|
///
|
||||||
|
/// Many players will silently ignore this, especially if the uri
|
||||||
|
/// does not match `supportedUriSchemes` and `supportedMimeTypes`.
|
||||||
|
Q_INVOKABLE void openUri(const QString& uri);
|
||||||
|
/// Play the next song.
|
||||||
|
///
|
||||||
|
/// May only be called if `canGoNext` is true.
|
||||||
|
Q_INVOKABLE void next();
|
||||||
|
/// Play the previous song, or go back to the beginning of the current one.
|
||||||
|
///
|
||||||
|
/// May only be called if `canGoPrevious` is true.
|
||||||
|
Q_INVOKABLE void previous();
|
||||||
|
/// Change `position` by an offset.
|
||||||
|
///
|
||||||
|
/// Even if `positionSupported` is false and you cannot set `position`,
|
||||||
|
/// this function may work.
|
||||||
|
///
|
||||||
|
/// May only be called if `canSeek` is true.
|
||||||
|
Q_INVOKABLE void seek(qreal offset);
|
||||||
|
|
||||||
|
[[nodiscard]] bool isValid() const;
|
||||||
|
[[nodiscard]] QString address() const;
|
||||||
|
|
||||||
|
[[nodiscard]] bool canControl() const;
|
||||||
|
[[nodiscard]] bool canSeek() const;
|
||||||
|
[[nodiscard]] bool canGoNext() const;
|
||||||
|
[[nodiscard]] bool canGoPrevious() const;
|
||||||
|
[[nodiscard]] bool canPlay() const;
|
||||||
|
[[nodiscard]] bool canPause() const;
|
||||||
|
[[nodiscard]] bool canQuit() const;
|
||||||
|
[[nodiscard]] bool canRaise() const;
|
||||||
|
[[nodiscard]] bool canSetFullscreen() const;
|
||||||
|
|
||||||
|
[[nodiscard]] QString identity() const;
|
||||||
|
|
||||||
|
[[nodiscard]] qlonglong positionMs() const;
|
||||||
|
[[nodiscard]] qreal position() const;
|
||||||
|
[[nodiscard]] bool positionSupported() const;
|
||||||
|
void setPosition(qreal position);
|
||||||
|
|
||||||
|
[[nodiscard]] qreal length() const;
|
||||||
|
[[nodiscard]] bool lengthSupported() const;
|
||||||
|
|
||||||
|
[[nodiscard]] qreal volume() const;
|
||||||
|
[[nodiscard]] bool volumeSupported() const;
|
||||||
|
void setVolume(qreal volume);
|
||||||
|
|
||||||
|
[[nodiscard]] QVariantMap metadata() const;
|
||||||
|
|
||||||
|
[[nodiscard]] MprisPlaybackState::Enum playbackState() const;
|
||||||
|
void setPlaybackState(MprisPlaybackState::Enum playbackState);
|
||||||
|
|
||||||
|
[[nodiscard]] MprisLoopState::Enum loopState() const;
|
||||||
|
[[nodiscard]] bool loopSupported() const;
|
||||||
|
void setLoopState(MprisLoopState::Enum loopState);
|
||||||
|
|
||||||
|
[[nodiscard]] qreal rate() const;
|
||||||
|
[[nodiscard]] qreal minRate() const;
|
||||||
|
[[nodiscard]] qreal maxRate() const;
|
||||||
|
void setRate(qreal rate);
|
||||||
|
|
||||||
|
[[nodiscard]] bool shuffle() const;
|
||||||
|
[[nodiscard]] bool shuffleSupported() const;
|
||||||
|
void setShuffle(bool shuffle);
|
||||||
|
|
||||||
|
[[nodiscard]] bool fullscreen() const;
|
||||||
|
void setFullscreen(bool fullscreen);
|
||||||
|
|
||||||
|
[[nodiscard]] QList<QString> supportedUriSchemes() const;
|
||||||
|
[[nodiscard]] QList<QString> supportedMimeTypes() const;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
QSDOC_HIDE void ready();
|
||||||
|
void canControlChanged();
|
||||||
|
void canPlayChanged();
|
||||||
|
void canPauseChanged();
|
||||||
|
void canSeekChanged();
|
||||||
|
void canGoNextChanged();
|
||||||
|
void canGoPreviousChanged();
|
||||||
|
void canQuitChanged();
|
||||||
|
void canRaiseChanged();
|
||||||
|
void canSetFullscreenChanged();
|
||||||
|
void identityChanged();
|
||||||
|
void positionChanged();
|
||||||
|
void positionSupportedChanged();
|
||||||
|
void lengthChanged();
|
||||||
|
void lengthSupportedChanged();
|
||||||
|
void volumeChanged();
|
||||||
|
void volumeSupportedChanged();
|
||||||
|
void metadataChanged();
|
||||||
|
void playbackStateChanged();
|
||||||
|
void loopStateChanged();
|
||||||
|
void loopSupportedChanged();
|
||||||
|
void rateChanged();
|
||||||
|
void minRateChanged();
|
||||||
|
void maxRateChanged();
|
||||||
|
void shuffleChanged();
|
||||||
|
void shuffleSupportedChanged();
|
||||||
|
void fullscreenChanged();
|
||||||
|
void supportedUriSchemesChanged();
|
||||||
|
void supportedMimeTypesChanged();
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onGetAllFinished();
|
||||||
|
void onPositionChanged();
|
||||||
|
void onExportedPositionChanged();
|
||||||
|
void onSeek(qlonglong time);
|
||||||
|
void onMetadataChanged();
|
||||||
|
void onPlaybackStatusChanged();
|
||||||
|
void onLoopStatusChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
// clang-format off
|
||||||
|
dbus::DBusPropertyGroup appProperties;
|
||||||
|
dbus::DBusProperty<QString> pIdentity {this->appProperties, "Identity"};
|
||||||
|
dbus::DBusProperty<bool> pCanQuit {this->appProperties, "CanQuit"};
|
||||||
|
dbus::DBusProperty<bool> pCanRaise {this->appProperties, "CanRaise"};
|
||||||
|
dbus::DBusProperty<bool> pFullscreen {this->appProperties, "Fullscreen", false, false};
|
||||||
|
dbus::DBusProperty<bool> pCanSetFullscreen {this->appProperties, "CanSetFullscreen", false, false};
|
||||||
|
dbus::DBusProperty<QList<QString>> pSupportedUriSchemes {this->appProperties, "SupportedUriSchemes"};
|
||||||
|
dbus::DBusProperty<QList<QString>> pSupportedMimeTypes {this->appProperties, "SupportedMimeTypes"};
|
||||||
|
|
||||||
|
dbus::DBusPropertyGroup playerProperties;
|
||||||
|
dbus::DBusProperty<bool> pCanControl {this->playerProperties, "CanControl"};
|
||||||
|
dbus::DBusProperty<bool> pCanPlay {this->playerProperties, "CanPlay"};
|
||||||
|
dbus::DBusProperty<bool> pCanPause {this->playerProperties, "CanPause"};
|
||||||
|
dbus::DBusProperty<bool> pCanSeek {this->playerProperties, "CanSeek"};
|
||||||
|
dbus::DBusProperty<bool> pCanGoNext {this->playerProperties, "CanGoNext"};
|
||||||
|
dbus::DBusProperty<bool> pCanGoPrevious {this->playerProperties, "CanGoPrevious"};
|
||||||
|
dbus::DBusProperty<qlonglong> pPosition {this->playerProperties, "Position", 0, false}; // "required"
|
||||||
|
dbus::DBusProperty<double> pVolume {this->playerProperties, "Volume", 1, false}; // "required"
|
||||||
|
dbus::DBusProperty<QVariantMap> pMetadata {this->playerProperties, "Metadata"};
|
||||||
|
dbus::DBusProperty<QString> pPlaybackStatus {this->playerProperties, "PlaybackStatus"};
|
||||||
|
dbus::DBusProperty<QString> pLoopStatus {this->playerProperties, "LoopStatus", "", false};
|
||||||
|
dbus::DBusProperty<double> pRate {this->playerProperties, "Rate", 1, false}; // "required"
|
||||||
|
dbus::DBusProperty<double> pMinRate {this->playerProperties, "MinimumRate", 1, false}; // "required"
|
||||||
|
dbus::DBusProperty<double> pMaxRate {this->playerProperties, "MaximumRate", 1, false}; // "required"
|
||||||
|
dbus::DBusProperty<bool> pShuffle {this->playerProperties, "Shuffle", false, false};
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
|
MprisPlaybackState::Enum mPlaybackState = MprisPlaybackState::Stopped;
|
||||||
|
MprisLoopState::Enum mLoopState = MprisLoopState::None;
|
||||||
|
QDateTime lastPositionTimestamp;
|
||||||
|
QDateTime pausedTime;
|
||||||
|
qlonglong mLength = -1;
|
||||||
|
|
||||||
|
DBusMprisPlayerApp* app = nullptr;
|
||||||
|
DBusMprisPlayer* player = nullptr;
|
||||||
|
QString mTrackId;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace qs::service::mpris
|
126
src/services/mpris/watcher.cpp
Normal file
126
src/services/mpris/watcher.cpp
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
#include "watcher.hpp"
|
||||||
|
|
||||||
|
#include <qcontainerfwd.h>
|
||||||
|
#include <qdbusconnection.h>
|
||||||
|
#include <qdbusconnectioninterface.h>
|
||||||
|
#include <qdbusservicewatcher.h>
|
||||||
|
#include <qlist.h>
|
||||||
|
#include <qlogging.h>
|
||||||
|
#include <qloggingcategory.h>
|
||||||
|
#include <qobject.h>
|
||||||
|
#include <qqmllist.h>
|
||||||
|
#include <qtmetamacros.h>
|
||||||
|
#include <qtypes.h>
|
||||||
|
|
||||||
|
#include "player.hpp"
|
||||||
|
|
||||||
|
namespace qs::service::mpris {
|
||||||
|
|
||||||
|
Q_LOGGING_CATEGORY(logMprisWatcher, "quickshell.service.mpris.watcher", QtWarningMsg);
|
||||||
|
|
||||||
|
MprisWatcher::MprisWatcher(QObject* parent): QObject(parent) {
|
||||||
|
qCDebug(logMprisWatcher) << "Starting MprisWatcher";
|
||||||
|
|
||||||
|
auto bus = QDBusConnection::sessionBus();
|
||||||
|
|
||||||
|
if (!bus.isConnected()) {
|
||||||
|
qCWarning(logMprisWatcher) << "Could not connect to DBus. Mpris service will not work.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
QObject::connect(&this->serviceWatcher, &QDBusServiceWatcher::serviceRegistered, this, &MprisWatcher::onServiceRegistered);
|
||||||
|
QObject::connect(&this->serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, &MprisWatcher::onServiceUnregistered);
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
|
this->serviceWatcher.setWatchMode(
|
||||||
|
QDBusServiceWatcher::WatchForUnregistration | QDBusServiceWatcher::WatchForRegistration
|
||||||
|
);
|
||||||
|
|
||||||
|
this->serviceWatcher.addWatchedService("org.mpris.MediaPlayer2*");
|
||||||
|
this->serviceWatcher.setConnection(bus);
|
||||||
|
|
||||||
|
this->registerExisting();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MprisWatcher::registerExisting() {
|
||||||
|
const QStringList& list = QDBusConnection::sessionBus().interface()->registeredServiceNames();
|
||||||
|
for (const QString& service: list) {
|
||||||
|
if (service.startsWith("org.mpris.MediaPlayer2")) {
|
||||||
|
qCDebug(logMprisWatcher).noquote() << "Found Mpris service" << service;
|
||||||
|
this->registerPlayer(service);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MprisWatcher::onServiceRegistered(const QString& service) {
|
||||||
|
if (service.startsWith("org.mpris.MediaPlayer2")) {
|
||||||
|
qCDebug(logMprisWatcher).noquote() << "Mpris service " << service << " registered.";
|
||||||
|
this->registerPlayer(service);
|
||||||
|
} else {
|
||||||
|
qCWarning(logMprisWatcher) << "Got a registration event for untracked service" << service;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MprisWatcher::onServiceUnregistered(const QString& service) {
|
||||||
|
if (auto* player = this->mPlayers.value(service)) {
|
||||||
|
player->deleteLater();
|
||||||
|
this->mPlayers.remove(service);
|
||||||
|
qCDebug(logMprisWatcher) << "Unregistered MprisPlayer" << service;
|
||||||
|
} else {
|
||||||
|
qCWarning(logMprisWatcher) << "Got service unregister event for untracked service" << service;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MprisWatcher::onPlayerReady() {
|
||||||
|
auto* player = qobject_cast<MprisPlayer*>(this->sender());
|
||||||
|
this->readyPlayers.push_back(player);
|
||||||
|
emit this->playersChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MprisWatcher::onPlayerDestroyed(QObject* object) {
|
||||||
|
auto* player = static_cast<MprisPlayer*>(object); // NOLINT
|
||||||
|
|
||||||
|
if (this->readyPlayers.removeOne(player)) {
|
||||||
|
emit this->playersChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QQmlListProperty<MprisPlayer> MprisWatcher::players() {
|
||||||
|
return QQmlListProperty<MprisPlayer>(
|
||||||
|
this,
|
||||||
|
nullptr,
|
||||||
|
&MprisWatcher::playersCount,
|
||||||
|
&MprisWatcher::playerAt
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
qsizetype MprisWatcher::playersCount(QQmlListProperty<MprisPlayer>* property) {
|
||||||
|
return static_cast<MprisWatcher*>(property->object)->readyPlayers.count(); // NOLINT
|
||||||
|
}
|
||||||
|
|
||||||
|
MprisPlayer* MprisWatcher::playerAt(QQmlListProperty<MprisPlayer>* property, qsizetype index) {
|
||||||
|
return static_cast<MprisWatcher*>(property->object)->readyPlayers.at(index); // NOLINT
|
||||||
|
}
|
||||||
|
|
||||||
|
void MprisWatcher::registerPlayer(const QString& address) {
|
||||||
|
if (this->mPlayers.contains(address)) {
|
||||||
|
qCDebug(logMprisWatcher) << "Skipping duplicate registration of MprisPlayer" << address;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* player = new MprisPlayer(address, this);
|
||||||
|
if (!player->isValid()) {
|
||||||
|
qCWarning(logMprisWatcher) << "Ignoring invalid MprisPlayer registration of" << address;
|
||||||
|
delete player;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->mPlayers.insert(address, player);
|
||||||
|
QObject::connect(player, &MprisPlayer::ready, this, &MprisWatcher::onPlayerReady);
|
||||||
|
QObject::connect(player, &QObject::destroyed, this, &MprisWatcher::onPlayerDestroyed);
|
||||||
|
|
||||||
|
qCDebug(logMprisWatcher) << "Registered MprisPlayer" << address;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace qs::service::mpris
|
53
src/services/mpris/watcher.hpp
Normal file
53
src/services/mpris/watcher.hpp
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <qdbuscontext.h>
|
||||||
|
#include <qdbusinterface.h>
|
||||||
|
#include <qdbusservicewatcher.h>
|
||||||
|
#include <qhash.h>
|
||||||
|
#include <qlist.h>
|
||||||
|
#include <qloggingcategory.h>
|
||||||
|
#include <qobject.h>
|
||||||
|
#include <qqmlintegration.h>
|
||||||
|
#include <qqmllist.h>
|
||||||
|
#include <qtmetamacros.h>
|
||||||
|
#include <qtypes.h>
|
||||||
|
|
||||||
|
#include "player.hpp"
|
||||||
|
|
||||||
|
namespace qs::service::mpris {
|
||||||
|
|
||||||
|
///! Provides access to MprisPlayers.
|
||||||
|
class MprisWatcher: public QObject {
|
||||||
|
Q_OBJECT;
|
||||||
|
QML_NAMED_ELEMENT(Mpris);
|
||||||
|
QML_SINGLETON;
|
||||||
|
/// All connected MPRIS players.
|
||||||
|
Q_PROPERTY(QQmlListProperty<MprisPlayer> players READ players NOTIFY playersChanged);
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit MprisWatcher(QObject* parent = nullptr);
|
||||||
|
|
||||||
|
[[nodiscard]] QQmlListProperty<MprisPlayer> players();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void playersChanged();
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onServiceRegistered(const QString& service);
|
||||||
|
void onServiceUnregistered(const QString& service);
|
||||||
|
void onPlayerReady();
|
||||||
|
void onPlayerDestroyed(QObject* object);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static qsizetype playersCount(QQmlListProperty<MprisPlayer>* property);
|
||||||
|
static MprisPlayer* playerAt(QQmlListProperty<MprisPlayer>* property, qsizetype index);
|
||||||
|
|
||||||
|
void registerExisting();
|
||||||
|
void registerPlayer(const QString& address);
|
||||||
|
|
||||||
|
QDBusServiceWatcher serviceWatcher;
|
||||||
|
QHash<QString, MprisPlayer*> mPlayers;
|
||||||
|
QList<MprisPlayer*> readyPlayers;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace qs::service::mpris
|
|
@ -54,14 +54,14 @@ public:
|
||||||
dbus::DBusProperty<QString> status {this->properties, "Status"};
|
dbus::DBusProperty<QString> status {this->properties, "Status"};
|
||||||
dbus::DBusProperty<QString> category {this->properties, "Category"};
|
dbus::DBusProperty<QString> category {this->properties, "Category"};
|
||||||
dbus::DBusProperty<quint32> windowId {this->properties, "WindowId"};
|
dbus::DBusProperty<quint32> windowId {this->properties, "WindowId"};
|
||||||
dbus::DBusProperty<QString> iconThemePath {this->properties, "IconThemePath"};
|
dbus::DBusProperty<QString> iconThemePath {this->properties, "IconThemePath", "", false};
|
||||||
dbus::DBusProperty<QString> iconName {this->properties, "IconName"};
|
dbus::DBusProperty<QString> iconName {this->properties, "IconName", "", false}; // IconPixmap may be set
|
||||||
dbus::DBusProperty<DBusSniIconPixmapList> iconPixmaps {this->properties, "IconPixmap"};
|
dbus::DBusProperty<DBusSniIconPixmapList> iconPixmaps {this->properties, "IconPixmap", {}, false}; // IconName may be set
|
||||||
dbus::DBusProperty<QString> overlayIconName {this->properties, "OverlayIconName"};
|
dbus::DBusProperty<QString> overlayIconName {this->properties, "OverlayIconName"};
|
||||||
dbus::DBusProperty<DBusSniIconPixmapList> overlayIconPixmaps {this->properties, "OverlayIconPixmap"};
|
dbus::DBusProperty<DBusSniIconPixmapList> overlayIconPixmaps {this->properties, "OverlayIconPixmap"};
|
||||||
dbus::DBusProperty<QString> attentionIconName {this->properties, "AttentionIconName"};
|
dbus::DBusProperty<QString> attentionIconName {this->properties, "AttentionIconName"};
|
||||||
dbus::DBusProperty<DBusSniIconPixmapList> attentionIconPixmaps {this->properties, "AttentionIconPixmap"};
|
dbus::DBusProperty<DBusSniIconPixmapList> attentionIconPixmaps {this->properties, "AttentionIconPixmap"};
|
||||||
dbus::DBusProperty<QString> attentionMovieName {this->properties, "AttentionMovieName"};
|
dbus::DBusProperty<QString> attentionMovieName {this->properties, "AttentionMovieName", "", false};
|
||||||
dbus::DBusProperty<DBusSniTooltip> tooltip {this->properties, "ToolTip"};
|
dbus::DBusProperty<DBusSniTooltip> tooltip {this->properties, "ToolTip"};
|
||||||
dbus::DBusProperty<bool> isMenu {this->properties, "ItemIsMenu"};
|
dbus::DBusProperty<bool> isMenu {this->properties, "ItemIsMenu"};
|
||||||
dbus::DBusProperty<QDBusObjectPath> menuPath {this->properties, "Menu"};
|
dbus::DBusProperty<QDBusObjectPath> menuPath {this->properties, "Menu"};
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
#include <qguiapplication.h>
|
#include <qguiapplication.h>
|
||||||
|
#include <qlogging.h>
|
||||||
#include <qqml.h>
|
#include <qqml.h>
|
||||||
|
#include <qtenvironmentvariables.h>
|
||||||
|
|
||||||
#include "../core/plugin.hpp"
|
#include "../core/plugin.hpp"
|
||||||
|
|
||||||
|
@ -10,7 +12,19 @@
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
class WaylandPlugin: public QuickshellPlugin {
|
class WaylandPlugin: public QuickshellPlugin {
|
||||||
bool applies() override { return QGuiApplication::platformName() == "wayland"; }
|
bool applies() override {
|
||||||
|
auto isWayland = QGuiApplication::platformName() == "wayland";
|
||||||
|
|
||||||
|
if (!isWayland && !qEnvironmentVariable("WAYLAND_DISPLAY").isEmpty()) {
|
||||||
|
qWarning() << "--- WARNING ---";
|
||||||
|
qWarning() << "WAYLAND_DISPLAY is present but QT_QPA_PLATFORM is"
|
||||||
|
<< QGuiApplication::platformName();
|
||||||
|
qWarning() << "If you are actually running wayland, set QT_QPA_PLATFORM to \"wayland\" or "
|
||||||
|
"most functionality will be broken.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return isWayland;
|
||||||
|
}
|
||||||
|
|
||||||
void registerTypes() override {
|
void registerTypes() override {
|
||||||
#ifdef QS_WAYLAND_WLR_LAYERSHELL
|
#ifdef QS_WAYLAND_WLR_LAYERSHELL
|
||||||
|
|
|
@ -114,6 +114,18 @@ void WlrLayershell::setAnchors(Anchors anchors) {
|
||||||
if (!anchors.verticalConstraint()) this->ProxyWindowBase::setHeight(this->mHeight);
|
if (!anchors.verticalConstraint()) this->ProxyWindowBase::setHeight(this->mHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool WlrLayershell::aboveWindows() const { return this->layer() > WlrLayer::Bottom; }
|
||||||
|
|
||||||
|
void WlrLayershell::setAboveWindows(bool aboveWindows) {
|
||||||
|
this->setLayer(aboveWindows ? WlrLayer::Top : WlrLayer::Bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WlrLayershell::focusable() const { return this->keyboardFocus() != WlrKeyboardFocus::None; }
|
||||||
|
|
||||||
|
void WlrLayershell::setFocusable(bool focusable) {
|
||||||
|
this->setKeyboardFocus(focusable ? WlrKeyboardFocus::OnDemand : WlrKeyboardFocus::None);
|
||||||
|
}
|
||||||
|
|
||||||
QString WlrLayershell::ns() const { return this->ext->ns(); }
|
QString WlrLayershell::ns() const { return this->ext->ns(); }
|
||||||
|
|
||||||
void WlrLayershell::setNamespace(QString ns) {
|
void WlrLayershell::setNamespace(QString ns) {
|
||||||
|
@ -190,6 +202,8 @@ WaylandPanelInterface::WaylandPanelInterface(QObject* parent)
|
||||||
QObject::connect(this->layer, &WlrLayershell::marginsChanged, this, &WaylandPanelInterface::marginsChanged);
|
QObject::connect(this->layer, &WlrLayershell::marginsChanged, this, &WaylandPanelInterface::marginsChanged);
|
||||||
QObject::connect(this->layer, &WlrLayershell::exclusiveZoneChanged, this, &WaylandPanelInterface::exclusiveZoneChanged);
|
QObject::connect(this->layer, &WlrLayershell::exclusiveZoneChanged, this, &WaylandPanelInterface::exclusiveZoneChanged);
|
||||||
QObject::connect(this->layer, &WlrLayershell::exclusionModeChanged, this, &WaylandPanelInterface::exclusionModeChanged);
|
QObject::connect(this->layer, &WlrLayershell::exclusionModeChanged, this, &WaylandPanelInterface::exclusionModeChanged);
|
||||||
|
QObject::connect(this->layer, &WlrLayershell::layerChanged, this, &WaylandPanelInterface::aboveWindowsChanged);
|
||||||
|
QObject::connect(this->layer, &WlrLayershell::keyboardFocusChanged, this, &WaylandPanelInterface::focusableChanged);
|
||||||
// clang-format on
|
// clang-format on
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -224,6 +238,8 @@ proxyPair(Anchors, anchors, setAnchors);
|
||||||
proxyPair(Margins, margins, setMargins);
|
proxyPair(Margins, margins, setMargins);
|
||||||
proxyPair(qint32, exclusiveZone, setExclusiveZone);
|
proxyPair(qint32, exclusiveZone, setExclusiveZone);
|
||||||
proxyPair(ExclusionMode::Enum, exclusionMode, setExclusionMode);
|
proxyPair(ExclusionMode::Enum, exclusionMode, setExclusionMode);
|
||||||
|
proxyPair(bool, focusable, setFocusable);
|
||||||
|
proxyPair(bool, aboveWindows, setAboveWindows);
|
||||||
|
|
||||||
#undef proxyPair
|
#undef proxyPair
|
||||||
// NOLINTEND
|
// NOLINTEND
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
#include <qtypes.h>
|
#include <qtypes.h>
|
||||||
|
|
||||||
#include "../core/doc.hpp"
|
#include "../core/doc.hpp"
|
||||||
|
#include "../core/panelinterface.hpp"
|
||||||
#include "../core/proxywindow.hpp"
|
#include "../core/proxywindow.hpp"
|
||||||
#include "wlr_layershell/window.hpp"
|
#include "wlr_layershell/window.hpp"
|
||||||
|
|
||||||
|
@ -54,6 +55,8 @@ class WlrLayershell: public ProxyWindowBase {
|
||||||
QSDOC_HIDE Q_PROPERTY(qint32 exclusiveZone READ exclusiveZone WRITE setExclusiveZone NOTIFY exclusiveZoneChanged);
|
QSDOC_HIDE Q_PROPERTY(qint32 exclusiveZone READ exclusiveZone WRITE setExclusiveZone NOTIFY exclusiveZoneChanged);
|
||||||
QSDOC_HIDE Q_PROPERTY(ExclusionMode::Enum exclusionMode READ exclusionMode WRITE setExclusionMode NOTIFY exclusionModeChanged);
|
QSDOC_HIDE Q_PROPERTY(ExclusionMode::Enum exclusionMode READ exclusionMode WRITE setExclusionMode NOTIFY exclusionModeChanged);
|
||||||
QSDOC_HIDE Q_PROPERTY(Margins margins READ margins WRITE setMargins NOTIFY marginsChanged);
|
QSDOC_HIDE Q_PROPERTY(Margins margins READ margins WRITE setMargins NOTIFY marginsChanged);
|
||||||
|
QSDOC_HIDE Q_PROPERTY(bool aboveWindows READ aboveWindows WRITE setAboveWindows NOTIFY layerChanged);
|
||||||
|
QSDOC_HIDE Q_PROPERTY(bool focusable READ focusable WRITE setFocusable NOTIFY keyboardFocusChanged);
|
||||||
QML_ATTACHED(WlrLayershell);
|
QML_ATTACHED(WlrLayershell);
|
||||||
QML_ELEMENT;
|
QML_ELEMENT;
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
@ -92,6 +95,12 @@ public:
|
||||||
[[nodiscard]] Margins margins() const;
|
[[nodiscard]] Margins margins() const;
|
||||||
void setMargins(Margins margins); // NOLINT
|
void setMargins(Margins margins); // NOLINT
|
||||||
|
|
||||||
|
[[nodiscard]] bool aboveWindows() const;
|
||||||
|
void setAboveWindows(bool aboveWindows);
|
||||||
|
|
||||||
|
[[nodiscard]] bool focusable() const;
|
||||||
|
void setFocusable(bool focusable);
|
||||||
|
|
||||||
static WlrLayershell* qmlAttachedProperties(QObject* object);
|
static WlrLayershell* qmlAttachedProperties(QObject* object);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
|
@ -161,6 +170,12 @@ public:
|
||||||
|
|
||||||
[[nodiscard]] ExclusionMode::Enum exclusionMode() const override;
|
[[nodiscard]] ExclusionMode::Enum exclusionMode() const override;
|
||||||
void setExclusionMode(ExclusionMode::Enum exclusionMode) override;
|
void setExclusionMode(ExclusionMode::Enum exclusionMode) override;
|
||||||
|
|
||||||
|
[[nodiscard]] bool aboveWindows() const override;
|
||||||
|
void setAboveWindows(bool aboveWindows) override;
|
||||||
|
|
||||||
|
[[nodiscard]] bool focusable() const override;
|
||||||
|
void setFocusable(bool focusable) override;
|
||||||
// NOLINTEND
|
// NOLINTEND
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
22
src/x11/CMakeLists.txt
Normal file
22
src/x11/CMakeLists.txt
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
find_package(XCB REQUIRED COMPONENTS XCB)
|
||||||
|
|
||||||
|
qt_add_library(quickshell-x11 STATIC
|
||||||
|
util.cpp
|
||||||
|
panel_window.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
qt_add_qml_module(quickshell-x11
|
||||||
|
URI Quickshell.X11
|
||||||
|
VERSION 0.1
|
||||||
|
)
|
||||||
|
|
||||||
|
add_library(quickshell-x11-init OBJECT init.cpp)
|
||||||
|
|
||||||
|
target_link_libraries(quickshell-x11 PRIVATE ${QT_DEPS} ${XCB_LIBRARIES})
|
||||||
|
target_link_libraries(quickshell-x11-init PRIVATE ${QT_DEPS} ${XCB_LIBRARIES})
|
||||||
|
|
||||||
|
qs_pch(quickshell-x11)
|
||||||
|
qs_pch(quickshell-x11plugin)
|
||||||
|
qs_pch(quickshell-x11-init)
|
||||||
|
|
||||||
|
target_link_libraries(quickshell PRIVATE quickshell-x11plugin quickshell-x11-init)
|
29
src/x11/init.cpp
Normal file
29
src/x11/init.cpp
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
#include <qguiapplication.h>
|
||||||
|
#include <qqml.h>
|
||||||
|
|
||||||
|
#include "../core/plugin.hpp"
|
||||||
|
#include "panel_window.hpp"
|
||||||
|
#include "util.hpp"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
class X11Plugin: public QuickshellPlugin {
|
||||||
|
bool applies() override { return QGuiApplication::platformName() == "xcb"; }
|
||||||
|
|
||||||
|
void init() override { XAtom::initAtoms(); }
|
||||||
|
|
||||||
|
void registerTypes() override {
|
||||||
|
qmlRegisterType<XPanelInterface>("Quickshell._X11Overlay", 1, 0, "PanelWindow");
|
||||||
|
|
||||||
|
qmlRegisterModuleImport(
|
||||||
|
"Quickshell",
|
||||||
|
QQmlModuleImportModuleAny,
|
||||||
|
"Quickshell._X11Overlay",
|
||||||
|
QQmlModuleImportLatest
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
QS_REGISTER_PLUGIN(X11Plugin);
|
||||||
|
|
||||||
|
} // namespace
|
431
src/x11/panel_window.cpp
Normal file
431
src/x11/panel_window.cpp
Normal file
|
@ -0,0 +1,431 @@
|
||||||
|
#include "panel_window.hpp"
|
||||||
|
#include <array>
|
||||||
|
#include <map>
|
||||||
|
|
||||||
|
#include <qevent.h>
|
||||||
|
#include <qlist.h>
|
||||||
|
#include <qnamespace.h>
|
||||||
|
#include <qobject.h>
|
||||||
|
#include <qqmlengine.h>
|
||||||
|
#include <qqmllist.h>
|
||||||
|
#include <qquickwindow.h>
|
||||||
|
#include <qscreen.h>
|
||||||
|
#include <qtmetamacros.h>
|
||||||
|
#include <qtypes.h>
|
||||||
|
#include <xcb/xproto.h>
|
||||||
|
|
||||||
|
#include "../core/generation.hpp"
|
||||||
|
#include "../core/panelinterface.hpp"
|
||||||
|
#include "../core/proxywindow.hpp"
|
||||||
|
#include "util.hpp"
|
||||||
|
|
||||||
|
class XPanelStack {
|
||||||
|
public:
|
||||||
|
static XPanelStack* instance() {
|
||||||
|
static XPanelStack* stack = nullptr; // NOLINT
|
||||||
|
|
||||||
|
if (stack == nullptr) {
|
||||||
|
stack = new XPanelStack();
|
||||||
|
}
|
||||||
|
|
||||||
|
return stack;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] const QList<XPanelWindow*>& panels(XPanelWindow* panel) {
|
||||||
|
return this->mPanels[EngineGeneration::findObjectGeneration(panel)];
|
||||||
|
}
|
||||||
|
|
||||||
|
void addPanel(XPanelWindow* panel) {
|
||||||
|
auto& panels = this->mPanels[EngineGeneration::findObjectGeneration(panel)];
|
||||||
|
if (!panels.contains(panel)) {
|
||||||
|
panels.push_back(panel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void removePanel(XPanelWindow* panel) {
|
||||||
|
auto& panels = this->mPanels[EngineGeneration::findObjectGeneration(panel)];
|
||||||
|
if (panels.removeOne(panel)) {
|
||||||
|
if (panels.isEmpty()) {
|
||||||
|
this->mPanels.erase(EngineGeneration::findObjectGeneration(panel));
|
||||||
|
}
|
||||||
|
|
||||||
|
// from the bottom up, update all panels
|
||||||
|
for (auto* panel: panels) {
|
||||||
|
panel->updateDimensions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::map<EngineGeneration*, QList<XPanelWindow*>> mPanels;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool XPanelEventFilter::eventFilter(QObject* watched, QEvent* event) {
|
||||||
|
if (event->type() == QEvent::PlatformSurface) {
|
||||||
|
auto* surfaceEvent = static_cast<QPlatformSurfaceEvent*>(event); // NOLINT
|
||||||
|
|
||||||
|
if (surfaceEvent->surfaceEventType() == QPlatformSurfaceEvent::SurfaceCreated) {
|
||||||
|
emit this->surfaceCreated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this->QObject::eventFilter(watched, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
XPanelWindow::XPanelWindow(QObject* parent): ProxyWindowBase(parent) {
|
||||||
|
QObject::connect(
|
||||||
|
&this->eventFilter,
|
||||||
|
&XPanelEventFilter::surfaceCreated,
|
||||||
|
this,
|
||||||
|
&XPanelWindow::xInit
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
XPanelWindow::~XPanelWindow() { XPanelStack::instance()->removePanel(this); }
|
||||||
|
|
||||||
|
void XPanelWindow::connectWindow() {
|
||||||
|
this->ProxyWindowBase::connectWindow();
|
||||||
|
|
||||||
|
this->window->installEventFilter(&this->eventFilter);
|
||||||
|
this->connectScreen();
|
||||||
|
// clang-format off
|
||||||
|
QObject::connect(this->window, &QQuickWindow::screenChanged, this, &XPanelWindow::connectScreen);
|
||||||
|
QObject::connect(this->window, &QQuickWindow::visibleChanged, this, &XPanelWindow::updatePanelStack);
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
|
// qt overwrites _NET_WM_STATE, so we have to use the qt api
|
||||||
|
// QXcbWindow::WindowType::Dock in qplatformwindow_p.h
|
||||||
|
// see QXcbWindow::setWindowFlags in qxcbwindow.cpp
|
||||||
|
this->window->setProperty("_q_xcb_wm_window_type", 0x000004);
|
||||||
|
|
||||||
|
// at least one flag needs to change for the above property to apply
|
||||||
|
this->window->setFlag(Qt::FramelessWindowHint);
|
||||||
|
this->updateAboveWindows();
|
||||||
|
this->updateFocusable();
|
||||||
|
|
||||||
|
if (this->window->handle() != nullptr) {
|
||||||
|
this->xInit();
|
||||||
|
this->updatePanelStack();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void XPanelWindow::setWidth(qint32 width) {
|
||||||
|
this->mWidth = width;
|
||||||
|
|
||||||
|
// only update the actual size if not blocked by anchors
|
||||||
|
if (!this->mAnchors.horizontalConstraint()) {
|
||||||
|
this->ProxyWindowBase::setWidth(width);
|
||||||
|
this->updateDimensions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void XPanelWindow::setHeight(qint32 height) {
|
||||||
|
this->mHeight = height;
|
||||||
|
|
||||||
|
// only update the actual size if not blocked by anchors
|
||||||
|
if (!this->mAnchors.verticalConstraint()) {
|
||||||
|
this->ProxyWindowBase::setHeight(height);
|
||||||
|
this->updateDimensions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Anchors XPanelWindow::anchors() const { return this->mAnchors; }
|
||||||
|
|
||||||
|
void XPanelWindow::setAnchors(Anchors anchors) {
|
||||||
|
if (this->mAnchors == anchors) return;
|
||||||
|
this->mAnchors = anchors;
|
||||||
|
this->updateDimensions();
|
||||||
|
emit this->anchorsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
qint32 XPanelWindow::exclusiveZone() const { return this->mExclusiveZone; }
|
||||||
|
|
||||||
|
void XPanelWindow::setExclusiveZone(qint32 exclusiveZone) {
|
||||||
|
if (this->mExclusiveZone == exclusiveZone) return;
|
||||||
|
this->mExclusiveZone = exclusiveZone;
|
||||||
|
const bool wasNormal = this->mExclusionMode == ExclusionMode::Normal;
|
||||||
|
this->setExclusionMode(ExclusionMode::Normal);
|
||||||
|
if (wasNormal) this->updateStrut();
|
||||||
|
emit this->exclusiveZoneChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
ExclusionMode::Enum XPanelWindow::exclusionMode() const { return this->mExclusionMode; }
|
||||||
|
|
||||||
|
void XPanelWindow::setExclusionMode(ExclusionMode::Enum exclusionMode) {
|
||||||
|
if (this->mExclusionMode == exclusionMode) return;
|
||||||
|
this->mExclusionMode = exclusionMode;
|
||||||
|
this->updateStrut();
|
||||||
|
emit this->exclusionModeChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
Margins XPanelWindow::margins() const { return this->mMargins; }
|
||||||
|
|
||||||
|
void XPanelWindow::setMargins(Margins margins) {
|
||||||
|
if (this->mMargins == margins) return;
|
||||||
|
this->mMargins = margins;
|
||||||
|
this->updateDimensions();
|
||||||
|
emit this->marginsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool XPanelWindow::aboveWindows() const { return this->mAboveWindows; }
|
||||||
|
|
||||||
|
void XPanelWindow::setAboveWindows(bool aboveWindows) {
|
||||||
|
if (this->mAboveWindows == aboveWindows) return;
|
||||||
|
this->mAboveWindows = aboveWindows;
|
||||||
|
this->updateAboveWindows();
|
||||||
|
emit this->aboveWindowsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool XPanelWindow::focusable() const { return this->mFocusable; }
|
||||||
|
|
||||||
|
void XPanelWindow::setFocusable(bool focusable) {
|
||||||
|
if (this->mFocusable == focusable) return;
|
||||||
|
this->mFocusable = focusable;
|
||||||
|
this->updateFocusable();
|
||||||
|
emit this->focusableChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void XPanelWindow::xInit() { this->updateDimensions(); }
|
||||||
|
|
||||||
|
void XPanelWindow::connectScreen() {
|
||||||
|
if (this->mTrackedScreen != nullptr) {
|
||||||
|
QObject::disconnect(this->mTrackedScreen, nullptr, this, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
this->mTrackedScreen = this->window->screen();
|
||||||
|
|
||||||
|
if (this->mTrackedScreen != nullptr) {
|
||||||
|
QObject::connect(
|
||||||
|
this->mTrackedScreen,
|
||||||
|
&QScreen::geometryChanged,
|
||||||
|
this,
|
||||||
|
&XPanelWindow::updateDimensions
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void XPanelWindow::updateDimensions() {
|
||||||
|
if (this->window == nullptr || this->window->handle() == nullptr) return;
|
||||||
|
|
||||||
|
auto screenGeometry = this->window->screen()->virtualGeometry();
|
||||||
|
|
||||||
|
if (this->mExclusionMode != ExclusionMode::Ignore) {
|
||||||
|
for (auto* panel: XPanelStack::instance()->panels(this)) {
|
||||||
|
// we only care about windows below us
|
||||||
|
if (panel == this) break;
|
||||||
|
|
||||||
|
int side = -1;
|
||||||
|
quint32 exclusiveZone = 0;
|
||||||
|
panel->getExclusion(side, exclusiveZone);
|
||||||
|
|
||||||
|
if (exclusiveZone == 0) continue;
|
||||||
|
|
||||||
|
auto zone = static_cast<qint32>(exclusiveZone);
|
||||||
|
|
||||||
|
screenGeometry.adjust(
|
||||||
|
side == 0 ? zone : 0,
|
||||||
|
side == 2 ? zone : 0,
|
||||||
|
side == 1 ? -zone : 0,
|
||||||
|
side == 3 ? -zone : 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto geometry = QRect();
|
||||||
|
|
||||||
|
if (this->mAnchors.horizontalConstraint()) {
|
||||||
|
geometry.setX(screenGeometry.x() + this->mMargins.mLeft);
|
||||||
|
geometry.setWidth(screenGeometry.width() - this->mMargins.mLeft - this->mMargins.mRight);
|
||||||
|
} else {
|
||||||
|
if (this->mAnchors.mLeft) {
|
||||||
|
geometry.setX(screenGeometry.x() + this->mMargins.mLeft);
|
||||||
|
} else if (this->mAnchors.mRight) {
|
||||||
|
geometry.setX(
|
||||||
|
screenGeometry.x() + screenGeometry.width() - this->mWidth - this->mMargins.mRight
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
geometry.setX(screenGeometry.x() + screenGeometry.width() / 2 - this->mWidth / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
geometry.setWidth(this->mWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->mAnchors.verticalConstraint()) {
|
||||||
|
geometry.setY(screenGeometry.y() + this->mMargins.mTop);
|
||||||
|
geometry.setHeight(screenGeometry.height() - this->mMargins.mTop - this->mMargins.mBottom);
|
||||||
|
} else {
|
||||||
|
if (this->mAnchors.mTop) {
|
||||||
|
geometry.setY(screenGeometry.y() + this->mMargins.mTop);
|
||||||
|
} else if (this->mAnchors.mBottom) {
|
||||||
|
geometry.setY(
|
||||||
|
screenGeometry.y() + screenGeometry.height() - this->mHeight - this->mMargins.mBottom
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
geometry.setY(screenGeometry.y() + screenGeometry.height() / 2 - this->mHeight / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
geometry.setHeight(this->mHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
this->window->setGeometry(geometry);
|
||||||
|
this->updateStrut();
|
||||||
|
}
|
||||||
|
|
||||||
|
void XPanelWindow::updatePanelStack() {
|
||||||
|
if (this->window->isVisible()) {
|
||||||
|
XPanelStack::instance()->addPanel(this);
|
||||||
|
} else {
|
||||||
|
XPanelStack::instance()->removePanel(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void XPanelWindow::getExclusion(int& side, quint32& exclusiveZone) {
|
||||||
|
if (this->mExclusionMode == ExclusionMode::Ignore) return;
|
||||||
|
|
||||||
|
auto& anchors = this->mAnchors;
|
||||||
|
if (anchors.mLeft || anchors.mRight || anchors.mTop || anchors.mBottom) {
|
||||||
|
if (!anchors.horizontalConstraint()
|
||||||
|
&& (anchors.verticalConstraint() || (!anchors.mTop && !anchors.mBottom)))
|
||||||
|
{
|
||||||
|
side = anchors.mLeft ? 0 : anchors.mRight ? 1 : -1;
|
||||||
|
} else if (!anchors.verticalConstraint()
|
||||||
|
&& (anchors.horizontalConstraint() || (!anchors.mLeft && !anchors.mRight)))
|
||||||
|
{
|
||||||
|
side = anchors.mTop ? 2 : anchors.mBottom ? 3 : -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (side == -1) return;
|
||||||
|
|
||||||
|
auto autoExclude = this->mExclusionMode == ExclusionMode::Auto;
|
||||||
|
|
||||||
|
if (autoExclude) {
|
||||||
|
if (side == 0 || side == 1) {
|
||||||
|
exclusiveZone = this->mWidth + (side == 0 ? this->mMargins.mLeft : this->mMargins.mRight);
|
||||||
|
} else {
|
||||||
|
exclusiveZone = this->mHeight + (side == 2 ? this->mMargins.mTop : this->mMargins.mBottom);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
exclusiveZone = this->mExclusiveZone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void XPanelWindow::updateStrut() {
|
||||||
|
if (this->window == nullptr || this->window->handle() == nullptr) return;
|
||||||
|
auto* conn = x11Connection();
|
||||||
|
|
||||||
|
int side = -1;
|
||||||
|
quint32 exclusiveZone = 0;
|
||||||
|
|
||||||
|
this->getExclusion(side, exclusiveZone);
|
||||||
|
|
||||||
|
if (side == -1 || this->mExclusionMode == ExclusionMode::Ignore) {
|
||||||
|
xcb_delete_property(conn, this->window->winId(), XAtom::_NET_WM_STRUT.atom());
|
||||||
|
xcb_delete_property(conn, this->window->winId(), XAtom::_NET_WM_STRUT_PARTIAL.atom());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto data = std::array<quint32, 12>();
|
||||||
|
data[side] = exclusiveZone;
|
||||||
|
|
||||||
|
// https://specifications.freedesktop.org/wm-spec/wm-spec-latest.html#idm45573693101552
|
||||||
|
// assuming "specified in root window coordinates" means relative to the window geometry
|
||||||
|
// in which case only the end position should be set, to the opposite extent.
|
||||||
|
data[side * 2 + 5] = side == 0 || side == 1 ? this->window->height() : this->window->width();
|
||||||
|
|
||||||
|
xcb_change_property(
|
||||||
|
conn,
|
||||||
|
XCB_PROP_MODE_REPLACE,
|
||||||
|
this->window->winId(),
|
||||||
|
XAtom::_NET_WM_STRUT.atom(),
|
||||||
|
XCB_ATOM_CARDINAL,
|
||||||
|
32,
|
||||||
|
4,
|
||||||
|
data.data()
|
||||||
|
);
|
||||||
|
|
||||||
|
xcb_change_property(
|
||||||
|
conn,
|
||||||
|
XCB_PROP_MODE_REPLACE,
|
||||||
|
this->window->winId(),
|
||||||
|
XAtom::_NET_WM_STRUT_PARTIAL.atom(),
|
||||||
|
XCB_ATOM_CARDINAL,
|
||||||
|
32,
|
||||||
|
12,
|
||||||
|
data.data()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void XPanelWindow::updateAboveWindows() {
|
||||||
|
if (this->window == nullptr) return;
|
||||||
|
|
||||||
|
this->window->setFlag(Qt::WindowStaysOnBottomHint, !this->mAboveWindows);
|
||||||
|
this->window->setFlag(Qt::WindowStaysOnTopHint, this->mAboveWindows);
|
||||||
|
}
|
||||||
|
|
||||||
|
void XPanelWindow::updateFocusable() {
|
||||||
|
if (this->window == nullptr) return;
|
||||||
|
this->window->setFlag(Qt::WindowDoesNotAcceptFocus, !this->mFocusable);
|
||||||
|
}
|
||||||
|
|
||||||
|
// XPanelInterface
|
||||||
|
|
||||||
|
XPanelInterface::XPanelInterface(QObject* parent)
|
||||||
|
: PanelWindowInterface(parent)
|
||||||
|
, panel(new XPanelWindow(this)) {
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
QObject::connect(this->panel, &ProxyWindowBase::windowConnected, this, &XPanelInterface::windowConnected);
|
||||||
|
QObject::connect(this->panel, &ProxyWindowBase::visibleChanged, this, &XPanelInterface::visibleChanged);
|
||||||
|
QObject::connect(this->panel, &ProxyWindowBase::backerVisibilityChanged, this, &XPanelInterface::backingWindowVisibleChanged);
|
||||||
|
QObject::connect(this->panel, &ProxyWindowBase::heightChanged, this, &XPanelInterface::heightChanged);
|
||||||
|
QObject::connect(this->panel, &ProxyWindowBase::widthChanged, this, &XPanelInterface::widthChanged);
|
||||||
|
QObject::connect(this->panel, &ProxyWindowBase::screenChanged, this, &XPanelInterface::screenChanged);
|
||||||
|
QObject::connect(this->panel, &ProxyWindowBase::windowTransformChanged, this, &XPanelInterface::windowTransformChanged);
|
||||||
|
QObject::connect(this->panel, &ProxyWindowBase::colorChanged, this, &XPanelInterface::colorChanged);
|
||||||
|
QObject::connect(this->panel, &ProxyWindowBase::maskChanged, this, &XPanelInterface::maskChanged);
|
||||||
|
|
||||||
|
// panel specific
|
||||||
|
QObject::connect(this->panel, &XPanelWindow::anchorsChanged, this, &XPanelInterface::anchorsChanged);
|
||||||
|
QObject::connect(this->panel, &XPanelWindow::marginsChanged, this, &XPanelInterface::marginsChanged);
|
||||||
|
QObject::connect(this->panel, &XPanelWindow::exclusiveZoneChanged, this, &XPanelInterface::exclusiveZoneChanged);
|
||||||
|
QObject::connect(this->panel, &XPanelWindow::exclusionModeChanged, this, &XPanelInterface::exclusionModeChanged);
|
||||||
|
QObject::connect(this->panel, &XPanelWindow::aboveWindowsChanged, this, &XPanelInterface::aboveWindowsChanged);
|
||||||
|
QObject::connect(this->panel, &XPanelWindow::focusableChanged, this, &XPanelInterface::focusableChanged);
|
||||||
|
// clang-format on
|
||||||
|
}
|
||||||
|
|
||||||
|
void XPanelInterface::onReload(QObject* oldInstance) {
|
||||||
|
QQmlEngine::setContextForObject(this->panel, QQmlEngine::contextForObject(this));
|
||||||
|
|
||||||
|
auto* old = qobject_cast<XPanelInterface*>(oldInstance);
|
||||||
|
this->panel->reload(old != nullptr ? old->panel : nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
QQmlListProperty<QObject> XPanelInterface::data() { return this->panel->data(); }
|
||||||
|
ProxyWindowBase* XPanelInterface::proxyWindow() const { return this->panel; }
|
||||||
|
QQuickItem* XPanelInterface::contentItem() const { return this->panel->contentItem(); }
|
||||||
|
bool XPanelInterface::isBackingWindowVisible() const { return this->panel->isVisibleDirect(); }
|
||||||
|
|
||||||
|
// NOLINTBEGIN
|
||||||
|
#define proxyPair(type, get, set) \
|
||||||
|
type XPanelInterface::get() const { return this->panel->get(); } \
|
||||||
|
void XPanelInterface::set(type value) { this->panel->set(value); }
|
||||||
|
|
||||||
|
proxyPair(bool, isVisible, setVisible);
|
||||||
|
proxyPair(qint32, width, setWidth);
|
||||||
|
proxyPair(qint32, height, setHeight);
|
||||||
|
proxyPair(QuickshellScreenInfo*, screen, setScreen);
|
||||||
|
proxyPair(QColor, color, setColor);
|
||||||
|
proxyPair(PendingRegion*, mask, setMask);
|
||||||
|
|
||||||
|
// panel specific
|
||||||
|
proxyPair(Anchors, anchors, setAnchors);
|
||||||
|
proxyPair(Margins, margins, setMargins);
|
||||||
|
proxyPair(qint32, exclusiveZone, setExclusiveZone);
|
||||||
|
proxyPair(ExclusionMode::Enum, exclusionMode, setExclusionMode);
|
||||||
|
proxyPair(bool, focusable, setFocusable);
|
||||||
|
proxyPair(bool, aboveWindows, setAboveWindows);
|
||||||
|
|
||||||
|
#undef proxyPair
|
||||||
|
// NOLINTEND
|
160
src/x11/panel_window.hpp
Normal file
160
src/x11/panel_window.hpp
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <qobject.h>
|
||||||
|
#include <qqmlintegration.h>
|
||||||
|
#include <qquickwindow.h>
|
||||||
|
#include <qscreen.h>
|
||||||
|
#include <qtclasshelpermacros.h>
|
||||||
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
|
#include "../core/doc.hpp"
|
||||||
|
#include "../core/panelinterface.hpp"
|
||||||
|
#include "../core/proxywindow.hpp"
|
||||||
|
|
||||||
|
class XPanelStack;
|
||||||
|
|
||||||
|
class XPanelEventFilter: public QObject {
|
||||||
|
Q_OBJECT;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit XPanelEventFilter(QObject* parent = nullptr): QObject(parent) {}
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void surfaceCreated();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool eventFilter(QObject* watched, QEvent* event) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
class XPanelWindow: public ProxyWindowBase {
|
||||||
|
QSDOC_BASECLASS(PanelWindowInterface);
|
||||||
|
Q_OBJECT;
|
||||||
|
// clang-format off
|
||||||
|
QSDOC_HIDE Q_PROPERTY(Anchors anchors READ anchors WRITE setAnchors NOTIFY anchorsChanged);
|
||||||
|
QSDOC_HIDE Q_PROPERTY(qint32 exclusiveZone READ exclusiveZone WRITE setExclusiveZone NOTIFY exclusiveZoneChanged);
|
||||||
|
QSDOC_HIDE Q_PROPERTY(ExclusionMode::Enum exclusionMode READ exclusionMode WRITE setExclusionMode NOTIFY exclusionModeChanged);
|
||||||
|
QSDOC_HIDE Q_PROPERTY(Margins margins READ margins WRITE setMargins NOTIFY marginsChanged);
|
||||||
|
QSDOC_HIDE Q_PROPERTY(bool aboveWindows READ aboveWindows WRITE setAboveWindows NOTIFY aboveWindowsChanged);
|
||||||
|
QSDOC_HIDE Q_PROPERTY(bool focusable READ focusable WRITE setFocusable NOTIFY focusableChanged);
|
||||||
|
// clang-format on
|
||||||
|
QML_ELEMENT;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit XPanelWindow(QObject* parent = nullptr);
|
||||||
|
~XPanelWindow() override;
|
||||||
|
Q_DISABLE_COPY_MOVE(XPanelWindow);
|
||||||
|
|
||||||
|
void connectWindow() override;
|
||||||
|
|
||||||
|
void setWidth(qint32 width) override;
|
||||||
|
void setHeight(qint32 height) override;
|
||||||
|
|
||||||
|
[[nodiscard]] Anchors anchors() const;
|
||||||
|
void setAnchors(Anchors anchors);
|
||||||
|
|
||||||
|
[[nodiscard]] qint32 exclusiveZone() const;
|
||||||
|
void setExclusiveZone(qint32 exclusiveZone);
|
||||||
|
|
||||||
|
[[nodiscard]] ExclusionMode::Enum exclusionMode() const;
|
||||||
|
void setExclusionMode(ExclusionMode::Enum exclusionMode);
|
||||||
|
|
||||||
|
[[nodiscard]] Margins margins() const;
|
||||||
|
void setMargins(Margins margins);
|
||||||
|
|
||||||
|
[[nodiscard]] bool aboveWindows() const;
|
||||||
|
void setAboveWindows(bool aboveWindows);
|
||||||
|
|
||||||
|
[[nodiscard]] bool focusable() const;
|
||||||
|
void setFocusable(bool focusable);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
QSDOC_HIDE void anchorsChanged();
|
||||||
|
QSDOC_HIDE void exclusiveZoneChanged();
|
||||||
|
QSDOC_HIDE void exclusionModeChanged();
|
||||||
|
QSDOC_HIDE void marginsChanged();
|
||||||
|
QSDOC_HIDE void aboveWindowsChanged();
|
||||||
|
QSDOC_HIDE void focusableChanged();
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void xInit();
|
||||||
|
void connectScreen();
|
||||||
|
void updateDimensions();
|
||||||
|
void updatePanelStack();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void getExclusion(int& side, quint32& exclusiveZone);
|
||||||
|
void updateStrut();
|
||||||
|
void updateAboveWindows();
|
||||||
|
void updateFocusable();
|
||||||
|
|
||||||
|
QPointer<QScreen> mTrackedScreen = nullptr;
|
||||||
|
bool mAboveWindows = true;
|
||||||
|
bool mFocusable = false;
|
||||||
|
Anchors mAnchors;
|
||||||
|
Margins mMargins;
|
||||||
|
qint32 mExclusiveZone = 0;
|
||||||
|
ExclusionMode::Enum mExclusionMode = ExclusionMode::Auto;
|
||||||
|
XPanelEventFilter eventFilter;
|
||||||
|
|
||||||
|
friend class XPanelStack;
|
||||||
|
};
|
||||||
|
|
||||||
|
class XPanelInterface: public PanelWindowInterface {
|
||||||
|
Q_OBJECT;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit XPanelInterface(QObject* parent = nullptr);
|
||||||
|
|
||||||
|
void onReload(QObject* oldInstance) override;
|
||||||
|
|
||||||
|
[[nodiscard]] ProxyWindowBase* proxyWindow() const override;
|
||||||
|
[[nodiscard]] QQuickItem* contentItem() const override;
|
||||||
|
|
||||||
|
// NOLINTBEGIN
|
||||||
|
[[nodiscard]] bool isVisible() const override;
|
||||||
|
[[nodiscard]] bool isBackingWindowVisible() const override;
|
||||||
|
void setVisible(bool visible) override;
|
||||||
|
|
||||||
|
[[nodiscard]] qint32 width() const override;
|
||||||
|
void setWidth(qint32 width) override;
|
||||||
|
|
||||||
|
[[nodiscard]] qint32 height() const override;
|
||||||
|
void setHeight(qint32 height) override;
|
||||||
|
|
||||||
|
[[nodiscard]] QuickshellScreenInfo* screen() const override;
|
||||||
|
void setScreen(QuickshellScreenInfo* screen) override;
|
||||||
|
|
||||||
|
[[nodiscard]] QColor color() const override;
|
||||||
|
void setColor(QColor color) override;
|
||||||
|
|
||||||
|
[[nodiscard]] PendingRegion* mask() const override;
|
||||||
|
void setMask(PendingRegion* mask) override;
|
||||||
|
|
||||||
|
[[nodiscard]] QQmlListProperty<QObject> data() override;
|
||||||
|
|
||||||
|
// panel specific
|
||||||
|
|
||||||
|
[[nodiscard]] Anchors anchors() const override;
|
||||||
|
void setAnchors(Anchors anchors) override;
|
||||||
|
|
||||||
|
[[nodiscard]] Margins margins() const override;
|
||||||
|
void setMargins(Margins margins) override;
|
||||||
|
|
||||||
|
[[nodiscard]] qint32 exclusiveZone() const override;
|
||||||
|
void setExclusiveZone(qint32 exclusiveZone) override;
|
||||||
|
|
||||||
|
[[nodiscard]] ExclusionMode::Enum exclusionMode() const override;
|
||||||
|
void setExclusionMode(ExclusionMode::Enum exclusionMode) override;
|
||||||
|
|
||||||
|
[[nodiscard]] bool aboveWindows() const override;
|
||||||
|
void setAboveWindows(bool aboveWindows) override;
|
||||||
|
|
||||||
|
[[nodiscard]] bool focusable() const override;
|
||||||
|
void setFocusable(bool focusable) override;
|
||||||
|
// NOLINTEND
|
||||||
|
|
||||||
|
private:
|
||||||
|
XPanelWindow* panel;
|
||||||
|
|
||||||
|
friend class WlrLayershell;
|
||||||
|
};
|
55
src/x11/util.cpp
Normal file
55
src/x11/util.cpp
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
#include "util.hpp"
|
||||||
|
|
||||||
|
#include <qbytearray.h>
|
||||||
|
#include <qguiapplication.h>
|
||||||
|
#include <qguiapplication_platform.h>
|
||||||
|
#include <xcb/xcb.h>
|
||||||
|
#include <xcb/xproto.h>
|
||||||
|
|
||||||
|
xcb_connection_t* x11Connection() {
|
||||||
|
static xcb_connection_t* conn = nullptr; // NOLINT
|
||||||
|
|
||||||
|
if (conn == nullptr) {
|
||||||
|
if (auto* x11Application = dynamic_cast<QGuiApplication*>(QGuiApplication::instance())
|
||||||
|
->nativeInterface<QNativeInterface::QX11Application>())
|
||||||
|
{
|
||||||
|
conn = x11Application->connection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOLINTBEGIN
|
||||||
|
XAtom XAtom::_NET_WM_STRUT {};
|
||||||
|
XAtom XAtom::_NET_WM_STRUT_PARTIAL {};
|
||||||
|
// NOLINTEND
|
||||||
|
|
||||||
|
void XAtom::initAtoms() {
|
||||||
|
_NET_WM_STRUT.init("_NET_WM_STRUT");
|
||||||
|
_NET_WM_STRUT_PARTIAL.init("_NET_WM_STRUT_PARTIAL");
|
||||||
|
}
|
||||||
|
|
||||||
|
void XAtom::init(const QByteArray& name) {
|
||||||
|
this->cookie = xcb_intern_atom(x11Connection(), 0, name.length(), name.data());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool XAtom::isValid() {
|
||||||
|
this->resolve();
|
||||||
|
return this->mAtom != XCB_ATOM_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
const xcb_atom_t& XAtom::atom() {
|
||||||
|
this->resolve();
|
||||||
|
return this->mAtom;
|
||||||
|
}
|
||||||
|
|
||||||
|
void XAtom::resolve() {
|
||||||
|
if (!this->resolved) {
|
||||||
|
this->resolved = true;
|
||||||
|
|
||||||
|
auto* reply = xcb_intern_atom_reply(x11Connection(), this->cookie, nullptr);
|
||||||
|
if (reply != nullptr) this->mAtom = reply->atom;
|
||||||
|
free(reply); // NOLINT
|
||||||
|
}
|
||||||
|
}
|
29
src/x11/util.hpp
Normal file
29
src/x11/util.hpp
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <qbytearray.h>
|
||||||
|
#include <qtclasshelpermacros.h>
|
||||||
|
#include <xcb/xcb.h>
|
||||||
|
#include <xcb/xproto.h>
|
||||||
|
|
||||||
|
xcb_connection_t* x11Connection();
|
||||||
|
|
||||||
|
class XAtom {
|
||||||
|
public:
|
||||||
|
[[nodiscard]] bool isValid();
|
||||||
|
[[nodiscard]] const xcb_atom_t& atom();
|
||||||
|
|
||||||
|
// NOLINTBEGIN
|
||||||
|
static XAtom _NET_WM_STRUT;
|
||||||
|
static XAtom _NET_WM_STRUT_PARTIAL;
|
||||||
|
// NOLINTEND
|
||||||
|
|
||||||
|
static void initAtoms();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void init(const QByteArray& name);
|
||||||
|
void resolve();
|
||||||
|
|
||||||
|
bool resolved = false;
|
||||||
|
xcb_atom_t mAtom = XCB_ATOM_NONE;
|
||||||
|
xcb_intern_atom_cookie_t cookie {};
|
||||||
|
};
|
Loading…
Reference in a new issue