chore: remove unused wl-clipboard support

This commit is contained in:
sithlord48 2026-06-18 16:22:11 -04:00 committed by Nick Bolton
parent 16cd4695cc
commit df3746777b
18 changed files with 0 additions and 1281 deletions

View file

@ -77,7 +77,6 @@ This section contains general options it will begin with `[core]`
| computerName | string | Name used to identify the computer [default: machine's hostname] |
| useHooks | `true` or `false` | If Windows uses hooks or not [default: true] |
| language | 639 language | The language to display the GUI in [default: en] |
| wlClipboard | `true` or `false` | When true the wl-clipboard backend will be enabled [default: false] |
| enableEnterCommand | `true` or `false` | Should the enter command be triggered when the screen is entered [defaut: false] |
| enterCommand | command | A command to run when the screen is entered. |
| enableExitCommand | `true` or `false` | Should the exit command be triggered when the screen is exited [defaut: false] |

View file

@ -56,7 +56,6 @@ public:
inline static const auto Display = QStringLiteral("core/display");
inline static const auto UseHooks = QStringLiteral("core/useHooks");
inline static const auto Language = QStringLiteral("core/language");
inline static const auto UseWlClipboard = QStringLiteral("core/wlClipboard");
inline static const auto EnableEnterCommand = QStringLiteral("core/enableEnterCommand");
inline static const auto ScreenEnterCommand = QStringLiteral("core/enterCommand");
inline static const auto EnableExitCommand = QStringLiteral("core/enableExitCommand");
@ -226,7 +225,6 @@ private:
, Settings::Core::ComputerName
, Settings::Core::Display
, Settings::Core::UseHooks
, Settings::Core::UseWlClipboard
, Settings::Core::Language
, Settings::Daemon::ConfigFile
, Settings::Daemon::Elevate
@ -280,7 +278,6 @@ private:
, Settings::Gui::ShowVersionInTitle
, Settings::Gui::IgnoreMissingKeyboardLayouts
, Settings::Core::PreventSleep
, Settings::Core::UseWlClipboard
, Settings::Core::EnableEnterCommand
, Settings::Core::EnableExitCommand
, Settings::Client::DynamicConnectionRetry

View file

@ -130,7 +130,6 @@ void SettingsDialog::initConnections() const
connect(ui->cbElevateDaemon, &QCheckBox::toggled, this, &SettingsDialog::setButtonBoxEnabledButtons);
connect(ui->cbAutoUpdate, &QCheckBox::toggled, this, &SettingsDialog::setButtonBoxEnabledButtons);
connect(ui->cbGuiDebug, &QCheckBox::toggled, this, &SettingsDialog::setButtonBoxEnabledButtons);
connect(ui->cbUseWlClipboard, &QCheckBox::toggled, this, &SettingsDialog::setButtonBoxEnabledButtons);
connect(ui->cbShowVersion, &QCheckBox::toggled, this, &SettingsDialog::setButtonBoxEnabledButtons);
connect(ui->cbRequireClientCert, &QCheckBox::toggled, this, &SettingsDialog::setButtonBoxEnabledButtons);
connect(ui->groupLogToFile, &QGroupBox::toggled, this, &SettingsDialog::setButtonBoxEnabledButtons);
@ -242,7 +241,6 @@ void SettingsDialog::accept()
Settings::setValue(Settings::Security::CheckPeers, ui->cbRequireClientCert->isChecked());
Settings::setValue(Settings::Core::Language, I18N::nativeTo639Name(ui->comboLanguage->currentText()));
Settings::setValue(Settings::Log::GuiDebug, ui->cbGuiDebug->isChecked());
Settings::setValue(Settings::Core::UseWlClipboard, ui->cbUseWlClipboard->isChecked());
Settings::setValue(Settings::Gui::ShowVersionInTitle, ui->cbShowVersion->isChecked());
Settings::setValue(Settings::Core::EnableEnterCommand, ui->cbRunEnterCommand->isChecked());
Settings::setValue(Settings::Core::EnableExitCommand, ui->cbRunExitCommand->isChecked());
@ -271,7 +269,6 @@ void SettingsDialog::loadFromConfig()
ui->cbElevateDaemon->setChecked(Settings::value(Settings::Daemon::Elevate).toBool());
ui->cbAutoUpdate->setChecked(Settings::value(Settings::Gui::AutoUpdateCheck).toBool());
ui->cbGuiDebug->setChecked(Settings::value(Settings::Log::GuiDebug).toBool());
ui->cbUseWlClipboard->setChecked(Settings::value(Settings::Core::UseWlClipboard).toBool());
ui->cbShowVersion->setChecked(Settings::value(Settings::Gui::ShowVersionInTitle).toBool());
ui->cbRunEnterCommand->setChecked(Settings::value(Settings::Core::EnableEnterCommand).toBool());
ui->cbRunExitCommand->setChecked(Settings::value(Settings::Core::EnableExitCommand).toBool());
@ -402,14 +399,6 @@ void SettingsDialog::updateControls()
ui->groupService->setVisible(false);
}
// wl-clipboard support only works on wayland.
// options should only be available when we are *not* running on wayland.
if (deskflow::platform::isWayland()) {
ui->cbUseWlClipboard->setEnabled(writable);
} else if (ui->widgetWlClipboard->isVisibleTo(ui->tabAdvanced)) {
ui->widgetWlClipboard->setVisible(false);
}
ui->widgetLogFilename->setEnabled(writable && logToFile);
updateTlsControls();
@ -443,7 +432,6 @@ bool SettingsDialog::isModified() const
(ui->cbElevateDaemon->isChecked() != Settings::value(Settings::Daemon::Elevate).toBool()) ||
(ui->cbAutoUpdate->isChecked() != Settings::value(Settings::Gui::AutoUpdateCheck).toBool()) ||
(ui->cbGuiDebug->isChecked() != Settings::value(Settings::Log::GuiDebug).toBool()) ||
(ui->cbUseWlClipboard->isChecked() != Settings::value(Settings::Core::UseWlClipboard).toBool()) ||
(ui->cbShowVersion->isChecked() != Settings::value(Settings::Gui::ShowVersionInTitle).toBool()) ||
(ui->rbIconMono->isChecked() != Settings::value(Settings::Gui::SymbolicTrayIcon).toBool()) ||
(ui->groupService->isChecked() != (processMode == Settings::ProcessMode::Service)) ||
@ -479,7 +467,6 @@ bool SettingsDialog::isDefault() const
(ui->cbElevateDaemon->isChecked() == Settings::defaultValue(Settings::Daemon::Elevate).toBool()) &&
(ui->cbAutoUpdate->isChecked() == Settings::defaultValue(Settings::Gui::AutoUpdateCheck).toBool()) &&
(ui->cbGuiDebug->isChecked() == Settings::defaultValue(Settings::Log::GuiDebug).toBool()) &&
(ui->cbUseWlClipboard->isChecked() == Settings::defaultValue(Settings::Core::UseWlClipboard).toBool()) &&
(ui->cbShowVersion->isChecked() == Settings::defaultValue(Settings::Gui::ShowVersionInTitle).toBool()) &&
(ui->rbIconMono->isChecked() == Settings::defaultValue(Settings::Gui::SymbolicTrayIcon).toBool()) &&
(ui->groupService->isChecked() == (processMode == Settings::ProcessMode::Service)) &&
@ -508,7 +495,6 @@ void SettingsDialog::resetToDefault()
ui->cbElevateDaemon->setChecked(Settings::defaultValue(Settings::Daemon::Elevate).toBool());
ui->cbAutoUpdate->setChecked(Settings::defaultValue(Settings::Gui::AutoUpdateCheck).toBool());
ui->cbGuiDebug->setChecked(Settings::defaultValue(Settings::Log::GuiDebug).toBool());
ui->cbUseWlClipboard->setChecked(Settings::defaultValue(Settings::Core::UseWlClipboard).toBool());
ui->cbShowVersion->setChecked(Settings::defaultValue(Settings::Gui::ShowVersionInTitle).toBool());
ui->cbRunEnterCommand->setChecked(Settings::defaultValue(Settings::Core::EnableEnterCommand).toBool());
ui->cbRunExitCommand->setChecked(Settings::defaultValue(Settings::Core::EnableExitCommand).toBool());

View file

@ -807,29 +807,6 @@
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="widgetWlClipboard" native="true">
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QCheckBox" name="cbUseWlClipboard">
<property name="text">
<string>Enable wl-clipboard support</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="lblWlClipboard">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Requires the wl-clipboard package&lt;/p&gt;&lt;p&gt;When using wl-clipboard v2.2.1, there is a focus stealing bug that may make Deskflow harder to use. This has been fixed when using the wl-clipboard master branch, unless your Compositor lacks wlroots-data-control protocol support.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QFormLayout" name="formLayout_3">
<property name="fieldGrowthPolicy">

View file

@ -151,10 +151,6 @@ elseif(UNIX)
EiKeyState.h
EiScreen.cpp
EiScreen.h
WlClipboardCollection.cpp
WlClipboardCollection.h
WlClipboard.cpp
WlClipboard.h
)
# The Portal sources also require EI.
if(LIBPORTAL_FOUND)

View file

@ -1,411 +0,0 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2025 Deskflow Developers
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#include "platform/WlClipboard.h"
#include "base/Log.h"
#include <chrono>
#include <common/Settings.h>
#include <fcntl.h>
#include <poll.h>
#include <spawn.h>
#include <sys/wait.h>
#include <unistd.h>
#include <QDateTime>
#include <QProcess>
#include <QStandardPaths>
namespace {
inline static const auto s_copyApp = QStringLiteral("wl-copy");
inline static const auto s_pasteApp = QStringLiteral("wl-paste");
// wl-clipboard args
inline static const auto s_listTypes = QStringLiteral("--list-types");
inline static const auto s_isPrimary = QStringLiteral("--primary");
inline static const auto s_noNewLine = QStringLiteral("-n");
inline static const auto s_readType = QStringLiteral("-t%1");
// MIME types for different clipboard formats
inline static const auto s_mimeTypeText = QStringLiteral("text/plain;charset=utf-8");
inline static const auto s_mimeTypeHtml = QStringLiteral("text/html");
inline static const auto s_mimeTypeBmp = QStringLiteral("image/bmp");
// Additional HTML MIME type variants
const char *const s_mimeTypeHtmlUtf8 = "text/html;charset=UTF-8";
const char *const s_mimeTypeHtmlWindows = "HTML Format";
// Command timeout (milliseconds)
const int kCacheValidityMs = 100;
const int kMonitorIntervalMs = 1000;
const int kMaxConsecutiveErrors = 5;
} // namespace
WlClipboard::WlClipboard(ClipboardID id) : m_id(id), m_useClipboard(id == kClipboardClipboard)
{
// Initialize cached data
for (int i = 0; i < static_cast<int>(Format::TotalFormats); ++i) {
m_cachedAvailable[i] = false;
}
}
WlClipboard::~WlClipboard()
{
stopMonitoring();
for (auto &cmd : m_runningWlCopies) {
cmd->close();
cmd->waitForFinished(100);
}
m_runningWlCopies.clear();
}
ClipboardID WlClipboard::getID() const
{
return m_id;
}
bool WlClipboard::isAvailable()
{
return !QStandardPaths::findExecutable(s_copyApp).isEmpty() && !QStandardPaths::findExecutable(s_pasteApp).isEmpty();
}
bool WlClipboard::isEnabled()
{
return Settings::value(Settings::Core::UseWlClipboard).toBool();
}
void WlClipboard::startMonitoring()
{
if (m_monitoring) {
return;
}
m_stopMonitoring = false;
m_monitoring = true;
m_monitorThread = std::make_unique<std::thread>(&WlClipboard::monitorClipboard, this);
}
void WlClipboard::stopMonitoring()
{
if (!m_monitoring) {
return;
}
m_stopMonitoring = true;
m_monitoring = false;
if (m_monitorThread && m_monitorThread->joinable()) {
m_monitorThread->join();
}
m_monitorThread.reset();
}
bool WlClipboard::hasChanged() const
{
return m_hasChanged.load();
}
bool WlClipboard::empty()
{
if (!m_open) {
return false;
}
auto cmd = new QProcess(this);
cmd->setProgram(s_copyApp);
m_runningWlCopies.append(cmd);
connect(cmd, &QProcess::finished, this, [&] { m_runningWlCopies.removeAll(cmd); });
QStringList args = {s_noNewLine, ""};
if (!m_useClipboard)
args.prepend(s_isPrimary);
cmd->setArguments(args);
cmd->start();
bool success = cmd->waitForStarted(100);
if (success) {
// Update ownership and cache only if command succeeded
std::scoped_lock<std::mutex> lock(m_cacheMutex);
updateOwnership(true);
invalidateCache();
}
return success;
}
void WlClipboard::add(Format format, const std::string &data)
{
if (!m_open) {
return;
}
if (format == Format::HTML) {
return;
}
auto mimeType = formatToMimeType(format);
if (mimeType.isEmpty()) {
LOG_WARN("unsupported clipboard format: %d", format);
return;
}
auto cmd = new QProcess(this);
cmd->setProgram(s_copyApp);
m_runningWlCopies.append(cmd);
connect(cmd, &QProcess::finished, this, [&] { m_runningWlCopies.removeAll(cmd); });
QStringList args = {s_noNewLine, s_readType.arg(mimeType), QString::fromStdString(data)};
if (!m_useClipboard)
args.prepend(s_isPrimary);
cmd->setArguments(args);
cmd->start();
if (cmd->waitForStarted(100)) {
std::scoped_lock<std::mutex> lock(m_cacheMutex);
updateOwnership(true);
invalidateCache();
}
}
bool WlClipboard::open(Time time) const
{
if (m_open) {
LOG_DEBUG("failed to open clipboard: already opened");
return false;
}
m_open = true;
m_time = time;
return true;
}
void WlClipboard::close() const
{
if (!m_open) {
return;
}
LOG_DEBUG("close clipboard");
m_open = false;
const_cast<WlClipboard *>(this)->invalidateCache();
}
IClipboard::Time WlClipboard::getTime() const
{
return m_time;
}
bool WlClipboard::has(Format format) const
{
if (!m_open) {
return false;
}
std::scoped_lock<std::mutex> lock(m_cacheMutex);
// Check cache validity
Time currentTime = getCurrentTime();
if (m_cached && (currentTime - m_cachedTime) < kCacheValidityMs) {
return m_cachedAvailable[static_cast<int>(format)];
}
if (const auto availableTypes = getAvailableMimeTypes(); availableTypes.isEmpty()) {
// No types available - mark all formats as unavailable
for (int i = 0; i < static_cast<int>(Format::TotalFormats); ++i) {
m_cachedAvailable[i] = false;
m_cachedData[i].clear();
}
} else {
using enum IClipboard::Format;
// Check each format against available types
for (int i = 0; i < static_cast<int>(TotalFormats); ++i) {
auto currentFormat = static_cast<Format>(i);
const auto mimeType = formatToMimeType(currentFormat);
m_cachedAvailable[i] = false;
if (!mimeType.isEmpty()) {
for (const auto &available : availableTypes) {
if (available == mimeType || (currentFormat == Text && available == QStringLiteral("text/plain")) ||
(currentFormat == HTML && available.startsWith(QStringLiteral("text/html")))) {
m_cachedAvailable[i] = true;
break;
}
}
}
}
}
m_cached = true;
m_cachedTime = currentTime;
return m_cachedAvailable[static_cast<int>(format)];
}
std::string WlClipboard::get(Format format) const
{
if (!m_open) {
return std::string();
}
std::scoped_lock<std::mutex> lock(m_cacheMutex);
// Return cached data if available and valid
if (m_cached && m_cachedAvailable[static_cast<int>(format)] && !m_cachedData[static_cast<int>(format)].empty()) {
return m_cachedData[static_cast<int>(format)];
}
auto mimeType = formatToMimeType(format);
if (mimeType.isEmpty()) {
return std::string();
}
QProcess cmd;
cmd.setProgram(s_pasteApp);
QStringList args = {s_noNewLine, s_readType.arg(mimeType)};
if (!m_useClipboard)
args.append(s_isPrimary);
cmd.setArguments(args);
cmd.start();
cmd.waitForFinished();
auto data = cmd.readAll().toStdString();
// Update cache
m_cachedData[static_cast<int>(format)] = data;
m_cachedAvailable[static_cast<int>(format)] = !data.empty();
m_cached = true;
m_cachedTime = getCurrentTime();
return data;
}
QString WlClipboard::formatToMimeType(Format format) const
{
switch (format) {
using enum IClipboard::Format;
case Text:
return s_mimeTypeText;
case HTML:
return s_mimeTypeHtml;
case Bitmap:
return s_mimeTypeBmp;
default:
return {};
}
}
IClipboard::Format WlClipboard::mimeTypeToFormat(const QString &mimeType) const
{
using enum IClipboard::Format;
if (mimeType == s_mimeTypeText || mimeType == QStringLiteral("text/plain")) {
return Text;
}
if (mimeType == s_mimeTypeHtml || mimeType == s_mimeTypeHtmlUtf8 || mimeType == s_mimeTypeHtmlWindows ||
mimeType.contains("text/html")) {
return HTML;
}
if (mimeType == s_mimeTypeBmp) {
return Bitmap;
}
return Text; // Default fallback
}
QStringList WlClipboard::getAvailableMimeTypes() const
{
QProcess cmd;
cmd.setProgram(s_pasteApp);
QStringList args = {s_listTypes};
if (!m_useClipboard)
args.append(s_isPrimary);
cmd.setArguments(args);
cmd.start();
cmd.waitForFinished();
const static QChar newLine = QLatin1Char('\n');
return QString::fromLocal8Bit(cmd.readAll()).split(newLine);
}
void WlClipboard::monitorClipboard()
{
QStringList lastTypes;
int consecutiveErrors = 0;
while (!m_stopMonitoring) {
std::this_thread::sleep_for(std::chrono::milliseconds(kMonitorIntervalMs));
try {
// Check if clipboard content has changed by comparing available types
const auto currentTypes = getAvailableMimeTypes();
// Reset error counter on successful operation
consecutiveErrors = 0;
if (currentTypes != lastTypes) {
m_hasChanged = true;
lastTypes = currentTypes;
// Clear cache when clipboard changes
std::scoped_lock<std::mutex> lock(m_cacheMutex);
invalidateCache();
updateOwnership(false);
}
} catch (const std::exception &e) {
LOG_WARN("clipboard monitoring error: %s", e.what());
if (++consecutiveErrors >= kMaxConsecutiveErrors) {
LOG_ERR("too many consecutive errors in clipboard monitoring, stopping");
break;
}
} catch (...) {
LOG_WARN("clipboard monitoring unknown error");
if (++consecutiveErrors >= kMaxConsecutiveErrors) {
LOG_ERR("too many consecutive errors in clipboard monitoring, stopping");
break;
}
}
}
}
IClipboard::Time WlClipboard::getCurrentTime() const
{
return static_cast<Time>(QDateTime::currentMSecsSinceEpoch());
}
bool WlClipboard::isOwned() const
{
return m_owned;
}
void WlClipboard::resetChanged()
{
m_hasChanged = false;
// Clear cache when resetting change flag to force fresh data retrieval
std::scoped_lock<std::mutex> lock(m_cacheMutex);
invalidateCache();
}
void WlClipboard::updateOwnership(bool owned)
{
m_owned = owned;
}
void WlClipboard::invalidateCache()
{
m_cached = false;
m_cachedTime = 0;
for (int i = 0; i < static_cast<int>(Format::TotalFormats); ++i) {
m_cachedData[i].clear();
m_cachedAvailable[i] = false;
}
}

View file

@ -1,119 +0,0 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2025 Deskflow Developers
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#pragma once
#include "deskflow/ClipboardTypes.h"
#include "deskflow/IClipboard.h"
#include <atomic>
#include <fcntl.h>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <QObject>
#include <QString>
class QProcess;
//! Wayland clipboard implementation using wl-copy/wl-paste
/*!
This class implements clipboard functionality for Wayland environments
by using the wl-clipboard utilities (wl-copy and wl-paste).
*/
class WlClipboard : public QObject, public IClipboard
{
Q_OBJECT
public:
explicit WlClipboard(ClipboardID id);
WlClipboard(WlClipboard const &) = delete;
WlClipboard(WlClipboard &&) = delete;
~WlClipboard() override;
WlClipboard &operator=(WlClipboard const &) = delete;
WlClipboard &operator=(WlClipboard &&) = delete;
//! Get clipboard ID
ClipboardID getID() const;
//! Check if wl-clipboard tools are available
static bool isAvailable();
//! Check if WlClipboard is enabled
static bool isEnabled();
//! Start monitoring clipboard changes
void startMonitoring();
//! Stop monitoring clipboard changes
void stopMonitoring();
//! Check if clipboard has changed
bool hasChanged() const;
//! Reset the changed flag and clear cache
void resetChanged();
// IClipboard overrides
bool empty() override;
void add(Format format, const std::string &data) override;
bool open(Time time) const override;
void close() const override;
Time getTime() const override;
bool has(Format format) const override;
std::string get(Format format) const override;
private:
//! Convert IClipboard format to MIME type
QString formatToMimeType(Format format) const;
//! Convert MIME type to IClipboard format
Format mimeTypeToFormat(const QString &mimeType) const;
//! Get available MIME types from clipboard
QStringList getAvailableMimeTypes() const;
//! Monitor clipboard changes in background thread
void monitorClipboard();
//! Get current clipboard serial/timestamp
Time getCurrentTime() const;
//! Check if we own the clipboard
bool isOwned() const;
//! Update our ownership status
void updateOwnership(bool owned);
//! Invalidate cached clipboard data
void invalidateCache();
private:
ClipboardID m_id;
mutable bool m_open = false;
mutable Time m_time = 0;
mutable Time m_cachedTime = 0;
mutable bool m_owned = false;
mutable std::atomic<bool> m_hasChanged = false;
// Cached clipboard data
mutable std::mutex m_cacheMutex;
mutable bool m_cached = false;
mutable std::string m_cachedData[static_cast<int>(Format::TotalFormats)];
mutable bool m_cachedAvailable[static_cast<int>(Format::TotalFormats)];
// Background monitoring
std::unique_ptr<std::thread> m_monitorThread;
std::atomic<bool> m_monitoring = false;
std::atomic<bool> m_stopMonitoring = false;
// Clipboard selection type (true = clipboard, false = primary)
bool m_useClipboard;
// Hold a ref to running wl-copy processes
QList<QProcess *> m_runningWlCopies;
};

View file

@ -1,138 +0,0 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2025 Deskflow Developers
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#include "platform/WlClipboardCollection.h"
#include "base/Log.h"
#include "deskflow/ClipboardTypes.h"
namespace deskflow {
WlClipboardCollection::WlClipboardCollection()
{
initialize();
}
WlClipboardCollection::~WlClipboardCollection()
{
cleanup();
}
bool WlClipboardCollection::isAvailable() const
{
return m_available;
}
IClipboard *WlClipboardCollection::getClipboard(ClipboardID id) const
{
if (!m_available || id >= m_clipboards.size()) {
return nullptr;
}
return m_clipboards[id].get();
}
bool WlClipboardCollection::hasChanged() const
{
if (!m_available) {
return false;
}
for (const auto &clipboard : m_clipboards) {
if (clipboard && clipboard->hasChanged()) {
return true;
}
}
return false;
}
void WlClipboardCollection::startMonitoring()
{
if (!m_available || m_monitoring) {
return;
}
for (const auto &clipboard : m_clipboards) {
if (clipboard) {
clipboard->startMonitoring();
}
}
m_monitoring = true;
}
void WlClipboardCollection::stopMonitoring()
{
if (!m_available || !m_monitoring) {
return;
}
for (const auto &clipboard : m_clipboards) {
if (clipboard) {
clipboard->stopMonitoring();
}
}
m_monitoring = false;
}
void WlClipboardCollection::resetChanged() const
{
if (!m_available) {
return;
}
for (const auto &clipboard : m_clipboards) {
if (clipboard) {
clipboard->resetChanged();
}
}
}
void WlClipboardCollection::initialize()
{
if (!WlClipboard::isEnabled()) {
LOG_DEBUG("wl-clipboard setting disabled");
return;
}
if (!WlClipboard::isAvailable()) {
LOG_WARN("wl-clipboard tools not found, clipboard functionality disabled");
return;
}
// Create clipboard instances for each clipboard type
m_clipboards.resize(kClipboardEnd);
try {
// Primary clipboard (selection)
m_clipboards[kClipboardSelection] = std::make_unique<WlClipboard>(kClipboardSelection);
// Standard clipboard
m_clipboards[kClipboardClipboard] = std::make_unique<WlClipboard>(kClipboardClipboard);
m_available = true;
LOG_VERBOSE("initialized Wayland clipboard support");
} catch (const std::exception &e) {
LOG_ERR("failed to initialize clipboard: %s", e.what());
cleanup();
m_available = false;
}
}
void WlClipboardCollection::cleanup()
{
if (m_monitoring) {
stopMonitoring();
}
m_clipboards.clear();
m_available = false;
}
} // namespace deskflow

View file

@ -1,61 +0,0 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2025 Deskflow Developers
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#pragma once
#include "deskflow/ClipboardTypes.h"
#include "deskflow/IClipboard.h"
#include "platform/WlClipboard.h"
#include <memory>
#include <vector>
namespace deskflow {
//! Clipboard manager for EiScreen
/*!
This class manages clipboard operations for the EiScreen implementation.
It automatically detects the best available clipboard backend and provides
a unified interface for clipboard operations.
*/
class WlClipboardCollection
{
public:
WlClipboardCollection();
~WlClipboardCollection();
//! Check if clipboard functionality is available
bool isAvailable() const;
//! Get clipboard for specific ID
IClipboard *getClipboard(ClipboardID id) const;
//! Check if any clipboard has changed
bool hasChanged() const;
//! Start monitoring clipboard changes
void startMonitoring();
//! Stop monitoring clipboard changes
void stopMonitoring();
//! Reset change detection
void resetChanged() const;
private:
//! Initialize clipboard backends
void initialize();
//! Cleanup clipboard backends
void cleanup();
private:
std::vector<std::unique_ptr<WlClipboard>> m_clipboards;
bool m_available = false;
bool m_monitoring = false;
};
} // namespace deskflow

View file

@ -34,15 +34,4 @@ elseif(UNIX)
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/src/lib/platform"
)
endif()
# Add Wayland clipboard tests when Wayland support is available
if(LIBEI_FOUND AND LIBPORTAL_FOUND)
create_test(
NAME WlClipboardTests
DEPENDS platform
LIBS base arch
SOURCE WlClipboardTests.cpp
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/src/lib/platform"
)
endif()
endif()

View file

@ -1,377 +0,0 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2025 Deskflow Developers
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#include "WlClipboardTests.h"
#include "common/LogLevel.h"
#include "common/PlatformInfo.h"
#include "deskflow/ClipboardTypes.h"
#include "platform/WlClipboard.h"
#include <chrono>
#include <functional>
#include <thread>
#include <QTest>
void WlClipboardTests::defaultCtor()
{
WlClipboard clipboard(kClipboardClipboard);
QCOMPARE(kClipboardClipboard, clipboard.getID());
WlClipboard primaryClipboard(kClipboardSelection);
QCOMPARE(kClipboardSelection, primaryClipboard.getID());
}
void WlClipboardTests::isAvailable()
{
// This test may fail on systems without wl-clipboard installed
// In CI environments, we might need to skip this test or mock the commands
bool available = WlClipboard::isAvailable();
// We don't assert true/false here because it depends on system setup
// Just verify the method doesn't crash and returns a boolean
QVERIFY(available == true || available == false);
}
void WlClipboardTests::initTestCase()
{
m_arch.init();
m_log.setFilter(LogLevel::Level::Verbose);
// Only run tests if Wayland clipboard tools are available
if (!deskflow::platform::isWayland()) {
QSKIP("not running on wayland skipping Wayland clipboard tests");
}
// Only run tests if Wayland clipboard tools are available
if (!WlClipboard::isAvailable()) {
QSKIP("wl-clipboard tools not available, skipping Wayland clipboard tests");
}
// Test if Wayland commands actually work (not just exist)
WlClipboard testClipboard(kClipboardClipboard);
if (!testClipboard.open(0)) {
QSKIP("Failed to open Wayland clipboard, likely no Wayland session");
}
// Try to clear clipboard - if this fails, we're probably not in a Wayland environment
if (!testClipboard.empty()) {
testClipboard.close();
QSKIP("Wayland clipboard operations not working, skipping tests");
}
testClipboard.close();
}
void WlClipboardTests::cleanupTestCase()
{
// Clean up by emptying clipboards
try {
WlClipboard clipboard(kClipboardClipboard);
if (clipboard.open(0)) {
clipboard.empty();
clipboard.close();
}
} catch (...) {
// Ignore cleanup errors
}
}
void WlClipboardTests::open()
{
WlClipboard clipboard(kClipboardClipboard);
QVERIFY(clipboard.open(0));
// Opening again should return false
QVERIFY(!clipboard.open(1));
clipboard.close();
// Should be able to open again after closing
QVERIFY(clipboard.open(2));
clipboard.close();
}
void WlClipboardTests::empty()
{
WlClipboard clipboard(kClipboardClipboard);
QVERIFY(clipboard.open(0));
// First add some known content
clipboard.add(IClipboard::Format::Text, m_testString);
QVERIFY(clipboard.has(IClipboard::Format::Text));
QCOMPARE(clipboard.get(IClipboard::Format::Text), m_testString);
// Now empty the clipboard
QVERIFY(clipboard.empty());
// Wait for the empty operation to complete and verify our content is gone
// Use longer timeout for clipboard operations
QVERIFY(waitForClipboardEmpty(clipboard, m_testString, 3000));
clipboard.close();
}
void WlClipboardTests::singleFormat()
{
WlClipboard clipboard(kClipboardClipboard);
QVERIFY(clipboard.open(0));
// Clear clipboard first - if this fails, skip test
if (!clipboard.empty()) {
clipboard.close();
QSKIP("Wayland clipboard operations not working in test environment");
}
// Add text data
clipboard.add(IClipboard::Format::Text, m_testString);
// Wait for the data to be available
if (!waitForClipboardContent(clipboard, IClipboard::Format::Text, m_testString)) {
clipboard.close();
QSKIP("Wl-clipboard did not receive contents in time");
}
// Add different text data - this should replace the previous data
if (!clipboard.empty()) {
clipboard.close();
QSKIP("Wayland clipboard clear operations not working in test environment");
}
// Skip the wait for empty since we know clipboard operations aren't fully functional
clipboard.add(IClipboard::Format::Text, m_testString2);
clipboard.close();
}
void WlClipboardTests::multipleFormats()
{
WlClipboard clipboard(kClipboardClipboard);
QVERIFY(clipboard.open(0));
// Clear clipboard first
QVERIFY(clipboard.empty());
// Add text data first
clipboard.add(IClipboard::Format::Text, m_testString);
// Note: wl-clipboard typically handles one format at a time
// So we test formats separately rather than simultaneously
if (!waitForClipboardContent(clipboard, IClipboard::Format::Text, m_testString)) {
clipboard.close();
QSKIP("Wl-clipboard did not receive contents in time");
}
// HTML format is currently not supported in WlClipboard implementation
// So we skip HTML testing and just verify text format works
// Clear and add different text data to test format replacement
QVERIFY(clipboard.empty());
QVERIFY(waitForClipboardEmpty(clipboard, m_testString));
clipboard.add(IClipboard::Format::Text, m_testString2);
if (!waitForClipboardContent(clipboard, IClipboard::Format::Text, m_testString2)) {
clipboard.close();
QSKIP("Wl-clipboard did not receive contents in time");
}
clipboard.close();
}
void WlClipboardTests::hasFormat()
{
WlClipboard clipboard(kClipboardClipboard);
QVERIFY(clipboard.open(0));
// Clear clipboard first
QVERIFY(clipboard.empty());
// Wait for empty to take effect with longer timeout
QVERIFY(waitForClipboardEmpty(clipboard, "any", 3000));
// Initially should not have any formats
QVERIFY(!clipboard.has(IClipboard::Format::Text));
QVERIFY(!clipboard.has(IClipboard::Format::HTML));
QVERIFY(!clipboard.has(IClipboard::Format::Bitmap));
// Add text and verify
clipboard.add(IClipboard::Format::Text, m_testString);
if (!waitForClipboardContent(clipboard, IClipboard::Format::Text, m_testString)) {
clipboard.close();
QSKIP("Wl-clipboard did not receive contents in time");
}
QVERIFY(clipboard.has(IClipboard::Format::Text));
QVERIFY(!clipboard.has(IClipboard::Format::HTML)); // HTML not supported
QVERIFY(!clipboard.has(IClipboard::Format::Bitmap));
clipboard.close();
}
void WlClipboardTests::getTime()
{
WlClipboard clipboard(kClipboardClipboard);
QVERIFY(clipboard.open(100));
// Should return the time passed to open()
QCOMPARE(clipboard.getTime(), static_cast<IClipboard::Time>(100));
clipboard.close();
QVERIFY(clipboard.open(200));
QCOMPARE(clipboard.getTime(), static_cast<IClipboard::Time>(200));
clipboard.close();
}
void WlClipboardTests::monitoring()
{
WlClipboard clipboard(kClipboardClipboard);
QVERIFY(clipboard.open(0));
// Clear clipboard first
QVERIFY(clipboard.empty());
// Start monitoring
clipboard.startMonitoring();
// Initially should not have changed
QVERIFY(!clipboard.hasChanged());
// Make a change to the clipboard using a separate clipboard instance
// to simulate external changes
WlClipboard externalClipboard(kClipboardClipboard);
if (externalClipboard.open(1)) {
externalClipboard.empty();
externalClipboard.add(IClipboard::Format::Text, m_testString);
externalClipboard.close();
}
// Wait for monitoring thread to detect change
auto changeDetected = [&clipboard]() { return clipboard.hasChanged(); };
waitForClipboardCondition(clipboard, changeDetected, 1000);
// Stop monitoring
clipboard.stopMonitoring();
clipboard.close();
// Note: Change detection might not work reliably in all test environments
// This test mainly verifies that monitoring doesn't crash
}
void WlClipboardTests::primaryClipboard()
{
// Test that primary clipboard works independently from regular clipboard
WlClipboard clipboard(kClipboardClipboard);
WlClipboard primaryClipboard(kClipboardSelection);
QVERIFY(clipboard.open(0));
QVERIFY(primaryClipboard.open(1));
// Clear both clipboards
QVERIFY(clipboard.empty());
QVERIFY(primaryClipboard.empty());
// Add different data to each
clipboard.add(IClipboard::Format::Text, m_testString);
primaryClipboard.add(IClipboard::Format::Text, m_testString2);
if (!waitForClipboardContent(clipboard, IClipboard::Format::Text, m_testString) ||
!waitForClipboardContent(primaryClipboard, IClipboard::Format::Text, m_testString2)) {
clipboard.close();
primaryClipboard.close();
QSKIP("Wl-clipboard did not receive contents in time");
}
// Make sure they are different
QCOMPARE_NE(clipboard.get(IClipboard::Format::Text), primaryClipboard.get(IClipboard::Format::Text));
clipboard.close();
primaryClipboard.close();
}
void WlClipboardTests::closeWithoutOpen()
{
WlClipboard clipboard(kClipboardClipboard);
// Should be safe to call close without open
clipboard.close();
// After close, open should work
QVERIFY(clipboard.open(0));
clipboard.close();
}
void WlClipboardTests::addWithoutOpen()
{
WlClipboard clipboard(kClipboardClipboard);
// Should not crash when adding without open
clipboard.add(IClipboard::Format::Text, m_testString);
// Should still be able to open and use normally
QVERIFY(clipboard.open(0));
clipboard.close();
}
void WlClipboardTests::getWithoutOpen()
{
WlClipboard clipboard(kClipboardClipboard);
// Should return empty string when getting without open
std::string result = clipboard.get(IClipboard::Format::Text);
QVERIFY(result.empty());
// Should return false for has() without open
QVERIFY(!clipboard.has(IClipboard::Format::Text));
// Should still be able to open and use normally
QVERIFY(clipboard.open(0));
clipboard.close();
}
bool WlClipboardTests::waitForClipboardCondition(WlClipboard &, std::function<bool()> condition, int timeoutMs)
{
auto startTime = std::chrono::steady_clock::now();
auto timeout = std::chrono::milliseconds(timeoutMs);
while (std::chrono::steady_clock::now() - startTime < timeout) {
if (condition()) {
return true;
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
return false;
}
bool WlClipboardTests::waitForClipboardEmpty(WlClipboard &clipboard, const std::string &previousContent, int timeoutMs)
{
auto condition = [&clipboard, &previousContent]() {
// Check if clipboard is empty or no longer contains our previous content
if (!clipboard.has(IClipboard::Format::Text)) {
return true;
}
std::string currentContent = clipboard.get(IClipboard::Format::Text);
// Consider it empty if content is empty or significantly different
return currentContent.empty() || currentContent != previousContent;
};
return waitForClipboardCondition(clipboard, condition, timeoutMs);
}
bool WlClipboardTests::waitForClipboardContent(
WlClipboard &clipboard, IClipboard::Format format, const std::string &expectedContent, int timeoutMs
)
{
auto condition = [&clipboard, format, &expectedContent]() {
if (!clipboard.has(format)) {
return false;
}
return clipboard.get(format) == expectedContent;
};
return waitForClipboardCondition(clipboard, condition, timeoutMs);
}
QTEST_MAIN(WlClipboardTests)

View file

@ -1,71 +0,0 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2025 Deskflow Developers
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#pragma once
#include "arch/Arch.h"
#include "base/Log.h"
#include "deskflow/ClipboardTypes.h"
#if WINAPI_LIBEI || WINAPI_PORTAL
#include "platform/WlClipboard.h"
#endif
#include <QTest>
#include <functional>
//! Unit tests for WlClipboard class
/*!
Tests the Wayland clipboard implementation that uses wl-copy and wl-paste
utilities for clipboard operations. These tests verify basic functionality
like opening/closing the clipboard, adding/getting data in different formats,
and monitoring clipboard changes.
Note: These tests require wl-clipboard tools to be installed and will be
skipped if they are not available or if Wayland support is not compiled in.
*/
class WlClipboardTests : public QObject
{
Q_OBJECT
private Q_SLOTS:
// Test constructor and basic availability
void defaultCtor();
void isAvailable();
// Core clipboard functionality tests
#if WINAPI_LIBEI || WINAPI_PORTAL
void initTestCase();
void cleanupTestCase();
void open();
void empty();
void singleFormat();
void multipleFormats();
void hasFormat();
void getTime();
void monitoring();
void primaryClipboard();
void closeWithoutOpen();
void addWithoutOpen();
void getWithoutOpen();
#endif
private:
Arch m_arch;
Log m_log;
#if WINAPI_LIBEI || WINAPI_PORTAL
const std::string m_testString = "deskflow test string";
const std::string m_testString2 = "Another test string";
const std::string m_testHtml = "<html><body>Test HTML</body></html>";
// clang-format off
// Helper methods for robust testing
bool waitForClipboardCondition(WlClipboard &clipboard, std::function<bool()> condition, int timeoutMs = 2000);
bool waitForClipboardEmpty(WlClipboard &clipboard, const std::string &previousContent, int timeoutMs = 2000);
bool waitForClipboardContent(WlClipboard &clipboard, IClipboard::Format format, const std::string &expectedContent, int timeoutMs = 2000);
// clang-format on
#endif
};

View file

@ -1260,14 +1260,6 @@ Al habilitar esta opción, se deshabilitará la interfaz gráfica de usuario (GU
<source>Enable GUI debug messages</source>
<translation type="unfinished">Habilitar mensajes de depuración de la interfaz gráfica de usuario</translation>
</message>
<message>
<source>Enable wl-clipboard support</source>
<translation type="unfinished">Habilitar la compatibilidad con wl-clipboard</translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Requires the wl-clipboard package&lt;/p&gt;&lt;p&gt;When using wl-clipboard v2.2.1, there is a focus stealing bug that may make Deskflow harder to use. This has been fixed when using the wl-clipboard master branch, unless your Compositor lacks wlroots-data-control protocol support.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation type="unfinished">&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Requiere el paquete wl-clipboard&lt;/p&gt;&lt;p&gt;Al usar wl-clipboard v2.2.1, existe un error que provoca la pérdida del foco y que puede dificultar el uso de Deskflow. Este error se ha corregido al usar la rama principal de wl-clipboard, a menos que su Compositor no sea compatible con el protocolo wlroots-data-control.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<source>Automatic</source>
<translation type="unfinished">Automática</translation>

View file

@ -1260,14 +1260,6 @@ L&apos;abilitazione di questa impostazione disabiliterà l&apos;interfaccia graf
<source>Enable GUI debug messages</source>
<translation>Abilita i messaggi di debug della GUI</translation>
</message>
<message>
<source>Enable wl-clipboard support</source>
<translation>Abilita il supporto wl-clipboard</translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Requires the wl-clipboard package&lt;/p&gt;&lt;p&gt;When using wl-clipboard v2.2.1, there is a focus stealing bug that may make Deskflow harder to use. This has been fixed when using the wl-clipboard master branch, unless your Compositor lacks wlroots-data-control protocol support.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Richiede il pacchetto wl-clipboard&lt;/p&gt;&lt;p&gt;Quando si utilizza wl-clipboard v2.2.1, si verifica un bug di furto del focus che potrebbe rendere Deskflow più difficile da usare. Questo problema è stato risolto quando si utilizza il ramo master di wl-clipboard, a meno che il proprio compositore non supporti il ​​protocollo wlroots-data-control.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<source>Automatic</source>
<translation type="unfinished">Automatica</translation>

View file

@ -1262,14 +1262,6 @@ Enabling this setting will disable the server config GUI.</source>
<source>Reset to default values</source>
<translation>デフォルト値にリセットする</translation>
</message>
<message>
<source>Enable wl-clipboard support</source>
<translation>wl-clipboard によるクリップボード対応を有効にする</translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Requires the wl-clipboard package&lt;/p&gt;&lt;p&gt;When using wl-clipboard v2.2.1, there is a focus stealing bug that may make Deskflow harder to use. This has been fixed when using the wl-clipboard master branch, unless your Compositor lacks wlroots-data-control protocol support.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;wl-clipboard パッケージが必要です。&lt;/p&gt;&lt;p&gt;wl-clipboard v2.2.1 を使用すると、フォーカス盗用のバグにより Deskflow の使い勝手が悪くなる可能性があります。この問題は wl-clipboard のマスターブランチで修正されていますが、使用しているコンポジターが wlroots-data-control プロトコルに対応している必要があります。&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<source>Automatic</source>
<translation>自動</translation>

View file

@ -1260,14 +1260,6 @@ Enabling this setting will disable the server config GUI.</source>
<source>Reset to default values</source>
<translation type="unfinished">기본값으로 재설정</translation>
</message>
<message>
<source>Enable wl-clipboard support</source>
<translation>wl-clipboard 지원 사용</translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Requires the wl-clipboard package&lt;/p&gt;&lt;p&gt;When using wl-clipboard v2.2.1, there is a focus stealing bug that may make Deskflow harder to use. This has been fixed when using the wl-clipboard master branch, unless your Compositor lacks wlroots-data-control protocol support.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;wl-clipboard 패키지가 필요합니다.&lt;/p&gt;&lt;p&gt;wl-clipboard v2.2.1 사용 시 포커스 탈취 버그로 인해 Deskflow 사용이 어려워질 수 있습니다. 이 문제는 wl-clipboard master 브랜치를 사용하면 해결되지만, 사용 중인 컴포지터가 wlroots-data-control 프로토콜을 지원하지 않으면 해결이 안 될 수 있습니다.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<source>Automatic</source>
<translation type="unfinished">자동</translation>

View file

@ -1202,10 +1202,6 @@ Enabling this setting will disable the server config GUI.</source>
<source>Language</source>
<translation>Язык</translation>
</message>
<message>
<source>Enable wl-clipboard support</source>
<translation>Включить поддержку wl-clipboard</translation>
</message>
<message>
<source>TLS Certificate Regenerated</source>
<translation>Сертификат TLS перевыпущен</translation>
@ -1262,10 +1258,6 @@ Enabling this setting will disable the server config GUI.</source>
<source>Reset to default values</source>
<translation type="unfinished">Сбросить до значений по умолчанию</translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Requires the wl-clipboard package&lt;/p&gt;&lt;p&gt;When using wl-clipboard v2.2.1, there is a focus stealing bug that may make Deskflow harder to use. This has been fixed when using the wl-clipboard master branch, unless your Compositor lacks wlroots-data-control protocol support.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Требуется пакет wl-clipboard. В версии 2.2.1 есть ошибка перехвата фокуса.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<source>Automatic</source>
<translation>Автоматически</translation>

View file

@ -1262,14 +1262,6 @@ Enabling this setting will disable the server config GUI.</source>
<source>Reset to default values</source>
<translation type="unfinished">重置为默认值</translation>
</message>
<message>
<source>Enable wl-clipboard support</source>
<translation>启用 wl-clipboard 支持</translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Requires the wl-clipboard package&lt;/p&gt;&lt;p&gt;When using wl-clipboard v2.2.1, there is a focus stealing bug that may make Deskflow harder to use. This has been fixed when using the wl-clipboard master branch, unless your Compositor lacks wlroots-data-control protocol support.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;需要 wl-clipboard 包&lt;/p&gt;&lt;p&gt;使用 wl-clipboard v2.2.1 时存在一个焦点抢夺 Bug,可能导致 Deskflow 使用不便。该问题已在 wl-clipboard 的 master 分支中修复,除非您的合成器不支持 wlroots-data-control 协议。&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<source>Automatic</source>
<translation type="unfinished">自动的</translation>