diff --git a/kded/CMakeLists.txt b/kded/CMakeLists.txt --- a/kded/CMakeLists.txt +++ b/kded/CMakeLists.txt @@ -46,6 +46,7 @@ set(QML_FILES qml/Osd.qml qml/OsdItem.qml + qml/OsdSelector.qml qml/OutputIdentifier.qml ) diff --git a/kded/daemon.h b/kded/daemon.h --- a/kded/daemon.h +++ b/kded/daemon.h @@ -26,6 +26,7 @@ #include #include "generator.h" +#include "osdmanager.h" class QTimer; @@ -61,6 +62,7 @@ void setMonitorForChanges(bool enabled); void outputConnectedChanged(); void showOutputIdentifier(); + void applyOsdAction(KScreen::OsdAction *self, KScreen::OsdAction::Action action); Q_SIGNALS: void outputConnected(const QString &outputName); diff --git a/kded/daemon.cpp b/kded/daemon.cpp --- a/kded/daemon.cpp +++ b/kded/daemon.cpp @@ -96,6 +96,8 @@ connect(action, &QAction::triggered, [&](bool) { displayButton(); }); new KScreenAdaptor(this); + // Initialize OSD manager to register its dbus interface + KScreen::OsdManager::self(); m_buttonTimer->setInterval(300); m_buttonTimer->setSingleShot(true); @@ -181,10 +183,49 @@ doApplyConfig(config); } +void KScreenDaemon::applyOsdAction(KScreen::OsdAction *self, KScreen::OsdAction::Action action) +{ + self->deleteLater(); + + switch (action) { + case KScreen::OsdAction::NoAction: + qCDebug(KSCREEN_KDED) << "OSD: no action"; + return; + case KScreen::OsdAction::SwitchToInternal: + qCDebug(KSCREEN_KDED) << "OSD: swutch to internal"; + doApplyConfig(Generator::self()->displaySwitch(Generator::TurnOffExternal)); + return; + case KScreen::OsdAction::SwitchToExternal: + qCDebug(KSCREEN_KDED) << "OSD: switch to external"; + doApplyConfig(Generator::self()->displaySwitch(Generator::TurnOffEmbedded)); + return; + case KScreen::OsdAction::ExtendLeft: + qCDebug(KSCREEN_KDED) << "OSD: extend left"; + doApplyConfig(Generator::self()->displaySwitch(Generator::ExtendToLeft)); + return; + case KScreen::OsdAction::ExtendRight: + qCDebug(KSCREEN_KDED) << "OSD: extend right"; + doApplyConfig(Generator::self()->displaySwitch(Generator::ExtendToRight)); + return; + case KScreen::OsdAction::Clone: + qCDebug(KSCREEN_KDED) << "OSD: clone"; + doApplyConfig(Generator::self()->displaySwitch(Generator::Clone)); + return; + } + + Q_UNREACHABLE(); +} + void KScreenDaemon::applyIdealConfig() { - qCDebug(KSCREEN_KDED) << "Applying ideal config"; - doApplyConfig(Generator::self()->idealConfig(m_monitoredConfig)); + if (m_monitoredConfig->connectedOutputs().count() < 2) { + doApplyConfig(Generator::self()->idealConfig(m_monitoredConfig)); + } else { + qCDebug(KSCREEN_KDED) << "Getting ideal config from user..."; + auto action = KScreen::OsdManager::self()->showActionSelector(); + connect(action, &KScreen::OsdAction::selected, + this, &KScreenDaemon::applyOsdAction); + } } void logConfig(const KScreen::ConfigPtr &config) { diff --git a/kded/osd.h b/kded/osd.h --- a/kded/osd.h +++ b/kded/osd.h @@ -26,6 +26,8 @@ #include +#include "osdmanager.h" + namespace KDeclarative { class QmlObject; } @@ -44,14 +46,21 @@ void showGenericOsd(const QString &icon, const QString &text); void showOutputIdentifier(const KScreen::OutputPtr output); + void showActionSelector(); + void hideOsd(); + +Q_SIGNALS: + void osdActionSelected(OsdAction::Action action); + +private Q_SLOTS: + void onOsdActionSelected(int action); + void onOutputAvailabilityChanged(); private: - void hideOsd(); void showOsd(); void updatePosition(); KScreen::OutputPtr m_output; - QRect m_outputGeometry; KDeclarative::QmlObject *m_osdObject = nullptr; QTimer *m_osdTimer = nullptr; int m_timeout = 0; diff --git a/kded/osd.cpp b/kded/osd.cpp --- a/kded/osd.cpp +++ b/kded/osd.cpp @@ -19,7 +19,7 @@ #include "osd.h" #include "utils.h" -#include "debug.h" +#include "kscreen_daemon_debug.h" #include @@ -49,19 +49,26 @@ } m_timeout = m_osdObject->rootObject()->property("timeout").toInt(); - m_osdTimer = new QTimer(this); m_osdTimer->setSingleShot(true); connect(m_osdTimer, &QTimer::timeout, this, &Osd::hideOsd); + + connect(output.data(), &KScreen::Output::isConnectedChanged, + this, &Osd::onOutputAvailabilityChanged); + connect(output.data(), &KScreen::Output::isEnabledChanged, + this, &Osd::onOutputAvailabilityChanged); + connect(output.data(), &KScreen::Output::currentModeIdChanged, + this, &Osd::updatePosition); + connect(output.data(), &KScreen::Output::destroyed, + this, &Osd::hideOsd); } Osd::~Osd() { } void Osd::showGenericOsd(const QString &icon, const QString &text) { - m_outputGeometry = m_output->geometry(); auto *rootObject = m_osdObject->rootObject(); rootObject->setProperty("itemSource", QStringLiteral("OsdItem.qml")); rootObject->setProperty("infoText", text); @@ -72,8 +79,6 @@ void Osd::showOutputIdentifier(const KScreen::OutputPtr output) { - m_outputGeometry = output->geometry(); - auto *rootObject = m_osdObject->rootObject(); auto mode = output->currentMode(); QSize realSize = mode->size(); @@ -86,20 +91,52 @@ showOsd(); } +void Osd::showActionSelector() { + auto *rootObject = m_osdObject->rootObject(); + rootObject->setProperty("itemSource", QStringLiteral("OsdSelector.qml")); + rootObject->setProperty("timeout", 0); + rootObject->setProperty("outputOnly", false); + auto osdItem = rootObject->property("osdItem").value(); + connect(osdItem, SIGNAL(clicked(int)), + this, SLOT(onOsdActionSelected(int))); + m_timeout = 0; // no timeout for this one + showOsd(); +} + +void Osd::onOsdActionSelected(int action) +{ + Q_EMIT osdActionSelected(static_cast(action)); + hideOsd(); +} + +void Osd::onOutputAvailabilityChanged() +{ + if (m_output) { + if (!m_output->isConnected() || !m_output->isEnabled() || !m_output->currentMode()) { + hideOsd(); + } + } else { + hideOsd(); + } +} + + void Osd::updatePosition() { - if (!m_outputGeometry.isValid()) { + const auto geometry = m_output->geometry(); + if (!geometry.isValid()) { + hideOsd(); return; } auto *rootObject = m_osdObject->rootObject(); const int dialogWidth = rootObject->property("width").toInt(); const int dialogHeight = rootObject->property("height").toInt(); - const int relx = m_outputGeometry.x(); - const int rely = m_outputGeometry.y(); - const int pos_x = relx + (m_outputGeometry.width() - dialogWidth) / 2; - const int pos_y = rely + (m_outputGeometry.height() - dialogHeight) / 2; + const int relx = geometry.x(); + const int rely = geometry.y(); + const int pos_x = relx + (geometry.width() - dialogWidth) / 2; + const int pos_y = rely + (geometry.height() - dialogHeight) / 2; rootObject->setProperty("x", pos_x); rootObject->setProperty("y", pos_y); @@ -114,16 +151,22 @@ // only animate on X11, wayland plugin doesn't support this and // pukes loads of warnings into our logs if (qGuiApp->platformName() == QLatin1String("xcb")) { - rootObject->setProperty("animateOpacity", false); - rootObject->setProperty("opacity", 1); - rootObject->setProperty("visible", true); - rootObject->setProperty("animateOpacity", true); - rootObject->setProperty("opacity", 0); + if (rootObject->property("timeout").toInt() > 0) { + rootObject->setProperty("animateOpacity", false); + rootObject->setProperty("opacity", 1); + rootObject->setProperty("visible", true); + rootObject->setProperty("animateOpacity", true); + rootObject->setProperty("opacity", 0); + } else { + rootObject->setProperty("visible", true); + } } else { rootObject->setProperty("visible", true); } - updatePosition(); - m_osdTimer->start(m_timeout); + QTimer::singleShot(0, this, &Osd::updatePosition); + if (m_timeout > 0) { + m_osdTimer->start(m_timeout); + } } void Osd::hideOsd() diff --git a/kded/osdmanager.h b/kded/osdmanager.h --- a/kded/osdmanager.h +++ b/kded/osdmanager.h @@ -32,6 +32,28 @@ class Osd; class Output; +class OsdAction : public QObject +{ + Q_OBJECT +public: + enum Action { + NoAction, + SwitchToExternal, + SwitchToInternal, + Clone, + ExtendLeft, + ExtendRight + }; + Q_ENUM(Action) + +Q_SIGNALS: + void selected(OsdAction *self, Action action); + +protected: + explicit OsdAction(QObject *parent = nullptr); +}; + + class OsdManager : public QObject { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.kscreen.osdService") @@ -43,6 +65,7 @@ public Q_SLOTS: void showOutputIdentifiers(); void showOsd(const QString &icon, const QString &text); + OsdAction *showActionSelector(); private: OsdManager(QObject *parent = nullptr); diff --git a/kded/osdmanager.cpp b/kded/osdmanager.cpp --- a/kded/osdmanager.cpp +++ b/kded/osdmanager.cpp @@ -18,22 +18,47 @@ #include "osdmanager.h" #include "osd.h" -#include "debug.h" +#include "kscreen_daemon_debug.h" #include #include #include #include +#include + namespace KScreen { OsdManager* OsdManager::s_instance = nullptr; +OsdAction::OsdAction(QObject *parent) + : QObject(parent) +{ +} + +class OsdActionImpl : public OsdAction +{ + Q_OBJECT +public: + OsdActionImpl(QObject *parent = nullptr) + : OsdAction(parent) + {} + + void setOsd(Osd *osd) { + connect(osd, &Osd::osdActionSelected, + this, [this](Action action) { + Q_EMIT selected(this, action); + }); + } +}; + OsdManager::OsdManager(QObject *parent) : QObject(parent) , m_cleanupTimer(new QTimer(this)) { + qmlRegisterUncreatableType("org.kde.KScreen", 1, 0, "OsdAction", "You cannot create OsdAction"); + // free up memory when the osd hasn't been used for more than 1 minute m_cleanupTimer->setInterval(60000); m_cleanupTimer->setSingleShot(true); @@ -61,7 +86,6 @@ void OsdManager::showOutputIdentifiers() { - qDebug() << "SHOWOUTPUTIDENTIFIERS"; connect(new KScreen::GetConfigOperation(), &KScreen::GetConfigOperation::finished, this, &OsdManager::slotIdentifyOutputs); } @@ -107,7 +131,7 @@ continue; } KScreen::Osd* osd = nullptr; - if (m_osds.keys().contains(output->name())) { + if (m_osds.contains(output->name())) { osd = m_osds.value(output->name()); } else { osd = new KScreen::Osd(output, this); @@ -120,5 +144,77 @@ ); } +OsdAction *OsdManager::showActionSelector() +{ + qDeleteAll(m_osds); + m_osds.clear(); + + OsdActionImpl *action = new OsdActionImpl(this); + connect(action, &OsdActionImpl::selected, + this, [this]() { + for (auto osd : qAsConst(m_osds)) { + osd->hideOsd(); + } + }); + connect(new KScreen::GetConfigOperation(), &KScreen::GetConfigOperation::finished, + this, [this, action](const KScreen::ConfigOperation *op) { + if (op->hasError()) { + qCWarning(KSCREEN_KDED) << op->errorString(); + return; + } + // Show selector on alll enabled screens + const auto outputs = op->config()->outputs(); + KScreen::OutputPtr osdOutput; + for (const auto &output : outputs) { + if (!output->isConnected() || !output->isEnabled() || !output->currentMode()) { + continue; + } + + // Prefer laptop screen + if (output->type() == KScreen::Output::Panel) { + osdOutput = output; + break; + } + + // Fallback to primary + if (output->isPrimary()) { + osdOutput = output; + break; + } + } + // no laptop or primary screen, just take the first usable one + if (!osdOutput) { + for (const auto &output : outputs) { + if (output->isConnected() && output->isEnabled() && output->currentMode()) { + osdOutput = output; + break; + } + } + } + + if (!osdOutput) { + // huh!? + return; + } + + KScreen::Osd* osd = nullptr; + if (m_osds.contains(osdOutput->name())) { + osd = m_osds.value(osdOutput->name()); + } else { + osd = new KScreen::Osd(osdOutput, this); + m_osds.insert(osdOutput->name(), osd); + } + action->setOsd(osd); + osd->showActionSelector(); + m_cleanupTimer->start(); + } + ); + + return action; } + + +} + +#include "osdmanager.moc" diff --git a/kded/qml/Osd.qml b/kded/qml/Osd.qml --- a/kded/qml/Osd.qml +++ b/kded/qml/Osd.qml @@ -36,6 +36,7 @@ property string modeName property bool animateOpacity: false property string itemSource + property QtObject osdItem Behavior on opacity { SequentialAnimation { @@ -47,14 +48,15 @@ easing.type: Easing.InQuad } } - enabled: root.animateOpacity + enabled: root.timeout > 0 && root.animateOpacity } mainItem: Loader { source: itemSource onItemChanged: { if (item != undefined) { item.rootItem = root; + root.osdItem = item } } diff --git a/kded/qml/OsdSelector.qml b/kded/qml/OsdSelector.qml new file mode 100644 --- /dev/null +++ b/kded/qml/OsdSelector.qml @@ -0,0 +1,116 @@ +/* + * Copyright 2017 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import QtQuick 2.5 +import QtQuick.Window 2.2 + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 2.0 as PlasmaComponents +import org.kde.plasma.extras 2.0 as PlasmaExtras + +import org.kde.KScreen 1.0 + +Item { + id: root + property QtObject rootItem + + signal clicked(int actionId) + + height: Math.min(units.gridUnit * 15, Screen.desktopAvailableHeight / 5) + width: buttonRow.width + + PlasmaComponents.ButtonRow { + id: buttonRow + + exclusive: false + + height: parent.height - label.height - ((units.smallSpacing/2) * 3) + width: (actionRepeater.model.length * height) + ((actionRepeater.model.length - 1) * buttonRow.spacing); + + Repeater { + id: actionRepeater + model: [ + { + iconSource: "osd-shutd-screen", + label: qsTr("Switch to external screen"), + action: OsdAction.SwitchToExternal + }, + { + iconSource: "osd-shutd-laptop", + label: qsTr("Switch to laptop screen"), + action: OsdAction.SwitchToInternal + }, + { + iconSource: "osd-duplicate", + label: qsTr("Duplicate outputs"), + action: OsdAction.Clone + }, + { + iconSource: "osd-sbs-left", + label: qsTr("Extend to left"), + action: OsdAction.ExtendLeft + }, + { + iconSource: "osd-sbs-sright", + label: qsTr("Extend to right"), + action: OsdAction.ExtendRight + }, + { + iconSource: "dialog-cancel", + label: qsTr("Do nothing"), + action: OsdAction.NoAction + } + ] + delegate: PlasmaComponents.Button { + PlasmaCore.IconItem { + source: modelData.iconSource + height: buttonRow.height - ((units.smallSpacing / 2) * 3) + width: height + anchors.centerIn: parent + } + height: parent.height + width: height + + onHoveredChanged: rootItem.infoText = (hovered ? modelData.label : "") + + onClicked: root.clicked(modelData.action) + } + } + } + + // TODO: keep? remove? + PlasmaExtras.Heading { + id: label + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right + margins: Math.floor(units.smallSpacing / 2) + } + + text: rootItem.infoText + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + maximumLineCount: 2 + elide: Text.ElideLeft + minimumPointSize: theme.defaultFont.pointSize + fontSizeMode: Text.HorizontalFit + } + + Component.onCompleted: print("OsdSelector loaded..."); +} + diff --git a/tests/osd/CMakeLists.txt b/tests/osd/CMakeLists.txt --- a/tests/osd/CMakeLists.txt +++ b/tests/osd/CMakeLists.txt @@ -1,4 +1,7 @@ -include_directories(${CMAKE_SOURCE_DIR}/kcm/src) +include_directories( + ${CMAKE_SOURCE_DIR}/kcm/src + ${CMAKE_BINARY_DIR}/kded +) add_executable(osdtest main.cpp osdtest.cpp diff --git a/tests/osd/main.cpp b/tests/osd/main.cpp --- a/tests/osd/main.cpp +++ b/tests/osd/main.cpp @@ -33,23 +33,27 @@ QStringLiteral("Icon to use for OSD"), QStringLiteral("preferences-desktop-display-randr")); QCommandLineOption message = QCommandLineOption(QStringList() << QStringLiteral("m") << "message", QStringLiteral("Icon to use for OSD"), QStringLiteral("OSD Test")); + QCommandLineOption selector = QCommandLineOption({ QStringLiteral("s"), QStringLiteral("selector") }, + QStringLiteral("Show new screen action selector")); KScreen::OsdTest osdtest; QCommandLineParser parser; parser.addHelpOption(); parser.addOption(dbus); parser.addOption(outputid); parser.addOption(icon); parser.addOption(message); + parser.addOption(selector); parser.process(app); if (parser.isSet(dbus)) { osdtest.setUseDBus(true); } if (parser.isSet(outputid)) { - osdtest.showOutputIdentifiers(); + } else if (parser.isSet(selector)) { + osdtest.showActionSelector(); } else { osdtest.showGenericOsd(parser.value(icon), parser.value(message)); } diff --git a/tests/osd/osdtest.h b/tests/osd/osdtest.h --- a/tests/osd/osdtest.h +++ b/tests/osd/osdtest.h @@ -37,6 +37,7 @@ void showGenericOsd(const QString &icon, const QString &message); void showOutputIdentifiers(); + void showActionSelector(); private: bool m_useDBus = false; diff --git a/tests/osd/osdtest.cpp b/tests/osd/osdtest.cpp --- a/tests/osd/osdtest.cpp +++ b/tests/osd/osdtest.cpp @@ -75,5 +75,20 @@ } } +void OsdTest::showActionSelector() +{ + if (!m_useDBus) { + auto action = KScreen::OsdManager::self()->showActionSelector(); + connect(action, &KScreen::OsdAction::selected, + [](KScreen::OsdAction *self, KScreen::OsdAction::Action action) { + self->deleteLater(); + qCDebug(KSCREEN_KDED) << "Selected action:" << action; + qApp->quit(); + }); + } else { + qCWarning(KSCREEN_KDED) << "Implement me."; + QTimer::singleShot(100, qApp, &QCoreApplication::quit); + } +} } // ns