Commit b4baa322 authored by Kai Uwe Broulik's avatar Kai Uwe Broulik 🍇

Add applet with screen layouts and presentation mode

One of LiMux client's requirements is for display configuration to be easily accessible by mouse.
The OSD cannot be accessed by mouse, so this applet offers commonly used screen layouts in an
easily accessible place.

To keep the screen on during a presentation (when the application does not do that or the user is
actually demonstrating something on the machine itself) one needs to uncheck the non-userfriendly
labeled "Enable powermanagement" check box in the battery monitor. Since this is also affects "screen
setup", it is placed in this plasmoid as well.

The widget can be placed as an always-visible plasmoid in the panel or in System Tray where it would
only show if presentation mode is enabled or more then one screen is connected.

Differential Revision: https://phabricator.kde.org/D14855
parent 97c91132
......@@ -52,6 +52,7 @@ find_package(KF5Screen ${MIN_LIBKSCREEN_VERSION} REQUIRED)
add_subdirectory(icons)
add_subdirectory(kcm)
add_subdirectory(kded)
add_subdirectory(plasmoid)
add_subdirectory(tests)
add_subdirectory(console)
......
......@@ -9,6 +9,7 @@ set(kscreen_daemon_SRCS
device.cpp
osd.cpp
osdmanager.cpp
osdaction.cpp
${CMAKE_SOURCE_DIR}/kcm/src/utils.cpp
)
......
/*************************************************************************************
* Copyright (C) 2012 by Alejandro Fiestas Olivares <afiestas@kde.org> *
* Copyright 2016 by Sebastian Kügler <sebas@kde.org> *
* Copyright (c) 2018 Kai Uwe Broulik <kde@broulik.de> *
* Work sponsored by the LiMux project of *
* the city of Munich. *
* *
* This program is free software; you can redistribute it and/or *
* modify it under the terms of the GNU General Public License *
......@@ -174,6 +177,20 @@ void KScreenDaemon::applyKnownConfig()
doApplyConfig(config);
}
void KScreenDaemon::applyLayoutPreset(const QString &presetName)
{
const QMetaEnum actionEnum = QMetaEnum::fromType<KScreen::OsdAction::Action>();
Q_ASSERT(actionEnum.isValid());
bool ok;
auto action = static_cast<KScreen::OsdAction::Action>(actionEnum.keyToValue(qPrintable(presetName), &ok));
if (!ok) {
qCWarning(KSCREEN_KDED) << "Cannot apply unknown screen layout preset named" << presetName;
return;
}
applyOsdAction(action);
}
void KScreenDaemon::applyOsdAction(KScreen::OsdAction::Action action)
{
switch (action) {
......
......@@ -62,7 +62,10 @@ class Q_DECL_EXPORT KScreenDaemon : public KDEDModule
void showOutputIdentifier();
void applyOsdAction(KScreen::OsdAction::Action action);
// DBus
void applyLayoutPreset(const QString &presetName);
Q_SIGNALS:
// DBus
void outputConnected(const QString &outputName);
void unknownOutputConnected(const QString &outputName);
......
......@@ -2,6 +2,9 @@
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="org.kde.KScreen">
<method name="applyLayoutPreset">
<arg type="s" name="presetName" direction="in" />
</method>
<signal name="outputConnected">
<arg type="s" name="outputName" direction="out" />
</signal>
......
/*
* Copyright 2016 Sebastian Kügler <sebas@kde.org>
* Copyright (c) 2018 Kai Uwe Broulik <kde@broulik.de>
* Work sponsored by the LiMux project of
* the city of Munich.
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "osdaction.h"
#include <KLocalizedString>
using namespace KScreen;
OsdAction::OsdAction(QObject *parent)
: QObject(parent)
{
}
QVector<int> OsdAction::actionOrder() const
{
return {
SwitchToExternal,
SwitchToInternal,
Clone,
ExtendLeft,
ExtendRight,
NoAction
};
}
QString OsdAction::actionLabel(OsdAction::Action action) const
{
switch (action) {
// this is built by both daemon and plasmoid, needs explicit translation domain here
case SwitchToExternal: return i18nd("kscreen", "Switch to external screen");
case SwitchToInternal: return i18nd("kscreen", "Switch to laptop screen");
case Clone: return i18nd("kscreen", "Unify outputs");
case ExtendLeft: return i18nd("kscreen", "Extend to left");
case ExtendRight: return i18nd("kscreen", "Extend to right");
case NoAction: return i18nd("kscreen", "Leave unchanged");
}
Q_UNREACHABLE();
return QString();
}
QString OsdAction::actionIconName(OsdAction::Action action) const
{
switch (action) {
case SwitchToExternal: return QStringLiteral("osd-shutd-laptop");
case SwitchToInternal: return QStringLiteral("osd-shutd-screen");
case Clone: return QStringLiteral("osd-duplicate");
case ExtendLeft: return QStringLiteral("osd-sbs-left");
case ExtendRight: return QStringLiteral("osd-sbs-sright");
case NoAction: return QStringLiteral("dialog-cancel");
}
Q_UNREACHABLE();
return QString();
}
#include "osdaction.moc"
/*
* Copyright 2016 Sebastian Kügler <sebas@kde.org>
* Copyright (c) 2018 Kai Uwe Broulik <kde@broulik.de>
* Work sponsored by the LiMux project of
* the city of Munich.
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#pragma once
#include <QObject>
#include <QString>
#include <QVector>
namespace KScreen {
class OsdAction : public QObject
{
Q_OBJECT
public:
enum Action {
NoAction,
SwitchToExternal,
SwitchToInternal,
Clone,
ExtendLeft,
ExtendRight
};
Q_ENUM(Action)
explicit OsdAction(QObject *parent = nullptr);
Q_INVOKABLE QVector<int> actionOrder() const;
Q_INVOKABLE QString actionLabel(Action action) const;
Q_INVOKABLE QString actionIconName(Action action) const;
Q_SIGNALS:
void selected(Action action);
};
} // namespace KScreen
......@@ -30,11 +30,6 @@
namespace KScreen {
OsdAction::OsdAction(QObject *parent)
: QObject(parent)
{
}
class OsdActionImpl : public OsdAction
{
Q_OBJECT
......@@ -56,7 +51,9 @@ OsdManager::OsdManager(QObject *parent)
: QObject(parent)
, m_cleanupTimer(new QTimer(this))
{
qmlRegisterUncreatableType<OsdAction>("org.kde.KScreen", 1, 0, "OsdAction", QStringLiteral("You cannot create OsdAction"));
qmlRegisterSingletonType<KScreen::OsdAction>("org.kde.KScreen", 1, 0, "OsdAction", [](QQmlEngine *, QJSEngine *) -> QObject* {
return new KScreen::OsdAction();
});
// free up memory when the osd hasn't been used for more than 1 minute
m_cleanupTimer->setInterval(60000);
......
......@@ -24,6 +24,8 @@
#include <QMap>
#include <QTimer>
#include "osdaction.h"
class QmlObject;
namespace KScreen {
......@@ -32,28 +34,6 @@ class ConfigOperation;
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(Action action);
protected:
explicit OsdAction(QObject *parent = nullptr);
};
class OsdManager : public QObject {
Q_OBJECT
Q_CLASSINFO("D-Bus Interface", "org.kde.kscreen.osdService")
......
......@@ -46,38 +46,15 @@ PlasmaCore.Dialog {
Repeater {
id: actionRepeater
property int currentIndex: 0
model: [
{
iconSource: "osd-shutd-laptop",
label: i18n("Switch to external screen"),
action: OsdAction.SwitchToExternal
},
{
iconSource: "osd-shutd-screen",
label: i18n("Switch to laptop screen"),
action: OsdAction.SwitchToInternal
},
{
iconSource: "osd-duplicate",
label: i18n("Unify outputs"),
action: OsdAction.Clone
},
{
iconSource: "osd-sbs-left",
label: i18n("Extend to left"),
action: OsdAction.ExtendLeft
},
{
iconSource: "osd-sbs-sright",
label: i18n("Extend to right"),
action: OsdAction.ExtendRight
},
{
iconSource: "dialog-cancel",
label: i18n("Leave unchanged"),
action: OsdAction.NoAction
model: {
return OsdAction.actionOrder().map(function (layout) {
return {
iconSource: OsdAction.actionIconName(layout),
label: OsdAction.actionLabel(layout),
action: layout
}
]
});
}
delegate: PlasmaComponents.Button {
property var action: modelData.action
Accessible.name: modelData.label
......
add_definitions(-DTRANSLATION_DOMAIN=\"plasma_applet_org.kde.kscreen\")
set(kscreenapplet_SRCS
kscreenapplet.cpp
../kded/osdaction.cpp
)
add_library(plasma_applet_kscreen MODULE ${kscreenapplet_SRCS})
kcoreaddons_desktop_to_json(plasma_applet_kscreen package/metadata.desktop)
target_link_libraries(plasma_applet_kscreen
Qt5::Qml
Qt5::DBus
KF5::I18n
KF5::Plasma
KF5::Screen)
install(TARGETS plasma_applet_kscreen DESTINATION ${KDE_INSTALL_PLUGINDIR}/plasma/applets)
plasma_install_package(package org.kde.kscreen)
#! /usr/bin/env bash
$XGETTEXT `find . -name \*.qml -o -name \*.cpp` -o $podir/plasma_applet_org.kde.kscreen.pot
/*
* Copyright (c) 2018 Kai Uwe Broulik <kde@broulik.de>
* Work sponsored by the LiMux project of
* the city of Munich.
*
* 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) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "kscreenapplet.h"
#include <QQmlEngine> // for qmlRegisterType
#include <QMetaEnum>
#include <QDBusConnection>
#include <QDBusMessage>
#include <KScreen/Config>
#include <KScreen/ConfigMonitor>
#include <KScreen/GetConfigOperation>
#include <KScreen/Output>
#include "../kded/osdaction.h"
#include <algorithm>
KScreenApplet::KScreenApplet(QObject *parent, const QVariantList &data)
: Plasma::Applet(parent, data)
{
}
KScreenApplet::~KScreenApplet() = default;
void KScreenApplet::init()
{
qmlRegisterSingletonType<KScreen::OsdAction>("org.kde.private.kscreen", 1, 0, "OsdAction", [](QQmlEngine *, QJSEngine *) -> QObject* {
return new KScreen::OsdAction();
});
connect(new KScreen::GetConfigOperation(KScreen::GetConfigOperation::NoEDID), &KScreen::ConfigOperation::finished,
this, [this](KScreen::ConfigOperation *op) {
m_screenConfiguration = qobject_cast<KScreen::GetConfigOperation *>(op)->config();
KScreen::ConfigMonitor::instance()->addConfig(m_screenConfiguration);
connect(KScreen::ConfigMonitor::instance(), &KScreen::ConfigMonitor::configurationChanged, this, &KScreenApplet::checkOutputs);
checkOutputs();
});
}
int KScreenApplet::connectedOutputCount() const
{
return m_connectedOutputCount;
}
void KScreenApplet::applyLayoutPreset(Action action)
{
const QMetaEnum actionEnum = QMetaEnum::fromType<KScreen::OsdAction::Action>();
Q_ASSERT(actionEnum.isValid());
const QString presetName = QString::fromLatin1(actionEnum.valueToKey(action));
if (presetName.isEmpty()) {
return;
}
QDBusMessage msg = QDBusMessage::createMethodCall(
QStringLiteral("org.kde.kded5"),
QStringLiteral("/modules/kscreen"),
QStringLiteral("org.kde.KScreen"),
QStringLiteral("applyLayoutPreset")
);
msg.setArguments({presetName});
QDBusConnection::sessionBus().call(msg, QDBus::NoBlock);
}
void KScreenApplet::checkOutputs()
{
if (!m_screenConfiguration) {
return;
}
const int oldConnectedOutputCount = m_connectedOutputCount;
const auto outputs = m_screenConfiguration->outputs();
m_connectedOutputCount = std::count_if(outputs.begin(), outputs.end(), [](const KScreen::OutputPtr &output) {
return output->isConnected();
});
if (m_connectedOutputCount != oldConnectedOutputCount) {
emit connectedOutputCountChanged();
}
}
K_EXPORT_PLASMA_APPLET_WITH_JSON(kscreen, KScreenApplet, "metadata.json")
#include "kscreenapplet.moc"
/*
* Copyright (c) 2018 Kai Uwe Broulik <kde@broulik.de>
* Work sponsored by the LiMux project of
* the city of Munich.
*
* 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) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#pragma once
#include <Plasma/Applet>
#include <KScreen/Types>
class KScreenApplet : public Plasma::Applet
{
Q_OBJECT
/**
* The number of currently connected (not neccessarily enabled) outputs
*/
Q_PROPERTY(int connectedOutputCount READ connectedOutputCount NOTIFY connectedOutputCountChanged)
public:
explicit KScreenApplet(QObject *parent, const QVariantList &data);
~KScreenApplet() override;
enum Action {
SwitchToExternal,
SwitchToInternal,
Clone,
ExtendLeft,
ExtendRight
};
Q_ENUM(Action)
void init() override;
int connectedOutputCount() const;
Q_INVOKABLE void applyLayoutPreset(Action action);
signals:
void connectedOutputCountChanged();
private:
void checkOutputs();
KScreen::ConfigPtr m_screenConfiguration;
int m_connectedOutputCount = 0;
};
/*
* Copyright (c) 2018 Kai Uwe Broulik <kde@broulik.de>
* Work sponsored by the LiMux project of
* the city of Munich.
*
* 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) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
import QtQuick 2.8
import QtQuick.Layouts 1.1
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
ColumnLayout {
spacing: units.smallSpacing
PlasmaComponents.CheckBox {
id: checkBox
Layout.fillWidth: true
// Remove spacing between checkbox and the explanatory label below
Layout.bottomMargin: -parent.spacing
text: i18n("Enable Presentation Mode")
onCheckedChanged: {
if (checked === root.presentationModeEnabled) {
return;
}
// disable CheckBox while job is running
checkBox.enabled = false;
var service = pmSource.serviceForSource("PowerDevil");
if (checked) {
var op = service.operationDescription("beginSuppressingScreenPowerManagement");
op.reason = i18n("User enabled presentation mode");
var job = service.startOperationCall(op);
job.finished.connect(function (job) {
presentationModeCookie = job.result;
checkBox.enabled = true;
});
} else {
var op = service.operationDescription("stopSuppressingScreenPowerManagement");
op.cookie = presentationModeCookie;
var job = service.startOperationCall(op);
job.finished.connect(function (job) {
presentationModeCookie = -1;
checkBox.enabled = true;
});
}
}
}
// so we can align the labels below with the checkbox
PlasmaComponents.CheckBox {
id: checkBoxMetrics
visible: false
}
PlasmaExtras.DescriptiveLabel {
Layout.fillWidth: true
Layout.leftMargin: checkBoxMetrics.width
font.pointSize: theme.smallestFont.pointSize
text: i18n("This will prevent your screen and computer from turning off automatically.")
wrapMode: Text.WordWrap
}
RowLayout {
Layout.fillWidth: true
Layout.leftMargin: checkBoxMetrics.width
spacing: units.smallSpacing
PlasmaCore.IconItem {
Layout.preferredWidth: units.iconSizes.medium
Layout.preferredHeight: units.iconSizes.medium
source: pmSource.inhibitions[0] ? pmSource.inhibitions[0].Icon || "" : ""
visible: valid
}
PlasmaComponents.Label {
Layout.fillWidth: true
Layout.maximumWidth: Math.min(units.gridUnit * 20, implicitWidth)
font.pointSize: theme.smallestFont.pointSize
wrapMode: Text.WordWrap
elide: Text.ElideRight
textFormat: Text.PlainText
text: {
var inhibitions = pmSource.inhibitions;
if (inhibitions.length > 1) {
return i18ncp("Some Application and n others enforce presentation mode",
"%2 and %1 other application are enforcing presentation mode.",
"%2 and %1 other applications are enforcing presentation mode.",
inhibitions.length - 1, inhibitions[0].Name) // plural only works on %1
} else if (inhibitions.length === 1) {
if (!inhibitions[0].Reason) {
return i18nc("Some Application enforce presentation mode",
"%1 is enforcing presentation mode.", inhibitions[0].Name)
} else {
return i18nc("Some Application enforce presentation mode: Reason provided by the app",
"%1 is enforcing presentation mode: %2", inhibitions[0].Name, inhibitions[0].Reason)
}
} else {
return "";
}
}
}
}
}
/*
* Copyright (c) 2018 Kai Uwe Broulik <kde@broulik.de>
* Work sponsored by the LiMux project of
* the city of Munich.
*
* 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) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
import QtQuick 2.8
import QtQuick.Layouts 1.1
import org.kde.plasma.plasmoid 2.0
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
ColumnLayout {
spacing: 0
states: [
State {
// only makes sense to offer screen layout setup if there's more than one screen connected
when: plasmoid.nativeInterface.connectedOutputCount < 2
PropertyChanges {