Commit a82be242 authored by Ismael Asensio's avatar Ismael Asensio
Browse files

[kwinrules] Launch full KCM when editing from window menu

When rules configuration is invoked from window `Alt+F3` menu,
we call a custom binary `kwin_rules_dialog` which currently provides
only the rule edition dialog by embedding `RulesEditor.qml` within a
QQuickView.

This MR changes that behavior to call the full KCM from the menu.
The code to match previous rules, or compose a new one based on window
properties has been ported to the KCM from the dialog, so the overall
interaction is similar.

It has several advantages:
 - uses only one entry-point to the code
 - adds discoverability to the full KCM (I guess many users know how to
   create a rule, but not where to delete it later)

And a drawback:
 - only one instance of the KCM can be called at a time, so it will show an
   error when calling it from two different windows, or if the KCM is open
   in System Settings

This drawback can be solved after adding argument passing via dBus in KCM
infraestructure.

BUG: 433837
CCBUG: 417923
parent 38a15996
......@@ -16,6 +16,7 @@ set(kwinrules_SRCS
optionsmodel.cpp
ruleitem.cpp
rulesmodel.cpp
rulebookmodel.cpp
)
kconfig_add_kcfg_files(kwinrules_SRCS ../../rulesettings.kcfgc)
......@@ -33,6 +34,7 @@ set(kcm_libs
Qt::Quick
Qt::QuickWidgets
KF5::KCMUtils
KF5::I18n
KF5::QuickAddons
KF5::WindowSystem
......@@ -44,11 +46,11 @@ if (KWIN_BUILD_ACTIVITIES)
endif()
target_link_libraries(KWinRulesObjects ${kcm_libs} ${kwin_kcm_rules_XCB_LIBS})
add_executable(kwin_rules_dialog main.cpp rulesdialog.cpp)
add_executable(kwin_rules_dialog main.cpp)
target_link_libraries(kwin_rules_dialog KWinRulesObjects)
install(TARGETS kwin_rules_dialog DESTINATION ${KDE_INSTALL_LIBEXECDIR})
add_library(kcm_kwinrules MODULE kcmrules.cpp rulebookmodel.cpp)
add_library(kcm_kwinrules MODULE kcmrules.cpp)
target_link_libraries(kcm_kwinrules KWinRulesObjects)
kcoreaddons_desktop_to_json(kcm_kwinrules "kcm_kwinrules.desktop" SERVICE_TYPES kcmodule.desktop)
......
......@@ -40,6 +40,12 @@ KCMKWinRules::KCMKWinRules(QObject *parent, const QVariantList &arguments)
" KWin as your window manager. If you do use a different window manager, please refer to its documentation"
" for how to customize window behavior.</p>"));
QStringList argList;
for (const QVariant &arg : arguments) {
argList << arg.toString();
}
parseArguments(argList);
connect(m_rulesModel, &RulesModel::descriptionChanged, this, [this]{
if (m_editIndex.isValid()) {
m_ruleBookModel->setDescriptionAt(m_editIndex.row(), m_rulesModel->description());
......@@ -49,14 +55,69 @@ KCMKWinRules::KCMKWinRules(QObject *parent, const QVariantList &arguments)
connect(m_ruleBookModel, &RulesModel::dataChanged, this, &KCMKWinRules::updateNeedsSave);
}
void KCMKWinRules::parseArguments(const QStringList &args)
{
// When called from window menu, "uuid" and "whole-app" are set in arguments list
bool nextArgIsUuid = false;
QUuid uuid = QUuid();
// TODO: Use a better argument parser
for (const QString &arg : args) {
if (arg == QLatin1String("uuid")) {
nextArgIsUuid = true;
} else if (nextArgIsUuid) {
uuid = QUuid(arg);
nextArgIsUuid = false;
} else if (arg.startsWith("uuid=")) {
uuid = arg.mid(strlen("uuid="));
} else if (arg == QLatin1String("whole-app")) {
m_wholeApp = true;
}
}
if (uuid.isNull()) {
qDebug() << "Invalid window uuid.";
return;
}
// Get the Window properties
QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.kde.KWin"),
QStringLiteral("/KWin"),
QStringLiteral("org.kde.KWin"),
QStringLiteral("getWindowInfo"));
message.setArguments({uuid.toString()});
QDBusPendingReply<QVariantMap> async = QDBusConnection::sessionBus().asyncCall(message);
QDBusPendingCallWatcher *callWatcher = new QDBusPendingCallWatcher(async, this);
connect(callWatcher, &QDBusPendingCallWatcher::finished, this, [this, uuid](QDBusPendingCallWatcher *self) {
QDBusPendingReply<QVariantMap> reply = *self;
self->deleteLater();
if (!reply.isValid() || reply.value().isEmpty()) {
qDebug() << "Error retrieving properties for window" << uuid;
return;
}
qDebug() << "Retrieved properties for window" << uuid;
m_winProperties = reply.value();
if (m_alreadyLoaded) {
createRuleFromProperties();
}
});
}
void KCMKWinRules::load()
{
m_ruleBookModel->load();
setNeedsSave(false);
m_editIndex = QModelIndex();
emit editIndexChanged();
if (!m_winProperties.isEmpty() && !m_alreadyLoaded) {
createRuleFromProperties();
} else {
m_editIndex = QModelIndex();
emit editIndexChanged();
}
setNeedsSave(false);
m_alreadyLoaded = true;
}
void KCMKWinRules::save()
......@@ -76,6 +137,26 @@ void KCMKWinRules::updateNeedsSave()
emit needsSaveChanged();
}
void KCMKWinRules::createRuleFromProperties()
{
if (m_winProperties.isEmpty()) {
return;
}
QModelIndex matchedIndex = m_ruleBookModel->findRuleWithProperties(m_winProperties, m_wholeApp);
if (!matchedIndex.isValid()) {
m_ruleBookModel->insertRow(0);
m_ruleBookModel->setRuleAt(0, ruleForProperties(m_winProperties, m_wholeApp));
matchedIndex = m_ruleBookModel->index(0);
updateNeedsSave();
}
editRule(matchedIndex.row());
m_rulesModel->setSuggestedProperties(m_winProperties);
m_winProperties.clear();
}
void KCMKWinRules::saveCurrentRule()
{
if (m_editIndex.isValid() && needsSave()) {
......@@ -249,6 +330,84 @@ void KCMKWinRules::importFromFile(const QUrl &path)
updateNeedsSave();
}
// Code adapted from original `findRule()` method in `kwin_rules_dialog::main.cpp`
Rules *KCMKWinRules::ruleForProperties(const QVariantMap &windowProperties, bool wholeApp) const
{
const QByteArray wmclass_class = windowProperties.value("resourceClass").toByteArray().toLower();
const QByteArray wmclass_name = windowProperties.value("resourceName").toByteArray().toLower();
const QByteArray role = windowProperties.value("role").toByteArray().toLower();
const NET::WindowType type = static_cast<NET::WindowType>(windowProperties.value("type").toInt());
const QString title = windowProperties.value("caption").toString();
const QByteArray machine = windowProperties.value("clientMachine").toByteArray();
Rules *rule = new Rules();
if (wholeApp) {
rule->description = i18n("Application settings for %1", QString::fromLatin1(wmclass_class));
// TODO maybe exclude some types? If yes, then also exclude them when searching.
rule->types = NET::AllTypesMask;
rule->titlematch = Rules::UnimportantMatch;
rule->clientmachine = machine; // set, but make unimportant
rule->clientmachinematch = Rules::UnimportantMatch;
rule->windowrolematch = Rules::UnimportantMatch;
if (wmclass_name == wmclass_class) {
rule->wmclasscomplete = false;
rule->wmclass = wmclass_class;
rule->wmclassmatch = Rules::ExactMatch;
} else {
// WM_CLASS components differ - perhaps the app got -name argument
rule->wmclasscomplete = true;
rule->wmclass = wmclass_name + ' ' + wmclass_class;
rule->wmclassmatch = Rules::ExactMatch;
}
return rule;
}
rule->description = i18n("Window settings for %1", QString::fromLatin1(wmclass_class));
if (type == NET::Unknown) {
rule->types = NET::NormalMask;
} else {
rule->types = NET::WindowTypeMask(1 << type); // convert type to its mask
}
rule->title = title; // set, but make unimportant
rule->titlematch = Rules::UnimportantMatch;
rule->clientmachine = machine; // set, but make unimportant
rule->clientmachinematch = Rules::UnimportantMatch;
if (!role.isEmpty() && role != "unknown" && role != "unnamed") { // Qt sets this if not specified
rule->windowrole = role;
rule->windowrolematch = Rules::ExactMatch;
if (wmclass_name == wmclass_class) {
rule->wmclasscomplete = false;
rule->wmclass = wmclass_class;
rule->wmclassmatch = Rules::ExactMatch;
} else {
// WM_CLASS components differ - perhaps the app got -name argument
rule->wmclasscomplete = true;
rule->wmclass = wmclass_name + ' ' + wmclass_class;
rule->wmclassmatch = Rules::ExactMatch;
}
} else { // no role set
if (wmclass_name != wmclass_class) {
rule->wmclasscomplete = true;
rule->wmclass = wmclass_name + ' ' + wmclass_class;
rule->wmclassmatch = Rules::ExactMatch;
} else {
// This is a window that has no role set, and both components of WM_CLASS
// match (possibly only differing in case), which most likely means either
// the application doesn't give a damn about distinguishing its various
// windows, or it's an app that uses role for that, but this window
// lacks it for some reason. Use non-complete WM_CLASS matching, also
// include window title in the matching, and pray it causes many more positive
// matches than negative matches.
rule->titlematch = Rules::ExactMatch;
rule->wmclasscomplete = false;
rule->wmclass = wmclass_class;
rule->wmclassmatch = Rules::ExactMatch;
}
}
return rule;
}
K_PLUGIN_CLASS_WITH_JSON(KCMKWinRules, "kcm_kwinrules.json");
} // namespace
......
......@@ -51,12 +51,19 @@ private slots:
private:
int editIndex() const;
void saveCurrentRule();
void parseArguments(const QStringList &args);
void createRuleFromProperties();
Rules *ruleForProperties(const QVariantMap &windowProperties, bool wholeApp) const;
private:
RuleBookModel *m_ruleBookModel;
RulesModel* m_rulesModel;
QPersistentModelIndex m_editIndex;
bool m_alreadyLoaded = false;
QVariantMap m_winProperties;
bool m_wholeApp = false;
};
} // namespace
/*
SPDX-FileCopyrightText: 2004 Lubos Lunak <l.lunak@kde.org>
SPDX-FileCopyrightText: 2018 Nicolas Fella <nicolas.fella@gmx.de>
SPDX-FileCopyrightText: 2020 Ismael Asensio <isma.af@gmail.com>
SPDX-License-Identifier: GPL-2.0-or-later
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#include <QCommandLineParser>
#include <QApplication>
#include <kconfig.h>
#include <KLocalizedString>
#include <kwindowsystem.h>
#include "rulebooksettings.h"
#include "rulesdialog.h"
#include "../../rules.h"
#include <QByteArray>
#include <QDBusConnection>
#include <QDBusMessage>
#include <QDBusPendingCallWatcher>
#include <QDBusPendingReply>
#include <QCommandLineParser>
#include <QIcon>
#include <QUuid>
Q_DECLARE_METATYPE(NET::WindowType)
namespace KWin
{
static Rules *findRule(const QVector<Rules *> &rules, const QVariantMap &data, bool whole_app)
{
QByteArray wmclass_class = data.value("resourceClass").toByteArray().toLower();
QByteArray wmclass_name = data.value("resourceName").toByteArray().toLower();
QByteArray role = data.value("role").toByteArray().toLower();
NET::WindowType type = data.value("type").value<NET::WindowType>();
QString title = data.value("caption").toString();
QByteArray machine = data.value("clientMachine").toByteArray();
Rules* best_match = nullptr;
int match_quality = 0;
for (const auto rule : rules) {
// try to find an exact match, i.e. not a generic rule
int quality = 0;
bool generic = true;
if (rule->wmclassmatch != Rules::ExactMatch)
continue; // too generic
if (!rule->matchWMClass(wmclass_class, wmclass_name))
continue;
// from now on, it matches the app - now try to match for a specific window
if (rule->wmclasscomplete) {
quality += 1;
generic = false; // this can be considered specific enough (old X apps)
}
if (!whole_app) {
if (rule->windowrolematch != Rules::UnimportantMatch) {
quality += rule->windowrolematch == Rules::ExactMatch ? 5 : 1;
generic = false;
}
if (rule->titlematch != Rules::UnimportantMatch) {
quality += rule->titlematch == Rules::ExactMatch ? 3 : 1;
generic = false;
}
if (rule->types != NET::AllTypesMask) {
int bits = 0;
for (unsigned int bit = 1;
bit < 1U << 31;
bit <<= 1)
if (rule->types & bit)
++bits;
if (bits == 1)
quality += 2;
}
if (generic) // ignore generic rules, use only the ones that are for this window
continue;
} else {
if (rule->types == NET::AllTypesMask)
quality += 2;
}
if (!rule->matchType(type)
|| !rule->matchRole(role)
|| !rule->matchTitle(title)
|| !rule->matchClientMachine(machine, data.value("localhost").toBool()))
continue;
if (quality > match_quality) {
best_match = rule;
match_quality = quality;
}
}
if (best_match != nullptr)
return best_match;
Rules* ret = new Rules;
if (whole_app) {
ret->description = i18n("Application settings for %1", QString::fromLatin1(wmclass_class));
// TODO maybe exclude some types? If yes, then also exclude them above
// when searching.
ret->types = NET::AllTypesMask;
ret->titlematch = Rules::UnimportantMatch;
ret->clientmachine = machine; // set, but make unimportant
ret->clientmachinematch = Rules::UnimportantMatch;
ret->windowrolematch = Rules::UnimportantMatch;
if (wmclass_name == wmclass_class) {
ret->wmclasscomplete = false;
ret->wmclass = wmclass_class;
ret->wmclassmatch = Rules::ExactMatch;
} else {
// WM_CLASS components differ - perhaps the app got -name argument
ret->wmclasscomplete = true;
ret->wmclass = wmclass_name + ' ' + wmclass_class;
ret->wmclassmatch = Rules::ExactMatch;
}
return ret;
}
ret->description = i18n("Window settings for %1", QString::fromLatin1(wmclass_class));
if (type == NET::Unknown)
ret->types = NET::NormalMask;
else
ret->types = NET::WindowTypeMask( 1 << type); // convert type to its mask
ret->title = title; // set, but make unimportant
ret->titlematch = Rules::UnimportantMatch;
ret->clientmachine = machine; // set, but make unimportant
ret->clientmachinematch = Rules::UnimportantMatch;
if (!role.isEmpty()
&& role != "unknown" && role != "unnamed") { // Qt sets this if not specified
ret->windowrole = role;
ret->windowrolematch = Rules::ExactMatch;
if (wmclass_name == wmclass_class) {
ret->wmclasscomplete = false;
ret->wmclass = wmclass_class;
ret->wmclassmatch = Rules::ExactMatch;
} else {
// WM_CLASS components differ - perhaps the app got -name argument
ret->wmclasscomplete = true;
ret->wmclass = wmclass_name + ' ' + wmclass_class;
ret->wmclassmatch = Rules::ExactMatch;
}
} else { // no role set
if (wmclass_name != wmclass_class) {
ret->wmclasscomplete = true;
ret->wmclass = wmclass_name + ' ' + wmclass_class;
ret->wmclassmatch = Rules::ExactMatch;
} else {
// This is a window that has no role set, and both components of WM_CLASS
// match (possibly only differing in case), which most likely means either
// the application doesn't give a damn about distinguishing its various
// windows, or it's an app that uses role for that, but this window
// lacks it for some reason. Use non-complete WM_CLASS matching, also
// include window title in the matching, and pray it causes many more positive
// matches than negative matches.
ret->titlematch = Rules::ExactMatch;
ret->wmclasscomplete = false;
ret->wmclass = wmclass_class;
ret->wmclassmatch = Rules::ExactMatch;
}
}
return ret;
}
static void edit(const QVariantMap &data, bool whole_app)
{
RuleBookSettings settings(KConfig::NoGlobals);
QVector<Rules *> rules = settings.rules();
Rules *orig_rule = findRule(rules, data, whole_app);
RulesDialog dlg;
if (whole_app)
dlg.setWindowTitle(i18nc("Window caption for the application wide rules dialog", "Edit Application-Specific Settings"));
// dlg.edit() creates new Rules instance if edited
Rules* edited_rule = dlg.edit(orig_rule, data, true);
if (edited_rule == nullptr || edited_rule->isEmpty()) {
rules.removeAll(orig_rule);
delete orig_rule;
if (orig_rule != edited_rule)
delete edited_rule;
} else if (edited_rule != orig_rule) {
int pos = rules.indexOf(orig_rule);
if (pos != -1)
rules[ pos ] = edited_rule;
else
rules.prepend(edited_rule);
delete orig_rule;
}
settings.setRules(rules);
settings.save();
// Send signal to all kwin instances
QDBusMessage message =
QDBusMessage::createSignal("/KWin", "org.kde.KWin", "reloadConfig");
QDBusConnection::sessionBus().send(message);
qApp->quit();
}
} // namespace
#include <KCMultiDialog>
#include <KLocalizedString>
int main(int argc, char* argv[])
{
......@@ -194,48 +21,39 @@ int main(int argc, char* argv[])
KLocalizedString::setApplicationDomain("kcm_kwinrules");
app.setAttribute(Qt::AA_UseHighDpiPixmaps, true);
app.setApplicationDisplayName(i18n("KWin"));
app.setApplicationName("kwin_rules_dialog");
app.setApplicationVersion("1.0");
bool whole_app = false;
QUuid uuid;
{
QCommandLineParser parser;
parser.setApplicationDescription(i18n("KWin helper utility"));
parser.addOption(QCommandLineOption("uuid", i18n("KWin id of the window for special window settings."), "uuid"));
parser.addOption(QCommandLineOption("whole-app", i18n("Whether the settings should affect all windows of the application.")));
parser.process(app);
app.setWindowIcon(QIcon::fromTheme("preferences-system-windows-actions"));
app.setApplicationVersion("2.0");
uuid = QUuid::fromString(parser.value("uuid"));
whole_app = parser.isSet("whole-app");
}
QCommandLineParser parser;
parser.setApplicationDescription(i18n("KWinRules KCM launcher"));
parser.addOption(QCommandLineOption("uuid", i18n("KWin id of the window for special window settings."), "uuid"));
parser.addOption(QCommandLineOption("whole-app", i18n("Whether the settings should affect all windows of the application.")));
parser.process(app);
const QUuid uuid = QUuid::fromString(parser.value("uuid"));
const bool whole_app = parser.isSet("whole-app");
if (uuid.isNull()) {
printf("%s\n", qPrintable(i18n("This helper utility is not supposed to be called directly.")));
return 1;
}
QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.kde.KWin"),
QStringLiteral("/KWin"),
QStringLiteral("org.kde.KWin"),
QStringLiteral("getWindowInfo"));
message.setArguments({uuid.toString()});
QDBusPendingReply<QVariantMap> async = QDBusConnection::sessionBus().asyncCall(message);
QDBusPendingCallWatcher *callWatcher = new QDBusPendingCallWatcher(async, &app);
QObject::connect(callWatcher, &QDBusPendingCallWatcher::finished, &app,
[&whole_app] (QDBusPendingCallWatcher *self) {
QDBusPendingReply<QVariantMap> reply = *self;
self->deleteLater();
if (!reply.isValid() || reply.value().isEmpty()) {
qApp->quit();
return;
}
KWin::edit(reply.value(), whole_app);
}
);
app.setApplicationDisplayName((whole_app) ? i18nc("Window caption for the application wide rules dialog", "Edit Application-Specific Settings")
: i18n("Edit Window-Specific Settings"));
QStringList kcm_args;
kcm_args << QStringLiteral("uuid=%1").arg(uuid.toString());
if (whole_app) {
kcm_args << QStringLiteral("whole-app");
}
KCMultiDialog *dialog = new KCMultiDialog;
dialog->addModule(QStringLiteral("kcm_kwinrules"), kcm_args);
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->show();
app.setQuitOnLastWindowClosed(true);
return app.exec();
}
......@@ -16,9 +16,7 @@ import org.kde.kcms.kwinrules 1.0
ScrollViewKCM {
id: rulesEditor
property var rulesModel: kcm.rulesModel
title: rulesModel.description
title: kcm.rulesModel.description
view: ListView {
id: rulesView
......@@ -73,7 +71,7 @@ ScrollViewKCM {
visible: warningList.count > 0
Repeater {
id: warningList
model: rulesModel.warningMessages
model: kcm.rulesModel.warningMessages
delegate: Kirigami.InlineMessage {
text: modelData
......@@ -104,8 +102,8 @@ ScrollViewKCM {
enabled: !propertySheet.sheetOpen && !errorSheet.sheetOpen
onClicked: {
overlayModel.onlySuggestions = true;
rulesModel.detectWindowProperties(Math.max(delaySpin.value * 1000,
Kirigami.Units.shortDuration));
kcm.rulesModel.detectWindowProperties(Math.max(delaySpin.value * 1000,
Kirigami.Units.shortDuration));
}
}
QQC2.SpinBox {
......@@ -123,7 +121,7 @@ ScrollViewKCM {
}
Connections {
target: rulesModel
target: kcm.rulesModel
function onShowSuggestions() {
overlayModel.onlySuggestions = true;
propertySheet.sheetOpen = true;
......@@ -268,7 +266,7 @@ ScrollViewKCM {
KSortFilterProxyModel {
id: enabledRulesModel
sourceModel: rulesModel
sourceModel: kcm.rulesModel
filterRowCallback: (source_row, source_parent) => {
var index = sourceModel.index(source_row, 0, source_parent);
return sourceModel.data(index, RulesModel.EnabledRole);
......@@ -277,7 +275,7 @@ ScrollViewKCM {
KSortFilterProxyModel {
id: overlayModel
sourceModel: rulesModel
sourceModel: kcm.rulesModel
property bool onlySuggestions: false
onOnlySuggestionsChanged: {
......
/*
SPDX-FileCopyrightText: 2004 Lubos Lunak <l.lunak@kde.org>
SPDX-FileCopyrightText: 2020 Ismael Asensio <isma.af@gmail.com>
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
......@@ -6,6 +7,7 @@
#include "rulebookmodel.h"
#include <KLocalizedString>
namespace KWin
{
......@@ -170,4 +172,88 @@ void RuleBookModel::save()
m_ruleBook->save();
}
// Code adapted from original `findRule()` method in `kwin_rules_dialog::main.cpp`
QModelIndex RuleBookModel::findRuleWithProperties(const QVariantMap &info, bool wholeApp) const
{
const QByteArray wmclass_class = info.value("resourceClass").toByteArray().toLower();
const QByteArray wmclass_name = info.value("resourceName").toByteArray().toLower();
const QByteArray role = info.value("role").toByteArray().toLower();
const NET::WindowType type = static_cast<NET::WindowType>(info.value("type").toInt());
const QString title = info.value("caption").toString();
const QByteArray machine = info.value("clientMachine").toByteArray();
const bool isLocalHost = info.value("localhost").toBool();
int bestMatchRow = -1;