Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ add_subdirectory(test_hide_window)
add_subdirectory(test_data_control_manager)
add_subdirectory(test_region_watch)
add_subdirectory(test_capture)
add_subdirectory(test_snap_target)
add_subdirectory(test_window_appearance)
add_subdirectory(test_decoration)
add_subdirectory(test_super_overlay_surface)
Expand Down
56 changes: 56 additions & 0 deletions examples/test_snap_target/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
find_package(PkgConfig REQUIRED)
find_package(Qt6 REQUIRED COMPONENTS Core Gui WaylandClient Quick)
if(Qt6_VERSION VERSION_GREATER_EQUAL 6.10)
find_package(Qt6 REQUIRED COMPONENTS WaylandClientPrivate)
endif()
find_package(TreelandProtocols REQUIRED)
pkg_get_variable(WAYLAND_PROTOCOLS_DATADIR wayland-protocols pkgdatadir)

set(BIN_NAME test-snap-target)

qt_add_executable(${BIN_NAME}
main.cpp
screencapture.cpp
snapproxy.cpp
)

qt_generate_wayland_protocol_client_sources(${BIN_NAME}
FILES
${WAYLAND_PROTOCOLS_DATADIR}/staging/ext-foreign-toplevel-list/ext-foreign-toplevel-list-v1.xml
${WAYLAND_PROTOCOLS_DATADIR}/staging/ext-image-capture-source/ext-image-capture-source-v1.xml
${WAYLAND_PROTOCOLS_DATADIR}/staging/ext-image-copy-capture/ext-image-copy-capture-v1.xml
)

qt_generate_wayland_protocol_client_sources(${BIN_NAME}
FILES
${TREELAND_PROTOCOLS_DATA_DIR}/treeland-snap-target-unstable-v1.xml
)

qt_generate_wayland_protocol_client_sources(${BIN_NAME}
FILES
${WAYLAND_PROTOCOLS_DATADIR}/staging/xdg-toplevel-tag/xdg-toplevel-tag-v1.xml
)

# xdg-toplevel-tag references xdg_toplevel_interface from xdg-shell
qt_generate_wayland_protocol_client_sources(${BIN_NAME}
FILES
${WAYLAND_PROTOCOLS_DATADIR}/stable/xdg-shell/xdg-shell.xml
)

qt_add_qml_module(${BIN_NAME}
URI TestSnapTarget
VERSION 1.0
QML_FILES
Main.qml
)

target_link_libraries(${BIN_NAME}
PRIVATE
Qt6::Core
Qt6::Gui
Qt6::WaylandClient
Qt6::WaylandClientPrivate
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Qt6::Quick
)

install(TARGETS ${BIN_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}")
180 changes: 180 additions & 0 deletions examples/test_snap_target/Main.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// Copyright (C) 2026 UnionTech Software Technology Co., Ltd.
// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

import QtQuick

// One instance per output. `snapController` is injected as a per-window
// context property so that each window tracks its own output origin while
// sharing the process-wide snap session.
Window {
id: root
visible: true
flags: Qt.FramelessWindowHint
color: "transparent"
width: snapController.screenWidth
height: snapController.screenHeight

// Press Escape to quit the demo
Shortcut {
sequence: "Escape"
onActivated: snapController.quit()
}

// Frozen snapshot of this output, captured before any mask window was
// mapped. Only present when started with --background.
Image {
id: backgroundImage
anchors.fill: parent
fillMode: Image.Stretch
source: snapController.backgroundSource
visible: snapController.backgroundEnabled
}

Rectangle {
id: snapHighlight
color: Qt.rgba(0.2, 0.6, 1.0, 0.1)
border.color: "#3399FF"
border.width: 3
visible: snapController.snapVisible && !snapController.confirmed
x: snapController.snapX
y: snapController.snapY
width: snapController.snapWidth
height: snapController.snapHeight
radius: 4
}

// Frozen selection highlight after confirmation
Rectangle {
id: frozenSelection
color: Qt.rgba(0.2, 0.6, 1.0, 0.15)
border.color: "#3399FF"
border.width: 4
visible: snapController.confirmed
x: snapController.snapX
y: snapController.snapY
width: snapController.snapWidth
height: snapController.snapHeight
radius: 4
}

// Full-screen click area — only active before confirmation
MouseArea {
id: clickCatcher
anchors.fill: parent
enabled: !snapController.confirmed
cursorShape: Qt.CrossCursor
onClicked: snapController.confirmSelection()
}

// Demo toolbar (non-functional) shown after confirmation
Rectangle {
id: toolbar
z: 9
visible: snapController.confirmed
color: "#2B2B2B"
border.color: "#555555"
border.width: 1
radius: 6
height: 48
width: 260

// Center toolbar above the frozen selection, clamped to this output
x: snapController.confirmed
? Math.max(8, Math.min(root.width - width - 8,
snapController.snapX + snapController.snapWidth / 2 - width / 2))
: 0
y: snapController.confirmed
? (snapController.snapY - height - 8 >= 8
? snapController.snapY - height - 8
: Math.min(root.height - height - 8,
snapController.snapY + snapController.snapHeight + 8))
: 0

Row {
anchors.centerIn: parent
spacing: 10

Repeater {
model: [
{ icon: "✎", label: "Annotate" },
{ icon: "⌨", label: "Text" },
{ icon: "⏲", label: "Delay" },
{ icon: "💾", label: "Save" }
]

Rectangle {
width: 52
height: 36
color: "transparent"
radius: 4

Text {
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: 2
text: modelData.icon
font.pixelSize: 18
color: "#FFFFFF"
}

Text {
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottomMargin: 2
text: modelData.label
font.pixelSize: 9
color: "#AAAAAA"
}

MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
// Demo only - no actual functionality
}
}
}
}
}

// Close button on the toolbar's right edge
Rectangle {
anchors.right: parent.right
anchors.rightMargin: 6
anchors.verticalCenter: parent.verticalCenter
width: 24
height: 24
radius: 12
color: "transparent"

Text {
anchors.centerIn: parent
text: "×"
font.pixelSize: 18
color: "#FFFFFF"
}

MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: snapController.quit()
}
}
}

// Debug overlay - one per output
Text {
z: 10
x: 10
y: 10
text: "confirmed=" + snapController.confirmed
+ " vis=" + snapController.snapVisible
+ " region=(" + snapController.snapX + "," + snapController.snapY
+ " " + snapController.snapWidth + "x" + snapController.snapHeight + ")"
+ " proc=" + snapController.processName
+ " output=(" + snapController.screenX + "," + snapController.screenY
+ " " + root.width + "x" + root.height + ")"
color: "red"
font.pixelSize: 14
}
}
153 changes: 153 additions & 0 deletions examples/test_snap_target/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// Copyright (C) 2026 UnionTech Software Technology Co., Ltd.
// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#include "screencapture.h"
#include "snapproxy.h"

#include <QCommandLineOption>

Check warning on line 7 in examples/test_snap_target/main.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QCommandLineOption> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QCommandLineParser>

Check warning on line 8 in examples/test_snap_target/main.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QCommandLineParser> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QDebug>

Check warning on line 9 in examples/test_snap_target/main.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QDebug> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QGuiApplication>

Check warning on line 10 in examples/test_snap_target/main.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QGuiApplication> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QHash>

Check warning on line 11 in examples/test_snap_target/main.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QHash> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QQmlApplicationEngine>

Check warning on line 12 in examples/test_snap_target/main.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QQmlApplicationEngine> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QQmlComponent>

Check warning on line 13 in examples/test_snap_target/main.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QQmlComponent> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QQmlContext>

Check warning on line 14 in examples/test_snap_target/main.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QQmlContext> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QQuickImageProvider>

Check warning on line 15 in examples/test_snap_target/main.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QQuickImageProvider> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QQuickWindow>

Check warning on line 16 in examples/test_snap_target/main.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QQuickWindow> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QScreen>
#include <QTimer>

#include <utility>

class BackgroundImageProvider : public QQuickImageProvider
{
public:
BackgroundImageProvider()
: QQuickImageProvider(QQuickImageProvider::Image)
{
}

QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize) override
{
Q_UNUSED(requestedSize);
const QImage image = m_images.value(id);
if (size)
*size = image.size();
return image;
}

static QString idForGeometry(const QRect &geometry)
{
return QStringLiteral("%1_%2_%3_%4")
.arg(geometry.x())
.arg(geometry.y())
.arg(geometry.width())
.arg(geometry.height());
}

void addImage(const QRect &geometry, const QImage &image)
{
m_images.insert(idForGeometry(geometry), image);
}

bool hasImage(const QString &id) const
{
return m_images.contains(id);
}

private:
QHash<QString, QImage> m_images;
};

int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);

QCommandLineParser parser;
parser.setApplicationDescription("Treeland snap-target demo client");
parser.addHelpOption();
const QCommandLineOption backgroundOption(
QStringLiteral("background"),
QStringLiteral("Capture each output with ext-image-copy-capture before showing the "
"selection UI and use the per-output images as window backgrounds."));
parser.addOption(backgroundOption);
parser.process(app);

// Capture backgrounds before any mask surface is mapped, so the masks do
// not show up in their own screenshots.
QList<ScreenCapture::CapturedOutput> capturedOutputs;
if (parser.isSet(backgroundOption)) {
ScreenCapture capture;
capturedOutputs = capture.captureOutputs();
if (capturedOutputs.isEmpty())
qWarning() << "Background capture failed, falling back to a transparent background";
}

// One snap session per process, shared by every per-output mask window.
SnapSession session;

auto *backgroundProvider = new BackgroundImageProvider;
for (const auto &output : std::as_const(capturedOutputs))
backgroundProvider->addImage(output.logicalGeometry, output.image);

QQmlApplicationEngine engine;
engine.addImageProvider("captureBackground", backgroundProvider);

QQmlComponent component(&engine, QUrl(QStringLiteral("qrc:/qt/qml/TestSnapTarget/Main.qml")));
if (component.isError()) {
qWarning() << component.errors();
return -1;
}

const QList<QScreen *> screens = QGuiApplication::screens();
if (screens.isEmpty()) {
qWarning() << "No outputs available";
return -1;
}

struct MaskWindow
{
SnapController *controller = nullptr;
QQuickWindow *window = nullptr;
QScreen *screen = nullptr;
};

QList<MaskWindow> masks;
for (QScreen *screen : screens) {
auto *controller = new SnapController(&session, &session);
auto *context = new QQmlContext(engine.rootContext());
context->setContextProperty(QStringLiteral("snapController"), controller);

QObject *object = component.create(context);
auto *window = qobject_cast<QQuickWindow *>(object);
if (!window) {
qWarning() << "Failed to create mask window for output" << screen->name()
<< component.errors();
delete object;
continue;
}

window->setScreen(screen);

if (parser.isSet(backgroundOption)) {
const QString id = BackgroundImageProvider::idForGeometry(screen->geometry());
if (backgroundProvider->hasImage(id)) {
controller->setBackgroundSource(QStringLiteral("image://captureBackground/") + id);
controller->setBackgroundEnabled(true);
}
}

masks.append({ controller, window, screen });
}

if (masks.isEmpty())
return -1;

QTimer::singleShot(0, [&session, masks]() {
for (const MaskWindow &mask : masks)
mask.controller->setup(mask.window, mask.screen);
session.start();
});

return app.exec();
}
Loading
Loading