diff --git a/kcmkwin/kwinrules/package/contents/ui/RulesEditor.qml b/kcmkwin/kwinrules/package/contents/ui/RulesEditor.qml index 7ca478bd7..34257cd6f 100644 --- a/kcmkwin/kwinrules/package/contents/ui/RulesEditor.qml +++ b/kcmkwin/kwinrules/package/contents/ui/RulesEditor.qml @@ -1,271 +1,272 @@ /* * 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.14 import QtQuick.Layouts 1.14 import QtQuick.Controls 2.14 as QQC2 import org.kde.kirigami 2.12 as Kirigami import org.kde.kcm 1.2 import org.kde.kitemmodels 1.0 import org.kde.kcms.kwinrules 1.0 ScrollViewKCM { id: rulesEditor property var rulesModel: kcm.rulesModel title: rulesModel.description view: ListView { id: rulesView clip: true model: enabledRulesModel delegate: RuleItemDelegate {} section { property: "section" delegate: Kirigami.ListSectionHeader { label: section } } Kirigami.PlaceholderMessage { id: hintArea visible: rulesView.count <= 4 anchors { // We need to center on the free space below contentItem, not the full ListView. // Setting both top and bottom anchors (or using anchors.fill) stretches the component // and distorts the spacing between its internal items. // This is fine as long as we have a single item here. horizontalCenter: parent.horizontalCenter top: parent.contentItem.bottom bottom: parent.bottom } width: parent.width - (units.largeSpacing * 4) helpfulAction: QQC2.Action { text: i18n("Add Properties...") icon.name: "list-add-symbolic" onTriggered: { propertySheet.open(); } } } } // FIXME: InlineMessage.qml:241:13: QML Label: Binding loop detected for property "verticalAlignment" header: Kirigami.InlineMessage { Layout.fillWidth: true Layout.fillHeight: true text: rulesModel.warningMessage visible: text != "" } footer: RowLayout { QQC2.Button { text: checked ? i18n("Close") : i18n("Add Properties...") icon.name: checked ? "dialog-close" : "list-add-symbolic" checkable: true checked: propertySheet.sheetOpen visible: !hintArea.visible || checked onToggled: { propertySheet.sheetOpen = checked; } } Item { Layout.fillWidth: true } QQC2.Button { text: i18n("Detect Window Properties") icon.name: "edit-find" onClicked: { overlayModel.onlySuggestions = true; rulesModel.detectWindowProperties(delaySpin.value); } } QQC2.SpinBox { id: delaySpin Layout.preferredWidth: Kirigami.Units.gridUnit * 8 from: 0 to: 30 textFromValue: (value, locale) => { return (value == 0) ? i18n("Instantly") : i18np("After %1 second", "After %1 seconds", value) } } } Connections { target: rulesModel onSuggestionsChanged: { propertySheet.sheetOpen = true; } } Kirigami.OverlaySheet { id: propertySheet parent: view header: Kirigami.Heading { text: i18n("Select properties") } footer: Kirigami.SearchField { id: searchField horizontalAlignment: Text.AlignLeft } ListView { id: overlayView model: overlayModel Layout.preferredWidth: Kirigami.Units.gridUnit * 28 section { property: "section" delegate: Kirigami.ListSectionHeader { label: section } } delegate: Kirigami.AbstractListItem { id: propertyDelegate highlighted: false width: ListView.view.width RowLayout { Kirigami.Icon { source: model.icon Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium Layout.alignment: Qt.AlignVCenter } QQC2.Label { id: itemNameLabel text: model.name horizontalAlignment: Qt.AlignLeft Layout.preferredWidth: implicitWidth Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter QQC2.ToolTip { text: model.description visible: hovered && (model.description != "") } } QQC2.Label { id: suggestedLabel text: formatValue(model.suggested, model.type, model.options) horizontalAlignment: Text.AlignRight elide: Text.ElideRight opacity: 0.7 Layout.maximumWidth: propertyDelegate.width - itemNameLabel.implicitWidth - Kirigami.Units.gridUnit * 6 Layout.alignment: Qt.AlignVCenter QQC2.ToolTip { text: suggestedLabel.text visible: hovered && suggestedLabel.truncated } } QQC2.ToolButton { icon.name: (model.enabled) ? "dialog-ok-apply" : "list-add-symbolic" opacity: propertyDelegate.hovered ? 1 : 0 onClicked: propertyDelegate.clicked() Layout.preferredWidth: implicitWidth Layout.leftMargin: -Kirigami.Units.smallSpacing Layout.rightMargin: -Kirigami.Units.smallSpacing Layout.alignment: Qt.AlignVCenter } } onClicked: { model.enabled = true; if (model.suggested != null) { model.value = model.suggested; model.suggested = null; } } } } onSheetOpenChanged: { searchField.text = ""; if (sheetOpen) { searchField.forceActiveFocus(); } else { overlayModel.onlySuggestions = false; } } } function formatValue(value, type, options) { if (value == null) { return ""; } switch (type) { case RuleItem.Boolean: return value ? i18n("Yes") : i18n("No"); case RuleItem.Percentage: return i18n("%1 %", value); - case RuleItem.Coordinate: - var point = value.split(','); - return i18nc("Coordinates (x, y)", "(%1, %2)", point[0], point[1]); + case RuleItem.Point: + return i18nc("Coordinates (x, y)", "(%1, %2)", value.x, value.y); + case RuleItem.Size: + return i18nc("Size (width, height)", "(%1, %2)", value.width, value.height); case RuleItem.Option: return options.textOfValue(value); case RuleItem.FlagsOption: var selectedValue = value.toString(2).length - 1; return options.textOfValue(selectedValue); } return value; } KSortFilterProxyModel { id: enabledRulesModel sourceModel: rulesModel filterRowCallback: (source_row, source_parent) => { var index = sourceModel.index(source_row, 0, source_parent); return sourceModel.data(index, RulesModel.EnabledRole); } } KSortFilterProxyModel { id: overlayModel sourceModel: rulesModel property bool onlySuggestions: false onOnlySuggestionsChanged: { invalidateFilter(); } filterString: searchField.text.trim().toLowerCase() filterRowCallback: (source_row, source_parent) => { var index = sourceModel.index(source_row, 0, source_parent); var hasSuggestion = sourceModel.data(index, RulesModel.SuggestedValueRole) != null; var isOptional = sourceModel.data(index, RulesModel.SelectableRole); var isEnabled = sourceModel.data(index, RulesModel.EnabledRole); var showItem = hasSuggestion || (!onlySuggestions && isOptional && !isEnabled); if (!showItem) { return false; } if (filterString != "") { return sourceModel.data(index, RulesModel.NameRole).toLowerCase().includes(filterString) } return true; } } } diff --git a/kcmkwin/kwinrules/package/contents/ui/ValueEditor.qml b/kcmkwin/kwinrules/package/contents/ui/ValueEditor.qml index 0686b3cd8..95fdfc110 100644 --- a/kcmkwin/kwinrules/package/contents/ui/ValueEditor.qml +++ b/kcmkwin/kwinrules/package/contents/ui/ValueEditor.qml @@ -1,200 +1,201 @@ /* * 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.14 import QtQuick.Layouts 1.14 import QtQuick.Controls 2.14 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.Point: return coordinateEditor + case RuleItem.Size: return coordinateEditor case RuleItem.Shortcut: return shortcutEditor default: return emptyEditor } } Component { id: emptyEditor Item {} } Component { id: booleanEditor RowLayout { Item { Layout.fillWidth: true } QQC2.RadioButton { text: i18n("Yes") checked: ruleValue Layout.margins: Kirigami.Units.smallSpacing onToggled: valueEditor.valueEdited(checked) } QQC2.RadioButton { text: i18n("No") checked: !ruleValue Layout.margins: Kirigami.Units.smallSpacing onToggled: valueEditor.valueEdited(!checked) } } } Component { id: stringEditor QQC2.TextField { property bool isTextEdited: false text: ruleValue horizontalAlignment: Text.AlignLeft 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 OptionsComboBox { flat: true model: ruleOptions multipleChoice: true selectionMask: ruleValue onActivated: { valueEditor.valueEdited(selectionMask); } } } 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)) horizontalAlignment: Qt.AlignRight Layout.minimumWidth: maxPercentage.width + Kirigami.Units.smallSpacing Layout.margins: Kirigami.Units.smallSpacing } TextMetrics { id: maxPercentage text: i18n("%1 %", 100) } } } Component { id: coordinateEditor RowLayout { id: coordItem spacing: Kirigami.Units.smallSpacing - property var coords: ruleValue ? ruleValue.split(',') : [0, 0] + readonly property var coord: (controlType == RuleItem.Size) ? Qt.size(coordX.value, coordY.value) + : Qt.point(coordX.value, coordY.value) + onCoordChanged: valueEditor.valueEdited(coord) QQC2.SpinBox { id: coordX editable: true Layout.preferredWidth: 50 // 50% Layout.fillWidth: true from: 0 to: 4098 - value: coords[0] - onValueModified: valueEditor.valueEdited(coordX.value + "," + coordY.value) + value: (controlType == RuleItem.Size) ? ruleValue.width : ruleValue.x } QQC2.Label { id: coordSeparator Layout.preferredWidth: implicitWidth text: i18nc("(x, y) coordinates separator in size/position","x") horizontalAlignment: Text.AlignHCenter } QQC2.SpinBox { id: coordY editable: true from: 0 to: 4098 Layout.preferredWidth: 50 // 50% Layout.fillWidth: true - value: coords[1] - onValueModified: valueEditor.valueEdited(coordX.value + "," + coordY.value) + value: (controlType == RuleItem.Size) ? ruleValue.height : ruleValue.y } } } Component { id: shortcutEditor RowLayout { Item { Layout.fillWidth: true } KQC.KeySequenceItem { keySequence: ruleValue onCaptureFinished: valueEditor.valueEdited(keySequence) } } } } diff --git a/kcmkwin/kwinrules/ruleitem.cpp b/kcmkwin/kwinrules/ruleitem.cpp index 0542cd1a5..a4efce7f1 100644 --- a/kcmkwin/kwinrules/ruleitem.cpp +++ b/kcmkwin/kwinrules/ruleitem.cpp @@ -1,225 +1,224 @@ /* * 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 "ruleitem.h" namespace KWin { RuleItem::RuleItem(const QString &key, const RulePolicy::Type policyType, const RuleItem::Type type, const QString &name, const QString §ion, const QIcon &icon, const QString &description) : m_key(key) , m_type(type) , m_name(name) , m_section(section) , m_icon(icon) , m_description(description) , m_flags(NoFlags) , m_enabled(false) , m_policy(new RulePolicy(policyType)) , m_options(nullptr) { reset(); } RuleItem::~RuleItem() { delete m_policy; delete m_options; } void RuleItem::reset() { m_enabled = hasFlag(AlwaysEnabled) | hasFlag(StartEnabled); m_value = typedValue(QVariant(), m_type); m_suggestedValue = QVariant(); m_policy->resetValue(); if (m_options) { m_options->resetValue(); } } QString RuleItem::key() const { return m_key; } QString RuleItem::name() const { return m_name; } QString RuleItem::section() const { return m_section; } QString RuleItem::iconName() const { return m_icon.name(); } QIcon RuleItem::icon() const { return m_icon; } QString RuleItem::description() const { return m_description; } bool RuleItem::isEnabled() const { return m_enabled; } void RuleItem::setEnabled(bool enabled) { m_enabled = enabled | hasFlag(AlwaysEnabled); } bool RuleItem::hasFlag(RuleItem::Flags flag) const { return m_flags.testFlag(flag); } void RuleItem::setFlag(RuleItem::Flags flag, bool active) { m_flags.setFlag(flag, active); } RuleItem::Type RuleItem::type() const { return m_type; } QVariant RuleItem::value() const { if (m_type == Option) { return m_options->value(); } return m_value; } void RuleItem::setValue(QVariant value) { if (m_type == Option) { m_options->setValue(value); } m_value = typedValue(value, m_type); } QVariant RuleItem::suggestedValue() const { return m_suggestedValue; } void RuleItem::setSuggestedValue(QVariant value, bool forceValue) { if (forceValue) { setValue(value); } m_suggestedValue = value.isNull() ? QVariant() : typedValue(value, m_type); } QVariant RuleItem::options() const { if (!m_options) { return QVariant(); } return QVariant::fromValue(m_options); } void RuleItem::setOptionsData(const QList &data) { if (!m_options) { if (m_type != Option && m_type != FlagsOption) { return; } m_options = new OptionsModel(); } m_options->updateModelData(data); m_options->setValue(m_value); } int RuleItem::policy() const { return m_policy->value(); } void RuleItem::setPolicy(int policy) { m_policy->setValue(policy); } RulePolicy::Type RuleItem::policyType() const { return m_policy->type(); } QVariant RuleItem::policyModel() const { return QVariant::fromValue(m_policy); } QString RuleItem::policyKey() const { return m_policy->policyKey(m_key); } QVariant RuleItem::typedValue(const QVariant &value, const RuleItem::Type type) { switch (type) { case Undefined: case Option: return value; case Boolean: return value.toBool(); case Integer: case Percentage: return value.toInt(); case FlagsOption: // HACK: Currently, the only user of this is "types" property if (value.toInt() == -1) { //NET:AllTypesMask return 0x3FF - 0x040; //All possible flags minus NET::Override (deprecated) } return value.toInt(); - case Coordinate: - if (value.toString().isEmpty()) { - return QStringLiteral("0,0"); - } - return value.toString(); + case Point: + return value.toPoint(); + case Size: + return value.toSize(); case String: return value.toString().trimmed(); case Shortcut: return value.toString(); } return value; } } //namespace diff --git a/kcmkwin/kwinrules/ruleitem.h b/kcmkwin/kwinrules/ruleitem.h index 41271286f..a8fe25ec2 100644 --- a/kcmkwin/kwinrules/ruleitem.h +++ b/kcmkwin/kwinrules/ruleitem.h @@ -1,127 +1,128 @@ /* * 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 . */ #ifndef KWIN_RULEITEM_H #define KWIN_RULEITEM_H #include "optionsmodel.h" #include #include namespace KWin { class RuleItem : public QObject { Q_OBJECT public: enum Type { Undefined, Boolean, String, Integer, Option, FlagsOption, Percentage, - Coordinate, + Point, + Size, Shortcut }; Q_ENUM(Type) enum Flags { NoFlags = 0, AlwaysEnabled = 1u << 0, StartEnabled = 1u << 1, AffectsWarning = 1u << 2, AffectsDescription = 1u << 3, AllFlags = 0b1111 }; public: RuleItem() {}; RuleItem(const QString &key, const RulePolicy::Type policyType, const Type type, const QString &name, const QString §ion, const QIcon &icon = QIcon::fromTheme("window"), const QString &description = QString("") ); ~RuleItem(); QString key() const; QString name() const; QString section() const; QIcon icon() const; QString iconName() const; QString description() const; bool isEnabled() const; void setEnabled(bool enabled); bool hasFlag(RuleItem::Flags flag) const; void setFlag(RuleItem::Flags flag, bool active=true); Type type() const; QVariant value() const; void setValue(QVariant value); QVariant suggestedValue() const; void setSuggestedValue(QVariant value, bool forceValue = false); QVariant options() const; void setOptionsData(const QList &data); RulePolicy::Type policyType() const; int policy() const; // int belongs to anonymous enum in Rules:: void setPolicy(int policy); // int belongs to anonymous enum in Rules:: QVariant policyModel() const; QString policyKey() const; void reset(); private: static QVariant typedValue(const QVariant &value, const Type type); private: QString m_key; RuleItem::Type m_type; QString m_name; QString m_section; QIcon m_icon; QString m_description; QFlags m_flags; bool m_enabled; QVariant m_value; QVariant m_suggestedValue; RulePolicy *m_policy; OptionsModel *m_options; }; } //namespace #endif //KWIN_RULEITEM_H diff --git a/kcmkwin/kwinrules/rulesmodel.cpp b/kcmkwin/kwinrules/rulesmodel.cpp index 60398d68b..bdf1309e4 100644 --- a/kcmkwin/kwinrules/rulesmodel.cpp +++ b/kcmkwin/kwinrules/rulesmodel.cpp @@ -1,821 +1,819 @@ /* * 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) { qmlRegisterUncreatableType("org.kde.kcms.kwinrules", 1, 0, "RuleItem", QStringLiteral("Do not create objects of type RuleItem")); qmlRegisterUncreatableType("org.kde.kcms.kwinrules", 1, 0, "RulesModel", QStringLiteral("Do not create objects of type RulesModel")); populateRuleList(); } RulesModel::~RulesModel() { } 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")}, {SuggestedValueRole, QByteArrayLiteral("suggested")}, }; } 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 (!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(); case SuggestedValueRole: return rule->suggestedValue(); } return QVariant(); } bool RulesModel::setData(const QModelIndex &index, const QVariant &value, int role) { 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; case SuggestedValueRole: if (value == rule->suggestedValue()) { return true; } rule->setSuggestedValue(value); break; default: return false; } emit dataChanged(index, index, QVector{role}); if (rule->hasFlag(RuleItem::AffectsDescription)) { emit descriptionChanged(); } if (rule->hasFlag(RuleItem::AffectsWarning)) { emit warningMessageChanged(); } 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.value(key); } QString RulesModel::description() const { const QString desc = m_rules["description"]->value().toString(); if (!desc.isEmpty()) { return desc; } return defaultDescription(); } void RulesModel::setDescription(const QString &description) { setData(index(0, 0), description, RulesModel::ValueRole); } 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"); } QString RulesModel::warningMessage() const { if (wmclassWarning()) { return 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."); } return QString(); } bool RulesModel::wmclassWarning() 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()); rule->reset(); if (!configItem) { continue; } const bool isEnabled = configPolicyItem ? configPolicyItem->property() != Rules::Unused : !configItem->property().toString().isEmpty(); rule->setEnabled(isEnabled); const QVariant value = configItem->property(); rule->setValue(value); if (configPolicyItem) { const int policy = configPolicyItem->property().toInt(); rule->setPolicy(policy); } } endResetModel(); emit descriptionChanged(); emit warningMessageChanged(); } 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 auto description = addRule(new RuleItem(QLatin1String("description"), RulePolicy::NoPolicy, RuleItem::String, i18n("Description"), i18n("Window matching"), QIcon::fromTheme("entry-edit"))); description->setFlag(RuleItem::AlwaysEnabled); description->setFlag(RuleItem::AffectsDescription); // Window matching auto wmclass = addRule(new RuleItem(QLatin1String("wmclass"), RulePolicy::StringMatch, RuleItem::String, i18n("Window class (application)"), i18n("Window matching"), QIcon::fromTheme("window"))); wmclass->setFlag(RuleItem::AlwaysEnabled); wmclass->setFlag(RuleItem::AffectsDescription); wmclass->setFlag(RuleItem::AffectsWarning); auto wmclasscomplete = addRule(new RuleItem(QLatin1String("wmclasscomplete"), RulePolicy::NoPolicy, RuleItem::Boolean, i18n("Match whole window class"), i18n("Window matching"), QIcon::fromTheme("window"))); wmclasscomplete->setFlag(RuleItem::AlwaysEnabled); auto types = addRule(new RuleItem(QLatin1String("types"), RulePolicy::NoPolicy, RuleItem::FlagsOption, i18n("Window types"), i18n("Window matching"), QIcon::fromTheme("window-duplicate"))); types->setOptionsData(windowTypesModelData()); types->setFlag(RuleItem::AlwaysEnabled); types->setFlag(RuleItem::AffectsWarning); addRule(new RuleItem(QLatin1String("windowrole"), RulePolicy::NoPolicy, RuleItem::String, i18n("Window role"), i18n("Window matching"), QIcon::fromTheme("dialog-object-properties"))); auto title = addRule(new RuleItem(QLatin1String("title"), RulePolicy::StringMatch, RuleItem::String, i18n("Window title"), i18n("Window matching"), QIcon::fromTheme("edit-comment"))); title->setFlag(RuleItem::AffectsDescription); addRule(new RuleItem(QLatin1String("clientmachine"), RulePolicy::StringMatch, RuleItem::String, i18n("Machine (hostname)"), i18n("Window matching"), QIcon::fromTheme("computer"))); // Size & Position addRule(new RuleItem(QLatin1String("position"), - RulePolicy::SetRule, RuleItem::Coordinate, + RulePolicy::SetRule, RuleItem::Point, i18n("Position"), i18n("Size & Position"), QIcon::fromTheme("transform-move"))); addRule(new RuleItem(QLatin1String("size"), - RulePolicy::SetRule, RuleItem::Coordinate, + RulePolicy::SetRule, RuleItem::Size, i18n("Size"), i18n("Size & Position"), QIcon::fromTheme("image-resize-symbolic"))); addRule(new RuleItem(QLatin1String("maximizehoriz"), RulePolicy::SetRule, RuleItem::Boolean, i18n("Maximized horizontally"), i18n("Size & Position"), QIcon::fromTheme("resizecol"))); addRule(new RuleItem(QLatin1String("maximizevert"), RulePolicy::SetRule, RuleItem::Boolean, i18n("Maximized vertically"), i18n("Size & Position"), QIcon::fromTheme("resizerow"))); auto desktop = addRule(new RuleItem(QLatin1String("desktop"), RulePolicy::SetRule, RuleItem::Option, i18n("Virtual Desktop"), i18n("Size & Position"), QIcon::fromTheme("virtual-desktops"))); desktop->setOptionsData(virtualDesktopsModelData()); #ifdef KWIN_BUILD_ACTIVITIES m_activities = new KActivities::Consumer(this); auto activity = addRule(new RuleItem(QLatin1String("activity"), RulePolicy::SetRule, RuleItem::Option, i18n("Activity"), i18n("Size & Position"), QIcon::fromTheme("activities"))); activity->setOptionsData(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"), QIcon::fromTheme("osd-shutd-screen"))); addRule(new RuleItem(QLatin1String("fullscreen"), RulePolicy::SetRule, RuleItem::Boolean, i18n("Fullscreen"), i18n("Size & Position"), QIcon::fromTheme("view-fullscreen"))); addRule(new RuleItem(QLatin1String("minimize"), RulePolicy::SetRule, RuleItem::Boolean, i18n("Minimized"), i18n("Size & Position"), QIcon::fromTheme("window-minimize"))); addRule(new RuleItem(QLatin1String("shade"), RulePolicy::SetRule, RuleItem::Boolean, i18n("Shaded"), i18n("Size & Position"), QIcon::fromTheme("window-shade"))); auto placement = addRule(new RuleItem(QLatin1String("placement"), RulePolicy::ForceRule, RuleItem::Option, i18n("Initial placement"), i18n("Size & Position"), QIcon::fromTheme("region"))); placement->setOptionsData(placementModelData()); addRule(new RuleItem(QLatin1String("ignoregeometry"), RulePolicy::SetRule, RuleItem::Boolean, i18n("Ignore requested geometry"), i18n("Size & Position"), QIcon::fromTheme("view-time-schedule-baselined-remove"), 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, + RulePolicy::ForceRule, RuleItem::Size, i18n("Minimum Size"), i18n("Size & Position"), QIcon::fromTheme("image-resize-symbolic"))); addRule(new RuleItem(QLatin1String("maxsize"), - RulePolicy::ForceRule, RuleItem::Coordinate, + RulePolicy::ForceRule, RuleItem::Size, i18n("Maximum Size"), i18n("Size & Position"), QIcon::fromTheme("image-resize-symbolic"))); addRule(new RuleItem(QLatin1String("strictgeometry"), RulePolicy::ForceRule, RuleItem::Boolean, i18n("Obey geometry restrictions"), i18n("Size & Position"), QIcon::fromTheme("transform-crop-and-resize"), 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"), QIcon::fromTheme("window-keep-above"))); addRule(new RuleItem(QLatin1String("below"), RulePolicy::SetRule, RuleItem::Boolean, i18n("Keep below"), i18n("Arrangement & Access"), QIcon::fromTheme("window-keep-below"))); addRule(new RuleItem(QLatin1String("skiptaskbar"), RulePolicy::SetRule, RuleItem::Boolean, i18n("Skip taskbar"), i18n("Arrangement & Access"), QIcon::fromTheme("kt-show-statusbar"), i18n("Window shall (not) appear in the taskbar."))); addRule(new RuleItem(QLatin1String("skippager"), RulePolicy::SetRule, RuleItem::Boolean, i18n("Skip pager"), i18n("Arrangement & Access"), QIcon::fromTheme("org.kde.plasma.pager"), 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"), QIcon::fromTheme("preferences-system-windows-effect-flipswitch"), i18n("Window shall (not) appear in the Alt+Tab list"))); addRule(new RuleItem(QLatin1String("shortcut"), RulePolicy::SetRule, RuleItem::Shortcut, i18n("Shortcut"), i18n("Arrangement & Access"), QIcon::fromTheme("configure-shortcuts"))); // Appearance & Fixes addRule(new RuleItem(QLatin1String("noborder"), RulePolicy::SetRule, RuleItem::Boolean, i18n("No titlebar and frame"), i18n("Appearance & Fixes"), QIcon::fromTheme("dialog-cancel"))); auto decocolor = addRule(new RuleItem(QLatin1String("decocolor"), RulePolicy::ForceRule, RuleItem::Option, i18n("Titlebar color scheme"), i18n("Appearance & Fixes"), QIcon::fromTheme("preferences-desktop-theme"))); decocolor->setOptionsData(colorSchemesModelData()); addRule(new RuleItem(QLatin1String("opacityactive"), RulePolicy::ForceRule, RuleItem::Percentage, i18n("Active opacity"), i18n("Appearance & Fixes"), QIcon::fromTheme("edit-opacity"))); addRule(new RuleItem(QLatin1String("opacityinactive"), RulePolicy::ForceRule, RuleItem::Percentage, i18n("Inactive opacity"), i18n("Appearance & Fixes"), QIcon::fromTheme("edit-opacity"))); auto fsplevel = addRule(new RuleItem(QLatin1String("fsplevel"), RulePolicy::ForceRule, RuleItem::Option, i18n("Focus stealing prevention"), i18n("Appearance & Fixes"), QIcon::fromTheme("preferences-system-windows-effect-glide"), 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."))); fsplevel->setOptionsData(focusModelData()); auto fpplevel = addRule(new RuleItem(QLatin1String("fpplevel"), RulePolicy::ForceRule, RuleItem::Option, i18n("Focus protection"), i18n("Appearance & Fixes"), QIcon::fromTheme("preferences-system-windows-effect-minimize"), 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."))); fpplevel->setOptionsData(focusModelData()); addRule(new RuleItem(QLatin1String("acceptfocus"), RulePolicy::ForceRule, RuleItem::Boolean, i18n("Accept focus"), i18n("Appearance & Fixes"), QIcon::fromTheme("preferences-desktop-cursors"), 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"), QIcon::fromTheme("input-keyboard-virtual-off"), 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"), QIcon::fromTheme("dialog-close"))); auto type = addRule(new RuleItem(QLatin1String("type"), RulePolicy::ForceRule, RuleItem::Option, i18n("Set window type"), i18n("Appearance & Fixes"), QIcon::fromTheme("window-duplicate"))); type->setOptionsData(windowTypesModelData()); addRule(new RuleItem(QLatin1String("desktopfile"), RulePolicy::SetRule, RuleItem::String, i18n("Desktop file name"), i18n("Appearance & Fixes"), QIcon::fromTheme("application-x-desktop"))); addRule(new RuleItem(QLatin1String("blockcompositing"), RulePolicy::ForceRule, RuleItem::Boolean, i18n("Block compositing"), i18n("Appearance & Fixes"), QIcon::fromTheme("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::setWindowProperties(const QVariantMap &info, bool forceValue) { // 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()); + const QPoint position = QPoint(info.value("x").toInt(), info.value("y").toInt()); + const QSize size = QSize(info.value("width").toInt(), info.value("height").toInt()); m_rules["position"]->setSuggestedValue(position, forceValue); m_rules["size"]->setSuggestedValue(size, forceValue); m_rules["minsize"]->setSuggestedValue(size, forceValue); m_rules["maxsize"]->setSuggestedValue(size, forceValue); NET::WindowType window_type = static_cast(info.value("type", 0).toInt()); if (window_type == NET::Unknown) { window_type = NET::Normal; } m_rules["types"]->setSuggestedValue(1 << window_type, forceValue); 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)); m_rules[ruleKey]->setSuggestedValue(info.value(property), forceValue); } emit dataChanged(index(0), index(rowCount()-1), {RulesModel::SuggestedValueRole}); if (!forceValue) { emit suggestionsChanged(); } } 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++) { const QModelIndex index = schemesModel->index(r, 0); modelData << OptionsModel::Data{ QFileInfo(index.data(Qt::UserRole).toString()).baseName(), index.data(Qt::DisplayRole).toString(), index.data(Qt::DecorationRole).value() }; } return modelData; } 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(); setWindowProperties(windowInfo); } ); } } //namespace