diff --git a/kcmkwin/kwinrules/kcmrules.cpp b/kcmkwin/kwinrules/kcmrules.cpp index b1b485035..00064a18e 100644 --- a/kcmkwin/kwinrules/kcmrules.cpp +++ b/kcmkwin/kwinrules/kcmrules.cpp @@ -1,305 +1,305 @@ /* * Copyright (c) 2004 Lubos Lunak * Copyright (c) 2020 Ismael Asensio * * 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 . */ #include "kcmrules.h" #include #include #include #include #include #include namespace { const QString s_configFile { QLatin1String("kwinrulesrc") }; } namespace KWin { KCMKWinRules::KCMKWinRules(QObject *parent, const QVariantList &arguments) : KQuickAddons::ConfigModule(parent, arguments) , m_ruleBook(new RuleBookSettings(this)) , m_rulesModel(new RulesModel(this)) { auto about = new KAboutData(QStringLiteral("kcm_kwinrules_qml"), i18n("Window Rules"), QStringLiteral("1.0"), QString(), KAboutLicense::GPL); about->addAuthor(i18n("Ismael Asensio"), i18n("Author"), QStringLiteral("isma.af@gmail.com")); setAboutData(about); setQuickHelp(i18n("

Window-specific Settings

Here you can customize window settings specifically only" " for some windows.

" "

Please note that this configuration will not take effect if you do not use" " KWin as your window manager. If you do use a different window manager, please refer to its documentation" " for how to customize window behavior.

")); connect(m_rulesModel, &RulesModel::descriptionChanged, this, [this]{ if (m_editingIndex >=0 && m_editingIndex < m_ruleBook->count()) { m_rules.at(m_editingIndex)->description = m_rulesModel->description(); emit ruleBookModelChanged(); } } ); connect(m_rulesModel, &RulesModel::dataChanged, this, &KCMKWinRules::updateNeedsSave); } KCMKWinRules::~KCMKWinRules() { - delete m_ruleBook; + qDeleteAll(m_rules); } QStringList KCMKWinRules::ruleBookModel() const { QStringList ruleDescriptionList; for (const Rules *rule : qAsConst(m_rules)) { ruleDescriptionList.append(rule->description); } return ruleDescriptionList; } void KCMKWinRules::load() { m_ruleBook->load(); m_rules = m_ruleBook->rules(); setNeedsSave(false); emit ruleBookModelChanged(); // Check if current index is no longer valid if (m_editingIndex >= m_rules.count()) { m_editingIndex = -1; pop(); emit editingIndexChanged(); } // Reset current index for rule editor if (m_editingIndex > 0) { m_rulesModel->importFromRules(m_rules.at(m_editingIndex)); } } void KCMKWinRules::save() { saveCurrentRule(); m_ruleBook->setRules(m_rules); m_ruleBook->save(); QDBusMessage message = QDBusMessage::createSignal("/KWin", "org.kde.KWin", "reloadConfig"); QDBusConnection::sessionBus().send(message); } void KCMKWinRules::updateState() { m_ruleBook->setCount(m_rules.count()); emit editingIndexChanged(); emit ruleBookModelChanged(); updateNeedsSave(); } void KCMKWinRules::updateNeedsSave() { setNeedsSave(true); emit needsSaveChanged(); } void KCMKWinRules::saveCurrentRule() { if (m_editingIndex < 0) { return; } if (needsSave()) { delete(m_rules[m_editingIndex]); m_rules[m_editingIndex] = m_rulesModel->exportToRules(); } } int KCMKWinRules::editingIndex() const { return m_editingIndex; } void KCMKWinRules::editRule(int index) { if (index < 0 || index >= m_rules.count()) { return; } saveCurrentRule(); m_editingIndex = index; m_rulesModel->importFromRules(m_rules.at(m_editingIndex)); emit editingIndexChanged(); // Show and move to Rules Editor page if (depth() < 2) { push(QStringLiteral("RulesEditor.qml")); } setCurrentIndex(1); } -void KCMKWinRules::newRule() +void KCMKWinRules::createRule() { m_rules.append(new Rules()); updateState(); const int newIndex = m_rules.count() - 1; editRule(newIndex); saveCurrentRule(); } void KCMKWinRules::removeRule(int index) { - const int lastIndex = m_rules.count() - 1; - if (index < 0 || index > lastIndex) { + if (index < 0 || index >= m_rules.count()) { return; } if (m_editingIndex == index) { m_editingIndex = -1; pop(); } + delete(m_rules.at(index)); m_rules.removeAt(index); updateState(); } void KCMKWinRules::moveRule(int sourceIndex, int destIndex) { const int lastIndex = m_rules.count() - 1; if (sourceIndex == destIndex || (sourceIndex < 0 || sourceIndex > lastIndex) || (destIndex < 0 || destIndex > lastIndex)) { return; } m_rules.move(sourceIndex, destIndex); if (m_editingIndex == sourceIndex) { m_editingIndex = destIndex; emit editingIndexChanged(); } else if (m_editingIndex > sourceIndex && m_editingIndex <= destIndex) { m_editingIndex -= 1; emit editingIndexChanged(); } else if (m_editingIndex < sourceIndex && m_editingIndex >= destIndex) { m_editingIndex += 1; emit editingIndexChanged(); } emit ruleBookModelChanged(); } void KCMKWinRules::exportRule(int index) { Q_ASSERT(index >= 0 && index < m_rules.count()); saveCurrentRule(); const QString description = m_rules.at(index)->description; const QString defaultPath = QDir(QDir::home()).filePath(description + QStringLiteral(".kwinrule")); const QString path = QFileDialog::getSaveFileName(nullptr, i18n("Export Rules"), defaultPath, i18n("KWin Rules (*.kwinrule)")); if (path.isEmpty()) return; const auto config = KSharedConfig::openConfig(path, KConfig::SimpleConfig); RuleSettings settings(config, m_rules.at(index)->description); settings.setDefaults(); m_rules.at(index)->write(&settings); settings.save(); } void KCMKWinRules::importRules() { QString path = QFileDialog::getOpenFileName(nullptr, i18n("Import Rules"), QDir::home().absolutePath(), i18n("KWin Rules (*.kwinrule)")); if (path.isEmpty()) { return; } const auto config = KSharedConfig::openConfig(path, KConfig::SimpleConfig); const QStringList groups = config->groupList(); if (groups.isEmpty()) { return; } for (const QString &groupName : groups) { RuleSettings settings(config, groupName); const bool remove = settings.deleteRule(); const QString importDescription = settings.description(); if (importDescription.isEmpty()) { continue; } // Try to find a rule with the same description to replace int newIndex = -2; for (int index = 0; index < m_rules.count(); index++) { if (m_rules.at(index)->description == importDescription) { newIndex = index; break; } } if (remove) { removeRule(newIndex); continue; } Rules *newRule = new Rules(&settings); if (newIndex < 0) { m_rules.append(newRule); } else { delete m_rules[newIndex]; m_rules[newIndex] = newRule; } // Reset rule editor if the current rule changed when importing if (m_editingIndex == newIndex) { m_rulesModel->importFromRules(m_rules.at(m_editingIndex)); } } updateState(); } K_PLUGIN_CLASS_WITH_JSON(KCMKWinRules, "kcm_kwinrules_qml.json"); } // namespace #include "kcmrules.moc" diff --git a/kcmkwin/kwinrules/kcmrules.h b/kcmkwin/kwinrules/kcmrules.h index 760355cfc..d6d5e71af 100644 --- a/kcmkwin/kwinrules/kcmrules.h +++ b/kcmkwin/kwinrules/kcmrules.h @@ -1,82 +1,81 @@ /* * Copyright (c) 2020 Ismael Asensio * * 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 . */ #pragma once #include #include "rulebooksettings.h" #include "rulesmodel.h" #include #include namespace KWin { class KCMKWinRules : public KQuickAddons::ConfigModule { Q_OBJECT Q_PROPERTY(QStringList ruleBookModel READ ruleBookModel NOTIFY ruleBookModelChanged) Q_PROPERTY(int editingIndex READ editingIndex NOTIFY editingIndexChanged) Q_PROPERTY(RulesModel *rulesModel MEMBER m_rulesModel CONSTANT) public: explicit KCMKWinRules(QObject *parent, const QVariantList &arguments); - ~KCMKWinRules(); + ~KCMKWinRules() override; -public slots: - void load() override; - void save() override; + Q_INVOKABLE void editRule(int index); - QStringList ruleBookModel() const; - - int editingIndex() const; - void editRule(int index); + Q_INVOKABLE void createRule(); + Q_INVOKABLE void removeRule(int index); + Q_INVOKABLE void moveRule(int sourceIndex, int destIndex); - void newRule(); - void removeRule(int index); - void moveRule(int sourceIndex, int destIndex); + Q_INVOKABLE void exportRule(int index); + Q_INVOKABLE void importRules(); - void exportRule(int index); - void importRules(); +public slots: + void load() override; + void save() override; signals: void ruleBookModelChanged(); void editingIndexChanged(); private slots: void updateNeedsSave(); void updateState(); private: + QStringList ruleBookModel() const; + int editingIndex() const; void saveCurrentRule(); private: RuleBookSettings *m_ruleBook; QVector m_rules; RulesModel* m_rulesModel; int m_editingIndex = -1; }; } // namespace diff --git a/kcmkwin/kwinrules/optionsmodel.cpp b/kcmkwin/kwinrules/optionsmodel.cpp index 62bd5d74b..f94a94428 100644 --- a/kcmkwin/kwinrules/optionsmodel.cpp +++ b/kcmkwin/kwinrules/optionsmodel.cpp @@ -1,195 +1,195 @@ /* * Copyright (c) 2020 Ismael Asensio * * 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 . */ #include "optionsmodel.h" #include namespace KWin { QHash OptionsModel::roleNames() const { return { {Qt::DisplayRole, QByteArrayLiteral("text")}, {Qt::UserRole, QByteArrayLiteral("value")}, {Qt::DecorationRole, QByteArrayLiteral("icon")}, {Qt::ToolTipRole, QByteArrayLiteral("description")}, {Qt::UserRole + 1, QByteArrayLiteral("iconName")}, }; } int OptionsModel::rowCount(const QModelIndex &parent) const { if (parent.isValid()) { return 0; } return m_data.size(); } QVariant OptionsModel::data(const QModelIndex &index, int role) const { - if (!index.isValid() || index.column() != 0 || index.row() < 0 || index.row() >= int(m_data.size())) { + if (!checkIndex(index, CheckIndexOption::IndexIsValid | CheckIndexOption::ParentIsInvalid)) { return QVariant(); } const Data data = m_data.at(index.row()); switch (role) { case Qt::DisplayRole: return data.text; case Qt::UserRole: return data.value; case Qt::DecorationRole: return data.icon; case Qt::UserRole + 1: return data.icon.name(); case Qt::ToolTipRole: return data.description; } return QVariant(); } int OptionsModel::selectedIndex() const { return m_index; } QVariant OptionsModel::value() const { if (m_data.isEmpty()) { return QVariant(); } return m_data.at(m_index).value; } void OptionsModel::setValue(QVariant value) { if (this->value() == value) { return; } for (int index = 0; index < m_data.count(); index++) { if (m_data.at(index).value != value) { continue; } if (m_index != index) { m_index = index; emit selectedIndexChanged(index); } } } void OptionsModel::resetValue() { m_index = 0; emit selectedIndexChanged(m_index); } void OptionsModel::updateModelData(const QList &data) { beginResetModel(); m_data = data; endResetModel(); } RulePolicy::Type RulePolicy::type() const { return m_type; } int RulePolicy::value() const { if (m_type == RulePolicy::NoPolicy) { return Rules::Apply; // To simplify external checks when rule has no policy } return OptionsModel::value().toInt(); } QString RulePolicy::policyKey(const QString &key) const { switch (m_type) { case NoPolicy: return QString(); case StringMatch: return QStringLiteral("%1match").arg(key); case SetRule: case ForceRule: return QStringLiteral("%1rule").arg(key); } return QString(); } QList RulePolicy::policyOptions(RulePolicy::Type type) { static const auto stringMatchOptions = QList { {Rules::UnimportantMatch, i18n("Unimportant")}, {Rules::ExactMatch, i18n("Exact Match")}, {Rules::SubstringMatch, i18n("Substring Match")}, {Rules::RegExpMatch, i18n("Regular Expression")} }; static const auto setRuleOptions = QList { {Rules::DontAffect, i18n("Do Not Affect"), i18n("The window property will not be affected and therefore the default handling for it will be used." "\nSpecifying this will block more generic window settings from taking effect.")}, {Rules::Apply, i18n("Apply Initially"), i18n("The window property will be only set to the given value after the window is created." "\nNo further changes will be affected.")}, {Rules::Remember, i18n("Remember"), i18n("The value of the window property will be remembered and, every time the window" " is created, the last remembered value will be applied.")}, {Rules::Force, i18n("Force"), i18n("The window property will be always forced to the given value.")}, {Rules::ApplyNow, i18n("Apply Now"), i18n("The window property will be set to the given value immediately and will not be affected later" "\n(this action will be deleted afterwards).")}, {Rules::ForceTemporarily, i18n("Force Temporarily"), i18n("The window property will be forced to the given value until it is hidden" "\n(this action will be deleted after the window is hidden).")} }; static auto forceRuleOptions = QList { setRuleOptions.at(0), // Rules::DontAffect setRuleOptions.at(3), // Rules::Force setRuleOptions.at(5), // Rules::ForceTemporarily }; switch (type) { case NoPolicy: return {}; case StringMatch: return stringMatchOptions; case SetRule: return setRuleOptions; case ForceRule: return forceRuleOptions; } return {}; } } //namespace diff --git a/kcmkwin/kwinrules/package/contents/ui/RuleItemDelegate.qml b/kcmkwin/kwinrules/package/contents/ui/RuleItemDelegate.qml index 8f4ad3b04..8c7fdb226 100644 --- a/kcmkwin/kwinrules/package/contents/ui/RuleItemDelegate.qml +++ b/kcmkwin/kwinrules/package/contents/ui/RuleItemDelegate.qml @@ -1,108 +1,105 @@ /* * Copyright (c) 2020 Ismael Asensio * * 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 . */ import QtQuick 2.12 import QtQuick.Layouts 1.12 import QtQuick.Controls 2.12 as QQC2 import org.kde.kirigami 2.10 as Kirigami Kirigami.AbstractListItem { id: ruleDelegate - focus: true - property var ruleEnabled: model.enabled + property bool ruleEnabled: model.enabled Kirigami.Theme.colorSet: Kirigami.Theme.View RowLayout { - spacing: Kirigami.Units.smallSpacing anchors { left: parent.left right: parent.right margins: Kirigami.Units.smallSpacing } QQC2.CheckBox { id: itemEnabled opacity: model.selectable ? 1 : 0 checked: model.enabled onToggled: { model.enabled = checked; } } Kirigami.Icon { id: itemIcon source: model.icon Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium Layout.rightMargin: Kirigami.Units.smallSpacing QQC2.ToolTip { text: model.description visible: hovered && (model.description != "") } } QQC2.Label { text: model.name Layout.minimumWidth: 12 * Kirigami.Units.gridUnit + elide: Text.ElideRight } Item { Layout.fillWidth: true } OptionsComboBox { id: policyCombo Layout.minimumWidth: 6 * Kirigami.Units.gridUnit Layout.maximumWidth: 12 * Kirigami.Units.gridUnit Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter flat: true visible: count > 0 enabled: ruleEnabled model: policyModel onActivated: { - print ("Policy changed for rule " + key + ": " + currentValue); policy = currentValue; } } ValueEditor { id: valueEditor Layout.minimumWidth: 9 * Kirigami.Units.gridUnit Layout.maximumWidth: policyCombo.visible ? 20 * Kirigami.Units.gridUnit : 32 * Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing Layout.fillWidth: true Layout.alignment: Qt.AlignRight | Qt.AlignVCenter enabled: model.enabled ruleValue: model.value ruleOptions: model.options controlType: model.type onValueEdited: (value) => { - print ("Rule changed: " + model.key + " = " + value); model.value = value; } } } } diff --git a/kcmkwin/kwinrules/package/contents/ui/RulesEditor.qml b/kcmkwin/kwinrules/package/contents/ui/RulesEditor.qml index a1d92edc8..92d9cc1e2 100644 --- a/kcmkwin/kwinrules/package/contents/ui/RulesEditor.qml +++ b/kcmkwin/kwinrules/package/contents/ui/RulesEditor.qml @@ -1,122 +1,112 @@ /* * Copyright (c) 2020 Ismael Asensio * * 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 . */ import QtQuick 2.12 import QtQuick.Layouts 1.12 import QtQuick.Controls 2.12 as QQC2 import org.kde.kirigami 2.10 as Kirigami import org.kde.kcm 1.2 ScrollViewKCM { id: rulesEditor property var rulesModel: kcm.rulesModel title: rulesModel.description header: RowLayout { id: filterBar - Kirigami.ActionTextField { + Kirigami.SearchField { id: searchField Layout.fillWidth: true placeholderText: i18n("Search...") focusSequence: "Ctrl+F" onTextChanged: { rulesModel.filter.searchText = text; } - - rightActions: [ - Kirigami.Action { - iconName: "edit-clear" - visible: searchField.text !== "" - onTriggered: { - searchField.text = "" - searchField.accepted() - } - } - ] } QQC2.ToolButton { id: showAllButton icon.name: checked ? 'view-visible' : 'view-hidden' - text: i18n("Show all rules") + text: i18n("Show All Rules") checkable: true enabled: searchField.text.trim() == "" - checked: rulesModel.filter.showAll - onToggled: { - rulesModel.filter.showAll = checked; + Binding { + target: rulesModel.filter + property: "showAll" + value: showAllButton.checked } } } view: ListView { id: rulesView clip: true model: rulesModel.filter delegate: RuleItemDelegate {} section { property: "section" delegate: Kirigami.ListSectionHeader { label: section } } } // FIXME: InlineMessage.qml:241:13: QML Label: Binding loop detected for property "verticalAlignment" footer: GridLayout { id: kcmFooter columns: 2 QQC2.Button { Layout.fillWidth: true text: i18n("Detect window properties") icon.name: "edit-find" // TODO: Better icon for "Detect window properties" onClicked: { rulesModel.detectWindowProperties(detection_delay.value); } } QQC2.SpinBox { id: detection_delay from: 0 to: 30 textFromValue: (value, locale) => { - return value == 0 ? i18n("instantly") - : i18np("after 1 second", "after %1 seconds", value) + return (value == 0) ? i18n("Instantly") + : i18np("After 1 second", "After %1 seconds", value) } } Kirigami.InlineMessage { id: warningMessage Layout.columnSpan: kcmFooter.columns Layout.fillWidth: true Layout.fillHeight: true visible: rulesModel.showWarning text: i18n("You have specified the window class as unimportant.\n" + - "This means the settings will possibly apply to windows from all " + - "applications. If you really want to create a generic setting, it is " + - "recommended you at least limit the window types to avoid special window " + - "types.") + "This means the settings will possibly apply to windows from all " + + "applications. If you really want to create a generic setting, it is " + + "recommended you at least limit the window types to avoid special window " + + "types.") } } } diff --git a/kcmkwin/kwinrules/package/contents/ui/RulesList.qml b/kcmkwin/kwinrules/package/contents/ui/RulesList.qml index dfa194401..b0c52f5b3 100644 --- a/kcmkwin/kwinrules/package/contents/ui/RulesList.qml +++ b/kcmkwin/kwinrules/package/contents/ui/RulesList.qml @@ -1,150 +1,149 @@ /* * Copyright (c) 2020 Ismael Asensio * * 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 . */ import QtQuick 2.12 import QtQuick.Layouts 1.12 import QtQuick.Controls 2.12 as QQC2 import org.kde.kcm 1.2 import org.kde.kirigami 2.5 as Kirigami ScrollViewKCM { id: rulesListKCM property int dragIndex: -1 property int dropIndex: -1 // FIXME: ScrollViewKCM.qml:73:13: QML Control: Binding loop detected for property "implicitHeight" implicitWidth: Kirigami.Units.gridUnit * 35 implicitHeight: Kirigami.Units.gridUnit * 25 ConfigModule.columnWidth: Kirigami.Units.gridUnit * 22 ConfigModule.buttons: ConfigModule.Apply view: ListView { id: ruleBookView clip: true - focus: true model: kcm.ruleBookModel delegate: Kirigami.DelegateRecycler { width: ruleBookView.width sourceComponent: ruleBookDelegate } currentIndex: kcm.editingIndex Rectangle { id: dropIndicator x: 0 z: 100 width: parent.width height: Kirigami.Units.smallSpacing color: Kirigami.Theme.highlightColor visible: (dropIndex >= 0) && (dropIndex != dragIndex) Connections { target: rulesListKCM onDropIndexChanged: { if (dropIndex >= 0) { // TODO: After Qt 5.13 we can use ListView.itemAtIndex(index) var dropItem = ruleBookView.contentItem.children[dropIndex]; dropIndicator.y = (dropIndex < dragIndex) ? dropItem.y : dropItem.y + dropItem.height; } } } } } footer: Kirigami.ActionToolBar { Layout.fillWidth: true alignment: Qt.AlignRight actions: [ Kirigami.Action { text: i18n("Import") iconName: "document-import" onTriggered: kcm.importRules(); } , Kirigami.Action { text: i18n("New") iconName: "list-add-symbolic" - onTriggered: kcm.newRule(); + onTriggered: kcm.createRule(); } ] } Component { id: ruleBookDelegate Kirigami.AbstractListItem { id: ruleBookItem highlighted: kcm.editIndex == index onClicked: { kcm.editRule(index); } RowLayout { //FIXME: If not used within DelegateRecycler, item goes on top of the first item when clicked //FIXME: Improve visuals and behavior when dragging on the list. Kirigami.ListItemDragHandle { listItem: ruleBookItem listView: ruleBookView onMoveRequested: { dragIndex = oldIndex; dropIndex = newIndex; } onDropped: { if (dropIndex >= 0 && dropIndex != dragIndex) { kcm.moveRule(dragIndex, dropIndex); } dragIndex = -1; dropIndex = -1; } } QQC2.Label { text: modelData } Kirigami.ActionToolBar { Layout.fillWidth: true alignment: Qt.AlignRight display: QQC2.Button.IconOnly opacity: ruleBookItem.hovered ? 1 : 0 focus: false actions: [ Kirigami.Action { text: i18n("Export") iconName: "document-export" onTriggered: kcm.exportRule(index); } , Kirigami.Action { text: i18n("Delete") iconName: "entry-delete" onTriggered: kcm.removeRule(index); } ] } } } } } diff --git a/kcmkwin/kwinrules/package/contents/ui/ValueEditor.qml b/kcmkwin/kwinrules/package/contents/ui/ValueEditor.qml index b75ce6111..8baa4b3a5 100644 --- a/kcmkwin/kwinrules/package/contents/ui/ValueEditor.qml +++ b/kcmkwin/kwinrules/package/contents/ui/ValueEditor.qml @@ -1,197 +1,196 @@ /* * Copyright (c) 2020 Ismael Asensio * * 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 . */ import QtQuick 2.12 import QtQuick.Layouts 1.12 import QtQuick.Controls 2.12 as QQC2 import org.kde.kirigami 2.10 as Kirigami import org.kde.kquickcontrols 2.0 as KQC import org.kde.kcms.kwinrules 1.0 Loader { id: valueEditor focus: true property var ruleValue property var ruleOptions property int controlType signal valueEdited(var value) sourceComponent: { switch (controlType) { case RuleItem.Boolean: return booleanEditor case RuleItem.String: return stringEditor case RuleItem.Integer: return integerEditor case RuleItem.Option: return optionEditor case RuleItem.FlagsOption: return flagsEditor case RuleItem.Percentage: return percentageEditor case RuleItem.Coordinate: return coordinateEditor case RuleItem.Shortcut: return shortcutEditor default: return emptyEditor } } Component { id: emptyEditor Item {} } Component { id: booleanEditor RowLayout { Item { Layout.fillWidth: true } QQC2.Switch { text: checked ? i18n("Yes") : i18n("No") checked: ruleValue onToggled: valueEditor.valueEdited(checked) } } } Component { id: stringEditor QQC2.TextField { property bool isTextEdited: false text: ruleValue onTextEdited: { isTextEdited = true; } onEditingFinished: { if (isTextEdited) { valueEditor.valueEdited(text); } isTextEdited = false; } } } Component { id: integerEditor QQC2.SpinBox { editable: true value: ruleValue onValueModified: valueEditor.valueEdited(value) } } Component { id: optionEditor OptionsComboBox { flat: true model: ruleOptions onActivated: (index) => { valueEditor.valueEdited(currentValue); } } } Component { id: flagsEditor RowLayout { Layout.minimumWidth: 10 * Kirigami.Units.gridUnit Layout.alignment: Qt.AlignRight; spacing: 0 Repeater { id: flagsRepeater model: ruleOptions QQC2.ToolButton { property int bit: model.value icon.name: model.iconName checkable: true checked: (ruleValue & (1 << bit)) == (1 << bit) QQC2.ToolTip.text: model.text QQC2.ToolTip.visible: hovered onToggled: { valueEditor.valueEdited((ruleValue & ~(1 << bit)) | (checked << bit)); } } } } } Component { id: percentageEditor RowLayout { QQC2.Slider { id: slider Layout.fillWidth: true from: 0 to: 100 value: ruleValue onMoved: valueEditor.valueEdited(Math.round(slider.value)) } QQC2.Label { text: i18n("%1 %", Math.round(slider.value)) width: 2 * Kirigami.Units.gridUnit } } } Component { id: coordinateEditor RowLayout { id: coordItem spacing: Kirigami.Units.smallSpacing property var coords: ruleValue ? ruleValue.split(',') : [0, 0] - //FIXME: QML RowLayout: Binding loop detected for property "coordWidth" property int coordWidth: (coordItem.width - coordSeparator.width) / 2 - coordItem.spacing QQC2.SpinBox { id: coordX editable: true implicitWidth: coordWidth from: 0 to: 4098 value: coords[0] onValueModified: valueEditor.valueEdited(coordX.value + "," + coordY.value) } QQC2.Label { id: coordSeparator text: i18nc("(x, y) coordinates separator in size/position","x") horizontalAlignment: Text.AlignHCenter } QQC2.SpinBox { id: coordY editable: true from: 0 to: 4098 implicitWidth: coordWidth value: coords[1] onValueModified: valueEditor.valueEdited(coordX.value + "," + coordY.value) } } } Component { id: shortcutEditor RowLayout { Item { Layout.fillWidth: true } KQC.KeySequenceItem { keySequence: ruleValue onCaptureFinished: valueEditor.valueEdited(keySequence) } } } } diff --git a/kcmkwin/kwinrules/rulesmodel.cpp b/kcmkwin/kwinrules/rulesmodel.cpp index ec7f3860d..7e678681d 100644 --- a/kcmkwin/kwinrules/rulesmodel.cpp +++ b/kcmkwin/kwinrules/rulesmodel.cpp @@ -1,836 +1,847 @@ /* * Copyright (c) 2004 Lubos Lunak * Copyright (c) 2020 Ismael Asensio * * 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 . */ #include "rulesmodel.h" #include #include #include #include #include #include #include #include #include #include namespace KWin { RulesModel::RulesModel(QObject *parent) : QAbstractListModel(parent) , m_filterModel(new RulesFilterModel(this)) { qmlRegisterUncreatableType("org.kde.kcms.kwinrules", 1, 0, "RuleItem", QStringLiteral("Do not create objects of type RuleItem")); qmlRegisterUncreatableType("org.kde.kcms.kwinrules", 1, 0, "RulesFilterModel", QStringLiteral("Do not create objects of type RulesFilterModel")); m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive); m_filterModel->setSourceModel(this); populateRuleList(); } RulesModel::~RulesModel() { - delete m_filterModel; - delete m_activities; } QHash< int, QByteArray > RulesModel::roleNames() const { return { {KeyRole, QByteArrayLiteral("key")}, {NameRole, QByteArrayLiteral("name")}, {IconRole, QByteArrayLiteral("icon")}, {IconNameRole, QByteArrayLiteral("iconName")}, {SectionRole, QByteArrayLiteral("section")}, {DescriptionRole, QByteArrayLiteral("description")}, {EnabledRole, QByteArrayLiteral("enabled")}, {SelectableRole, QByteArrayLiteral("selectable")}, {ValueRole, QByteArrayLiteral("value")}, {TypeRole, QByteArrayLiteral("type")}, {PolicyRole, QByteArrayLiteral("policy")}, {PolicyModelRole, QByteArrayLiteral("policyModel")}, {OptionsModelRole, QByteArrayLiteral("options")}, }; } int RulesModel::rowCount(const QModelIndex &parent) const { if (parent.isValid()) { return 0; } return m_ruleList.size(); } QVariant RulesModel::data(const QModelIndex &index, int role) const { - if (!index.isValid() || index.column() != 0 || index.row() < 0 || index.row() >= int(m_ruleList.size())) { + if (!checkIndex(index, CheckIndexOption::IndexIsValid | CheckIndexOption::ParentIsInvalid)) { return QVariant(); } const RuleItem *rule = m_ruleList.at(index.row()); switch (role) { case KeyRole: return rule->key(); case NameRole: return rule->name(); case IconRole: return rule->icon(); case IconNameRole: return rule->iconName(); case DescriptionRole: return rule->description(); case SectionRole: return rule->section(); case EnabledRole: return rule->isEnabled(); case SelectableRole: return !rule->hasFlag(RuleItem::AlwaysEnabled); case ValueRole: return rule->value(); case TypeRole: return rule->type(); case PolicyRole: return rule->policy(); case PolicyModelRole: return rule->policyModel(); case OptionsModelRole: return rule->options(); } return QVariant(); } bool RulesModel::setData(const QModelIndex &index, const QVariant &value, int role) { - if (!index.isValid() || index.column() != 0 || index.row() < 0 || index.row() >= int(m_ruleList.size())) { + if (!checkIndex(index, CheckIndexOption::IndexIsValid | CheckIndexOption::ParentIsInvalid)) { return false; } RuleItem *rule = m_ruleList.at(index.row()); switch (role) { case EnabledRole: + if (value.toBool() == rule->isEnabled()) { + return true; + } rule->setEnabled(value.toBool()); break; case ValueRole: + if (value == rule->value()) { + return true; + } rule->setValue(value); break; case PolicyRole: + if (value.toInt() == rule->policy()) { + return true; + } rule->setPolicy(value.toInt()); break; default: return false; } emit dataChanged(index, index, QVector{role}); if (rule->hasFlag(RuleItem::AffectsDescription)) { emit descriptionChanged(); } if (rule->hasFlag(RuleItem::AffectsWarning)) { emit showWarningChanged(); } return true; } RuleItem *RulesModel::addRule(RuleItem *rule) { m_ruleList << rule; m_rules.insert(rule->key(), rule); return rule; } bool RulesModel::hasRule(const QString& key) const { return m_rules.contains(key); } RuleItem *RulesModel::ruleItem(const QString& key) const { - return m_rules[key]; + return m_rules.value(key); } QString RulesModel::description() const { const QString desc = m_rules["description"]->value().toString(); if (!desc.isEmpty()) { return desc; } return defaultDescription(); } QString RulesModel::defaultDescription() const { const QString wmclass = m_rules["wmclass"]->value().toString(); const QString title = m_rules["title"]->isEnabled() ? m_rules["title"]->value().toString() : QString(); if (!title.isEmpty()) { return i18n("Window settings for %1", title); } if (!wmclass.isEmpty()) { return i18n("Settings for %1", wmclass); } return i18n("New window settings"); } bool RulesModel::isWarningShown() const { const bool no_wmclass = !m_rules["wmclass"]->isEnabled() || m_rules["wmclass"]->policy() == Rules::UnimportantMatch; const bool alltypes = !m_rules["types"]->isEnabled() || (m_rules["types"]->value() == 0) || (m_rules["types"]->value() == NET::AllTypesMask) || ((m_rules["types"]->value().toInt() | (1 << NET::Override)) == 0x3FF); return (no_wmclass && alltypes); } void RulesModel::readFromSettings(RuleSettings *settings) { beginResetModel(); for (RuleItem *rule : qAsConst(m_ruleList)) { const KConfigSkeletonItem *configItem = settings->findItem(rule->key()); const KConfigSkeletonItem *configPolicyItem = settings->findItem(rule->policyKey()); if (!configItem || (configPolicyItem && configPolicyItem->property() == Rules::Unused) || (configItem->property().toString().isEmpty())) { rule->reset(); continue; } rule->setEnabled(true); const QVariant value = configItem->property(); rule->setValue(value); if (rule->policyType() != RulePolicy::NoPolicy) { const int policy = configPolicyItem->property().toInt(); rule->setPolicy(policy); } } endResetModel(); emit descriptionChanged(); emit showWarningChanged(); } void RulesModel::writeToSettings(RuleSettings *settings) const { const QString description = m_rules["description"]->value().toString(); if (description.isEmpty()) { m_rules["description"]->setValue(defaultDescription()); } for (const RuleItem *rule : qAsConst(m_ruleList)) { KConfigSkeletonItem *configItem = settings->findItem(rule->key()); KConfigSkeletonItem *configPolicyItem = settings->findItem(rule->policyKey()); Q_ASSERT (configItem); if (rule->isEnabled()) { configItem->setProperty(rule->value()); if (configPolicyItem) { configPolicyItem->setProperty(rule->policy()); } } else { if (configPolicyItem) { configPolicyItem->setProperty(Rules::Unused); } else { // Rules without policy gets deactivated by an empty string configItem->setProperty(QString()); } } } } void RulesModel::importFromRules(Rules* rules) { QTemporaryFile tempFile; if (!tempFile.open()) { return; } const auto cfg = KSharedConfig::openConfig(tempFile.fileName(), KConfig::SimpleConfig); RuleSettings *settings = new RuleSettings(cfg, QStringLiteral("tempSettings")); settings->setDefaults(); if (rules) { rules->write(settings); } readFromSettings(settings); delete(settings); } Rules *RulesModel::exportToRules() const { QTemporaryFile tempFile; if (!tempFile.open()) { return nullptr; } const auto cfg = KSharedConfig::openConfig(tempFile.fileName(), KConfig::SimpleConfig); RuleSettings *settings = new RuleSettings(cfg, QStringLiteral("tempSettings")); writeToSettings(settings); Rules *rules = new Rules(settings); delete(settings); return rules; } void RulesModel::populateRuleList() { + qDeleteAll(m_ruleList); m_ruleList.clear(); //Rule description addRule(new RuleItem(QLatin1String("description"), RulePolicy::NoPolicy, RuleItem::String, i18n("Description"), i18n("Window matching"), QStringLiteral("entry-edit"))); m_rules["description"]->setFlags(RuleItem::AlwaysEnabled | RuleItem::AffectsDescription); // Window matching addRule(new RuleItem(QLatin1String("wmclass"), RulePolicy::StringMatch, RuleItem::String, i18n("Window class (application)"), i18n("Window matching"), QStringLiteral("window"))); m_rules["wmclass"]->setFlags(RuleItem::AlwaysEnabled | RuleItem::AffectsWarning | RuleItem::AffectsDescription); addRule(new RuleItem(QLatin1String("wmclasscomplete"), RulePolicy::NoPolicy, RuleItem::Boolean, i18n("Match whole window class"), i18n("Window matching"), QStringLiteral("window"))); m_rules["wmclasscomplete"]->setFlags(RuleItem::AlwaysEnabled); addRule(new RuleItem(QLatin1String("types"), RulePolicy::NoPolicy, RuleItem::FlagsOption, i18n("Window types"), i18n("Window matching"), QStringLiteral("window-duplicate"), windowTypesModelData())); m_rules["types"]->setFlags(RuleItem::AlwaysEnabled | RuleItem::AffectsWarning ); addRule(new RuleItem(QLatin1String("windowrole"), RulePolicy::NoPolicy, RuleItem::String, i18n("Window role"), i18n("Window matching"), QStringLiteral("dialog-object-properties"))); addRule(new RuleItem(QLatin1String("title"), RulePolicy::StringMatch, RuleItem::String, i18n("Window title"), i18n("Window matching"), QStringLiteral("edit-comment"))); m_rules["title"]->setFlags(RuleItem::AffectsDescription); addRule(new RuleItem(QLatin1String("clientmachine"), RulePolicy::StringMatch, RuleItem::String, i18n("Machine (hostname)"), i18n("Window matching"), QStringLiteral("computer"))); // Size & Position addRule(new RuleItem(QLatin1String("position"), RulePolicy::SetRule, RuleItem::Coordinate, i18n("Position"), i18n("Size & Position"), QStringLiteral("transform-move"))); addRule(new RuleItem(QLatin1String("size"), RulePolicy::SetRule, RuleItem::Coordinate, i18n("Size"), i18n("Size & Position"), QStringLiteral("image-resize-symbolic"))); addRule(new RuleItem(QLatin1String("maximizehoriz"), RulePolicy::SetRule, RuleItem::Boolean, i18n("Maximized horizontally"), i18n("Size & Position"), QStringLiteral("resizecol"))); addRule(new RuleItem(QLatin1String("maximizevert"), RulePolicy::SetRule, RuleItem::Boolean, i18n("Maximized vertically"), i18n("Size & Position"), QStringLiteral("resizerow"))); addRule(new RuleItem(QLatin1String("desktop"), RulePolicy::SetRule, RuleItem::Option, i18n("Virtual Desktop"), i18n("Size & Position"), QStringLiteral("virtual-desktops"), virtualDesktopsModelData())); #ifdef KWIN_BUILD_ACTIVITIES m_activities = new KActivities::Consumer(this); addRule(new RuleItem(QLatin1String("activity"), RulePolicy::SetRule, RuleItem::Option, i18n("Activity"), i18n("Size & Position"), QStringLiteral("activities"), activitiesModelData())); // Activites consumer may update the available activities later connect(m_activities, &KActivities::Consumer::activitiesChanged, this, [this] { m_rules["activity"]->setOptionsData(activitiesModelData()); }); connect(m_activities, &KActivities::Consumer::serviceStatusChanged, this, [this] { m_rules["activity"]->setOptionsData(activitiesModelData()); }); #endif addRule(new RuleItem(QLatin1String("screen"), RulePolicy::SetRule, RuleItem::Integer, i18n("Screen"), i18n("Size & Position"), QStringLiteral("osd-shutd-screen"))); addRule(new RuleItem(QLatin1String("fullscreen"), RulePolicy::SetRule, RuleItem::Boolean, i18n("Fullscreen"), i18n("Size & Position"), QStringLiteral("view-fullscreen"))); addRule(new RuleItem(QLatin1String("minimize"), RulePolicy::SetRule, RuleItem::Boolean, i18n("Minimized"), i18n("Size & Position"), QStringLiteral("window-minimize"))); addRule(new RuleItem(QLatin1String("shade"), RulePolicy::SetRule, RuleItem::Boolean, i18n("Shaded"), i18n("Size & Position"), QStringLiteral("window-shade"))); addRule(new RuleItem(QLatin1String("placement"), RulePolicy::ForceRule, RuleItem::Option, i18n("Initial placement"), i18n("Size & Position"), QStringLiteral("region"), placementModelData())); addRule(new RuleItem(QLatin1String("ignoregeometry"), RulePolicy::SetRule, RuleItem::Boolean, i18n("Ignore requested geometry"), i18n("Size & Position"), QStringLiteral("view-time-schedule-baselined-remove"))); m_rules["ignoregeometry"]->setDescription(i18n("Windows can ask to appear in a certain position.\n" "By default this overrides the placement strategy\n" "what might be nasty if the client abuses the feature\n" "to unconditionally popup in the middle of your screen.")); addRule(new RuleItem(QLatin1String("minsize"), RulePolicy::ForceRule, RuleItem::Coordinate, i18n("Minimum Size"), i18n("Size & Position"), QStringLiteral("image-resize-symbolic"))); addRule(new RuleItem(QLatin1String("maxsize"), RulePolicy::ForceRule, RuleItem::Coordinate, i18n("Maximum Size"), i18n("Size & Position"), QStringLiteral("image-resize-symbolic"))); addRule(new RuleItem(QLatin1String("strictgeometry"), RulePolicy::ForceRule, RuleItem::Boolean, i18n("Obey geometry restrictions"), i18n("Size & Position"), QStringLiteral("transform-crop-and-resize"))); m_rules["strictgeometry"]->setDescription(i18n("Eg. terminals or video players can ask to keep a certain aspect ratio\n" "or only grow by values larger than one\n" "(eg. by the dimensions of one character).\n" "This may be pointless and the restriction prevents arbitrary dimensions\n" "like your complete screen area.")); // Arrangement & Access addRule(new RuleItem(QLatin1String("above"), RulePolicy::SetRule, RuleItem::Boolean, i18n("Keep above"), i18n("Arrangement & Access"), QStringLiteral("window-keep-above"))); addRule(new RuleItem(QLatin1String("below"), RulePolicy::SetRule, RuleItem::Boolean, i18n("Keep below"), i18n("Arrangement & Access"), QStringLiteral("window-keep-below"))); addRule(new RuleItem(QLatin1String("skiptaskbar"), RulePolicy::SetRule, RuleItem::Boolean, i18n("Skip taskbar"), i18n("Arrangement & Access"), QStringLiteral("kt-show-statusbar"))); m_rules["skiptaskbar"]->setDescription(i18n("Window shall (not) appear in the taskbar.")); addRule(new RuleItem(QLatin1String("skippager"), RulePolicy::SetRule, RuleItem::Boolean, i18n("Skip pager"), i18n("Arrangement & Access"), QStringLiteral("org.kde.plasma.pager"))); m_rules["skippager"]->setDescription(i18n("Window shall (not) appear in the manager for virtual desktops")); addRule(new RuleItem(QLatin1String("skipswitcher"), RulePolicy::SetRule, RuleItem::Boolean, i18n("Skip switcher"), i18n("Arrangement & Access"), QStringLiteral("preferences-system-windows-effect-flipswitch"))); m_rules["skipswitcher"]->setDescription(i18n("Window shall (not) appear in the Alt+Tab list")); addRule(new RuleItem(QLatin1String("shortcut"), RulePolicy::SetRule, RuleItem::Shortcut, i18n("Shortcut"), i18n("Arrangement & Access"), QStringLiteral("configure-shortcuts"))); // Appearance & Fixes addRule(new RuleItem(QLatin1String("noborder"), RulePolicy::SetRule, RuleItem::Boolean, i18n("No titlebar and frame"), i18n("Appearance & Fixes"), QStringLiteral("dialog-cancel"))); addRule(new RuleItem(QLatin1String("decocolor"), RulePolicy::ForceRule, RuleItem::Option, i18n("Titlebar color scheme"), i18n("Appearance & Fixes"), QStringLiteral("preferences-desktop-theme"), colorSchemesModelData())); addRule(new RuleItem(QLatin1String("opacityactive"), RulePolicy::ForceRule, RuleItem::Percentage, i18n("Active opacity"), i18n("Appearance & Fixes"), QStringLiteral("edit-opacity"))); addRule(new RuleItem(QLatin1String("opacityinactive"), RulePolicy::ForceRule, RuleItem::Percentage, i18n("Inactive opacity"), i18n("Appearance & Fixes"), QStringLiteral("edit-opacity"))); addRule(new RuleItem(QLatin1String("fsplevel"), RulePolicy::ForceRule, RuleItem::Option, i18n("Focus stealing prevention"), i18n("Appearance & Fixes"), QStringLiteral("preferences-system-windows-effect-glide"), focusModelData())); m_rules["fsplevel"]->setDescription(i18n("KWin tries to prevent windows from taking the focus\n" "(\"activate\") while you're working in another window,\n" "but this may sometimes fail or superact.\n" "\"None\" will unconditionally allow this window to get the focus while\n" "\"Extreme\" will completely prevent it from taking the focus.")); addRule(new RuleItem(QLatin1String("fpplevel"), RulePolicy::ForceRule, RuleItem::Option, i18n("Focus protection"), i18n("Appearance & Fixes"), QStringLiteral("preferences-system-windows-effect-minimize"), focusModelData())); m_rules["fpplevel"]->setDescription(i18n("This controls the focus protection of the currently active window.\n" "None will always give the focus away,\n" "Extreme will keep it.\n" "Otherwise it's interleaved with the stealing prevention\n" "assigned to the window that wants the focus.")); addRule(new RuleItem(QLatin1String("acceptfocus"), RulePolicy::ForceRule, RuleItem::Boolean, i18n("Accept focus"), i18n("Appearance & Fixes"), QStringLiteral("preferences-desktop-cursors"))); m_rules["acceptfocus"]->setDescription(i18n("Windows may prevent to get the focus (activate) when being clicked.\n" "On the other hand you might wish to prevent a window\n" "from getting focused on a mouse click.")); addRule(new RuleItem(QLatin1String("disableglobalshortcuts"), RulePolicy::ForceRule, RuleItem::Boolean, i18n("Ignore global shortcuts"), i18n("Appearance & Fixes"), QStringLiteral("input-keyboard-virtual-off"))); m_rules["disableglobalshortcuts"]->setDescription(i18n("When used, a window will receive\n" "all keyboard inputs while it is active, including Alt+Tab etc.\n" "This is especially interesting for emulators or virtual machines.\n" "\n" "Be warned:\n" "you won't be able to Alt+Tab out of the window\n" "nor use any other global shortcut (such as Alt+F2 to show KRunner)\n" "while it's active!")); addRule(new RuleItem(QLatin1String("closeable"), RulePolicy::ForceRule, RuleItem::Boolean, i18n("Closeable"), i18n("Appearance & Fixes"), QStringLiteral("dialog-close"))); addRule(new RuleItem(QLatin1String("type"), RulePolicy::ForceRule, RuleItem::Option, i18n("Set window type"), i18n("Appearance & Fixes"), QStringLiteral("window-duplicate"), windowTypesModelData())); addRule(new RuleItem(QLatin1String("desktopfile"), RulePolicy::SetRule, RuleItem::String, i18n("Desktop file name"), i18n("Appearance & Fixes"), QStringLiteral("application-x-desktop"))); addRule(new RuleItem(QLatin1String("blockcompositing"), RulePolicy::ForceRule, RuleItem::Boolean, i18n("Block compositing"), i18n("Appearance & Fixes"), QStringLiteral("composite-track-on"))); } const QHash RulesModel::x11PropertyHash() { static const auto propertyToRule = QHash { /* The original detection dialog allows to choose depending on "Match complete window class": * if Match Complete == false: wmclass = "resourceClass" * if Match Complete == true: wmclass = "resourceName" + " " + "resourceClass" */ { "resourceName", "wmclass" }, { "caption", "title" }, { "role", "windowrole" }, { "clientMachine", "clientmachine" }, { "x11DesktopNumber", "desktop" }, { "maximizeHorizontal", "maximizehoriz" }, { "maximizeVertical", "maximizevert" }, { "minimized", "minimize" }, { "shaded", "shade" }, { "fullscreen", "fullscreen" }, { "keepAbove", "above" }, { "keepBelow", "below" }, { "noBorder", "noborder" }, { "skipTaskbar", "skiptaskbar" }, { "skipPager", "skippager" }, { "skipSwitcher", "skipswitcher" }, { "type", "type" }, { "desktopFile", "desktopfile" } }; return propertyToRule; }; void RulesModel::prefillProperties(const QVariantMap &info) { beginResetModel(); // Properties that cannot be directly applied via x11PropertyHash const QString position = QStringLiteral("%1,%2").arg(info.value("x").toInt()) .arg(info.value("y").toInt()); const QString size = QStringLiteral("%1,%2").arg(info.value("width").toInt()) .arg(info.value("height").toInt()); if (!m_rules["position"]->isEnabled()) { m_rules["position"]->setValue(position); } if (!m_rules["size"]->isEnabled()) { m_rules["size"]->setValue(size); } if (!m_rules["minsize"]->isEnabled()) { m_rules["minsize"]->setValue(size); } if (!m_rules["maxsize"]->isEnabled()) { m_rules["maxsize"]->setValue(size); } NET::WindowType window_type = static_cast(info.value("type", 0).toInt()); if (!m_rules["types"]->isEnabled() || m_rules["types"]->value() == 0) { if (window_type == NET::Unknown) { window_type = NET::Normal; } m_rules["types"]->setValue(1 << window_type); } const auto ruleForProperty = x11PropertyHash(); for (QString &property : info.keys()) { if (!ruleForProperty.contains(property)) { continue; } const QString ruleKey = ruleForProperty.value(property, QString()); Q_ASSERT(hasRule(ruleKey)); // Only prefill disabled or empty properties if (!m_rules[ruleKey]->isEnabled() || m_rules[ruleKey]->value().toString().isEmpty()) { m_rules[ruleKey]->setValue(info.value(property)); } } endResetModel(); emit descriptionChanged(); emit showWarningChanged(); } QList RulesModel::windowTypesModelData() const { static const auto modelData = QList { //TODO: Find/create better icons { NET::Normal, i18n("Normal Window") , QIcon::fromTheme("window") }, { NET::Dialog, i18n("Dialog Window") , QIcon::fromTheme("window-duplicate") }, { NET::Utility, i18n("Utility Window") , QIcon::fromTheme("dialog-object-properties") }, { NET::Dock, i18n("Dock (panel)") , QIcon::fromTheme("list-remove") }, { NET::Toolbar, i18n("Toolbar") , QIcon::fromTheme("tools") }, { NET::Menu, i18n("Torn-Off Menu") , QIcon::fromTheme("overflow-menu-left") }, { NET::Splash, i18n("Splash Screen") , QIcon::fromTheme("embosstool") }, { NET::Desktop, i18n("Desktop") , QIcon::fromTheme("desktop") }, // { NET::Override, i18n("Unmanaged Window") }, deprecated { NET::TopMenu, i18n("Standalone Menubar"), QIcon::fromTheme("open-menu-symbolic") } }; return modelData; } QList RulesModel::virtualDesktopsModelData() const { QList modelData; for (int desktopId = 1; desktopId <= KWindowSystem::numberOfDesktops(); ++desktopId) { modelData << OptionsModel::Data{ desktopId, QString::number(desktopId).rightJustified(2) + QStringLiteral(": ") + KWindowSystem::desktopName(desktopId), QIcon::fromTheme("virtual-desktops") }; } modelData << OptionsModel::Data{ NET::OnAllDesktops, i18n("All Desktops"), QIcon::fromTheme("window-pin") }; return modelData; } QList RulesModel::activitiesModelData() const { #ifdef KWIN_BUILD_ACTIVITIES QList modelData; // NULL_ID from kactivities/src/lib/core/consumer.cpp modelData << OptionsModel::Data{ QString::fromLatin1("00000000-0000-0000-0000-000000000000"), i18n("All Activities"), QIcon::fromTheme("activities") }; const auto activities = m_activities->activities(KActivities::Info::Running); if (m_activities->serviceStatus() == KActivities::Consumer::Running) { for (const QString &activityId : activities) { const KActivities::Info info(activityId); modelData << OptionsModel::Data{ activityId, info.name(), QIcon::fromTheme(info.icon()) }; } } return modelData; #else return {}; #endif } QList RulesModel::placementModelData() const { // From "placement.h" : Placement rule is stored as a string, not the enum value static const auto modelData = QList { { Placement::policyToString(Placement::Default), i18n("Default") }, { Placement::policyToString(Placement::NoPlacement), i18n("No Placement") }, { Placement::policyToString(Placement::Smart), i18n("Minimal Overlapping") }, { Placement::policyToString(Placement::Maximizing), i18n("Maximized") }, { Placement::policyToString(Placement::Cascade), i18n("Cascaded") }, { Placement::policyToString(Placement::Centered), i18n("Centered") }, { Placement::policyToString(Placement::Random), i18n("Random") }, { Placement::policyToString(Placement::ZeroCornered), i18n("In Top-Left Corner") }, { Placement::policyToString(Placement::UnderMouse), i18n("Under Mouse") }, { Placement::policyToString(Placement::OnMainWindow), i18n("On Main Window") } }; return modelData; } QList RulesModel::focusModelData() const { static const auto modelData = QList { { 0, i18n("None") }, { 1, i18n("Low") }, { 2, i18n("Normal") }, { 3, i18n("High") }, { 4, i18n("Extreme") } }; return modelData; } QList RulesModel::colorSchemesModelData() const { QList modelData; KColorSchemeManager schemes; QAbstractItemModel *schemesModel = schemes.model(); // Skip row 0, which is Default scheme for (int r = 1; r < schemesModel->rowCount(); r++) { - QModelIndex index = schemesModel->index(r, 0); + const QModelIndex index = schemesModel->index(r, 0); modelData << OptionsModel::Data{ - QFileInfo(schemesModel->data(index, Qt::UserRole).toString()).baseName(), - schemesModel->data(index, Qt::DisplayRole).toString(), - schemesModel->data(index, Qt::DecorationRole).value() + QFileInfo(index.data(Qt::UserRole).toString()).baseName(), + index.data(Qt::DisplayRole).toString(), + index.data(Qt::DecorationRole).value() }; } return modelData; } bool RulesFilterModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const { if (m_isSearching) { return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); } if (m_showAll) { return true; } - const QModelIndex index = sourceModel()->index(source_row, 0); - return sourceModel()->data(index, RulesModel::EnabledRole).toBool(); + return sourceModel()->index(source_row, 0).data(RulesModel::EnabledRole).toBool(); } void RulesFilterModel::setSearchText(const QString &text) { const QString searchText = text.trimmed(); m_isSearching = !searchText.isEmpty(); setFilterFixedString(searchText); } bool RulesFilterModel::showAll() const { return m_showAll; } void RulesFilterModel::setShowAll(bool showAll) { + if (m_showAll == showAll) { + return; + } + m_showAll = showAll; invalidateFilter(); emit showAllChanged(); } void RulesModel::detectWindowProperties(int secs) { QTimer::singleShot(secs*1000, this, &RulesModel::selectX11Window); } void RulesModel::selectX11Window() { QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.kde.KWin"), QStringLiteral("/KWin"), QStringLiteral("org.kde.KWin"), QStringLiteral("queryWindowInfo")); QDBusPendingReply async = QDBusConnection::sessionBus().asyncCall(message); QDBusPendingCallWatcher *callWatcher = new QDBusPendingCallWatcher(async, this); connect(callWatcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *self) { QDBusPendingReply reply = *self; self->deleteLater(); if (!reply.isValid()) { return; } const QVariantMap windowInfo = reply.value(); //TODO: Improve UI to suggest/select the detected properties. // For now, just prefill unused rules prefillProperties(windowInfo); } ); } } //namespace