Compare commits
	
		
			9 commits
		
	
	
		
			70c5cf1e16
			...
			42ea70e04c
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 42ea70e04c | |||
| cdd5729642 | |||
| a255889e5c | |||
| b05d98b56d | |||
| 0b529c6682 | |||
| cbdfba1a3f | |||
| 4eac0b40c3 | |||
| 48bdcf4db2 | |||
| 1fa87b7c5a | 
					 22 changed files with 1040 additions and 27 deletions
				
			
		| 
						 | 
				
			
			@ -134,8 +134,10 @@ void ProxyWindowBase::setScreen(QuickshellScreenInfo* screen) {
 | 
			
		|||
		QObject::connect(qscreen, &QObject::destroyed, this, &ProxyWindowBase::onScreenDestroyed);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (this->window == nullptr) this->mScreen = qscreen;
 | 
			
		||||
	else this->window->setScreen(qscreen);
 | 
			
		||||
	if (this->window == nullptr) {
 | 
			
		||||
		this->mScreen = qscreen;
 | 
			
		||||
		emit this->screenChanged();
 | 
			
		||||
	} else this->window->setScreen(qscreen);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ProxyWindowBase::onScreenDestroyed() { this->mScreen = nullptr; }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,7 +22,7 @@ message(STATUS "Found wayland-scanner at ${waylandscanner}")
 | 
			
		|||
 | 
			
		||||
execute_process(
 | 
			
		||||
	COMMAND pkg-config --variable=pkgdatadir wayland-protocols
 | 
			
		||||
	OUTPUT_VARIABLE WAYLAND_PROTOCOLS_DIR
 | 
			
		||||
	OUTPUT_VARIABLE WAYLAND_PROTOCOLS
 | 
			
		||||
	OUTPUT_STRIP_TRAILING_WHITESPACE
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -46,6 +46,7 @@ endfunction()
 | 
			
		|||
 | 
			
		||||
qt_add_library(quickshell-wayland STATIC
 | 
			
		||||
	wlr_layershell.cpp
 | 
			
		||||
	session_lock.cpp
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
qt_add_qml_module(quickshell-wayland URI Quickshell.Wayland)
 | 
			
		||||
| 
						 | 
				
			
			@ -54,6 +55,7 @@ qt_add_qml_module(quickshell-wayland URI Quickshell.Wayland)
 | 
			
		|||
add_library(quickshell-wayland-init OBJECT init.cpp)
 | 
			
		||||
 | 
			
		||||
add_subdirectory(wlr_layershell)
 | 
			
		||||
add_subdirectory(session_lock)
 | 
			
		||||
target_link_libraries(quickshell-wayland PRIVATE ${QT_DEPS})
 | 
			
		||||
target_link_libraries(quickshell-wayland-init PRIVATE ${QT_DEPS})
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,5 +3,6 @@ description = "Wayland specific Quickshell types"
 | 
			
		|||
headers = [
 | 
			
		||||
	"wlr_layershell/window.hpp",
 | 
			
		||||
	"wlr_layershell.hpp",
 | 
			
		||||
	"session_lock.hpp",
 | 
			
		||||
]
 | 
			
		||||
-----
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										282
									
								
								src/wayland/session_lock.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										282
									
								
								src/wayland/session_lock.cpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,282 @@
 | 
			
		|||
#include "session_lock.hpp"
 | 
			
		||||
 | 
			
		||||
#include <qcolor.h>
 | 
			
		||||
#include <qcoreapplication.h>
 | 
			
		||||
#include <qguiapplication.h>
 | 
			
		||||
#include <qlogging.h>
 | 
			
		||||
#include <qobject.h>
 | 
			
		||||
#include <qqmlcomponent.h>
 | 
			
		||||
#include <qqmlengine.h>
 | 
			
		||||
#include <qqmllist.h>
 | 
			
		||||
#include <qquickitem.h>
 | 
			
		||||
#include <qquickwindow.h>
 | 
			
		||||
#include <qscreen.h>
 | 
			
		||||
#include <qtmetamacros.h>
 | 
			
		||||
#include <qtypes.h>
 | 
			
		||||
 | 
			
		||||
#include "../core/qmlscreen.hpp"
 | 
			
		||||
#include "../core/reload.hpp"
 | 
			
		||||
#include "session_lock/session_lock.hpp"
 | 
			
		||||
 | 
			
		||||
void SessionLock::onReload(QObject* oldInstance) {
 | 
			
		||||
	auto* old = qobject_cast<SessionLock*>(oldInstance);
 | 
			
		||||
 | 
			
		||||
	if (old != nullptr) {
 | 
			
		||||
		QObject::disconnect(old->manager, nullptr, old, nullptr);
 | 
			
		||||
		this->manager = old->manager;
 | 
			
		||||
		this->manager->setParent(this);
 | 
			
		||||
	} else {
 | 
			
		||||
		this->manager = new SessionLockManager(this);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// clang-format off
 | 
			
		||||
	QObject::connect(this->manager, &SessionLockManager::locked, this, &SessionLock::secureStateChanged);
 | 
			
		||||
	QObject::connect(this->manager, &SessionLockManager::unlocked, this, &SessionLock::secureStateChanged);
 | 
			
		||||
 | 
			
		||||
	QObject::connect(this->manager, &SessionLockManager::unlocked, this, &SessionLock::unlock);
 | 
			
		||||
 | 
			
		||||
	auto* app = QCoreApplication::instance();
 | 
			
		||||
	auto* guiApp = qobject_cast<QGuiApplication*>(app);
 | 
			
		||||
 | 
			
		||||
	if (guiApp != nullptr) {
 | 
			
		||||
		QObject::connect(guiApp, &QGuiApplication::primaryScreenChanged, this, &SessionLock::onScreensChanged);
 | 
			
		||||
		QObject::connect(guiApp, &QGuiApplication::screenAdded, this, &SessionLock::onScreensChanged);
 | 
			
		||||
		QObject::connect(guiApp, &QGuiApplication::screenRemoved, this, &SessionLock::onScreensChanged);
 | 
			
		||||
	}
 | 
			
		||||
	// clang-format on
 | 
			
		||||
 | 
			
		||||
	if (this->lockTarget) {
 | 
			
		||||
		if (!this->manager->lock()) this->lockTarget = false;
 | 
			
		||||
		this->updateSurfaces(old);
 | 
			
		||||
	} else {
 | 
			
		||||
		this->setLocked(false);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void SessionLock::updateSurfaces(SessionLock* old) {
 | 
			
		||||
	if (this->manager->isLocked()) {
 | 
			
		||||
		auto screens = QGuiApplication::screens();
 | 
			
		||||
 | 
			
		||||
		auto map = this->surfaces.toStdMap();
 | 
			
		||||
		for (auto& [screen, surface]: map) {
 | 
			
		||||
			if (!screens.contains(screen)) {
 | 
			
		||||
				this->surfaces.remove(screen);
 | 
			
		||||
				surface->deleteLater();
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (this->mSurfaceComponent == nullptr) {
 | 
			
		||||
			qWarning() << "SessionLock.surface is null. Aborting lock.";
 | 
			
		||||
			this->unlock();
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for (auto* screen: screens) {
 | 
			
		||||
			if (!this->surfaces.contains(screen)) {
 | 
			
		||||
				auto* instanceObj = this->mSurfaceComponent->create(QQmlEngine::contextForObject(this));
 | 
			
		||||
				auto* instance = qobject_cast<SessionLockSurface*>(instanceObj);
 | 
			
		||||
 | 
			
		||||
				if (instance == nullptr) {
 | 
			
		||||
					qWarning() << "SessionLock.surface does not create a SessionLockSurface. Aborting lock.";
 | 
			
		||||
					this->unlock();
 | 
			
		||||
					return;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				instance->setParent(this);
 | 
			
		||||
				instance->setScreen(screen);
 | 
			
		||||
 | 
			
		||||
				auto* oldInstance = old == nullptr ? nullptr : old->surfaces.value(screen, nullptr);
 | 
			
		||||
				instance->onReload(oldInstance);
 | 
			
		||||
 | 
			
		||||
				this->surfaces[screen] = instance;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			for (auto* surface: this->surfaces.values()) {
 | 
			
		||||
				surface->show();
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void SessionLock::unlock() {
 | 
			
		||||
	if (this->isLocked()) {
 | 
			
		||||
		this->lockTarget = false;
 | 
			
		||||
		this->manager->unlock();
 | 
			
		||||
 | 
			
		||||
		for (auto* surface: this->surfaces) {
 | 
			
		||||
			surface->deleteLater();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		this->surfaces.clear();
 | 
			
		||||
 | 
			
		||||
		emit this->lockStateChanged();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void SessionLock::onScreensChanged() { this->updateSurfaces(); }
 | 
			
		||||
 | 
			
		||||
bool SessionLock::isLocked() const {
 | 
			
		||||
	return this->manager == nullptr ? this->lockTarget : this->manager->isLocked();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool SessionLock::isSecure() const {
 | 
			
		||||
	return this->manager != nullptr && SessionLockManager::isSecure();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void SessionLock::setLocked(bool locked) {
 | 
			
		||||
	if (this->isLocked() == locked) return;
 | 
			
		||||
	this->lockTarget = locked;
 | 
			
		||||
 | 
			
		||||
	if (this->manager == nullptr) {
 | 
			
		||||
		emit this->lockStateChanged();
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (locked) {
 | 
			
		||||
		if (!this->manager->lock()) this->lockTarget = false;
 | 
			
		||||
		this->updateSurfaces();
 | 
			
		||||
		if (this->lockTarget) emit this->lockStateChanged();
 | 
			
		||||
	} else {
 | 
			
		||||
		this->unlock(); // emits lockStateChanged
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
QQmlComponent* SessionLock::surfaceComponent() const { return this->mSurfaceComponent; }
 | 
			
		||||
 | 
			
		||||
void SessionLock::setSurfaceComponent(QQmlComponent* surfaceComponent) {
 | 
			
		||||
	if (this->mSurfaceComponent != nullptr) this->mSurfaceComponent->deleteLater();
 | 
			
		||||
	if (surfaceComponent != nullptr) surfaceComponent->setParent(this);
 | 
			
		||||
 | 
			
		||||
	this->mSurfaceComponent = surfaceComponent;
 | 
			
		||||
	emit this->surfaceComponentChanged();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
SessionLockSurface::SessionLockSurface(QObject* parent)
 | 
			
		||||
    : Reloadable(parent)
 | 
			
		||||
    , mContentItem(new QQuickItem())
 | 
			
		||||
    , ext(new LockWindowExtension(this)) {
 | 
			
		||||
	this->mContentItem->setParent(this);
 | 
			
		||||
 | 
			
		||||
	// clang-format off
 | 
			
		||||
	QObject::connect(this, &SessionLockSurface::widthChanged, this, &SessionLockSurface::onWidthChanged);
 | 
			
		||||
	QObject::connect(this, &SessionLockSurface::heightChanged, this, &SessionLockSurface::onHeightChanged);
 | 
			
		||||
	// clang-format on
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
SessionLockSurface::~SessionLockSurface() {
 | 
			
		||||
	if (this->window != nullptr) {
 | 
			
		||||
		this->window->deleteLater();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void SessionLockSurface::onReload(QObject* oldInstance) {
 | 
			
		||||
	if (auto* old = qobject_cast<SessionLockSurface*>(oldInstance)) {
 | 
			
		||||
		this->window = old->disownWindow();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (this->window == nullptr) {
 | 
			
		||||
		this->window = new QQuickWindow();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this->mContentItem->setParentItem(this->window->contentItem());
 | 
			
		||||
 | 
			
		||||
	this->mContentItem->setWidth(this->width());
 | 
			
		||||
	this->mContentItem->setHeight(this->height());
 | 
			
		||||
 | 
			
		||||
	if (this->mScreen != nullptr) this->window->setScreen(this->mScreen);
 | 
			
		||||
	this->window->setColor(this->mColor);
 | 
			
		||||
 | 
			
		||||
	// clang-format off
 | 
			
		||||
	QObject::connect(this->window, &QWindow::visibilityChanged, this, &SessionLockSurface::visibleChanged);
 | 
			
		||||
	QObject::connect(this->window, &QWindow::widthChanged, this, &SessionLockSurface::widthChanged);
 | 
			
		||||
	QObject::connect(this->window, &QWindow::heightChanged, this, &SessionLockSurface::heightChanged);
 | 
			
		||||
	QObject::connect(this->window, &QWindow::screenChanged, this, &SessionLockSurface::screenChanged);
 | 
			
		||||
	QObject::connect(this->window, &QQuickWindow::colorChanged, this, &SessionLockSurface::colorChanged);
 | 
			
		||||
	// clang-format on
 | 
			
		||||
 | 
			
		||||
	if (auto* parent = qobject_cast<SessionLock*>(this->parent())) {
 | 
			
		||||
		if (!this->ext->attach(this->window, parent->manager)) {
 | 
			
		||||
			qWarning(
 | 
			
		||||
			) << "Failed to attach LockWindowExtension to window. Surface will not behave correctly.";
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		qWarning(
 | 
			
		||||
		) << "SessionLockSurface parent is not a SessionLock. Surface will not behave correctly.";
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
QQuickWindow* SessionLockSurface::disownWindow() {
 | 
			
		||||
	QObject::disconnect(this->window, nullptr, this, nullptr);
 | 
			
		||||
	this->mContentItem->setParentItem(nullptr);
 | 
			
		||||
 | 
			
		||||
	auto* window = this->window;
 | 
			
		||||
	this->window = nullptr;
 | 
			
		||||
	return window;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void SessionLockSurface::show() { this->ext->setVisible(); }
 | 
			
		||||
 | 
			
		||||
QQuickItem* SessionLockSurface::contentItem() const { return this->mContentItem; }
 | 
			
		||||
 | 
			
		||||
bool SessionLockSurface::isVisible() const { return this->window->isVisible(); }
 | 
			
		||||
 | 
			
		||||
qint32 SessionLockSurface::width() const {
 | 
			
		||||
	if (this->window == nullptr) return 0;
 | 
			
		||||
	else return this->window->width();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
qint32 SessionLockSurface::height() const {
 | 
			
		||||
	if (this->window == nullptr) return 0;
 | 
			
		||||
	else return this->window->height();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
QuickshellScreenInfo* SessionLockSurface::screen() const {
 | 
			
		||||
	QScreen* qscreen = nullptr;
 | 
			
		||||
 | 
			
		||||
	if (this->window == nullptr) {
 | 
			
		||||
		if (this->mScreen != nullptr) qscreen = this->mScreen;
 | 
			
		||||
	} else {
 | 
			
		||||
		qscreen = this->window->screen();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return new QuickshellScreenInfo(
 | 
			
		||||
	    const_cast<SessionLockSurface*>(this), // NOLINT
 | 
			
		||||
	    qscreen
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void SessionLockSurface::setScreen(QScreen* qscreen) {
 | 
			
		||||
	if (this->mScreen != nullptr) {
 | 
			
		||||
		QObject::disconnect(this->mScreen, nullptr, this, nullptr);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (qscreen != nullptr) {
 | 
			
		||||
		QObject::connect(qscreen, &QObject::destroyed, this, &SessionLockSurface::onScreenDestroyed);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (this->window == nullptr) {
 | 
			
		||||
		this->mScreen = qscreen;
 | 
			
		||||
		emit this->screenChanged();
 | 
			
		||||
	} else this->window->setScreen(qscreen);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void SessionLockSurface::onScreenDestroyed() { this->mScreen = nullptr; }
 | 
			
		||||
 | 
			
		||||
QColor SessionLockSurface::color() const {
 | 
			
		||||
	if (this->window == nullptr) return this->mColor;
 | 
			
		||||
	else return this->window->color();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void SessionLockSurface::setColor(QColor color) {
 | 
			
		||||
	if (this->window == nullptr) {
 | 
			
		||||
		this->mColor = color;
 | 
			
		||||
		emit this->colorChanged();
 | 
			
		||||
	} else this->window->setColor(color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
QQmlListProperty<QObject> SessionLockSurface::data() {
 | 
			
		||||
	return this->mContentItem->property("data").value<QQmlListProperty<QObject>>();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void SessionLockSurface::onWidthChanged() { this->mContentItem->setWidth(this->width()); }
 | 
			
		||||
void SessionLockSurface::onHeightChanged() { this->mContentItem->setHeight(this->height()); }
 | 
			
		||||
							
								
								
									
										191
									
								
								src/wayland/session_lock.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										191
									
								
								src/wayland/session_lock.hpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,191 @@
 | 
			
		|||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <qcolor.h>
 | 
			
		||||
#include <qcontainerfwd.h>
 | 
			
		||||
#include <qguiapplication.h>
 | 
			
		||||
#include <qmap.h>
 | 
			
		||||
#include <qnamespace.h>
 | 
			
		||||
#include <qobject.h>
 | 
			
		||||
#include <qqmlcomponent.h>
 | 
			
		||||
#include <qqmlintegration.h>
 | 
			
		||||
#include <qquickitem.h>
 | 
			
		||||
#include <qquickwindow.h>
 | 
			
		||||
#include <qscreen.h>
 | 
			
		||||
#include <qtclasshelpermacros.h>
 | 
			
		||||
#include <qtmetamacros.h>
 | 
			
		||||
#include <qtypes.h>
 | 
			
		||||
 | 
			
		||||
#include "../core/qmlscreen.hpp"
 | 
			
		||||
#include "../core/reload.hpp"
 | 
			
		||||
#include "session_lock/session_lock.hpp"
 | 
			
		||||
 | 
			
		||||
class SessionLockSurface;
 | 
			
		||||
 | 
			
		||||
///! Wayland session locker.
 | 
			
		||||
/// Wayland session lock implemented using the [ext_session_lock_v1] protocol.
 | 
			
		||||
///
 | 
			
		||||
/// SessionLock will create an instance of its `surface` component for every screen when
 | 
			
		||||
/// `locked` is set to true. The `surface` component must create a [SessionLockSurface]
 | 
			
		||||
/// which will be displayed on each screen.
 | 
			
		||||
///
 | 
			
		||||
/// The below example will create a session lock that disappears when the button is clicked.
 | 
			
		||||
/// ```qml
 | 
			
		||||
/// SessionLock {
 | 
			
		||||
///   id: lock
 | 
			
		||||
///
 | 
			
		||||
///   SessionLockSurface {
 | 
			
		||||
///     Button {
 | 
			
		||||
///       text: "unlock me"
 | 
			
		||||
///       onClicked: lock.locked = false
 | 
			
		||||
///     }
 | 
			
		||||
///   }
 | 
			
		||||
/// }
 | 
			
		||||
///
 | 
			
		||||
/// // ...
 | 
			
		||||
/// lock.locked = true
 | 
			
		||||
/// ```
 | 
			
		||||
///
 | 
			
		||||
/// > [!WARNING] If the SessionLock is destroyed or quickshell exits without setting `locked`
 | 
			
		||||
/// > to false, conformant compositors will leave the screen locked and painted with a solid
 | 
			
		||||
/// > color.
 | 
			
		||||
/// >
 | 
			
		||||
/// > This is what makes the session lock secure. The lock dying will not expose your session,
 | 
			
		||||
/// > but it will render it inoperable.
 | 
			
		||||
///
 | 
			
		||||
/// [ext_session_lock_v1]: https://wayland.app/protocols/ext-session-lock-v1
 | 
			
		||||
/// [SessionLockSurface]: ../sessionlocksurface
 | 
			
		||||
class SessionLock: public Reloadable {
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
	// clang-format off
 | 
			
		||||
	/// Controls the lock state.
 | 
			
		||||
	///
 | 
			
		||||
	/// > [!WARNING] Only one SessionLock may be locked at a time. Attempting to enable a lock while
 | 
			
		||||
	/// > another lock is enabled will do nothing.
 | 
			
		||||
	Q_PROPERTY(bool locked READ isLocked WRITE setLocked NOTIFY lockStateChanged);
 | 
			
		||||
	/// The compositor lock state.
 | 
			
		||||
	///
 | 
			
		||||
	/// This is set to true once the compositor has confirmed all screens are covered with locks.
 | 
			
		||||
	Q_PROPERTY(bool secure READ isSecure NOTIFY secureStateChanged);
 | 
			
		||||
	/// The surface that will be created for each screen. Must create a [SessionLockSurface].
 | 
			
		||||
	///
 | 
			
		||||
	/// [SessionLockSurface]: ../sessionlocksurface
 | 
			
		||||
	Q_PROPERTY(QQmlComponent* surface READ surfaceComponent WRITE setSurfaceComponent NOTIFY surfaceComponentChanged);
 | 
			
		||||
	// clang-format on
 | 
			
		||||
	QML_ELEMENT;
 | 
			
		||||
	Q_CLASSINFO("DefaultProperty", "surface");
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	explicit SessionLock(QObject* parent = nullptr): Reloadable(parent) {}
 | 
			
		||||
 | 
			
		||||
	void onReload(QObject* oldInstance) override;
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] bool isLocked() const;
 | 
			
		||||
	void setLocked(bool locked);
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] bool isSecure() const;
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] QQmlComponent* surfaceComponent() const;
 | 
			
		||||
	void setSurfaceComponent(QQmlComponent* surfaceComponent);
 | 
			
		||||
 | 
			
		||||
signals:
 | 
			
		||||
	void lockStateChanged();
 | 
			
		||||
	void secureStateChanged();
 | 
			
		||||
	void surfaceComponentChanged();
 | 
			
		||||
 | 
			
		||||
private slots:
 | 
			
		||||
	void unlock();
 | 
			
		||||
	void onScreensChanged();
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	void updateSurfaces(SessionLock* old = nullptr);
 | 
			
		||||
 | 
			
		||||
	SessionLockManager* manager = nullptr;
 | 
			
		||||
	QQmlComponent* mSurfaceComponent = nullptr;
 | 
			
		||||
	QMap<QScreen*, SessionLockSurface*> surfaces;
 | 
			
		||||
	bool lockTarget = false;
 | 
			
		||||
 | 
			
		||||
	friend class SessionLockSurface;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
///! Surface to display with a `SessionLock`.
 | 
			
		||||
/// Surface displayed by a [SessionLock] when it is locked.
 | 
			
		||||
///
 | 
			
		||||
/// [SessionLock]: ../sessionlock
 | 
			
		||||
class SessionLockSurface: public Reloadable {
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
	// clang-format off
 | 
			
		||||
	Q_PROPERTY(QQuickItem* contentItem READ contentItem);
 | 
			
		||||
	/// If the surface has been made visible.
 | 
			
		||||
	///
 | 
			
		||||
	/// Note: SessionLockSurfaces will never become invisible, they will only be destroyed.
 | 
			
		||||
	Q_PROPERTY(bool visible READ isVisible NOTIFY visibleChanged);
 | 
			
		||||
	Q_PROPERTY(qint32 width READ width NOTIFY widthChanged);
 | 
			
		||||
	Q_PROPERTY(qint32 height READ height NOTIFY heightChanged);
 | 
			
		||||
	/// The screen that the surface is displayed on.
 | 
			
		||||
	Q_PROPERTY(QuickshellScreenInfo* screen READ screen NOTIFY screenChanged);
 | 
			
		||||
	/// The background color of the window. Defaults to white.
 | 
			
		||||
	///
 | 
			
		||||
	/// > [!WARNING] This seems to behave weirdly when using transparent colors on some systems.
 | 
			
		||||
	/// > Using a colored content item over a transparent window is the recommended way to work around this:
 | 
			
		||||
	/// > ```qml
 | 
			
		||||
	/// > ProxyWindow {
 | 
			
		||||
	/// >   Rectangle {
 | 
			
		||||
	/// >     anchors.fill: parent
 | 
			
		||||
	/// >     color: "#20ffffff"
 | 
			
		||||
	/// >
 | 
			
		||||
	/// >     // your content here
 | 
			
		||||
	/// >   }
 | 
			
		||||
	/// > }
 | 
			
		||||
	/// > ```
 | 
			
		||||
	/// > ... but you probably shouldn't make a transparent lock,
 | 
			
		||||
	/// > and most compositors will ignore an attempt to do so.
 | 
			
		||||
	Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged);
 | 
			
		||||
	Q_PROPERTY(QQmlListProperty<QObject> data READ data);
 | 
			
		||||
	// clang-format on
 | 
			
		||||
	QML_ELEMENT;
 | 
			
		||||
	Q_CLASSINFO("DefaultProperty", "data");
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	explicit SessionLockSurface(QObject* parent = nullptr);
 | 
			
		||||
	~SessionLockSurface() override;
 | 
			
		||||
	Q_DISABLE_COPY_MOVE(SessionLockSurface);
 | 
			
		||||
 | 
			
		||||
	void onReload(QObject* oldInstance) override;
 | 
			
		||||
	QQuickWindow* disownWindow();
 | 
			
		||||
 | 
			
		||||
	void show();
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] QQuickItem* contentItem() const;
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] bool isVisible() const;
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] qint32 width() const;
 | 
			
		||||
	[[nodiscard]] qint32 height() const;
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] QuickshellScreenInfo* screen() const;
 | 
			
		||||
	void setScreen(QScreen* qscreen);
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] QColor color() const;
 | 
			
		||||
	void setColor(QColor color);
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] QQmlListProperty<QObject> data();
 | 
			
		||||
 | 
			
		||||
signals:
 | 
			
		||||
	void visibleChanged();
 | 
			
		||||
	void widthChanged();
 | 
			
		||||
	void heightChanged();
 | 
			
		||||
	void screenChanged();
 | 
			
		||||
	void colorChanged();
 | 
			
		||||
 | 
			
		||||
private slots:
 | 
			
		||||
	void onScreenDestroyed();
 | 
			
		||||
	void onWidthChanged();
 | 
			
		||||
	void onHeightChanged();
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	QQuickWindow* window = nullptr;
 | 
			
		||||
	QQuickItem* mContentItem;
 | 
			
		||||
	QScreen* mScreen = nullptr;
 | 
			
		||||
	QColor mColor = Qt::white;
 | 
			
		||||
	LockWindowExtension* ext;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										12
									
								
								src/wayland/session_lock/CMakeLists.txt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/wayland/session_lock/CMakeLists.txt
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,12 @@
 | 
			
		|||
qt_add_library(quickshell-wayland-sessionlock STATIC
 | 
			
		||||
	manager.cpp
 | 
			
		||||
	surface.cpp
 | 
			
		||||
	lock.cpp
 | 
			
		||||
	shell_integration.cpp
 | 
			
		||||
	session_lock.cpp
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
wl_proto(quickshell-wayland-sessionlock ext-session-lock-v1 "${WAYLAND_PROTOCOLS}/staging/ext-session-lock/ext-session-lock-v1.xml")
 | 
			
		||||
target_link_libraries(quickshell-wayland-sessionlock PRIVATE ${QT_DEPS} wayland-client)
 | 
			
		||||
 | 
			
		||||
target_link_libraries(quickshell-wayland PRIVATE quickshell-wayland-sessionlock)
 | 
			
		||||
							
								
								
									
										48
									
								
								src/wayland/session_lock/lock.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/wayland/session_lock/lock.cpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,48 @@
 | 
			
		|||
#include "lock.hpp"
 | 
			
		||||
 | 
			
		||||
#include <qtmetamacros.h>
 | 
			
		||||
#include <wayland-ext-session-lock-v1-client-protocol.h>
 | 
			
		||||
 | 
			
		||||
#include "manager.hpp"
 | 
			
		||||
 | 
			
		||||
QSWaylandSessionLock::QSWaylandSessionLock(
 | 
			
		||||
    QSWaylandSessionLockManager* manager,
 | 
			
		||||
    ::ext_session_lock_v1* lock
 | 
			
		||||
)
 | 
			
		||||
    : manager(manager) {
 | 
			
		||||
	this->init(lock); // if isInitialized is false that means we already unlocked.
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
QSWaylandSessionLock::~QSWaylandSessionLock() {
 | 
			
		||||
	if (this->isInitialized()) {
 | 
			
		||||
		// This will intentionally lock the session if the lock is destroyed without calling unlock.
 | 
			
		||||
		this->destroy();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void QSWaylandSessionLock::unlock() {
 | 
			
		||||
	if (this->isInitialized()) {
 | 
			
		||||
		if (this->finished) this->destroy();
 | 
			
		||||
		else this->unlock_and_destroy();
 | 
			
		||||
 | 
			
		||||
		this->secure = false;
 | 
			
		||||
		this->manager->active = nullptr;
 | 
			
		||||
 | 
			
		||||
		emit this->unlocked();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool QSWaylandSessionLock::active() const { return this->isInitialized(); }
 | 
			
		||||
 | 
			
		||||
bool QSWaylandSessionLock::hasCompositorLock() const { return this->secure; }
 | 
			
		||||
 | 
			
		||||
void QSWaylandSessionLock::ext_session_lock_v1_locked() {
 | 
			
		||||
	this->secure = true;
 | 
			
		||||
	emit this->compositorLocked();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void QSWaylandSessionLock::ext_session_lock_v1_finished() {
 | 
			
		||||
	this->secure = false;
 | 
			
		||||
	this->finished = true;
 | 
			
		||||
	this->unlock();
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										39
									
								
								src/wayland/session_lock/lock.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/wayland/session_lock/lock.hpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,39 @@
 | 
			
		|||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <qobject.h>
 | 
			
		||||
#include <qtclasshelpermacros.h>
 | 
			
		||||
#include <qwayland-ext-session-lock-v1.h>
 | 
			
		||||
 | 
			
		||||
class QSWaylandSessionLockManager;
 | 
			
		||||
 | 
			
		||||
class QSWaylandSessionLock
 | 
			
		||||
    : public QObject
 | 
			
		||||
    , public QtWayland::ext_session_lock_v1 {
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	QSWaylandSessionLock(QSWaylandSessionLockManager* manager, ::ext_session_lock_v1* lock);
 | 
			
		||||
	~QSWaylandSessionLock() override;
 | 
			
		||||
	Q_DISABLE_COPY_MOVE(QSWaylandSessionLock);
 | 
			
		||||
 | 
			
		||||
	void unlock();
 | 
			
		||||
 | 
			
		||||
	// Returns true if the lock has not finished.
 | 
			
		||||
	[[nodiscard]] bool active() const;
 | 
			
		||||
	// Returns true if the compositor considers the session to be locked.
 | 
			
		||||
	[[nodiscard]] bool hasCompositorLock() const;
 | 
			
		||||
 | 
			
		||||
signals:
 | 
			
		||||
	void compositorLocked();
 | 
			
		||||
	void unlocked();
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	void ext_session_lock_v1_locked() override;
 | 
			
		||||
	void ext_session_lock_v1_finished() override;
 | 
			
		||||
 | 
			
		||||
	QSWaylandSessionLockManager* manager; // static and not dealloc'd
 | 
			
		||||
 | 
			
		||||
	// true when the compositor determines the session is locked
 | 
			
		||||
	bool secure = false;
 | 
			
		||||
	bool finished = false;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										23
									
								
								src/wayland/session_lock/manager.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/wayland/session_lock/manager.cpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,23 @@
 | 
			
		|||
#include "manager.hpp"
 | 
			
		||||
 | 
			
		||||
#include <qwaylandclientextension.h>
 | 
			
		||||
 | 
			
		||||
#include "lock.hpp"
 | 
			
		||||
 | 
			
		||||
QSWaylandSessionLockManager::QSWaylandSessionLockManager()
 | 
			
		||||
    : QWaylandClientExtensionTemplate<QSWaylandSessionLockManager>(1) {
 | 
			
		||||
	this->initialize();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
QSWaylandSessionLockManager::~QSWaylandSessionLockManager() { this->destroy(); }
 | 
			
		||||
 | 
			
		||||
QSWaylandSessionLock* QSWaylandSessionLockManager::acquireLock() {
 | 
			
		||||
	if (this->isLocked()) return nullptr;
 | 
			
		||||
	this->active = new QSWaylandSessionLock(this, this->lock());
 | 
			
		||||
	return this->active;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool QSWaylandSessionLockManager::isLocked() const { return this->active != nullptr; }
 | 
			
		||||
bool QSWaylandSessionLockManager::isSecure() const {
 | 
			
		||||
	return this->isLocked() && this->active->hasCompositorLock();
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										28
									
								
								src/wayland/session_lock/manager.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/wayland/session_lock/manager.hpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,28 @@
 | 
			
		|||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <qtclasshelpermacros.h>
 | 
			
		||||
#include <qwayland-ext-session-lock-v1.h>
 | 
			
		||||
#include <qwaylandclientextension.h>
 | 
			
		||||
 | 
			
		||||
#include "lock.hpp"
 | 
			
		||||
 | 
			
		||||
class QSWaylandSessionLockManager
 | 
			
		||||
    : public QWaylandClientExtensionTemplate<QSWaylandSessionLockManager>
 | 
			
		||||
    , public QtWayland::ext_session_lock_manager_v1 {
 | 
			
		||||
public:
 | 
			
		||||
	QSWaylandSessionLockManager();
 | 
			
		||||
	~QSWaylandSessionLockManager() override;
 | 
			
		||||
	Q_DISABLE_COPY_MOVE(QSWaylandSessionLockManager);
 | 
			
		||||
 | 
			
		||||
	// Create a new session lock if there is no currently active lock, otherwise null.
 | 
			
		||||
	QSWaylandSessionLock* acquireLock();
 | 
			
		||||
	[[nodiscard]] bool isLocked() const;
 | 
			
		||||
	[[nodiscard]] bool isSecure() const;
 | 
			
		||||
 | 
			
		||||
	static bool sessionLocked();
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	QSWaylandSessionLock* active = nullptr;
 | 
			
		||||
 | 
			
		||||
	friend class QSWaylandSessionLock;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										114
									
								
								src/wayland/session_lock/session_lock.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								src/wayland/session_lock/session_lock.cpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,114 @@
 | 
			
		|||
#include "session_lock.hpp"
 | 
			
		||||
 | 
			
		||||
#include <private/qwaylanddisplay_p.h>
 | 
			
		||||
#include <qlogging.h>
 | 
			
		||||
#include <qobject.h>
 | 
			
		||||
#include <qwindow.h>
 | 
			
		||||
 | 
			
		||||
#include "lock.hpp"
 | 
			
		||||
#include "manager.hpp"
 | 
			
		||||
#include "shell_integration.hpp"
 | 
			
		||||
#include "surface.hpp"
 | 
			
		||||
 | 
			
		||||
namespace {
 | 
			
		||||
QSWaylandSessionLockManager* manager() {
 | 
			
		||||
	static QSWaylandSessionLockManager* manager = nullptr; // NOLINT
 | 
			
		||||
 | 
			
		||||
	if (manager == nullptr) {
 | 
			
		||||
		manager = new QSWaylandSessionLockManager();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return manager;
 | 
			
		||||
}
 | 
			
		||||
} // namespace
 | 
			
		||||
 | 
			
		||||
bool SessionLockManager::lock() {
 | 
			
		||||
	if (this->isLocked() || SessionLockManager::sessionLocked()) return false;
 | 
			
		||||
	this->mLock = manager()->acquireLock();
 | 
			
		||||
	this->mLock->setParent(this);
 | 
			
		||||
 | 
			
		||||
	// clang-format off
 | 
			
		||||
	QObject::connect(this->mLock, &QSWaylandSessionLock::compositorLocked, this, &SessionLockManager::locked);
 | 
			
		||||
	QObject::connect(this->mLock, &QSWaylandSessionLock::unlocked, this, &SessionLockManager::unlocked);
 | 
			
		||||
	// clang-format on
 | 
			
		||||
	return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool SessionLockManager::unlock() {
 | 
			
		||||
	if (!this->isLocked()) return false;
 | 
			
		||||
	this->mLock->unlock();
 | 
			
		||||
	auto* lock = this->mLock;
 | 
			
		||||
	this->mLock = nullptr;
 | 
			
		||||
	delete lock;
 | 
			
		||||
	return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool SessionLockManager::isLocked() const { return this->mLock != nullptr; }
 | 
			
		||||
bool SessionLockManager::sessionLocked() { return manager()->isLocked(); }
 | 
			
		||||
bool SessionLockManager::isSecure() { return manager()->isSecure(); }
 | 
			
		||||
 | 
			
		||||
LockWindowExtension::~LockWindowExtension() {
 | 
			
		||||
	if (this->surface != nullptr) {
 | 
			
		||||
		this->surface->setExtension(nullptr);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
LockWindowExtension* LockWindowExtension::get(QWindow* window) {
 | 
			
		||||
	auto v = window->property("sessionlock_ext");
 | 
			
		||||
 | 
			
		||||
	if (v.canConvert<LockWindowExtension*>()) {
 | 
			
		||||
		return v.value<LockWindowExtension*>();
 | 
			
		||||
	} else {
 | 
			
		||||
		return nullptr;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool LockWindowExtension::attach(QWindow* window, SessionLockManager* manager) {
 | 
			
		||||
	if (this->surface != nullptr) throw "Cannot change the attached window of a LockWindowExtension";
 | 
			
		||||
 | 
			
		||||
	auto* current = LockWindowExtension::get(window);
 | 
			
		||||
	QtWaylandClient::QWaylandWindow* waylandWindow = nullptr;
 | 
			
		||||
 | 
			
		||||
	if (current != nullptr) {
 | 
			
		||||
		current->surface->setExtension(this);
 | 
			
		||||
	} else {
 | 
			
		||||
		// Qt appears to be resetting the window's screen on creation on some systems. This works around it.
 | 
			
		||||
		auto* screen = window->screen();
 | 
			
		||||
		window->create();
 | 
			
		||||
		window->setScreen(screen);
 | 
			
		||||
 | 
			
		||||
		waylandWindow = dynamic_cast<QtWaylandClient::QWaylandWindow*>(window->handle());
 | 
			
		||||
		if (waylandWindow == nullptr) {
 | 
			
		||||
			qWarning() << window << "is not a wayland window. Cannot create lock surface.";
 | 
			
		||||
			return false;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		static QSWaylandSessionLockIntegration* lockIntegration = nullptr; // NOLINT
 | 
			
		||||
		if (lockIntegration == nullptr) {
 | 
			
		||||
			lockIntegration = new QSWaylandSessionLockIntegration();
 | 
			
		||||
			if (!lockIntegration->initialize(waylandWindow->display())) {
 | 
			
		||||
				delete lockIntegration;
 | 
			
		||||
				lockIntegration = nullptr;
 | 
			
		||||
				qWarning() << "Failed to initialize lockscreen integration";
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		waylandWindow->setShellIntegration(lockIntegration);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this->setParent(window);
 | 
			
		||||
	window->setProperty("sessionlock_ext", QVariant::fromValue(this));
 | 
			
		||||
	this->lock = manager->mLock;
 | 
			
		||||
 | 
			
		||||
	if (waylandWindow != nullptr) {
 | 
			
		||||
		this->surface = new QSWaylandSessionLockSurface(waylandWindow);
 | 
			
		||||
		if (this->immediatelyVisible) this->surface->setVisible();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LockWindowExtension::setVisible() {
 | 
			
		||||
	if (this->surface == nullptr) this->immediatelyVisible = true;
 | 
			
		||||
	else this->surface->setVisible();
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										93
									
								
								src/wayland/session_lock/session_lock.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								src/wayland/session_lock/session_lock.hpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,93 @@
 | 
			
		|||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <qobject.h>
 | 
			
		||||
#include <qtclasshelpermacros.h>
 | 
			
		||||
#include <qtmetamacros.h>
 | 
			
		||||
#include <qwindow.h>
 | 
			
		||||
 | 
			
		||||
class QSWaylandSessionLock;
 | 
			
		||||
class QSWaylandSessionLockSurface;
 | 
			
		||||
class QSWaylandSessionLockIntegration;
 | 
			
		||||
 | 
			
		||||
class SessionLockManager: public QObject {
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	explicit SessionLockManager(QObject* parent = nullptr): QObject(parent) {}
 | 
			
		||||
 | 
			
		||||
	// Returns true if a lock was acquired.
 | 
			
		||||
	// If true is returned the caller must watch the global screen list and create/destroy
 | 
			
		||||
	// windows with an attached LockWindowExtension to match it.
 | 
			
		||||
	bool lock();
 | 
			
		||||
 | 
			
		||||
	// Returns true if the session was locked and is now unlocked.
 | 
			
		||||
	bool unlock();
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] bool isLocked() const;
 | 
			
		||||
 | 
			
		||||
	static bool sessionLocked();
 | 
			
		||||
	static bool isSecure();
 | 
			
		||||
 | 
			
		||||
signals:
 | 
			
		||||
	// This signal is sent once the compositor considers the session to be fully locked.
 | 
			
		||||
	// This corrosponds to the ext_session_lock_v1::locked event.
 | 
			
		||||
	void locked();
 | 
			
		||||
 | 
			
		||||
	// This signal is sent once the compositor considers the session to be unlocked.
 | 
			
		||||
	// This corrosponds to the ext_session_lock_v1::finished event.
 | 
			
		||||
	//
 | 
			
		||||
	// The session lock will end in one of three cases.
 | 
			
		||||
	// 1. unlock() is called.
 | 
			
		||||
	// 2. The SessionLockManager is destroyed.
 | 
			
		||||
	// 3. The compositor forcibly unlocks the session.
 | 
			
		||||
	//
 | 
			
		||||
	// After receiving this event the caller should destroy all of its lock surfaces.
 | 
			
		||||
	void unlocked();
 | 
			
		||||
 | 
			
		||||
private slots:
 | 
			
		||||
	//void onUnlocked();
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	QSWaylandSessionLock* mLock = nullptr;
 | 
			
		||||
 | 
			
		||||
	friend class LockWindowExtension;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class LockWindowExtension: public QObject {
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	explicit LockWindowExtension(QObject* parent = nullptr): QObject(parent) {}
 | 
			
		||||
	~LockWindowExtension() override;
 | 
			
		||||
	Q_DISABLE_COPY_MOVE(LockWindowExtension);
 | 
			
		||||
 | 
			
		||||
	// Attach this lock extension to the given window.
 | 
			
		||||
	// The extension is reparented to the window and replaces any existing lock extension.
 | 
			
		||||
	// Returns false if the window cannot be used.
 | 
			
		||||
	bool attach(QWindow* window, SessionLockManager* manager);
 | 
			
		||||
 | 
			
		||||
	// This must be called in place of QWindow::setVisible. Calling QWindow::setVisible will result in a crash.
 | 
			
		||||
	// To make a window invisible, destroy it as it cannot be recovered.
 | 
			
		||||
	void setVisible();
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] bool isLocked() const;
 | 
			
		||||
 | 
			
		||||
	static LockWindowExtension* get(QWindow* window);
 | 
			
		||||
 | 
			
		||||
signals:
 | 
			
		||||
	// This signal is sent once the compositor considers the session to be fully locked.
 | 
			
		||||
	// See SessionLockManager::locked for details.
 | 
			
		||||
	void locked();
 | 
			
		||||
 | 
			
		||||
	// After receiving this signal the window is no longer in use by the compositor
 | 
			
		||||
	// and should be destroyed. See SessionLockManager::unlocked for details.
 | 
			
		||||
	void unlocked();
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	QSWaylandSessionLockSurface* surface = nullptr;
 | 
			
		||||
	QSWaylandSessionLock* lock = nullptr;
 | 
			
		||||
	bool immediatelyVisible = false;
 | 
			
		||||
 | 
			
		||||
	friend class QSWaylandSessionLockSurface;
 | 
			
		||||
	friend class QSWaylandSessionLockIntegration;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										20
									
								
								src/wayland/session_lock/shell_integration.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/wayland/session_lock/shell_integration.cpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,20 @@
 | 
			
		|||
#include "shell_integration.hpp"
 | 
			
		||||
 | 
			
		||||
#include <private/qwaylandshellsurface_p.h>
 | 
			
		||||
#include <private/qwaylandwindow_p.h>
 | 
			
		||||
#include <qlogging.h>
 | 
			
		||||
 | 
			
		||||
#include "session_lock.hpp"
 | 
			
		||||
#include "surface.hpp"
 | 
			
		||||
 | 
			
		||||
QtWaylandClient::QWaylandShellSurface*
 | 
			
		||||
QSWaylandSessionLockIntegration::createShellSurface(QtWaylandClient::QWaylandWindow* window) {
 | 
			
		||||
	auto* lock = LockWindowExtension::get(window->window());
 | 
			
		||||
	if (lock == nullptr || lock->surface == nullptr || !lock->surface->isExposed()) {
 | 
			
		||||
		qFatal() << "Visibility canary failed. A window with a LockWindowExtension MUST be set to "
 | 
			
		||||
		            "visible via LockWindowExtension::setVisible";
 | 
			
		||||
		throw nullptr;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return lock->surface;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										13
									
								
								src/wayland/session_lock/shell_integration.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/wayland/session_lock/shell_integration.hpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,13 @@
 | 
			
		|||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <private/qwaylanddisplay_p.h>
 | 
			
		||||
#include <private/qwaylandshellintegration_p.h>
 | 
			
		||||
#include <private/qwaylandshellsurface_p.h>
 | 
			
		||||
#include <private/qwaylandwindow_p.h>
 | 
			
		||||
 | 
			
		||||
class QSWaylandSessionLockIntegration: public QtWaylandClient::QWaylandShellIntegration {
 | 
			
		||||
public:
 | 
			
		||||
	bool initialize(QtWaylandClient::QWaylandDisplay* /* display */) override { return true; }
 | 
			
		||||
	QtWaylandClient::QWaylandShellSurface* createShellSurface(QtWaylandClient::QWaylandWindow* window
 | 
			
		||||
	) override;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										115
									
								
								src/wayland/session_lock/surface.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								src/wayland/session_lock/surface.cpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,115 @@
 | 
			
		|||
#include "surface.hpp"
 | 
			
		||||
 | 
			
		||||
#include <private/qwaylandscreen_p.h>
 | 
			
		||||
#include <private/qwaylandshellsurface_p.h>
 | 
			
		||||
#include <private/qwaylandshmbackingstore_p.h>
 | 
			
		||||
#include <private/qwaylandsurface_p.h>
 | 
			
		||||
#include <private/qwaylandwindow_p.h>
 | 
			
		||||
#include <qlogging.h>
 | 
			
		||||
#include <qobject.h>
 | 
			
		||||
#include <qtypes.h>
 | 
			
		||||
 | 
			
		||||
#include "lock.hpp"
 | 
			
		||||
#include "session_lock.hpp"
 | 
			
		||||
 | 
			
		||||
QSWaylandSessionLockSurface::QSWaylandSessionLockSurface(QtWaylandClient::QWaylandWindow* window)
 | 
			
		||||
    : QtWaylandClient::QWaylandShellSurface(window) {
 | 
			
		||||
	auto* qwindow = window->window();
 | 
			
		||||
	this->setExtension(LockWindowExtension::get(qwindow));
 | 
			
		||||
 | 
			
		||||
	if (this->ext == nullptr) {
 | 
			
		||||
		qFatal() << "QSWaylandSessionLockSurface created with null LockWindowExtension";
 | 
			
		||||
		throw nullptr;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (this->ext->lock == nullptr) {
 | 
			
		||||
		qFatal() << "QSWaylandSessionLock for QSWaylandSessionLockSurface died";
 | 
			
		||||
		throw nullptr;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	wl_output* output = nullptr; // NOLINT (include)
 | 
			
		||||
	auto* waylandScreen = dynamic_cast<QtWaylandClient::QWaylandScreen*>(qwindow->screen()->handle());
 | 
			
		||||
 | 
			
		||||
	if (waylandScreen != nullptr) {
 | 
			
		||||
		output = waylandScreen->output();
 | 
			
		||||
	} else {
 | 
			
		||||
		qFatal() << "Session lock screen does not corrospond to a real screen. Force closing window";
 | 
			
		||||
		throw nullptr;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this->init(this->ext->lock->get_lock_surface(window->waylandSurface()->object(), output));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
QSWaylandSessionLockSurface::~QSWaylandSessionLockSurface() {
 | 
			
		||||
	if (this->ext != nullptr) this->ext->surface = nullptr;
 | 
			
		||||
	this->destroy();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool QSWaylandSessionLockSurface::isExposed() const { return this->configured; }
 | 
			
		||||
 | 
			
		||||
void QSWaylandSessionLockSurface::applyConfigure() {
 | 
			
		||||
	this->window()->resizeFromApplyConfigure(this->size);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool QSWaylandSessionLockSurface::handleExpose(const QRegion& region) {
 | 
			
		||||
	if (this->initBuf != nullptr) {
 | 
			
		||||
		// at this point qt's next commit to the surface will have a new buffer, and we can safely delete this one.
 | 
			
		||||
		delete this->initBuf;
 | 
			
		||||
		this->initBuf = nullptr;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return this->QtWaylandClient::QWaylandShellSurface::handleExpose(region);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void QSWaylandSessionLockSurface::setExtension(LockWindowExtension* ext) {
 | 
			
		||||
	if (ext == nullptr) {
 | 
			
		||||
		if (this->window() != nullptr) this->window()->window()->close();
 | 
			
		||||
	} else {
 | 
			
		||||
		if (this->ext != nullptr) {
 | 
			
		||||
			this->ext->surface = nullptr;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		this->ext = ext;
 | 
			
		||||
		this->ext->surface = this;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void QSWaylandSessionLockSurface::setVisible() {
 | 
			
		||||
	if (this->configured && !this->visible) this->initVisible();
 | 
			
		||||
	this->visible = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void QSWaylandSessionLockSurface::ext_session_lock_surface_v1_configure(
 | 
			
		||||
    quint32 serial,
 | 
			
		||||
    quint32 width,
 | 
			
		||||
    quint32 height
 | 
			
		||||
) {
 | 
			
		||||
	this->ack_configure(serial);
 | 
			
		||||
 | 
			
		||||
	this->size = QSize(static_cast<qint32>(width), static_cast<qint32>(height));
 | 
			
		||||
	if (!this->configured) {
 | 
			
		||||
		this->configured = true;
 | 
			
		||||
 | 
			
		||||
		this->window()->resizeFromApplyConfigure(this->size);
 | 
			
		||||
		this->window()->handleExpose(QRect(QPoint(), this->size));
 | 
			
		||||
		if (this->visible) this->initVisible();
 | 
			
		||||
	} else {
 | 
			
		||||
		// applyConfigureWhenPossible runs too late and causes a protocol error on reconfigure.
 | 
			
		||||
		this->window()->resizeFromApplyConfigure(this->size);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void QSWaylandSessionLockSurface::initVisible() {
 | 
			
		||||
	this->visible = true;
 | 
			
		||||
 | 
			
		||||
	// qt always commits a null buffer in QWaylandWindow::initWindow.
 | 
			
		||||
	// We attach a dummy buffer to satisfy ext_session_lock_v1.
 | 
			
		||||
	this->initBuf = new QtWaylandClient::QWaylandShmBuffer(
 | 
			
		||||
	    this->window()->display(),
 | 
			
		||||
	    this->size,
 | 
			
		||||
	    QImage::Format_ARGB32
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	this->window()->waylandSurface()->attach(this->initBuf->buffer(), 0, 0);
 | 
			
		||||
	this->window()->window()->setVisible(true);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										39
									
								
								src/wayland/session_lock/surface.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/wayland/session_lock/surface.hpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,39 @@
 | 
			
		|||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <private/qwaylandshellsurface_p.h>
 | 
			
		||||
#include <private/qwaylandshmbackingstore_p.h>
 | 
			
		||||
#include <private/qwaylandwindow_p.h>
 | 
			
		||||
#include <qregion.h>
 | 
			
		||||
#include <qtclasshelpermacros.h>
 | 
			
		||||
#include <qtypes.h>
 | 
			
		||||
#include <qwayland-ext-session-lock-v1.h>
 | 
			
		||||
 | 
			
		||||
#include "session_lock.hpp"
 | 
			
		||||
 | 
			
		||||
class QSWaylandSessionLockSurface
 | 
			
		||||
    : public QtWaylandClient::QWaylandShellSurface
 | 
			
		||||
    , public QtWayland::ext_session_lock_surface_v1 {
 | 
			
		||||
public:
 | 
			
		||||
	QSWaylandSessionLockSurface(QtWaylandClient::QWaylandWindow* window);
 | 
			
		||||
	~QSWaylandSessionLockSurface() override;
 | 
			
		||||
	Q_DISABLE_COPY_MOVE(QSWaylandSessionLockSurface);
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] bool isExposed() const override;
 | 
			
		||||
	void applyConfigure() override;
 | 
			
		||||
	bool handleExpose(const QRegion& region) override;
 | 
			
		||||
 | 
			
		||||
	void setExtension(LockWindowExtension* ext);
 | 
			
		||||
	void setVisible();
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	void
 | 
			
		||||
	ext_session_lock_surface_v1_configure(quint32 serial, quint32 width, quint32 height) override;
 | 
			
		||||
 | 
			
		||||
	void initVisible();
 | 
			
		||||
 | 
			
		||||
	LockWindowExtension* ext = nullptr;
 | 
			
		||||
	QSize size;
 | 
			
		||||
	bool configured = false;
 | 
			
		||||
	bool visible = false;
 | 
			
		||||
	QtWaylandClient::QWaylandShmBuffer* initBuf = nullptr;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -5,14 +5,13 @@
 | 
			
		|||
#include <private/qwaylandwindow_p.h>
 | 
			
		||||
 | 
			
		||||
#include "surface.hpp"
 | 
			
		||||
#include "wayland-wlr-layer-shell-unstable-v1-client-protocol.h"
 | 
			
		||||
 | 
			
		||||
QSWaylandLayerShellIntegration::QSWaylandLayerShellIntegration()
 | 
			
		||||
    : QtWaylandClient::QWaylandShellIntegrationTemplate<QSWaylandLayerShellIntegration>(4) {}
 | 
			
		||||
 | 
			
		||||
QSWaylandLayerShellIntegration::~QSWaylandLayerShellIntegration() {
 | 
			
		||||
	if (this->object() != nullptr) {
 | 
			
		||||
		zwlr_layer_shell_v1_destroy(this->object());
 | 
			
		||||
	if (this->isInitialized()) {
 | 
			
		||||
		this->destroy();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,7 @@
 | 
			
		|||
 | 
			
		||||
#include <private/qwaylandshellintegration_p.h>
 | 
			
		||||
#include <private/qwaylandshellsurface_p.h>
 | 
			
		||||
#include <qtwaylandclientexports.h>
 | 
			
		||||
#include <qtclasshelpermacros.h>
 | 
			
		||||
#include <qwayland-wlr-layer-shell-unstable-v1.h>
 | 
			
		||||
 | 
			
		||||
class QSWaylandLayerShellIntegration
 | 
			
		||||
| 
						 | 
				
			
			@ -11,10 +11,7 @@ class QSWaylandLayerShellIntegration
 | 
			
		|||
public:
 | 
			
		||||
	QSWaylandLayerShellIntegration();
 | 
			
		||||
	~QSWaylandLayerShellIntegration() override;
 | 
			
		||||
	QSWaylandLayerShellIntegration(QSWaylandLayerShellIntegration&&) = delete;
 | 
			
		||||
	QSWaylandLayerShellIntegration(const QSWaylandLayerShellIntegration&) = delete;
 | 
			
		||||
	void operator=(QSWaylandLayerShellIntegration&&) = delete;
 | 
			
		||||
	void operator=(const QSWaylandLayerShellIntegration&) = delete;
 | 
			
		||||
	Q_DISABLE_COPY_MOVE(QSWaylandLayerShellIntegration);
 | 
			
		||||
 | 
			
		||||
	QtWaylandClient::QWaylandShellSurface* createShellSurface(QtWaylandClient::QWaylandWindow* window
 | 
			
		||||
	) override;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,12 +23,11 @@
 | 
			
		|||
[[nodiscard]] QSize constrainedSize(const Anchors& anchors, const QSize& size) noexcept;
 | 
			
		||||
// clang-format on
 | 
			
		||||
 | 
			
		||||
// clang-format off
 | 
			
		||||
QSWaylandLayerSurface::QSWaylandLayerSurface(
 | 
			
		||||
    QSWaylandLayerShellIntegration* shell,
 | 
			
		||||
    QtWaylandClient::QWaylandWindow* window
 | 
			
		||||
): QtWaylandClient::QWaylandShellSurface(window) {
 | 
			
		||||
	// clang-format on
 | 
			
		||||
)
 | 
			
		||||
    : QtWaylandClient::QWaylandShellSurface(window) {
 | 
			
		||||
 | 
			
		||||
	auto* qwindow = window->window();
 | 
			
		||||
	this->ext = LayershellWindowExtension::get(qwindow);
 | 
			
		||||
| 
						 | 
				
			
			@ -37,7 +36,7 @@ QSWaylandLayerSurface::QSWaylandLayerSurface(
 | 
			
		|||
		throw "QSWaylandLayerSurface created with null LayershellWindowExtension";
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	wl_output* output = nullptr; // NOLINT (import)
 | 
			
		||||
	wl_output* output = nullptr; // NOLINT (include)
 | 
			
		||||
	if (this->ext->useWindowScreen) {
 | 
			
		||||
		auto* waylandScreen =
 | 
			
		||||
		    dynamic_cast<QtWaylandClient::QWaylandScreen*>(qwindow->screen()->handle());
 | 
			
		||||
| 
						 | 
				
			
			@ -45,8 +44,8 @@ QSWaylandLayerSurface::QSWaylandLayerSurface(
 | 
			
		|||
		if (waylandScreen != nullptr) {
 | 
			
		||||
			output = waylandScreen->output();
 | 
			
		||||
		} else {
 | 
			
		||||
			qWarning() << "Layershell screen is set but does not corrospond to a real screen. Letting "
 | 
			
		||||
			              "the compositor pick.";
 | 
			
		||||
			qWarning(
 | 
			
		||||
			) << "Layershell screen does not corrospond to a real screen. Letting the compositor pick.";
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,7 @@
 | 
			
		|||
 | 
			
		||||
#include <private/qwaylandshellsurface_p.h>
 | 
			
		||||
#include <private/qwaylandwindow_p.h>
 | 
			
		||||
#include <qtclasshelpermacros.h>
 | 
			
		||||
#include <qtwaylandclientexports.h>
 | 
			
		||||
#include <qtypes.h>
 | 
			
		||||
#include <qwayland-wlr-layer-shell-unstable-v1.h>
 | 
			
		||||
| 
						 | 
				
			
			@ -20,10 +21,7 @@ public:
 | 
			
		|||
	);
 | 
			
		||||
 | 
			
		||||
	~QSWaylandLayerSurface() override;
 | 
			
		||||
	QSWaylandLayerSurface(QSWaylandLayerSurface&&) = delete;
 | 
			
		||||
	QSWaylandLayerSurface(const QSWaylandLayerSurface&) = delete;
 | 
			
		||||
	void operator=(QSWaylandLayerSurface&&) = delete;
 | 
			
		||||
	void operator=(const QSWaylandLayerSurface&) = delete;
 | 
			
		||||
	Q_DISABLE_COPY_MOVE(QSWaylandLayerSurface);
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] bool isExposed() const override;
 | 
			
		||||
	void applyConfigure() override;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -54,7 +54,10 @@ bool LayershellWindowExtension::attach(QWindow* window) {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	if (!hasSurface) {
 | 
			
		||||
		// Qt appears to be resetting the window's screen on creation on some systems. This works around it.
 | 
			
		||||
		auto* screen = window->screen();
 | 
			
		||||
		window->create();
 | 
			
		||||
		window->setScreen(screen);
 | 
			
		||||
 | 
			
		||||
		auto* waylandWindow = dynamic_cast<QtWaylandClient::QWaylandWindow*>(window->handle());
 | 
			
		||||
		if (waylandWindow == nullptr) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -56,17 +56,12 @@ class LayershellWindowExtension: public QObject {
 | 
			
		|||
 | 
			
		||||
public:
 | 
			
		||||
	LayershellWindowExtension(QObject* parent = nullptr): QObject(parent) {}
 | 
			
		||||
	~LayershellWindowExtension() override = default;
 | 
			
		||||
	LayershellWindowExtension(LayershellWindowExtension&&) = delete;
 | 
			
		||||
	LayershellWindowExtension(const LayershellWindowExtension&) = delete;
 | 
			
		||||
	void operator=(LayershellWindowExtension&&) = delete;
 | 
			
		||||
	void operator=(const LayershellWindowExtension&) = delete;
 | 
			
		||||
 | 
			
		||||
	// returns the layershell extension if attached, otherwise nullptr
 | 
			
		||||
	static LayershellWindowExtension* get(QWindow* window);
 | 
			
		||||
 | 
			
		||||
	// Attach this layershell extension to the given window.
 | 
			
		||||
	// The extension is reparented to the window and replaces any existing extensions.
 | 
			
		||||
	// The extension is reparented to the window and replaces any existing layershell extension.
 | 
			
		||||
	// Returns false if the window cannot be used.
 | 
			
		||||
	bool attach(QWindow* window);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue