diff --git a/examples/test_virtual_output/CMakeLists.txt b/examples/test_virtual_output/CMakeLists.txt index 0d01f963fa..35a16fef24 100644 --- a/examples/test_virtual_output/CMakeLists.txt +++ b/examples/test_virtual_output/CMakeLists.txt @@ -14,11 +14,15 @@ qt_add_executable(${BIN_NAME} virtualoutput.cpp virtualoutputmanager.h virtualoutputmanager.cpp + wlroutputmanager.h + wlroutputmanager.cpp ) qt_generate_wayland_protocol_client_sources(${BIN_NAME} FILES ${TREELAND_PROTOCOLS_DATA_DIR}/treeland-virtual-output-manager-v1.xml + ${TREELAND_PROTOCOLS_DATA_DIR}/treeland-output-manager-v1.xml + ${CMAKE_SOURCE_DIR}/3rdparty/wlroots/protocol/wlr-output-management-unstable-v1.xml ) target_link_libraries(${BIN_NAME} diff --git a/examples/test_virtual_output/main.cpp b/examples/test_virtual_output/main.cpp index c8f30f7841..f927602ee6 100644 --- a/examples/test_virtual_output/main.cpp +++ b/examples/test_virtual_output/main.cpp @@ -5,20 +5,12 @@ #include -// ./test-virtual-output HDMI-A-1 VGA-1 - int main(int argc, char *argv[]) { qputenv("QT_QPA_PLATFORM", "wayland"); QApplication app(argc, argv); - // Print help tip if no screen names provided via argv - if (argc < 3) { - qInfo() << "Tip: Specify screen names to clone:"; - qInfo() << " ./test-virtual-output HDMI-A-1 VGA-1"; - } - - VirtualClient client(argc, argv); + VirtualClient client; return app.exec(); -} +} \ No newline at end of file diff --git a/examples/test_virtual_output/virtualclient.cpp b/examples/test_virtual_output/virtualclient.cpp index 2907d647c7..cb51114def 100644 --- a/examples/test_virtual_output/virtualclient.cpp +++ b/examples/test_virtual_output/virtualclient.cpp @@ -4,140 +4,768 @@ #include "virtualclient.h" #include "virtualoutput.h" #include "virtualoutputmanager.h" - -#include +#include "wlroutputmanager.h" #include +#include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include #include #include +#include #include -VirtualClient::VirtualClient(int argc, char *argv[], QObject *parent) - : QObject(parent) - , m_argc(argc) - , m_argv(argv) +#include +#include + + +TreelandOutputManager::TreelandOutputManager() + : QWaylandClientExtensionTemplate(2) { - m_manager = std::make_unique(); +} - if (m_argc >= 3) { - for (int i = 1; i < m_argc; ++i) { - m_screenNames.append(QString::fromUtf8(m_argv[i])); - } +TreelandOutputManager::~TreelandOutputManager() +{ + if (isInitialized()) + destroy(); +} + +void TreelandOutputManager::instantiate() +{ + initialize(); +} + +struct ::treeland_output_color_control_v1 *TreelandOutputManager::getColorControl(struct ::wl_output *output) +{ + if (!isInitialized()) + return nullptr; + return get_color_control(output); +} + + +class ColorControl : public QObject, public QtWayland::treeland_output_color_control_v1 +{ + Q_OBJECT +public: + ColorControl(struct ::treeland_output_color_control_v1 *obj, QObject *parent = nullptr) + : QObject(parent) + , QtWayland::treeland_output_color_control_v1(obj) + { + } + ~ColorControl() override + { + if (isInitialized()) + destroy(); + } + +Q_SIGNALS: + void brightnessChanged(int percent); + void colorTemperatureChanged(uint32_t kelvin); + +protected: + void treeland_output_color_control_v1_brightness(wl_fixed_t b) override + { + Q_EMIT brightnessChanged(static_cast(std::round(wl_fixed_to_double(b)))); + } + void treeland_output_color_control_v1_color_temperature(uint32_t t) override + { + Q_EMIT colorTemperatureChanged(t); } + void treeland_output_color_control_v1_result(uint32_t) override {} +}; + +VirtualClient::VirtualClient(QObject *parent) + : QObject(parent) +{ + m_virtualManager = std::make_unique(); + m_treelandManager = std::make_unique(); + m_wlrManager = std::make_unique(); - connect(m_manager.get(), &VirtualOutputManager::activeChanged, this, [this]() { - if (m_manager->isActive()) { + connect(m_virtualManager.get(), &VirtualOutputManager::activeChanged, this, [this]() { + if (m_virtualManager->isActive()) setupUi(); - } }); -} + connect(m_virtualManager.get(), &VirtualOutputManager::virtualOutputListReceived, + this, &VirtualClient::onVirtualOutputListReceived); + connect(m_virtualManager.get(), &VirtualOutputManager::virtualOutputModified, + this, &VirtualClient::onVirtualOutputModified); + + connect(m_treelandManager.get(), &TreelandOutputManager::primaryOutputChanged, + this, &VirtualClient::onPrimaryOutputChanged); -VirtualClient::~VirtualClient() = default; + connect(m_wlrManager.get(), &WlrOutputManager::headsChanged, this, &VirtualClient::onWlrHeadsChanged); + connect(m_wlrManager.get(), &WlrOutputManager::applyFinished, this, &VirtualClient::onWlrApplyFinished); -bool VirtualClient::isValid() const + m_virtualManager->instantiate(); + m_treelandManager->instantiate(); + m_wlrManager->instantiate(); +} + +VirtualClient::~VirtualClient() { - return m_manager && m_manager->isInitialized(); + delete m_colorControl; } void VirtualClient::setupUi() { + if (m_widget) + return; + m_widget = new QWidget; - m_widget->setAttribute(Qt::WA_TranslucentBackground); - m_widget->resize(640, 480); + m_widget->resize(640, 700); + + auto *mainLayout = new QVBoxLayout(m_widget); + mainLayout->setSpacing(8); + + m_infoLabel = new QLabel("Loading screen info..."); + m_infoLabel->setWordWrap(true); + mainLayout->addWidget(m_infoLabel); + + auto *selRow = new QHBoxLayout; + selRow->addWidget(new QLabel("Screen:")); + m_screenCombo = new QComboBox; + m_screenCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + selRow->addWidget(m_screenCombo); + + m_primaryBtn = new QPushButton("Primary"); + connect(m_primaryBtn, &QPushButton::clicked, this, &VirtualClient::onPrimaryClicked); + selRow->addWidget(m_primaryBtn); + + m_primaryLabel = new QLabel("Primary: —"); + m_primaryLabel->setStyleSheet("color: #4a90d9;"); + selRow->addWidget(m_primaryLabel); - m_createButton = new QPushButton("Create virtual output", m_widget); - m_createButton->setGeometry(0, 0, 150, 50); + m_disableBtn = new QPushButton("Disable"); + connect(m_disableBtn, &QPushButton::clicked, this, &VirtualClient::onDisableClicked); + selRow->addWidget(m_disableBtn); + selRow->addWidget(new QLabel("Mirror:")); + m_mirrorCombo = new QComboBox; + m_mirrorCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + selRow->addWidget(m_mirrorCombo); + mainLayout->addLayout(selRow); - m_restoreButton = new QPushButton("Restore settings", m_widget); - m_restoreButton->setGeometry(0, 60, 150, 50); + auto *settingsGroup = new QGroupBox("Screen Settings"); + auto *form = new QFormLayout(settingsGroup); + form->setSpacing(6); - m_listButton = new QPushButton("Get virtual output list", m_widget); - m_listButton->setGeometry(0, 120, 150, 50); + m_brightnessSlider = new QSlider(Qt::Horizontal); + m_brightnessSlider->setRange(0, 100); + m_brightnessSlider->setValue(100); + m_brightnessLabel = new QLabel("100%"); + connect(m_brightnessSlider, &QSlider::valueChanged, this, &VirtualClient::onBrightnessChanged); + auto *brightRow = new QHBoxLayout; + brightRow->addWidget(m_brightnessSlider); + brightRow->addWidget(m_brightnessLabel); + form->addRow("Brightness:", brightRow); - connect(m_createButton, &QPushButton::clicked, this, &VirtualClient::onCreateVirtualOutput); - connect(m_restoreButton, &QPushButton::clicked, this, &VirtualClient::onRestoreSettings); - connect(m_listButton, &QPushButton::clicked, this, &VirtualClient::onGetVirtualOutputList); + m_colorTempSlider = new QSlider(Qt::Horizontal); + m_colorTempSlider->setRange(1000, 20000); + m_colorTempSlider->setValue(6500); + m_colorTempLabel = new QLabel("6500K"); + connect(m_colorTempSlider, &QSlider::valueChanged, this, &VirtualClient::onColorTempChanged); + auto *ctRow = new QHBoxLayout; + ctRow->addWidget(m_colorTempSlider); + ctRow->addWidget(m_colorTempLabel); + form->addRow("Color Temp:", ctRow); - connect(m_manager.get(), - &VirtualOutputManager::virtualOutputListReceived, - this, - &VirtualClient::onVirtualOutputListReceived); + m_posX = new QSpinBox; + m_posX->setRange(-99999, 99999); + m_posY = new QSpinBox; + m_posY->setRange(-99999, 99999); + auto *posRow = new QHBoxLayout; + posRow->addWidget(new QLabel("X:")); + posRow->addWidget(m_posX); + posRow->addWidget(new QLabel("Y:")); + posRow->addWidget(m_posY); + form->addRow("Position:", posRow); + m_resolutionCombo = new QComboBox; + form->addRow("Resolution:", m_resolutionCombo); + + m_refreshCombo = new QComboBox; + form->addRow("Refresh:", m_refreshCombo); + + m_rotationCombo = new QComboBox; + m_rotationCombo->addItem("Normal", 0); + m_rotationCombo->addItem("90°", 1); + m_rotationCombo->addItem("180°", 2); + m_rotationCombo->addItem("270°", 3); + m_rotationCombo->addItem("Flipped", 4); + m_rotationCombo->addItem("Flipped 90°", 5); + m_rotationCombo->addItem("Flipped 180°", 6); + m_rotationCombo->addItem("Flipped 270°", 7); + form->addRow("Rotation:", m_rotationCombo); + + m_scaleCombo = new QComboBox; + for (double s : {0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 2.5, 3.0}) + m_scaleCombo->addItem(QString::number(s, 'f', 2), s); + m_scaleCombo->setCurrentText("1.00"); + form->addRow("Scale:", m_scaleCombo); + + mainLayout->addWidget(settingsGroup); + + mainLayout->addStretch(); + + connect(m_screenCombo, QOverload::of(&QComboBox::currentIndexChanged), + this, &VirtualClient::onScreenChanged); + connect(m_mirrorCombo, QOverload::of(&QComboBox::currentIndexChanged), + this, &VirtualClient::onMirrorChanged); + connect(m_resolutionCombo, QOverload::of(&QComboBox::currentIndexChanged), + this, &VirtualClient::onResolutionChanged); + + connect(m_posX, QOverload::of(&QSpinBox::valueChanged), + this, &VirtualClient::applyCurrentSettings); + connect(m_posY, QOverload::of(&QSpinBox::valueChanged), + this, &VirtualClient::applyCurrentSettings); + connect(m_resolutionCombo, QOverload::of(&QComboBox::currentIndexChanged), + this, &VirtualClient::applyCurrentSettings); + connect(m_refreshCombo, QOverload::of(&QComboBox::currentIndexChanged), + this, &VirtualClient::applyCurrentSettings); + connect(m_rotationCombo, QOverload::of(&QComboBox::currentIndexChanged), + this, &VirtualClient::applyCurrentSettings); + connect(m_scaleCombo, QOverload::of(&QComboBox::currentIndexChanged), + this, &VirtualClient::applyCurrentSettings); m_widget->show(); - // Collect available screens - QWindow *window = m_widget->windowHandle(); - QList screens; - if (window && window->handle()) { - QtWaylandClient::QWaylandWindow *waylandWindow = - static_cast(window->handle()); - screens = waylandWindow->display()->screens(); + m_virtualManager->getVirtualOutputList(); + + refreshScreenInfo(); +} - qInfo() << "Available screens:"; - for (auto *screen : screens) { - qInfo() << " Screen name:" << screen->name(); +struct ::wl_output *VirtualClient::getWlOutput(const QString &name) +{ + QWindow *window = m_widget ? m_widget->windowHandle() : nullptr; + if (!window || !window->handle()) + return nullptr; + auto *waylandWindow = static_cast(window->handle()); + auto screens = waylandWindow->display()->screens(); + for (auto *screen : screens) { + if (screen->name() == name) + return screen->output(); + } + return nullptr; +} + +void VirtualClient::refreshScreenInfo() +{ + if (!m_widget) + return; + + QString info; + info += "Physical Screens:
"; + + const auto screens = QGuiApplication::screens(); + for (auto *screen : screens) { + info += QString(" %1 — %2x%3 @ %4Hz, pos(%5,%6), scale %7
") + .arg(screen->name(), + QString::number(screen->geometry().width()), + QString::number(screen->geometry().height()), + QString::number(screen->refreshRate(), 'f', 1), + QString::number(screen->geometry().x()), + QString::number(screen->geometry().y()), + QString::number(screen->devicePixelRatio(), 'f', 1)); + } + + auto wlrHeads = m_wlrManager->heads(); + if (!wlrHeads.isEmpty()) { + info += "
Wlr Heads:
"; + for (const auto &h : wlrHeads) { + info += QString(" %1 — %2x%3 @ %4, pos(%5,%6), scale %7, rot %8
") + .arg(h.name, + h.currentMode >= 0 && h.currentMode < h.modes.size() + ? QString::number(h.modes[h.currentMode].size.width()) + : "?", + h.currentMode >= 0 && h.currentMode < h.modes.size() + ? QString::number(h.modes[h.currentMode].size.height()) + : "?", + h.currentMode >= 0 && h.currentMode < h.modes.size() + ? QString::number(h.modes[h.currentMode].refresh / 1000.0, 'f', 1) + : "?", + QString::number(h.position.x()), + QString::number(h.position.y()), + QString::number(h.scale, 'f', 2), + QString::number(h.transform)); + } + } + + info += "
Mirror Groups:
"; + if (m_mirrorGroups.isEmpty()) { + info += " (none)
"; + } else { + for (auto it = m_mirrorGroups.begin(); it != m_mirrorGroups.end(); ++it) { + const auto &outputs = it.value(); + if (outputs.size() >= 2) { + QStringList mirrors; + for (int i = 1; i < outputs.size(); ++i) + mirrors << outputs[i]; + info += QString(" %1 ← %2
").arg(outputs.first(), mirrors.join(", ")); + } } } + + m_infoLabel->setText(info); + + QStringList live; + for (auto *screen : QGuiApplication::screens()) { + const QString n = screen->name().trimmed(); + if (!n.isEmpty() && n != QLatin1String("screen")) + live << n; + } + for (const auto &h : m_wlrManager->heads()) { + if (!h.name.isEmpty() && !live.contains(h.name)) + live << h.name; + } + m_screenNames = live; + const QStringList names = m_screenNames; + + QStringList existing; + for (int i = 0; i < m_screenCombo->count(); ++i) + existing << m_screenCombo->itemText(i); + if (!names.isEmpty() && existing != names) { + QString current = m_screenCombo->currentText(); + m_screenCombo->blockSignals(true); + m_screenCombo->clear(); + for (const auto &n : names) + m_screenCombo->addItem(n); + if (!current.isEmpty()) + m_screenCombo->setCurrentText(current); + m_screenCombo->blockSignals(false); + if (m_screenCombo->currentIndex() < 0 && m_screenCombo->count() > 0) + m_screenCombo->setCurrentIndex(0); + } + refreshMirrorCombo(); + + if (m_resolutionCombo->count() == 0 && m_screenCombo->currentIndex() >= 0) + onScreenChanged(m_screenCombo->currentIndex()); + + // Re-acquire here: disable/enable can rebind the output while wl_output stays the same. + if (m_screenCombo->currentIndex() >= 0) + acquireColorControl(m_screenCombo->currentText()); } -void VirtualClient::onVirtualOutputListReceived(const QStringList &names){ - qInfo() << names; - for (const auto &name : names) { - auto *obj = m_manager->getVirtualOutput(name); - if (!obj) { - qWarning() << " Failed to get virtual output for:" << name; - continue; +void VirtualClient::onVirtualOutputModified(const QString &name) +{ + Q_UNUSED(name); + m_virtualManager->getVirtualOutputList(); +} + +void VirtualClient::refreshMirrorCombo() +{ + QString sel = m_screenCombo->currentText(); + const QString prev = m_mirrorCombo->currentData().toString(); + + QStringList items; + QList datas; + items << "None (no mirror)"; + datas << QVariant(); + for (int i = 0; i < m_screenCombo->count(); ++i) { + const QString name = m_screenCombo->itemText(i); + if (name != sel) { + items << name; + datas << name; } + } - auto *vo = new VirtualOutput(obj); + if (m_mirrorCombo->count() == items.size()) { + bool same = true; + for (int i = 0; i < items.size(); ++i) { + if (m_mirrorCombo->itemText(i) != items.at(i) + || m_mirrorCombo->itemData(i).toString() != datas.at(i).toString()) { + same = false; + break; + } + } + if (same) { + const int idx = m_mirrorCombo->findData(prev); + const int want = idx >= 0 ? idx : 0; + if (m_mirrorCombo->currentIndex() != want) + m_mirrorCombo->setCurrentIndex(want); + return; + } + } + + m_mirrorCombo->blockSignals(true); + m_mirrorCombo->clear(); + m_mirrorCombo->addItem("None (no mirror)", QString()); + for (int i = 1; i < items.size(); ++i) + m_mirrorCombo->addItem(items.at(i), datas.at(i)); + const int idx = m_mirrorCombo->findData(prev); + m_mirrorCombo->setCurrentIndex(idx >= 0 ? idx : 0); + m_mirrorCombo->blockSignals(false); +} + +void VirtualClient::onScreenChanged(int index) +{ + if (index < 0) + return; + const QString name = m_screenCombo->itemText(index); + + m_updatingUi = true; + + auto wlrHeads = m_wlrManager->heads(); + for (const auto &h : wlrHeads) { + if (h.name == name) { + m_posX->setValue(h.position.x()); + m_posY->setValue(h.position.y()); + m_disableBtn->setText(h.enabled ? "Disable" : "Enable"); + m_resolutionCombo->blockSignals(true); + m_resolutionCombo->clear(); + int preferredIdx = 0; + int currentIdx = 0; + QSize currentModeSize; + if (h.currentMode >= 0 && h.currentMode < h.modes.size()) + currentModeSize = h.modes[h.currentMode].size; + QSet seenSizes; + for (int i = 0; i < h.modes.size(); ++i) { + const auto &mode = h.modes[i]; + if (seenSizes.contains(mode.size)) + continue; + seenSizes.insert(mode.size); + QString label = QString("%1x%2").arg(mode.size.width()).arg(mode.size.height()); + if (mode.preferred) { + label += " (preferred)"; + preferredIdx = m_resolutionCombo->count(); + } + if (mode.size == currentModeSize) + currentIdx = m_resolutionCombo->count(); + m_resolutionCombo->addItem(label, i); + } + m_resolutionCombo->setCurrentIndex(h.currentMode >= 0 ? currentIdx : preferredIdx); + m_resolutionCombo->blockSignals(false); + + onResolutionChanged(m_resolutionCombo->currentIndex()); + + m_rotationCombo->setCurrentIndex(h.transform); + + int scaleIdx = m_scaleCombo->findText(QString::number(h.scale, 'f', 2)); + if (scaleIdx >= 0) + m_scaleCombo->setCurrentIndex(scaleIdx); - // Save as the current virtual output - m_currentOutput.reset(vo); + break; + } + } + + acquireColorControl(name); + + refreshMirrorCombo(); + if (m_screenToGroup.contains(name)) { + QString groupName = m_screenToGroup[name]; + auto outputs = m_mirrorGroups.value(groupName); + if (outputs.size() >= 2 && outputs.first() != name) { + m_mirrorCombo->blockSignals(true); + m_mirrorCombo->setCurrentText(outputs.first()); + m_mirrorCombo->blockSignals(false); + } } + m_primaryBtn->setEnabled(m_mirrorCombo->currentData().toString().isEmpty()); + updatePrimaryButtonText(); + m_updatingUi = false; } -void VirtualClient::onCreateVirtualOutput() +void VirtualClient::onResolutionChanged(int resIndex) { - // If already has a virtual output, do nothing - if (m_currentOutput && m_currentOutput->isInitialized()) { - qInfo() << "Virtual output already exists, skipping creation"; + QString name = m_screenCombo->currentText(); + m_refreshCombo->blockSignals(true); + m_refreshCombo->clear(); + if (resIndex < 0 || name.isEmpty()) { + m_refreshCombo->blockSignals(false); return; } - if (m_screenNames.isEmpty()) { - qInfo() << "No screens available to create virtual output"; + int modeIdx = m_resolutionCombo->currentData().toInt(); + auto wlrHeads = m_wlrManager->heads(); + for (const auto &h : wlrHeads) { + if (h.name == name && modeIdx >= 0 && modeIdx < h.modes.size()) { + const auto &selMode = h.modes[modeIdx]; + QList refreshList; + int currentRefresh = -1; + QSet seenRefresh; + for (int i = 0; i < h.modes.size(); ++i) { + const auto &mode = h.modes[i]; + if (mode.size != selMode.size) + continue; + if (seenRefresh.contains(mode.refresh)) + continue; + seenRefresh.insert(mode.refresh); + m_refreshCombo->addItem(QString::number(mode.refresh / 1000.0, 'f', 2) + "Hz", mode.refresh); + if (i == h.currentMode) + currentRefresh = mode.refresh; + refreshList.append(mode.refresh); + } + if (currentRefresh >= 0) { + int idx = refreshList.indexOf(currentRefresh); + if (idx >= 0) + m_refreshCombo->setCurrentIndex(idx); + } + break; + } + } + m_refreshCombo->blockSignals(false); +} + + +void VirtualClient::onMirrorChanged(int index) +{ + Q_UNUSED(index); + updateMirrorGroup(); + m_primaryBtn->setEnabled(m_mirrorCombo->currentData().toString().isEmpty()); + updatePrimaryButtonText(); +} + +void VirtualClient::onBrightnessChanged(int value) +{ + m_brightnessLabel->setText(QString("%1%").arg(value)); + if (m_colorControl) { + m_colorControl->set_brightness(wl_fixed_from_double(value)); + m_colorControl->commit(); + } +} + +void VirtualClient::onColorTempChanged(int value) +{ + m_colorTempLabel->setText(QString("%1K").arg(value)); + if (m_colorControl) { + m_colorControl->set_color_temperature(static_cast(value)); + m_colorControl->commit(); + } +} + +void VirtualClient::onColorControlBrightness(int percent) +{ + m_brightnessSlider->blockSignals(true); + m_brightnessSlider->setValue(percent); + m_brightnessSlider->blockSignals(false); + m_brightnessLabel->setText(QString("%1%").arg(percent)); +} + +void VirtualClient::onColorControlColorTemp(uint32_t kelvin) +{ + m_colorTempSlider->blockSignals(true); + m_colorTempSlider->setValue(static_cast(kelvin)); + m_colorTempSlider->blockSignals(false); + m_colorTempLabel->setText(QString("%1K").arg(kelvin)); +} + +void VirtualClient::acquireColorControl(const QString &screenName) +{ + if (m_colorControl) { + m_colorControl->destroy(); + delete m_colorControl; + m_colorControl = nullptr; + } + if (!m_treelandManager->isActive()) + return; + auto *wlOut = getWlOutput(screenName); + if (!wlOut) return; + auto *cc = m_treelandManager->getColorControl(wlOut); + if (!cc) + return; + m_colorControl = new ColorControl(cc, this); + connect(m_colorControl, &ColorControl::brightnessChanged, + this, &VirtualClient::onColorControlBrightness); + connect(m_colorControl, &ColorControl::colorTemperatureChanged, + this, &VirtualClient::onColorControlColorTemp); +} +void VirtualClient::applyCurrentSettings() +{ + if (m_updatingUi) + return; + QString name = m_screenCombo->currentText(); + if (name.isEmpty()) + return; + + int x = m_posX->value(); + int y = m_posY->value(); + + QSize modeSize; + int refresh = 0; + int modeIdx = m_resolutionCombo->currentData().toInt(); + auto wlrHeads = m_wlrManager->heads(); + for (const auto &h : wlrHeads) { + if (h.name == name) { + if (modeIdx >= 0 && modeIdx < h.modes.size()) { + modeSize = h.modes[modeIdx].size; + refresh = m_refreshCombo->currentData().toInt(); + } + break; + } } - // 实际使用需要判断主屏(被镜像的屏幕),将主屏放在wl_array的第一个,复制屏幕依次填充 - QByteArray screenNameArray; - for (auto screen : m_screenNames) { - screenNameArray.append(screen.toUtf8()); - screenNameArray.append('\0'); + int transform = m_rotationCombo->currentData().toInt(); + double scale = m_scaleCombo->currentData().toDouble(); + + m_wlrManager->applyConfig(name, true, QPoint(x, y), modeSize, refresh, transform, scale); +} + +void VirtualClient::onDisableClicked() +{ + QString name = m_screenCombo->currentText(); + if (name.isEmpty()) + return; + + bool enabled = false; + const auto wlrHeads = m_wlrManager->heads(); + for (const auto &h : wlrHeads) { + if (h.name == name) { + enabled = h.enabled; + break; + } + } + + if (enabled) { + m_wlrManager->applyConfig(name, false, QPoint(), QSize(), 0, 0, 1.0); + m_disableBtn->setText("Enable"); + } else { + applyCurrentSettings(); + m_disableBtn->setText("Disable"); + } +} + +void VirtualClient::onPrimaryClicked() +{ + QString name = m_screenCombo->currentText(); + if (name.isEmpty()) + return; + if (!m_treelandManager->isActive()) + return; + m_treelandManager->set_primary_output(name); +} + +void VirtualClient::onPrimaryOutputChanged(const QString &name) +{ + m_primaryName = name; + if (m_primaryLabel) { + m_primaryLabel->setText(QString("Primary: %1").arg(name.isEmpty() ? QStringLiteral("—") : name)); + } + updatePrimaryButtonText(); +} + +void VirtualClient::updatePrimaryButtonText() +{ + if (m_primaryBtn) { + const bool isPrimary = !m_primaryName.isEmpty() + && m_screenCombo && m_screenCombo->currentText() == m_primaryName; + m_primaryBtn->setText(isPrimary ? "Primary ✓" : "Primary"); + } +} +void VirtualClient::onWlrHeadsChanged() +{ + m_virtualManager->getVirtualOutputList(); + refreshScreenInfo(); +} + +void VirtualClient::onWlrApplyFinished(bool ok) +{ + Q_UNUSED(ok); + + refreshScreenInfo(); +} + +void VirtualClient::onVirtualOutputListReceived(const QStringList &names) +{ + for (auto it = m_virtualOutputs.begin(); it != m_virtualOutputs.end();) { + if (names.contains(it.key())) { + ++it; + continue; + } + qInfo() << " Dropping group no longer on server:" << it.key(); + if (it.value()) + it.value()->destroy(); + it = m_virtualOutputs.erase(it); + } + m_screenToGroup.clear(); + for (auto it = m_mirrorGroups.begin(); it != m_mirrorGroups.end();) { + if (!names.contains(it.key())) { + it = m_mirrorGroups.erase(it); + continue; + } + for (const auto &screen : it.value()) + m_screenToGroup.insert(screen, it.key()); + ++it; } - screenNameArray.append('\0'); - m_currentOutput.reset( - new VirtualOutput(m_manager->createVirtualOutput("copyscreen1", screenNameArray))); + for (const auto &name : names) { + if (m_virtualOutputs.contains(name)) { + continue; + } + auto *obj = m_virtualManager->getVirtualOutput(name); + if (!obj) { + qWarning() << " Failed to get virtual output for:" << name; + continue; + } + auto *vo = new VirtualOutput(obj); + m_virtualOutputs.insert(name, QSharedPointer(vo)); + connect(vo, &VirtualOutput::outputsReceived, this, &VirtualClient::onVirtualOutputOutputs); + } - qInfo() << "Virtual output created with screens:" << m_screenNames; + refreshScreenInfo(); } -void VirtualClient::onRestoreSettings() +void VirtualClient::onVirtualOutputOutputs(const QString &name, const QStringList &outputs) { - if (m_currentOutput && m_currentOutput->isInitialized()) { - m_currentOutput->destroy(); - m_currentOutput.reset(); - qInfo() << "Virtual output destroyed via restore"; + m_mirrorGroups[name] = outputs; + m_screenToGroup.clear(); + for (auto it = m_mirrorGroups.begin(); it != m_mirrorGroups.end(); ++it) { + for (const auto &screen : it.value()) { + m_screenToGroup.insert(screen, it.key()); + } } + refreshScreenInfo(); } -void VirtualClient::onGetVirtualOutputList() +void VirtualClient::updateMirrorGroup() { - if (m_currentOutput && m_currentOutput->isInitialized()) { - qInfo() << "Use already exit outputs: %s" << m_screenNames; + QString screen = m_screenCombo->currentText(); + QString mirror = m_mirrorCombo->currentData().toString(); + + if (screen.isEmpty()) return; + + if (m_screenToGroup.contains(screen)) { + QString oldGroup = m_screenToGroup[screen]; + if (auto existing = m_virtualOutputs.value(oldGroup)) { + existing->destroy(); + } + m_virtualOutputs.remove(oldGroup); + m_mirrorGroups.remove(oldGroup); + m_screenToGroup.clear(); + for (auto it = m_mirrorGroups.begin(); it != m_mirrorGroups.end(); ++it) { + for (const auto &s : it.value()) + m_screenToGroup.insert(s, it.key()); + } + } + + if (!mirror.isEmpty()) { + QByteArray screenNameArray; + screenNameArray.append(mirror.toUtf8()); + screenNameArray.append('\0'); + screenNameArray.append(screen.toUtf8()); + screenNameArray.append('\0'); + screenNameArray.append('\0'); + + QString groupName = QString("copyscreen_%1").arg(screen); + auto *obj = m_virtualManager->createVirtualOutput(groupName, screenNameArray); + if (obj) { + auto *vo = new VirtualOutput(obj); + m_virtualOutputs.insert(groupName, QSharedPointer(vo)); + connect(vo, &VirtualOutput::outputsReceived, this, &VirtualClient::onVirtualOutputOutputs); + m_mirrorGroups[groupName] = {mirror, screen}; + m_screenToGroup[screen] = groupName; + m_screenToGroup[mirror] = groupName; + } } - m_manager->getVirtualOutputList(); + + refreshScreenInfo(); } + +#include "virtualclient.moc" + diff --git a/examples/test_virtual_output/virtualclient.h b/examples/test_virtual_output/virtualclient.h index ec192fa468..cbdd9bb58d 100644 --- a/examples/test_virtual_output/virtualclient.h +++ b/examples/test_virtual_output/virtualclient.h @@ -2,40 +2,112 @@ // SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #pragma once +#include "qwayland-treeland-virtual-output-manager-v1.h" +#include "qwayland-treeland-output-manager-v1.h" + +#include #include #include -#include - +#include #include class QWidget; +class QLabel; +class QComboBox; +class QSlider; +class QSpinBox; class QPushButton; class VirtualOutputManager; class VirtualOutput; +class WlrOutputManager; +class ColorControl; + +class TreelandOutputManager + : public QWaylandClientExtensionTemplate + , public QtWayland::treeland_output_manager_v1 +{ + Q_OBJECT +public: + explicit TreelandOutputManager(); + ~TreelandOutputManager() override; + + void instantiate(); + + struct ::treeland_output_color_control_v1 *getColorControl(struct ::wl_output *output); + +Q_SIGNALS: + void primaryOutputChanged(const QString &name); + +protected: + void treeland_output_manager_v1_primary_output(const QString &output_name) override + { + Q_EMIT primaryOutputChanged(output_name); + } + +private: +}; class VirtualClient : public QObject { Q_OBJECT public: - explicit VirtualClient(int argc, char *argv[], QObject *parent = nullptr); + explicit VirtualClient(QObject *parent = nullptr); ~VirtualClient(); - bool isValid() const; - private: void setupUi(); - void onCreateVirtualOutput(); - void onRestoreSettings(); - void onGetVirtualOutputList(); + void refreshScreenInfo(); + void refreshMirrorCombo(); + void onScreenChanged(int index); + void onMirrorChanged(int index); + void onDisableClicked(); + void onPrimaryClicked(); + void onResolutionChanged(int resIndex); + void onBrightnessChanged(int value); + void onColorTempChanged(int value); + void onPrimaryOutputChanged(const QString &name); + void updatePrimaryButtonText(); + void applyCurrentSettings(); + void onColorControlBrightness(int percent); + void onColorControlColorTemp(uint32_t kelvin); + void acquireColorControl(const QString &screenName); + void onWlrHeadsChanged(); + void onVirtualOutputModified(const QString &name); + void onWlrApplyFinished(bool ok); void onVirtualOutputListReceived(const QStringList &names); + void onVirtualOutputOutputs(const QString &name, const QStringList &outputs); + + void updateMirrorGroup(); + + struct ::wl_output *getWlOutput(const QString &name); + + std::unique_ptr m_virtualManager; + std::unique_ptr m_treelandManager; + std::unique_ptr m_wlrManager; - std::unique_ptr m_manager; - QSharedPointer m_currentOutput; QWidget *m_widget = nullptr; - QPushButton *m_createButton = nullptr; - QPushButton *m_restoreButton = nullptr; - QPushButton *m_listButton = nullptr; + QLabel *m_infoLabel = nullptr; + QComboBox *m_screenCombo = nullptr; + QComboBox *m_mirrorCombo = nullptr; + QPushButton *m_disableBtn = nullptr; + QPushButton *m_primaryBtn = nullptr; + QLabel *m_primaryLabel = nullptr; + + QSlider *m_brightnessSlider = nullptr; + QLabel *m_brightnessLabel = nullptr; + QSlider *m_colorTempSlider = nullptr; + QLabel *m_colorTempLabel = nullptr; + QSpinBox *m_posX = nullptr; + QSpinBox *m_posY = nullptr; + QComboBox *m_resolutionCombo = nullptr; + QComboBox *m_refreshCombo = nullptr; + QComboBox *m_rotationCombo = nullptr; + QComboBox *m_scaleCombo = nullptr; + ColorControl *m_colorControl = nullptr; QStringList m_screenNames; - int m_argc = 0; - char **m_argv = nullptr; -}; + QString m_primaryName; + bool m_updatingUi = false; + QMap m_mirrorGroups; + QMap m_screenToGroup; + QMap> m_virtualOutputs; +}; \ No newline at end of file diff --git a/examples/test_virtual_output/virtualoutput.cpp b/examples/test_virtual_output/virtualoutput.cpp index b56d73a95a..12303405e2 100644 --- a/examples/test_virtual_output/virtualoutput.cpp +++ b/examples/test_virtual_output/virtualoutput.cpp @@ -16,23 +16,25 @@ VirtualOutput::~VirtualOutput() void VirtualOutput::treeland_virtual_output_v1_outputs(const QString &name, wl_array *outputs) { - if (!outputs || outputs->size == 0) { - qInfo() << " No outputs (not found or empty)"; - return; - } - - char *data = static_cast(outputs->data); - char *end = data + outputs->size; QStringList outputList; - while (data < end && *data != '\0') { - QString output = QString::fromUtf8(data); - outputList << output; - data += output.size() + 1; + if (outputs && outputs->size > 0) { + char *data = static_cast(outputs->data); + char *end = data + outputs->size; + + while (data < end && *data != '\0') { + QString output = QString::fromUtf8(data); + outputList << output; + data += output.size() + 1; + } } - qInfo() << "Screen group name:" << name; - qInfo() << " Outputs:" << outputList; + if (outputList.isEmpty()) { + qInfo() << "Screen group name:" << name << "(not found or empty)"; + } else { + qInfo() << "Screen group name:" << name; + qInfo() << " Outputs:" << outputList; + } Q_EMIT outputsReceived(name, outputList); } diff --git a/examples/test_virtual_output/virtualoutputmanager.cpp b/examples/test_virtual_output/virtualoutputmanager.cpp index af56ecd225..2444401604 100644 --- a/examples/test_virtual_output/virtualoutputmanager.cpp +++ b/examples/test_virtual_output/virtualoutputmanager.cpp @@ -5,7 +5,7 @@ #include -#define VIRTUAL_OUTPUT_MANAGER_V1_VERSION 1 +#define VIRTUAL_OUTPUT_MANAGER_V1_VERSION 3 VirtualOutputManager::VirtualOutputManager() : QWaylandClientExtensionTemplate( @@ -54,19 +54,27 @@ VirtualOutputManager::getVirtualOutput(const QString &name) void VirtualOutputManager::treeland_virtual_output_manager_v1_virtual_output_list( wl_array *names) { - if (!names || names->size == 0) + if (!names) return; - char *data = static_cast(names->data); - char *end = data + names->size; QStringList nameList; - - while (data < end && *data != '\0') { - QString name = QString::fromUtf8(data); - nameList << name; - data += name.size() + 1; + if (names->size > 0) { + char *data = static_cast(names->data); + char *end = data + names->size; + while (data < end && *data != '\0') { + QString name = QString::fromUtf8(data); + nameList << name; + data += name.size() + 1; + } } qInfo() << "Virtual output list:" << nameList; Q_EMIT virtualOutputListReceived(nameList); } + +void VirtualOutputManager::treeland_virtual_output_manager_v1_virtual_output_modified( + const QString &name) +{ + qInfo() << "Virtual output modified:" << name; + Q_EMIT virtualOutputModified(name); +} diff --git a/examples/test_virtual_output/virtualoutputmanager.h b/examples/test_virtual_output/virtualoutputmanager.h index 6231074fb3..3cd2848260 100644 --- a/examples/test_virtual_output/virtualoutputmanager.h +++ b/examples/test_virtual_output/virtualoutputmanager.h @@ -26,7 +26,9 @@ class VirtualOutputManager Q_SIGNALS: void virtualOutputListReceived(const QStringList &names); + void virtualOutputModified(const QString &name); protected: void treeland_virtual_output_manager_v1_virtual_output_list(wl_array *names) override; + void treeland_virtual_output_manager_v1_virtual_output_modified(const QString &name) override; }; diff --git a/examples/test_virtual_output/wlroutputmanager.cpp b/examples/test_virtual_output/wlroutputmanager.cpp new file mode 100644 index 0000000000..a604add6f3 --- /dev/null +++ b/examples/test_virtual_output/wlroutputmanager.cpp @@ -0,0 +1,305 @@ +// SPDX-FileCopyrightText: 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 "wlroutputmanager.h" + +#include "qwayland-wlr-output-management-unstable-v1.h" + +#include + +#include +#include + +#include + +struct ModeEntry { + struct ::zwlr_output_mode_v1 *object = nullptr; + QSize size; + int refresh = 0; + bool preferred = false; +}; + +class ModeTracker : public QtWayland::zwlr_output_mode_v1 +{ +public: + ModeTracker(struct ::zwlr_output_mode_v1 *obj, ModeEntry *entry) + : QtWayland::zwlr_output_mode_v1(obj) + , m_entry(entry) + { + } + + ~ModeTracker() override + { + if (!m_finished && isInitialized()) + release(); + } + +protected: + void zwlr_output_mode_v1_size(int32_t width, int32_t height) override + { + m_entry->size = QSize(width, height); + } + void zwlr_output_mode_v1_refresh(int32_t refresh) override + { + m_entry->refresh = refresh; + } + void zwlr_output_mode_v1_preferred() override + { + m_entry->preferred = true; + } + void zwlr_output_mode_v1_finished() override + { + m_finished = true; + m_entry->object = nullptr; + release(); + } + +private: + ModeEntry *m_entry = nullptr; + bool m_finished = false; +}; + +class HeadTracker : public QtWayland::zwlr_output_head_v1 +{ +public: + HeadTracker(struct ::zwlr_output_head_v1 *obj) + : QtWayland::zwlr_output_head_v1(obj) + { + } + + ~HeadTracker() override + { + if (!m_finished && isInitialized()) + release(); + } + + QString name; + QString description; + bool enabled = false; + QPoint position; + int transform = 0; + double scale = 1.0; + QSize physicalSize; + std::vector> modes; + std::vector> modeTrackers; + int currentModeIndex = -1; + bool finished = false; + +protected: + void zwlr_output_head_v1_name(const QString &name) override { this->name = name; } + void zwlr_output_head_v1_description(const QString &desc) override { this->description = desc; } + void zwlr_output_head_v1_physical_size(int32_t w, int32_t h) override + { + physicalSize = QSize(w, h); + } + void zwlr_output_head_v1_mode(struct ::zwlr_output_mode_v1 *mode) override + { + auto entry = std::make_unique(); + entry->object = mode; + modeTrackers.push_back(std::make_unique(mode, entry.get())); + modes.push_back(std::move(entry)); + } + void zwlr_output_head_v1_enabled(int32_t e) override { enabled = e != 0; } + void zwlr_output_head_v1_current_mode(struct ::zwlr_output_mode_v1 *mode) override + { + for (size_t i = 0; i < modes.size(); ++i) { + if (modes[i]->object == mode) { + currentModeIndex = static_cast(i); + return; + } + } + } + void zwlr_output_head_v1_position(int32_t x, int32_t y) override { position = QPoint(x, y); } + void zwlr_output_head_v1_transform(int32_t t) override { transform = t; } + void zwlr_output_head_v1_scale(wl_fixed_t s) override { scale = wl_fixed_to_double(s); } + void zwlr_output_head_v1_finished() override + { + m_finished = true; + finished = true; + release(); + } + void zwlr_output_head_v1_make(const QString &) override { /* not needed */ } + void zwlr_output_head_v1_model(const QString &) override { /* not needed */ } + void zwlr_output_head_v1_serial_number(const QString &) override { /* not needed */ } + void zwlr_output_head_v1_adaptive_sync(uint32_t) override { /* not needed */ } + +private: + bool m_finished = false; +}; + +class ConfigWatcher : public QObject, public QtWayland::zwlr_output_configuration_v1 +{ + Q_OBJECT +public: + ConfigWatcher(struct ::zwlr_output_configuration_v1 *obj, QObject *parent) + : QObject(parent) + , QtWayland::zwlr_output_configuration_v1(obj) + { + } + +Q_SIGNALS: + void result(bool ok); + +protected: + void zwlr_output_configuration_v1_succeeded() override + { + Q_EMIT result(true); + destroy(); + deleteLater(); + } + void zwlr_output_configuration_v1_failed() override + { + Q_EMIT result(false); + destroy(); + deleteLater(); + } + void zwlr_output_configuration_v1_cancelled() override + { + Q_EMIT result(false); + destroy(); + deleteLater(); + } +}; + + +class WlrOutputManagerPrivate +{ +public: + std::vector> heads; + uint32_t serial = 0; + bool finished = false; +}; + + +WlrOutputManager::WlrOutputManager() + : QWaylandClientExtensionTemplate(4) + , d(std::make_unique()) +{ +} + +WlrOutputManager::~WlrOutputManager() = default; + +void WlrOutputManager::instantiate() +{ + initialize(); +} + +QVector WlrOutputManager::heads() const +{ + QVector result; + for (const auto &ht : d->heads) { + if (ht->name.isEmpty()) + continue; + Head h; + h.name = ht->name; + h.description = ht->description; + h.enabled = ht->enabled; + h.position = ht->position; + h.transform = ht->transform; + h.scale = ht->scale; + h.physicalSize = ht->physicalSize; + int newIdx = 0; + h.currentMode = -1; + for (size_t i = 0; i < ht->modes.size(); ++i) { + const auto &m = ht->modes[i]; + if (!m->object) + continue; + if (static_cast(i) == ht->currentModeIndex) + h.currentMode = newIdx; + h.modes.append({m->size, m->refresh, m->preferred}); + ++newIdx; + } + result.append(h); + } + return result; +} + + +uint32_t WlrOutputManager::serial() const { return d->serial; } +void WlrOutputManager::applyConfig(const QString &headName, + bool enabled, + const QPoint &position, + const QSize &modeSize, + int refresh, + int transform, + double scale) +{ + if (!isActive() || d->heads.empty() || d->finished) { + qWarning() << "WlrOutputManager: applyConfig skipped (manager inactive/finished or no heads)"; + return; + } + + struct ::zwlr_output_configuration_v1 *configObj = create_configuration(d->serial); + if (!configObj) + return; + + auto *watcher = new ConfigWatcher(configObj, this); + connect(watcher, &ConfigWatcher::result, this, &WlrOutputManager::applyFinished); + + for (const auto &ht : d->heads) { + if (ht->name.isEmpty()) + continue; + if (ht->name == headName) { + if (!enabled) { + watcher->disable_head(ht->object()); + } else { + auto *cfgHead = watcher->enable_head(ht->object()); + QtWayland::zwlr_output_configuration_head_v1 cfg(cfgHead); + cfg.set_position(position.x(), position.y()); + bool modeSet = false; + for (const auto &mode : ht->modes) { + if (mode->object && mode->size == modeSize && (refresh <= 0 || mode->refresh == refresh)) { + cfg.set_mode(mode->object); + modeSet = true; + break; + } + } + if (!modeSet && !modeSize.isEmpty()) { + cfg.set_custom_mode(modeSize.width(), modeSize.height(), refresh); + } else if (!modeSet && ht->currentModeIndex >= 0 + && static_cast(ht->currentModeIndex) < ht->modes.size()) { + auto *mObj = ht->modes[ht->currentModeIndex]->object; + if (mObj) + cfg.set_mode(mObj); + } + cfg.set_transform(transform); + cfg.set_scale(wl_fixed_from_double(scale)); + } + } else if (ht->enabled) { + auto *cfgHead = watcher->enable_head(ht->object()); + QtWayland::zwlr_output_configuration_head_v1 cfg(cfgHead); + cfg.set_position(ht->position.x(), ht->position.y()); + if (ht->currentModeIndex >= 0 && static_cast(ht->currentModeIndex) < ht->modes.size()) { + auto *mObj = ht->modes[ht->currentModeIndex]->object; + if (mObj) + cfg.set_mode(mObj); + } + cfg.set_transform(ht->transform); + cfg.set_scale(wl_fixed_from_double(ht->scale)); + } else { + watcher->disable_head(ht->object()); + } + } + watcher->apply(); +} + +void WlrOutputManager::zwlr_output_manager_v1_head(struct ::zwlr_output_head_v1 *head) +{ + d->heads.push_back(std::make_unique(head)); +} + +void WlrOutputManager::zwlr_output_manager_v1_done(uint32_t serial) +{ + d->serial = serial; + d->heads.erase(std::remove_if(d->heads.begin(), d->heads.end(), + [](const auto &ht) { return ht->finished; }), + d->heads.end()); + Q_EMIT headsChanged(); +} + +void WlrOutputManager::zwlr_output_manager_v1_finished() +{ + d->finished = true; +} + +#include "wlroutputmanager.moc" \ No newline at end of file diff --git a/examples/test_virtual_output/wlroutputmanager.h b/examples/test_virtual_output/wlroutputmanager.h new file mode 100644 index 0000000000..c798d82103 --- /dev/null +++ b/examples/test_virtual_output/wlroutputmanager.h @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 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 +#pragma once + +#include "qwayland-wlr-output-management-unstable-v1.h" + +#include +#include +#include +#include + +#include + +class WlrOutputManagerPrivate; + +class WlrOutputManager + : public QWaylandClientExtensionTemplate + , public QtWayland::zwlr_output_manager_v1 +{ + Q_OBJECT +public: + struct Mode { + QSize size; + int refresh = 0; + bool preferred = false; + }; + + struct Head { + QString name; + QString description; + bool enabled = false; + QPoint position; + int transform = 0; + double scale = 1.0; + QSize physicalSize; + QVector modes; + int currentMode = -1; + }; + + explicit WlrOutputManager(); + ~WlrOutputManager() override; + + void instantiate(); + + QVector heads() const; + uint32_t serial() const; + + void applyConfig(const QString &headName, + bool enabled, + const QPoint &position, + const QSize &modeSize, + int refresh, + int transform, + double scale); + +Q_SIGNALS: + void headsChanged(); + void applyFinished(bool ok); + +protected: + void zwlr_output_manager_v1_head(struct ::zwlr_output_head_v1 *head) override; + void zwlr_output_manager_v1_done(uint32_t serial) override; + void zwlr_output_manager_v1_finished() override; + +private: + std::unique_ptr d; +}; \ No newline at end of file diff --git a/src/core/layersurfacecontainer.cpp b/src/core/layersurfacecontainer.cpp index 1496a31375..8878c17b41 100644 --- a/src/core/layersurfacecontainer.cpp +++ b/src/core/layersurfacecontainer.cpp @@ -129,7 +129,12 @@ void LayerSurfaceContainer::addSurfaceToContainer(SurfaceWrapper *surface) return; } auto container = getSurfaceContainer(output); - Q_ASSERT(container); + if (!container) { + qCWarning(lcTlShell) << "No layer surface container for output" << output->name() + << ", will close layer surface!"; + shell->closed(); + return; + } Q_ASSERT(!container->surfaces().contains(surface)); container->addSurface(surface); } diff --git a/src/core/rootsurfacecontainer.cpp b/src/core/rootsurfacecontainer.cpp index b925b466ce..11c980d627 100644 --- a/src/core/rootsurfacecontainer.cpp +++ b/src/core/rootsurfacecontainer.cpp @@ -24,6 +24,8 @@ #include +#include + WAYLIB_SERVER_USE_NAMESPACE namespace { @@ -163,7 +165,6 @@ void RootSurfaceContainer::addOutput(Output *output) void RootSurfaceContainer::removeOutput(Output *output) { m_outputModel->removeObject(output); - SurfaceContainer::removeOutput(output); for (auto *container : std::as_const(m_seatContainers)) { if (container->moveResizeSurface() && @@ -181,6 +182,8 @@ void RootSurfaceContainer::removeOutput(Output *output) } } + SurfaceContainer::removeOutput(output); + // ensure cursor within output const auto outputPos = output->outputItem()->position(); if (output->geometry().contains(m_cursor->position()) && m_primaryOutput) { @@ -403,11 +406,10 @@ Output *RootSurfaceContainer::primaryOutput() const void RootSurfaceContainer::setPrimaryOutput(Output *newPrimaryOutput, bool updateDconfig) { - if (m_primaryOutput == newPrimaryOutput) - return; - - m_primaryOutput = newPrimaryOutput; - Q_EMIT primaryOutputChanged(); + if (m_primaryOutput != newPrimaryOutput) { + m_primaryOutput = newPrimaryOutput; + Q_EMIT primaryOutputChanged(); + } if (updateDconfig) setPrimaryOutputConfig(newPrimaryOutput); } diff --git a/src/core/rootsurfacecontainer.h b/src/core/rootsurfacecontainer.h index 8048c6e8c1..8a2aec7fce 100644 --- a/src/core/rootsurfacecontainer.h +++ b/src/core/rootsurfacecontainer.h @@ -96,6 +96,8 @@ class RootSurfaceContainer : public SurfaceContainer void setPrimaryOutput(Output *newPrimaryOutput, bool updateDconfig = false); const QList &outputs() const; + void ensureCursorVisible(); + void addOutput(Output *output) override; void removeOutput(Output *output) override; @@ -112,6 +114,7 @@ class RootSurfaceContainer : public SurfaceContainer Output *targetOutput, Output *sourceOutput = nullptr); void ensureSurfaceNormalPositionValid(SurfaceWrapper *surface); + void updateSurfaceOutputs(SurfaceWrapper *surface); public Q_SLOTS: void startMove(SurfaceWrapper *surface); @@ -136,8 +139,6 @@ public Q_SLOTS: [[maybe_unused]] SurfaceWrapper::State newState, [[maybe_unused]] SurfaceWrapper::State oldState) override; - void ensureCursorVisible(); - void updateSurfaceOutputs(SurfaceWrapper *surface); QQuickItem *ensureEdgeTilePreview(); void onSeatAdded(WSeat *seat); void onSeatRemoved(WSeat *seat); diff --git a/src/modules/output-manager/outputmanagement.cpp b/src/modules/output-manager/outputmanagement.cpp index 5b56320fe6..58c59e1fb3 100644 --- a/src/modules/output-manager/outputmanagement.cpp +++ b/src/modules/output-manager/outputmanagement.cpp @@ -40,7 +40,11 @@ ColorControlV1Private::ColorControlV1Private(ColorControlV1 *_q, wl_resource *re , q(_q) , controlOutput(output) { + if (!output) + return; auto *outputConfig = output->config(); + if (!outputConfig) + return; send_brightness(outputConfig->brightness()); send_color_temperature(outputConfig->colorTemperature()); @@ -108,10 +112,10 @@ void ColorControlV1Private::send_brightness(qreal brightness) void ColorControlV1Private::commit(Resource *resource) { Q_UNUSED(resource); - if (!controlOutput) { - wl_resource_post_error(resource->handle, - WL_DISPLAY_ERROR_INVALID_OBJECT, - "Output has been destroyed"); + if (!controlOutput) { + // The output has been torn down (e.g. copy-mode restructuring), and + // the client is racing that teardown. Fail silently instead of + // posting a protocol error that kills the whole client. return; } @@ -125,8 +129,8 @@ void ColorControlV1Private::commit(Resource *resource) pendingColorTemperature = 0; } -ColorControlV1::ColorControlV1(wl_resource *resource, Output *output) - : QObject(output) +ColorControlV1::ColorControlV1(wl_resource *resource, Output *output, QObject *parent) + : QObject(parent) , d(new ColorControlV1Private(this, resource, output)) { } @@ -189,21 +193,12 @@ void OutputManagerV1Private::get_color_control(Resource *resource, uint32_t id, struct wl_resource *output) { - auto *wlr_output = wlr_output_from_resource(output); - if (!wlr_output) { - wl_resource_post_error(resource->handle, - WL_DISPLAY_ERROR_INVALID_OBJECT, - "Invalid output resource"); - return; - } - auto *o = Helper::instance()->getOutput(WOutput::fromHandle(wlr_output)); - if (!o) { - wl_resource_post_error(resource->handle, - WL_DISPLAY_ERROR_INVALID_OBJECT, - "Output not found"); - return; - } - + // Always create the color control resource, even when the output is + // unavailable (disabled/being torn down). Clients treat the new id as + // valid and may issue follow-up requests against it; skipping creation + // would make those requests reference a nonexistent object and kill the + // client with an "invalid object" error. A dangling control simply + // ignores commits until re-acquired on a valid output. auto *color_control_res = wl_resource_create(resource->client(), QtWaylandServer::treeland_output_color_control_v1::interface(), OutputManagerV1::ColorControlInterfaceVersion, @@ -213,7 +208,12 @@ void OutputManagerV1Private::get_color_control(Resource *resource, return; } - auto colorControl = new ColorControlV1(color_control_res, o); + auto *wlr_output = wlr_output_from_resource(output); + Output *o = nullptr; + if (wlr_output) { + o = Helper::instance()->getOutput(WOutput::fromHandle(wlr_output)); + } + auto colorControl = new ColorControlV1(color_control_res, o, q); Q_UNUSED(colorControl); } diff --git a/src/modules/output-manager/outputmanagement.h b/src/modules/output-manager/outputmanagement.h index cc158448de..fed1515516 100644 --- a/src/modules/output-manager/outputmanagement.h +++ b/src/modules/output-manager/outputmanagement.h @@ -20,7 +20,7 @@ class ColorControlV1 : public QObject ~ColorControlV1() override; private: - explicit ColorControlV1(wl_resource *resource, Output *output); + explicit ColorControlV1(wl_resource *resource, Output *output, QObject *parent); friend OutputManagerV1Private; std::unique_ptr d; }; diff --git a/src/modules/virtual-output/virtualoutputmanagerinterfacev1.cpp b/src/modules/virtual-output/virtualoutputmanagerinterfacev1.cpp index b8066f068b..a0e9468c8c 100644 --- a/src/modules/virtual-output/virtualoutputmanagerinterfacev1.cpp +++ b/src/modules/virtual-output/virtualoutputmanagerinterfacev1.cpp @@ -95,15 +95,16 @@ class VirtualOutputManagerInterfaceV1Private : public QtWaylandServer::treeland_ const QString &name, wl_array *outputs); - void attachDestroyCleanup(const QString &name, - VirtualOutputInterfaceV1 *virtualOutput); + void attachDestroyCleanup(VirtualOutputInterfaceV1 *virtualOutput); + void handleBeforeDestroy(VirtualOutputInterfaceV1 *virtualOutput); void storeVirtualOutput(const QString &name, const QStringList &outputs, VirtualOutputInterfaceV1 *virtualOutput = nullptr); + void notifyVirtualOutputModified(const QString &name); + protected: - // TODO(YaoBing Xiao): treeland-virtual-output-manager-v1 is missing the 'destroy' request. - // void destroy(Resource *resource) override; + void destroy(Resource *resource) override; void create_virtual_output(Resource *resource, uint32_t id, const QString &name, wl_array *outputs) override; void get_virtual_output_list(Resource *resource) override; void get_virtual_output(Resource *resource, const QString &name, uint32_t id) override; @@ -135,23 +136,46 @@ VirtualOutputInterfaceV1 *VirtualOutputManagerInterfaceV1Private::createVirtualO } auto virtualOutput = new VirtualOutputInterfaceV1(name, outputs, outputResource); - attachDestroyCleanup(name, virtualOutput); + attachDestroyCleanup(virtualOutput); return virtualOutput; } void VirtualOutputManagerInterfaceV1Private::attachDestroyCleanup( - const QString &name, VirtualOutputInterfaceV1 *virtualOutput) { QObject::connect(virtualOutput, &VirtualOutputInterfaceV1::beforeDestroy, - q, &VirtualOutputManagerInterfaceV1::destroyVirtualOutput); - - QObject::connect(virtualOutput, &VirtualOutputInterfaceV1::beforeDestroy, - q, [this, name]() { - m_configs.remove(name); + q, [this, virtualOutput]() { + handleBeforeDestroy(virtualOutput); }); } +void VirtualOutputManagerInterfaceV1Private::handleBeforeDestroy( + VirtualOutputInterfaceV1 *virtualOutput) +{ + for (auto it = m_configs.cbegin(); it != m_configs.cend(); ++it) { + if (it.value().virtualOutput != virtualOutput) + continue; + const QString name = it.key(); + Q_EMIT q->destroyVirtualOutput(virtualOutput); + auto current = m_configs.find(name); + if (current != m_configs.end() && current.value().virtualOutput == virtualOutput) { + m_configs.erase(current); + notifyVirtualOutputModified(name); + } + break; + } +} + +void VirtualOutputManagerInterfaceV1Private::notifyVirtualOutputModified(const QString &name) +{ + const auto resources = resourceMap(); + for (auto *resource : std::as_const(resources)) { + if (resource->version() >= 3) { + send_virtual_output_modified(resource->handle, name); + } + } +} + void VirtualOutputManagerInterfaceV1Private::storeVirtualOutput(const QString &name, const QStringList &outputs, VirtualOutputInterfaceV1 *virtualOutput) @@ -165,12 +189,13 @@ void VirtualOutputManagerInterfaceV1Private::storeVirtualOutput(const QString &n const QByteArray arrSend = outputs.join('\0').toLatin1(); virtualOutput->sendOutputs(name, arrSend); } + notifyVirtualOutputModified(name); } -// void VirtualOutputManagerInterfaceV1Private::destroy(Resource *resource) -// { -// wl_resource_destroy(resource->handle); -// } +void VirtualOutputManagerInterfaceV1Private::destroy(Resource *resource) +{ + wl_resource_destroy(resource->handle); +} void VirtualOutputManagerInterfaceV1Private::create_virtual_output(Resource *resource, uint32_t id, @@ -224,19 +249,12 @@ void VirtualOutputManagerInterfaceV1Private::get_virtual_output(Resource *resour const QString &name, uint32_t id) { - auto it = m_configs.find(name); - if (it == m_configs.end()) { - wl_resource_post_error(resource->handle, 0, - "Virtual output '%s' not found!", name.toUtf8().constData()); - return; - } - - auto &config = it.value(); wl_array arr; wl_array_init(&arr); auto cleanup = qScopeGuard([&arr] { wl_array_release(&arr); }); - if (!stringListToWlArray(config.outputs, &arr)) { + auto it = m_configs.find(name); + if (it != m_configs.end() && !stringListToWlArray(it.value().outputs, &arr)) { wl_client_post_no_memory(resource->client()); return; } @@ -245,9 +263,12 @@ void VirtualOutputManagerInterfaceV1Private::get_virtual_output(Resource *resour if (!virtualOutput) return; - // Send the outputs event to the requesting client - const QByteArray arrSend = config.outputs.join('\0').toLatin1(); - virtualOutput->sendOutputs(name, arrSend); + if (it != m_configs.end()) { + it.value().virtualOutput = virtualOutput; + virtualOutput->sendOutputs(name, it.value().outputs.join('\0').toLatin1()); + } else { + virtualOutput->sendOutputs(name, QByteArray()); + } } VirtualOutputManagerInterfaceV1::VirtualOutputManagerInterfaceV1(QObject *parent) @@ -298,11 +319,14 @@ void VirtualOutputManagerInterfaceV1::updateVirtualOutput(const QString &name, const QByteArray arrSend = outputs.join('\0').toLatin1(); it->virtualOutput->sendOutputs(name, arrSend); } + d->notifyVirtualOutputModified(name); } void VirtualOutputManagerInterfaceV1::removeVirtualOutput(const QString &name) { - d->m_configs.remove(name); + if (d->m_configs.remove(name)) { + d->notifyVirtualOutputModified(name); + } } VirtualOutputInterfaceV1::~VirtualOutputInterfaceV1() = default; diff --git a/src/modules/virtual-output/virtualoutputmanagerinterfacev1.h b/src/modules/virtual-output/virtualoutputmanagerinterfacev1.h index e4ecd5cf15..556ad5597e 100644 --- a/src/modules/virtual-output/virtualoutputmanagerinterfacev1.h +++ b/src/modules/virtual-output/virtualoutputmanagerinterfacev1.h @@ -26,7 +26,7 @@ class VirtualOutputManagerInterfaceV1 void updateVirtualOutput(const QString &name, const QStringList &outputs); void removeVirtualOutput(const QString &name); - static constexpr int InterfaceVersion = 1; + static constexpr int InterfaceVersion = 3; Q_SIGNALS: void requestCreateVirtualOutput(VirtualOutputInterfaceV1 *interface); diff --git a/src/output/outputmanager.cpp b/src/output/outputmanager.cpp index cea0b7f3f6..e480e4557b 100644 --- a/src/output/outputmanager.cpp +++ b/src/output/outputmanager.cpp @@ -144,7 +144,10 @@ OutputManager::CopyModeRestoreConfig OutputManager::copyModeRestoreConfig(int av } result.outputIds = copyOutputIds(); result.outputNames = outputNamesFromIds(result.outputIds); - if (result.outputIds.size() < 2 || result.outputNames.size() < 2) { + if (result.outputIds.size() < 2 || result.outputNames.size() != result.outputIds.size()) { + qCWarning(lcTlOutput) << "Not restoring copy mode: configured output ids are stale" + << "ids:" << result.outputIds + << "resolved:" << result.outputNames; return {}; } result.primaryOutput = findOutputById(result.outputIds.constFirst()); diff --git a/src/seat/helper.cpp b/src/seat/helper.cpp index 410f1c5504..83a862fc89 100644 --- a/src/seat/helper.cpp +++ b/src/seat/helper.cpp @@ -210,6 +210,105 @@ static bool seatConfigInitializationFinished(SeatUserDConfig *config) return config && (config->isInitializeSucceeded() || config->isInitializeFailed()); } +static QSize effectiveOutputLayoutSize(const WOutputState &state) +{ + int width = state.mode ? state.mode->width : state.customModeSize.width(); + int height = state.mode ? state.mode->height : state.customModeSize.height(); + switch (state.transform) { + case WOutput::Transform::R90: + case WOutput::Transform::R270: + case WOutput::Transform::Flipped90: + case WOutput::Transform::Flipped270: + std::swap(width, height); + break; + default: + break; + } + const qreal scale = state.scale > 0.0 ? state.scale : 1.0; + return QSize(qCeil(width / scale), qCeil(height / scale)); +} + +static bool enabledStatesOverlap(const QList &states) +{ + for (int i = 0; i < states.size(); ++i) { + if (!states[i].enabled) + continue; + const QRect a(QPoint(states[i].x, states[i].y), effectiveOutputLayoutSize(states[i])); + for (int j = i + 1; j < states.size(); ++j) { + if (!states[j].enabled) + continue; + const QRect b(QPoint(states[j].x, states[j].y), effectiveOutputLayoutSize(states[j])); + if (a.intersects(b)) + return true; + } + } + return false; +} + +static void deOverlapEnabledStates(QList &states) +{ + for (int round = 0; round < states.size(); ++round) { + QList placed; + bool changed = false; + int rightmostEdge = std::numeric_limits::min(); + for (auto &state : states) { + if (!state.enabled) + continue; + QRect rect(QPoint(state.x, state.y), effectiveOutputLayoutSize(state)); + const bool overlaps = std::any_of(placed.cbegin(), placed.cend(), + [&rect](const QRect &other) { return rect.intersects(other); }); + if (overlaps) { + state.x = rightmostEdge; + state.y = 0; + rect.moveTopLeft(QPoint(state.x, state.y)); + qCInfo(lcTlOutput) << "De-overlap: moved" << state.output->name() + << "to" << rect.topLeft() + << "because its requested position overlapped another enabled screen"; + changed = true; + } + placed.append(rect); + rightmostEdge = qMax(rightmostEdge, rect.right() + 1); + } + if (!changed) + break; + } +} + +static QSize effectiveSavedSize(int width, int height, qlonglong transform, double scale) +{ + switch (transform) { + case WL_OUTPUT_TRANSFORM_90: + case WL_OUTPUT_TRANSFORM_270: + case WL_OUTPUT_TRANSFORM_FLIPPED_90: + case WL_OUTPUT_TRANSFORM_FLIPPED_270: + std::swap(width, height); + break; + default: + break; + } + const qreal s = scale > 0.0 ? scale : 1.0; + return QSize(qCeil(width / s), qCeil(height / s)); +} + +static bool savedPositionOverlapsEnabledOutput(WOutputLayout *layout, + WOutput *output, + const QPoint &pos, + const QSize &size) +{ + if (!layout) + return false; + const QRect savedRect(pos, size); + for (auto *other : layout->outputs()) { + if (other == output || !other->isEnabled()) + continue; + wlr_box box; + wlr_output_layout_get_box(layout->handle(), other->handle(), &box); + if (QRect(box.x, box.y, box.width, box.height).intersects(savedRect)) + return true; + } + return false; +} + static wlr_output_mode *closestOutputMode(WOutput *output, int width, int height, @@ -680,6 +779,8 @@ void Helper::onOutputAdded(WOutput *output) if (!output->isEnabled()) { outputObject->enable(); } + if (auto *layout = m_rootSurfaceContainer->outputLayout()) + layout->pin(output); return; } @@ -706,7 +807,15 @@ void Helper::onOutputAdded(WOutput *output) wlr_output_state_set_enabled(newState.get(), true); if (auto *layout = m_rootSurfaceContainer->outputLayout()) { - layout->move(output, QPoint(static_cast(config->x()), static_cast(config->y()))); + const QPoint savedPos(static_cast(config->x()), static_cast(config->y())); + const QSize savedSize = effectiveSavedSize(width, height, transform, scale); + if (!savedPositionOverlapsEnabledOutput(layout, output, savedPos, savedSize)) { + layout->move(output, savedPos); + } else { + qCInfo(lcTlOutput) << "Saved position for" << output->name() + << savedPos << "overlaps an enabled output; keeping auto position"; + } + layout->pin(output); } if (auto *mode = closestOutputMode(output, width, height, refresh)) { @@ -753,6 +862,7 @@ void Helper::onOutputRemoved(WOutput *output) auto index = indexOfOutput(output); Q_ASSERT(index >= 0); const auto o = m_outputList.takeAt(index); + m_disabledOutputSurfaces.remove(o->getOutputId()); const auto &surfaces = getWorkspaceSurfaces(o); const QStringList copyOutputs = m_outputManagerHelper->copyOutputIds(); @@ -781,32 +891,10 @@ void Helper::onOutputRemoved(WOutput *output) } } - const int newCopySourceIndex = m_outputList.indexOf(newCopySource); - removeOutputFromRootContainer(newCopySource); - Output *normalCopySource = createNormalOutput(newCopySource->output()); - normalCopySource->enable(); - m_outputList.replace(newCopySourceIndex, normalCopySource); - newCopySource->deleteLater(); - - for (int i = 0; i < m_outputList.size(); ++i) { - Output *copyOutput = m_outputList.at(i); - if (copyOutput == normalCopySource - || !copyOutputs.contains(copyOutput->getOutputId())) { - continue; - } - - removeOutputFromRootContainer(copyOutput); - Output *replacement = createCopyOutput(copyOutput->output(), normalCopySource); - replacement->enable(); - m_rootSurfaceContainer->addOutput(replacement); - m_outputList.replace(i, replacement); - copyOutput->deleteLater(); - } - - m_rootSurfaceContainer->setPrimaryOutput(normalCopySource); - if (!surfaces.isEmpty()) { - moveSurfacesToOutput(surfaces, normalCopySource, o); - } + promoteCopyOutputToSource(newCopySource, surfaces, o, + [©Outputs](Output *candidate) { + return copyOutputs.contains(candidate->getOutputId()); + }); removeOutputFromRootContainer(o); // Persist only the active copy group. A subsequently connected @@ -818,35 +906,8 @@ void Helper::onOutputRemoved(WOutput *output) m_mode = OutputMode::Extension; Q_EMIT outputModeChanged(); - QList outputsToConvert; - QList oldOutputsToDelete; - - bool removedWasPrimary = (output == m_rootSurfaceContainer->primaryOutput()->output()); - Output *sourceCandidate = nullptr; - - for (int i = 0; i < m_outputList.size(); i++) { - Output *copyOutput = m_outputList.at(i); - - if (copyOutput->isSource()) { - if (!sourceCandidate) - sourceCandidate = copyOutput; - continue; - } - - removeOutputFromRootContainer(copyOutput); - Output *normalOutput = createNormalOutput(copyOutput->output()); - normalOutput->enable(); - saveCurrentOutputConfig(normalOutput); - - outputsToConvert.append(normalOutput); - oldOutputsToDelete.append(copyOutput); - - m_outputList.replace(i, normalOutput); - - if (!sourceCandidate) { - sourceCandidate = normalOutput; - } - } + const bool removedWasPrimary = (output == m_rootSurfaceContainer->primaryOutput()->output()); + Output *sourceCandidate = convertCopyOutputsToNormal([](Output *) { return false; }); if (removedWasPrimary && sourceCandidate) { m_rootSurfaceContainer->setPrimaryOutput(sourceCandidate); @@ -856,10 +917,6 @@ void Helper::onOutputRemoved(WOutput *output) } removeOutputFromRootContainer(o); - - for (auto oldOutput : std::as_const(oldOutputsToDelete)) { - delete oldOutput; - } } } else { @@ -928,49 +985,103 @@ void Helper::setGamma(struct wlr_gamma_control_manager_v1_set_gamma_event *event } } -void Helper::handleCopyModeOutputDisable(Output *affectedOutput) +bool Helper::handleCopyModeSourceDisabled(Output *disabledSource, + const QList &requestedDisabled) { - int affectedIndex = m_outputList.indexOf(affectedOutput); - if (affectedIndex < 0) { - qCWarning(lcTlCore) << "Disabled output not found in m_outputList"; - return; + Output *newSource = nullptr; + int enabledMirrors = 0; + for (int i = 0; i < m_outputList.size(); ++i) { + Output *output = m_outputList.at(i); + if (output == disabledSource || output->isSource()) + continue; + if (requestedDisabled.contains(output->output()) || !output->output()->isEnabled()) { + continue; + } + enabledMirrors++; + if (!newSource) + newSource = output; } + if (enabledMirrors < 2) + return false; - if (m_outputManagerHelper) { - m_outputManagerHelper->storeCopyOutputConfig(false); - } + const auto &surfaces = getWorkspaceSurfaces(disabledSource); + + promoteCopyOutputToSource( + newSource, + surfaces, + disabledSource, + [this, disabledSource, &requestedDisabled](Output *mirror) { + return mirror != disabledSource + && !mirror->isSource() + && mirror->output()->isEnabled() + && !requestedDisabled.contains(mirror->output()); + }); - m_mode = OutputMode::Extension; - Q_EMIT outputModeChanged(); + return true; +} - // Convert CopyOutputs to Normal outputs (independent displays) - // Keep the disabled output in the list - it will receive disable state through normal wlroots flow - Output *primaryCandidate = nullptr; - const auto &surfaces = getWorkspaceSurfaces(affectedOutput); - for (int i = 0; i < m_outputList.size(); i++) { - if (i == affectedIndex) { +Output *Helper::promoteCopyOutputToSource(Output *promotedMirror, + const QList &surfaces, + Output *surfacesFrom, + const std::function &shouldRetarget) +{ + const int newSourceIndex = m_outputList.indexOf(promotedMirror); + Output *normalSource = createNormalOutput(promotedMirror->output()); + normalSource->enable(); + m_outputList.replace(newSourceIndex, normalSource); + promotedMirror->deleteLater(); + + for (int i = 0; i < m_outputList.size(); ++i) { + Output *mirror = m_outputList.at(i); + if (mirror == normalSource || !shouldRetarget(mirror)) continue; - } + Output *replacement = createCopyOutput(mirror->output(), normalSource); + removeOutputFromRootContainer(mirror); + replacement->enable(); + m_rootSurfaceContainer->addOutput(replacement); + m_outputList.replace(i, replacement); + mirror->deleteLater(); + } + + m_rootSurfaceContainer->setPrimaryOutput(normalSource); + if (!surfaces.isEmpty()) + moveSurfacesToOutput(surfaces, normalSource, surfacesFrom); + return normalSource; +} +Output *Helper::convertCopyOutputsToNormal(const std::function &skip) +{ + Output *newPrimary = nullptr; + for (int i = 0; i < m_outputList.size(); ++i) { Output *copyOutput = m_outputList.at(i); - removeOutputFromRootContainer(copyOutput); + if (skip(copyOutput)) { + continue; + } + if (copyOutput->isSource()) { + if (!newPrimary && copyOutput->output()->isEnabled()) + newPrimary = copyOutput; + continue; + } Output *normalOutput = createNormalOutput(copyOutput->output()); normalOutput->enable(); saveCurrentOutputConfig(normalOutput); + if (!newPrimary) + newPrimary = normalOutput; copyOutput->deleteLater(); m_outputList.replace(i, normalOutput); - - if (!primaryCandidate) { - primaryCandidate = normalOutput; - } } + return newPrimary; +} - if (primaryCandidate) { - if (!surfaces.isEmpty()) { - moveSurfacesToOutput(surfaces, primaryCandidate, affectedOutput); - } - m_rootSurfaceContainer->setPrimaryOutput(primaryCandidate); - } +void Helper::convertCopyModeToExtension(Output *preservedOutput) +{ + m_mode = OutputMode::Extension; + Q_EMIT outputModeChanged(); + + Output *newPrimary = convertCopyOutputsToNormal( + [preservedOutput](Output *copyOutput) { return copyOutput == preservedOutput; }); + if (newPrimary) + m_rootSurfaceContainer->setPrimaryOutput(newPrimary); } void Helper::onOutputTestOrApply(wlr_output_configuration_v1 *config, bool onlyTest) @@ -1034,7 +1145,7 @@ void Helper::onOutputTestOrApply(wlr_output_configuration_v1 *config, bool onlyT } } - if (configsValid && hasNonZeroPosition) { + if (configsValid && hasNonZeroPosition && !enabledStatesOverlap(restoredStates)) { states = std::move(restoredStates); } } @@ -1077,18 +1188,6 @@ void Helper::onOutputTestOrApply(wlr_output_configuration_v1 *config, bool onlyT m_outputManager->sendResult(m_pendingOutputConfig.config, false); } - // Handle Copy Mode transition when primary output is disabled - if (m_mode == OutputMode::Copy) { - for (const auto &state : std::as_const(states)) { - if (!state.enabled) { - Output *affectedOutput = getOutput(state.output); - if (affectedOutput && affectedOutput == m_rootSurfaceContainer->primaryOutput()) { - handleCopyModeOutputDisable(affectedOutput); - break; - } - } - } - } m_pendingOutputConfig.config = config; m_pendingOutputConfig.states = states; @@ -1109,24 +1208,73 @@ void Helper::onOutputTestOrApply(wlr_output_configuration_v1 *config, bool onlyT } } - if (m_mode == OutputMode::Copy) { - // Output-management positions describe independent outputs. Convert copy - // proxies before applying the requested layout so their target-output - // binding cannot keep them overlapping the copy source at (0, 0). - for (int i = 0; i < m_outputList.size(); ++i) { - Output *copyOutput = m_outputList.at(i); - if (copyOutput->isSource()) { + Output *reEnabledCopySource = nullptr; + const QStringList configuredCopyOutputs = m_outputManagerHelper->copyOutputIds(); + if (configuredCopyOutputs.size() >= 2) { + for (const auto &state : std::as_const(states)) { + if (!state.enabled || state.output->isEnabled()) continue; + Output *output = getOutput(state.output); + if (output && output->isSource() + && output->getOutputId() == configuredCopyOutputs.constFirst() + && std::all_of(configuredCopyOutputs.cbegin(), configuredCopyOutputs.cend(), + [this](const QString &id) { return findOutputById(id); })) { + reEnabledCopySource = output; + break; } - - removeOutputFromRootContainer(copyOutput); - Output *normalOutput = createNormalOutput(copyOutput->output()); - copyOutput->deleteLater(); - m_outputList.replace(i, normalOutput); } } - if (m_mode != OutputMode::Extension) { + if (m_mode == OutputMode::Copy) { + Output *disabledSource = nullptr; + QList requestedDisabled; + bool enablingIndependentOutput = false; + bool topologyChanged = false; + for (const auto &state : std::as_const(states)) { + if (state.enabled != state.output->isEnabled()) { + topologyChanged = true; + qCInfo(lcTlOutput) << "[copy-disable] topology change" + << state.output->name() << "req:" << state.enabled + << "cur:" << state.output->isEnabled(); + if (!state.enabled) { + requestedDisabled.append(state.output); + if (Output *output = getOutput(state.output); output && output->isSource()) + disabledSource = output; + } else if (Output *output = getOutput(state.output); + output && output->isSource() + && output != m_rootSurfaceContainer->primaryOutput()) { + enablingIndependentOutput = true; + } + } + } + + if (topologyChanged && disabledSource) { + qCInfo(lcTlOutput) << "[copy-disable] disabling source" << disabledSource->output()->name(); + if (!handleCopyModeSourceDisabled(disabledSource, requestedDisabled)) { + qCInfo(lcTlOutput) << "[copy-disable] no mirror left, collapsing to extension"; + convertCopyModeToExtension(disabledSource); + } else { + qCInfo(lcTlOutput) << "[copy-disable] kept copy mode with new source"; + } + } else if (topologyChanged && (enablingIndependentOutput || reEnabledCopySource)) { + if (reEnabledCopySource) { + qCInfo(lcTlOutput) << "[copy-disable] re-enabling configured copy source" + << reEnabledCopySource->output()->name() + << ", restoring the original copy group"; + m_rootSurfaceContainer->setPrimaryOutput(reEnabledCopySource); + applyCopyModeToOutputs(reEnabledCopySource, getWorkspaceSurfaces(), + configuredCopyOutputs, false); + } else { + qCInfo(lcTlOutput) << "[copy-disable] enabling independent screen, collapsing to extension"; + convertCopyModeToExtension(nullptr); + } + } + } else if (m_mode == OutputMode::Extension && reEnabledCopySource) { + qCInfo(lcTlOutput) << "[copy-disable] re-enabling configured copy source in extension mode" + << reEnabledCopySource->output()->name() + << ", restoring copy mode"; + restoreConfiguredCopyMode(); + } else if (m_mode != OutputMode::Extension) { m_mode = OutputMode::Extension; Q_EMIT outputModeChanged(); } @@ -1147,6 +1295,15 @@ void Helper::onOutputTestOrApply(wlr_output_configuration_v1 *config, bool onlyT if (!state.enabled && state.output->isEnabled()) { const auto &surfaces = getWorkspaceSurfaces(outputObj); + DisabledOutputSurfaces &owned = m_disabledOutputSurfaces[outputObj->getOutputId()]; + owned.surfaces.clear(); + owned.positions.clear(); + for (auto *surface : surfaces) { + if (!surface) + continue; + owned.surfaces.append(surface); + owned.positions.append(surface->position()); + } m_outputManagerHelper->onScreenDisabled(outputObj, surfaces); } else if (state.enabled && !state.output->isEnabled()) { m_outputManagerHelper->clearCopyModeRestoreIntent(); @@ -1154,6 +1311,11 @@ void Helper::onOutputTestOrApply(wlr_output_configuration_v1 *config, bool onlyT } } + if (m_mode == OutputMode::Extension) { + deOverlapEnabledStates(states); + m_pendingOutputConfig.states = states; + } + for (const auto &state : std::as_const(states)) { // Skip outputs that have been removed (e.g., disabled in Copy mode) Output *output = getOutput(state.output); @@ -1225,6 +1387,8 @@ void Helper::onOutputTestOrApply(wlr_output_configuration_v1 *config, bool onlyT } } + if (!state.enabled) + qCInfo(lcTlOutput) << "[copy-disable] committing disable for" << state.output->name(); if (!outputHelper->setExtraState(extraState)) { qCWarning(lcTlCore) << "Failed to set extra state for output" << state.output->name(); m_outputManager->sendResult(config, false); @@ -1248,7 +1412,7 @@ void Helper::onOutputTestOrApply(wlr_output_configuration_v1 *config, bool onlyT return; } - if (committedState == extraState) { + auto syncLayoutAndRefresh = [&] { if (success && output) { auto *layout = self->m_rootSurfaceContainer->outputLayout(); if (layout && enabled && !layout->outputs().contains(output)) { @@ -1257,23 +1421,61 @@ void Helper::onOutputTestOrApply(wlr_output_configuration_v1 *config, bool onlyT layout->remove(output); } } - self->onOutputCommitFinished(config, success); if (success && committedState) { - bool wasStateOnlyCommit = (committedState->committed & (WLR_OUTPUT_STATE_MODE | - WLR_OUTPUT_STATE_SCALE | - WLR_OUTPUT_STATE_TRANSFORM | - WLR_OUTPUT_STATE_ENABLED)) && - !(committedState->committed & WLR_OUTPUT_STATE_BUFFER); - bool isDisable = (committedState->committed & WLR_OUTPUT_STATE_ENABLED) && !committedState->enabled; + const bool wasStateOnlyCommit = (committedState->committed & (WLR_OUTPUT_STATE_MODE | + WLR_OUTPUT_STATE_SCALE | + WLR_OUTPUT_STATE_TRANSFORM | + WLR_OUTPUT_STATE_ENABLED)) && + !(committedState->committed & WLR_OUTPUT_STATE_BUFFER); + const bool isDisable = (committedState->committed & WLR_OUTPUT_STATE_ENABLED) && !committedState->enabled; if (wasStateOnlyCommit && !isDisable) { renderWindow->update(viewport); } } + }; + + if (committedState == extraState) { + syncLayoutAndRefresh(); + self->onOutputCommitFinished(config, success); } else { - qCWarning(lcTlCore) << "Commit callback received unexpected state pointer!" - << "Expected:" << extraState.get() - << "Got:" << committedState.get(); - self->onOutputCommitFinished(config, false); + const bool actualMatches = [&] { + if (!output || output->isEnabled() != enabled || !committedState) + return false; + auto *wlrOutput = output->handle(); + if (!wlrOutput) + return false; + if ((extraState->committed & WLR_OUTPUT_STATE_MODE)) { + if (extraState->mode) { + if (wlrOutput->current_mode != extraState->mode) + return false; + } else if (!wlrOutput->current_mode + || wlrOutput->current_mode->width != extraState->custom_mode.width + || wlrOutput->current_mode->height != extraState->custom_mode.height + || wlrOutput->current_mode->refresh != extraState->custom_mode.refresh) { + return false; + } + } + if ((extraState->committed & WLR_OUTPUT_STATE_SCALE) + && wlrOutput->scale != extraState->scale) + return false; + if ((extraState->committed & WLR_OUTPUT_STATE_TRANSFORM) + && wlrOutput->transform != static_cast(extraState->transform)) + return false; + return true; + }(); + if (success && actualMatches) { + syncLayoutAndRefresh(); + qCInfo(lcTlOutput) << "Commit state superseded; actual output state matches intent" + << (output ? output->name() : QStringLiteral("")); + self->onOutputCommitFinished(config, true); + } else { + qCWarning(lcTlCore) << "Commit callback received unexpected state pointer!" + << "output:" << (output ? output->name() : QStringLiteral("")) + << "enabled:" << enabled + << "Expected:" << extraState.get() + << "Got:" << committedState.get(); + self->onOutputCommitFinished(config, false); + } } }, WOutputHelper::AfterCommitStage @@ -1308,10 +1510,8 @@ void Helper::onOutputCommitFinished(wlr_output_configuration_v1 *config, bool su if (m_pendingOutputConfig.pendingCommits == 0) { bool ok = m_pendingOutputConfig.allSuccess; if (ok) { - m_outputManagerHelper->storeSingleOutputConfig(); - // An output-management enable/disable transaction describes an - // extension/single-output topology, never a copy topology. - m_outputManagerHelper->storeCopyOutputConfig(false); + if (!m_globalConfig->createCopyOutput()) + m_outputManagerHelper->storeSingleOutputConfig(); const auto enabledOutputCount = std::count_if( m_pendingOutputConfig.states.cbegin(), @@ -1330,6 +1530,31 @@ void Helper::onOutputCommitFinished(wlr_output_configuration_v1 *config, bool su m_outputManagerHelper->onScreenEnabled(output); } + if (state.enabled) { + const QString outputId = output->getOutputId(); + const auto it = m_disabledOutputSurfaces.find(outputId); + if (it != m_disabledOutputSurfaces.end()) { + if (m_mode == OutputMode::Extension) { + const auto &recorded = it.value(); + for (int i = 0; i < recorded.surfaces.size(); ++i) { + SurfaceWrapper *surface = recorded.surfaces.at(i); + if (!surface) + continue; + surface->setPosition(recorded.positions.at(i)); + if (surface->ownsOutput() != output) + surface->setOwnsOutput(output); + m_rootSurfaceContainer->updateSurfaceOutputs(surface); + } + } + m_disabledOutputSurfaces.erase(it); + } + } + + if (state.enabled) { + if (auto *layout = m_rootSurfaceContainer->outputLayout()) + layout->pin(state.output); + } + auto *outputConfig = output->config(); const bool enabled = state.enabled; const bool preservePosition = enabled && enabledOutputCount == 1; @@ -1566,8 +1791,8 @@ void Helper::onSetCopyOutput(VirtualOutputInterfaceV1 *interface) if (m_rootSurfaceContainer->primaryOutput() == currentOutput) m_rootSurfaceContainer->setPrimaryOutput(mirrorOutput); - removeOutputFromRootContainer(currentOutput); Output *o = createCopyOutput(currentOutput->output(), mirrorOutput); + removeOutputFromRootContainer(currentOutput); currentOutput->deleteLater(); m_outputList.replace(i, o); m_rootSurfaceContainer->addOutput(o); @@ -1589,6 +1814,9 @@ void Helper::onSetCopyOutput(VirtualOutputInterfaceV1 *interface) void Helper::onRestoreCopyOutput(VirtualOutputInterfaceV1 *interface) { + if (interface->outputList().isEmpty()) { + return; + } const QString targetName = interface->outputList().at(0); if (!std::any_of(m_outputList.constBegin(), m_outputList.constEnd(), [&targetName](const Output *output) { return output->output()->name() == targetName; })) { @@ -1603,7 +1831,6 @@ void Helper::onRestoreCopyOutput(VirtualOutputInterfaceV1 *interface) if (currentOutput->output()->name() == targetName) continue; - removeOutputFromRootContainer(currentOutput); Output *o = createNormalOutput(currentOutput->output()); o->enable(); saveCurrentOutputConfig(o); @@ -3417,12 +3644,11 @@ void Helper::setOutputMode(OutputMode mode) continue; Output *o = nullptr; if (mode == OutputMode::Copy) { - removeOutputFromRootContainer(m_outputList.at(i)); o = createCopyOutput(m_outputList.at(i)->output(), m_rootSurfaceContainer->primaryOutput()); + removeOutputFromRootContainer(m_outputList.at(i)); m_rootSurfaceContainer->addOutput(o); } else if (mode == OutputMode::Extension) { - removeOutputFromRootContainer(m_outputList.at(i)); o = createNormalOutput(m_outputList.at(i)->output()); o->enable(); saveCurrentOutputConfig(o); @@ -3750,6 +3976,8 @@ Output *Helper::getOutputAtCursor() const { QPoint cursorPos = QCursor::pos(); for (auto output : std::as_const(m_outputList)) { + if (!output->output() || !output->output()->isEnabled()) + continue; QRectF outputGeometry(output->outputItem()->position(), output->outputItem()->size()); if (outputGeometry.contains(cursorPos)) { return output; @@ -3903,8 +4131,8 @@ void Helper::applyCopyModeToOutputs(Output *primaryOutput, continue; } - removeOutputFromRootContainer(existingOutput); Output *copyOutput = createCopyOutput(existingOutput->output(), primaryOutput); + removeOutputFromRootContainer(existingOutput); existingOutput->deleteLater(); m_outputList.replace(i, copyOutput); m_rootSurfaceContainer->addOutput(copyOutput); @@ -3926,6 +4154,8 @@ void Helper::applyCopyModeToOutputs(Output *primaryOutput, if (!surfaces.isEmpty()) { moveSurfacesToOutput(surfaces, primaryOutput, nullptr); } + + m_rootSurfaceContainer->ensureCursorVisible(); } bool Helper::restoreConfiguredCopyMode() @@ -4008,8 +4238,16 @@ void Helper::restoreExtensionModeFromConfig(bool preserveSingleOutputConfig) } if (auto *layout = m_rootSurfaceContainer->outputLayout()) { - layout->move(output, QPoint(static_cast(config->x()), - static_cast(config->y()))); + const QPoint savedPos(static_cast(config->x()), + static_cast(config->y())); + const QSize savedSize = effectiveSavedSize(width, height, transform, scale); + if (!savedPositionOverlapsEnabledOutput(layout, output, savedPos, savedSize)) { + layout->move(output, savedPos); + } else { + qCInfo(lcTlOutput) << "Saved extension position for" << output->name() + << savedPos << "overlaps an enabled output; keeping auto position"; + } + layout->pin(output); } WOutputStateGuard state; diff --git a/src/seat/helper.h b/src/seat/helper.h index a9211ec6c2..744565041e 100644 --- a/src/seat/helper.h +++ b/src/seat/helper.h @@ -30,9 +30,12 @@ #include #include +#include +#include #include #include #include +#include #include @@ -381,7 +384,21 @@ private Q_SLOTS: void moveSurfacesToOutput(const QList &surfaces, Output *targetOutput, Output *sourceOutput); - void handleCopyModeOutputDisable(Output *affectedOutput); + + struct DisabledOutputSurfaces { + QVector> surfaces; + QVector positions; + }; + QHash m_disabledOutputSurfaces; + + bool handleCopyModeSourceDisabled(Output *disabledSource, + const QList &requestedDisabled); + Output *promoteCopyOutputToSource(Output *promotedMirror, + const QList &surfaces, + Output *surfacesFrom, + const std::function &shouldRetarget); + Output *convertCopyOutputsToNormal(const std::function &skip); + void convertCopyModeToExtension(Output *preservedOutput); bool restoreConfiguredCopyMode(); void restoreExtensionModeFromConfig(bool preserveSingleOutputConfig = false); void restoreInitialOutputConfiguration(); diff --git a/waylib/src/server/kernel/woutputlayout.cpp b/waylib/src/server/kernel/woutputlayout.cpp index 826c9c842f..d8e9adda57 100644 --- a/waylib/src/server/kernel/woutputlayout.cpp +++ b/waylib/src/server/kernel/woutputlayout.cpp @@ -4,6 +4,7 @@ #include "woutputlayout.h" #include "private/woutputlayout_p.h" #include "woutput.h" +#include "wayliblogging.h" #include @@ -118,6 +119,26 @@ void WOutputLayout::move(WOutput *output, const QPoint &pos) d->updateImplicitSize(); } +void WOutputLayout::pin(WOutput *output) +{ + W_D(WOutputLayout); + Q_ASSERT(d->outputs.contains(output)); + Q_ASSERT(output->layout() == this); + + auto *l_output = wlr_output_layout_get(d->handle(), output->handle()); + if (Q_UNLIKELY(!l_output)) { + qCDebug(lcWlOutput) << "pin() skipped: output not in wlr layout" << output->name(); + return; + } + if (!l_output->auto_configured) { + qCDebug(lcWlOutput) << "pin() skipped: already manually positioned" << output->name(); + return; + } + + wlr_output_layout_add(d->handle(), output->handle(), l_output->x, l_output->y); + d->updateImplicitSize(); +} + void WOutputLayout::remove(WOutput *output) { W_D(WOutputLayout); diff --git a/waylib/src/server/kernel/woutputlayout.h b/waylib/src/server/kernel/woutputlayout.h index f642e2e1a1..507d601656 100644 --- a/waylib/src/server/kernel/woutputlayout.h +++ b/waylib/src/server/kernel/woutputlayout.h @@ -42,6 +42,8 @@ class WAYLIB_SERVER_EXPORT WOutputLayout : public QObject, public WObject void move(WOutput *output, const QPoint &pos); void remove(WOutput *output); + void pin(WOutput *output); + QList getIntersectedOutputs(const QRect &geometry) const; int implicitWidth() const; diff --git a/waylib/src/server/qtquick/woutputrenderwindow.cpp b/waylib/src/server/qtquick/woutputrenderwindow.cpp index da5eac2af7..5551c8730c 100644 --- a/waylib/src/server/qtquick/woutputrenderwindow.cpp +++ b/waylib/src/server/qtquick/woutputrenderwindow.cpp @@ -709,8 +709,10 @@ wlr_buffer *OutputHelper::renderLayer(LayerData *layer, bool *dontEndRenderAndRe } else { // Limitation max buffer const auto maxSize = qMax(source->width(), source->height()) * dpr; - pixelSize.rwidth() = qCeil(qMin(tmpSize.width(), maxSize)); - pixelSize.rheight() = qCeil(qMin(tmpSize.height(), maxSize)); + const qreal limitW = maxSize > 0 ? qMin(tmpSize.width(), maxSize) : tmpSize.width(); + const qreal limitH = maxSize > 0 ? qMin(tmpSize.height(), maxSize) : tmpSize.height(); + pixelSize.rwidth() = qCeil(limitW); + pixelSize.rheight() = qCeil(limitH); } if (mapRect.isEmpty()) {