/* SPDX-FileCopyrightText: 2004 Lubos Lunak SPDX-FileCopyrightText: 2020 Ismael Asensio SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ #include "rulesmodel.h" #if KWIN_BUILD_ACTIVITIES #include "activities.h" #endif #include #include #include #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")); qmlRegisterUncreatableType("org.kde.kcms.kwinrules", 1, 0, "OptionsModel", QStringLiteral("Do not create objects of type OptionsModel")); qDBusRegisterMetaType(); qDBusRegisterMetaType(); populateRuleList(); } RulesModel::~RulesModel() { } QHash 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) && !rule->hasFlag(RuleItem::SuggestionOnly); 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 (rule->hasFlag(RuleItem::SuggestionOnly)) { processSuggestion(rule->key(), value); } 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; } writeToSettings(rule); Q_EMIT dataChanged(index, index, QList{role}); if (rule->hasFlag(RuleItem::AffectsDescription)) { Q_EMIT descriptionChanged(); } if (rule->hasFlag(RuleItem::AffectsWarning)) { Q_EMIT warningMessagesChanged(); } return true; } QModelIndex RulesModel::indexOf(const QString &key) const { const QModelIndexList indexes = match(index(0), RulesModel::KeyRole, key, 1, Qt::MatchFixedString); if (indexes.isEmpty()) { return QModelIndex(); } return indexes.at(0); } 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(indexOf("description"), 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"); } void RulesModel::processSuggestion(const QString &key, const QVariant &value) { if (key == QLatin1String("wmclasshelper")) { setData(indexOf("wmclass"), value, RulesModel::ValueRole); setData(indexOf("wmclasscomplete"), true, RulesModel::ValueRole); } } QStringList RulesModel::warningMessages() const { QStringList messages; if (wmclassWarning()) { messages << 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."); } if (geometryWarning()) { messages << i18n("Some applications set their own geometry after starting," " overriding your initial settings for size and position. " "To enforce these settings, also force the property \"%1\" to \"Yes\".", m_rules["ignoregeometry"]->name()); } if (opacityWarning()) { messages << i18n("Readability may be impaired with extremely low opacity values. At 0%, the window becomes invisible."); } return messages; } 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); } bool RulesModel::geometryWarning() const { const bool ignoregeometry = m_rules["ignoregeometry"]->isEnabled() && m_rules["ignoregeometry"]->policy() == Rules::Force && m_rules["ignoregeometry"]->value() == true; const bool initialPos = m_rules["position"]->isEnabled() && (m_rules["position"]->policy() == Rules::Apply || m_rules["position"]->policy() == Rules::Remember); const bool initialSize = m_rules["size"]->isEnabled() && (m_rules["size"]->policy() == Rules::Apply || m_rules["size"]->policy() == Rules::Remember); const bool initialPlacement = m_rules["placement"]->isEnabled() && m_rules["placement"]->policy() == Rules::Force; return (!ignoregeometry && (initialPos || initialSize || initialPlacement)); } bool RulesModel::opacityWarning() const { auto opacityActive = m_rules["opacityactive"]; const bool lowOpacityActive = opacityActive->isEnabled() && opacityActive->policy() != Rules::Unused && opacityActive->policy() != Rules::DontAffect && opacityActive->value().toInt() < 25; auto opacityInactive = m_rules["opacityinactive"]; const bool lowOpacityInactive = opacityInactive->isEnabled() && opacityActive->policy() != Rules::Unused && opacityActive->policy() != Rules::DontAffect && opacityInactive->value().toInt() < 25; return lowOpacityActive || lowOpacityInactive; } RuleSettings *RulesModel::settings() const { return m_settings; } void RulesModel::setSettings(RuleSettings *settings) { if (m_settings == settings) { return; } beginResetModel(); m_settings = settings; for (RuleItem *rule : std::as_const(m_ruleList)) { const KConfigSkeletonItem *configItem = m_settings->findItem(rule->key()); const KConfigSkeletonItem *configPolicyItem = m_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(); Q_EMIT descriptionChanged(); Q_EMIT warningMessagesChanged(); } void RulesModel::writeToSettings(RuleItem *rule) { KConfigSkeletonItem *configItem = m_settings->findItem(rule->key()); KConfigSkeletonItem *configPolicyItem = m_settings->findItem(rule->policyKey()); if (!configItem) { return; } if (rule->isEnabled()) { configItem->setProperty(rule->value()); if (configPolicyItem) { configPolicyItem->setProperty(rule->policy()); } } else { configItem->setDefault(); if (configPolicyItem) { configPolicyItem->setDefault(); } } } 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); // Helper item to store the detected whole window class when detecting properties auto wmclasshelper = addRule(new RuleItem(QLatin1String("wmclasshelper"), RulePolicy::NoPolicy, RuleItem::String, i18n("Whole window class"), i18n("Window matching"), QIcon::fromTheme("window"))); wmclasshelper->setFlag(RuleItem::SuggestionOnly); auto types = addRule(new RuleItem(QLatin1String("types"), RulePolicy::NoPolicy, RuleItem::NetTypes, 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::StringMatch, 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 auto position = addRule(new RuleItem(QLatin1String("position"), RulePolicy::SetRule, RuleItem::Point, i18n("Position"), i18n("Size & Position"), QIcon::fromTheme("transform-move"))); position->setFlag(RuleItem::AffectsWarning); auto size = addRule(new RuleItem(QLatin1String("size"), RulePolicy::SetRule, RuleItem::Size, i18n("Size"), i18n("Size & Position"), QIcon::fromTheme("transform-scale"))); size->setFlag(RuleItem::AffectsWarning); 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"))); RuleItem *desktops; if (KWindowSystem::isPlatformX11()) { // Single selection of Virtual Desktop on X11 desktops = new RuleItem(QLatin1String("desktops"), RulePolicy::SetRule, RuleItem::Option, i18n("Virtual desktop"), i18n("Size & Position"), QIcon::fromTheme("virtual-desktops")); } else { // Multiple selection on Wayland desktops = new RuleItem(QLatin1String("desktops"), RulePolicy::SetRule, RuleItem::OptionList, i18n("Virtual desktops"), i18n("Size & Position"), QIcon::fromTheme("virtual-desktops")); } addRule(desktops); desktops->setOptionsData(virtualDesktopsModelData()); connect(this, &RulesModel::virtualDesktopsUpdated, this, [this]() { m_rules["desktops"]->setOptionsData(virtualDesktopsModelData()); const QModelIndex index = indexOf("desktops"); Q_EMIT dataChanged(index, index, {OptionsModelRole}); }); updateVirtualDesktops(); #if KWIN_BUILD_ACTIVITIES m_activities = new KActivities::Consumer(this); auto activity = addRule(new RuleItem(QLatin1String("activity"), RulePolicy::SetRule, RuleItem::OptionList, i18n("Activities"), i18n("Size & Position"), QIcon::fromTheme("activities"))); activity->setOptionsData(activitiesModelData()); // Activites consumer may update the available activities later auto updateActivities = [this]() { m_rules["activity"]->setOptionsData(activitiesModelData()); const QModelIndex index = indexOf("activity"); Q_EMIT dataChanged(index, index, {OptionsModelRole}); }; connect(m_activities, &KActivities::Consumer::activitiesChanged, this, updateActivities); connect(m_activities, &KActivities::Consumer::serviceStatusChanged, this, updateActivities); #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()); placement->setFlag(RuleItem::AffectsWarning); auto ignoregeometry = addRule(new RuleItem(QLatin1String("ignoregeometry"), RulePolicy::SetRule, RuleItem::Boolean, i18n("Ignore requested geometry"), i18n("Size & Position"), QIcon::fromTheme("view-time-schedule-baselined-remove"), xi18nc("@info:tooltip", "Some applications can set their own geometry, overriding the window manager preferences. " "Setting this property overrides their placement requests." "" "This affects Size and Position " "but not Maximized or Fullscreen states." "" "Note that the position can also be used to map to a different Screen"))); ignoregeometry->setFlag(RuleItem::AffectsWarning); addRule(new RuleItem(QLatin1String("minsize"), RulePolicy::ForceRule, RuleItem::Size, i18n("Minimum Size"), i18n("Size & Position"), QIcon::fromTheme("transform-scale"))); addRule(new RuleItem(QLatin1String("maxsize"), RulePolicy::ForceRule, RuleItem::Size, i18n("Maximum Size"), i18n("Size & Position"), QIcon::fromTheme("transform-scale"))); addRule(new RuleItem(QLatin1String("strictgeometry"), RulePolicy::ForceRule, RuleItem::Boolean, i18n("Obey geometry restrictions"), i18n("Size & Position"), QIcon::fromTheme("transform-crop-and-resize"), xi18nc("@info:tooltip", "Some apps like video players or terminals can ask KWin to constrain them to " "certain aspect ratios or only grow by values larger than the dimensions of one " "character. Use this property to ignore such restrictions and allow those windows " "to be resized to arbitrary sizes." "" "This can be helpful for windows that can't quite fit the full screen area when " "maximized."))); // Arrangement & Access addRule(new RuleItem(QLatin1String("above"), RulePolicy::SetRule, RuleItem::Boolean, i18n("Keep above other windows"), i18n("Arrangement & Access"), QIcon::fromTheme("window-keep-above"))); addRule(new RuleItem(QLatin1String("below"), RulePolicy::SetRule, RuleItem::Boolean, i18n("Keep below other windows"), 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"), i18nc("@info:tooltip", "Controls whether or not the window appears in the Task Manager."))); addRule(new RuleItem(QLatin1String("skippager"), RulePolicy::SetRule, RuleItem::Boolean, i18n("Skip pager"), i18n("Arrangement & Access"), QIcon::fromTheme("org.kde.plasma.pager"), i18nc("@info:tooltip", "Controls whether or not the window appears in the Virtual Desktop manager."))); addRule(new RuleItem(QLatin1String("skipswitcher"), RulePolicy::SetRule, RuleItem::Boolean, i18n("Skip switcher"), i18n("Arrangement & Access"), QIcon::fromTheme("preferences-system-windows-effect-flipswitch"), xi18nc("@info:tooltip", "Controls whether or not the window appears in the Alt+Tab window 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()); auto opacityactive = addRule(new RuleItem(QLatin1String("opacityactive"), RulePolicy::ForceRule, RuleItem::Percentage, i18n("Active opacity"), i18n("Appearance & Fixes"), QIcon::fromTheme("edit-opacity"))); opacityactive->setFlag(RuleItem::AffectsWarning); auto opacityinactive = addRule(new RuleItem(QLatin1String("opacityinactive"), RulePolicy::ForceRule, RuleItem::Percentage, i18n("Inactive opacity"), i18n("Appearance & Fixes"), QIcon::fromTheme("edit-opacity"))); opacityinactive->setFlag(RuleItem::AffectsWarning); 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"), xi18nc("@info:tooltip", "KWin tries to prevent windows that were opened without direct user action from raising " "themselves and taking focus while you're currently interacting with another window. This " "property can be used to change the level of focus stealing prevention applied to " "individual windows and apps." "" "Here's what will happen to a window opened without your direct action at each level of " "focus stealing prevention:" "" "" "None: The window will be raised and focused." "Low: Focus stealing prevention will be applied, " "but in the case of a situation KWin considers ambiguous, the window will be raised and " "focused." "Normal: Focus stealing prevention will be " "applied, but in the case of a situation KWin considers ambiguous, the window will " "not be raised and focused." "High: The window will only be raised and focused " "if it belongs to the same app as the currently-focused window." "Extreme: The window will never be raised and " "focused." ""))); 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"), xi18nc("@info:tooltip", "This property controls the focus protection level of the currently active " "window. It is used to override the focus stealing prevention applied to new windows that " "are opened without your direct action." "" "Here's what happens to new windows that are opened without your direct action at each " "level of focus protection while the window with this property applied to it has focus:" "" "" "None: Newly-opened windows always raise " "themselves and take focus." "Low: Focus stealing prevention will be applied " "to the newly-opened window, but in the case of a situation KWin considers ambiguous, the " "window will be raised and focused." "Normal: Focus stealing prevention will be applied " "to the newly-opened window, but in the case of a situation KWin considers ambiguous, the " "window will not be raised and focused." "High: Newly-opened windows will only raise " "themselves and take focus if they belongs to the same app as the currently-focused " "window." "Extreme: Newly-opened windows never raise " "themselves and take 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("Controls whether or not the window becomes focused when clicked."))); addRule(new RuleItem(QLatin1String("disableglobalshortcuts"), RulePolicy::ForceRule, RuleItem::Boolean, i18n("Ignore global shortcuts"), i18n("Appearance & Fixes"), QIcon::fromTheme("input-keyboard-virtual-off"), xi18nc("@info:tooltip", "Use this property to prevent global keyboard shortcuts from working while " "the window is focused. This can be useful for apps like emulators or virtual " "machines that handle some of the same shortcuts themselves." "" "Note that you won't be able to Alt+Tab out of the window " "or use any other global shortcuts such as Alt+Space to " "activate KRunner."))); addRule(new RuleItem(QLatin1String("closeable"), RulePolicy::ForceRule, RuleItem::Boolean, i18n("Closeable"), i18n("Appearance & Fixes"), QIcon::fromTheme("dialog-close"))); 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"))); auto layer = addRule(new RuleItem(QLatin1String("layer"), RulePolicy::ForceRule, RuleItem::Option, i18n("Layer"), i18n("Appearance & Fixes"), QIcon::fromTheme("view-sort"))); layer->setOptionsData(layerModelData()); addRule(new RuleItem(QLatin1String("adaptivesync"), RulePolicy::ForceRule, RuleItem::Boolean, i18n("Adaptive sync"), i18n("Appearance & Fixes"), QIcon::fromTheme("monitor-symbolic"))); addRule(new RuleItem(QLatin1String("tearing"), RulePolicy::ForceRule, RuleItem::Boolean, i18n("Allow tearing"), i18n("Appearance & Fixes"), QIcon::fromTheme("monitor-symbolic"))); } const QHash RulesModel::x11PropertyHash() { static const auto propertyToRule = QHash{ {"caption", "title"}, {"role", "windowrole"}, {"clientMachine", "clientmachine"}, {"maximizeHorizontal", "maximizehoriz"}, {"maximizeVertical", "maximizevert"}, {"minimized", "minimize"}, {"shaded", "shade"}, {"fullscreen", "fullscreen"}, {"keepAbove", "above"}, {"keepBelow", "below"}, {"noBorder", "noborder"}, {"skipTaskbar", "skiptaskbar"}, {"skipPager", "skippager"}, {"skipSwitcher", "skipswitcher"}, {"desktopFile", "desktopfile"}, {"desktops", "desktops"}, {"layer", "layer"}, }; return propertyToRule; }; void RulesModel::setSuggestedProperties(const QVariantMap &info) { // Properties that cannot be directly applied via x11PropertyHash 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); m_rules["size"]->setSuggestedValue(size); m_rules["minsize"]->setSuggestedValue(size); m_rules["maxsize"]->setSuggestedValue(size); 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); const QString wmsimpleclass = info.value("resourceClass").toString(); const QString wmcompleteclass = QStringLiteral("%1 %2").arg(info.value("resourceName").toString(), info.value("resourceClass").toString()); // This window is not providing the class according to spec (WM_CLASS on X11, appId on Wayland) // Notify the user that this is a bug within the application, so there's nothing we can do if (wmsimpleclass.isEmpty()) { Q_EMIT showErrorMessage(i18n("Window class not available"), xi18nc("@info", "This application is not providing a class for the window, " "so KWin cannot use it to match and apply any rules. " "If you still want to apply some rules to it, " "try to match other properties like the window title instead." "Please consider reporting this bug to the application's developers.")); } m_rules["wmclass"]->setSuggestedValue(wmsimpleclass); m_rules["wmclasshelper"]->setSuggestedValue(wmcompleteclass); #if KWIN_BUILD_ACTIVITIES const QStringList activities = info.value("activities").toStringList(); m_rules["activity"]->setSuggestedValue(activities.isEmpty() ? QStringList{Activities::nullUuid()} : activities); #endif 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)); } Q_EMIT dataChanged(index(0), index(rowCount() - 1), {RulesModel::SuggestedValueRole}); } QList RulesModel::windowTypesModelData() const { static const auto modelData = QList{ // TODO: Find/create better icons {0, i18n("All window types"), {}, {}, OptionsModel::SelectAllOption}, {1 << NET::Normal, i18n("Normal window"), QIcon::fromTheme("window")}, {1 << NET::Dialog, i18n("Dialog window"), QIcon::fromTheme("window-duplicate")}, {1 << NET::Utility, i18n("Utility window"), QIcon::fromTheme("dialog-object-properties")}, {1 << NET::Dock, i18n("Dock (panel)"), QIcon::fromTheme("spinbox-decrease")}, // see bug 492341 {1 << NET::Toolbar, i18n("Toolbar"), QIcon::fromTheme("tools")}, {1 << NET::Menu, i18n("Torn-off menu"), QIcon::fromTheme("overflow-menu-left")}, {1 << NET::Splash, i18n("Splash screen"), QIcon::fromTheme("embosstool")}, {1 << NET::Desktop, i18n("Desktop"), QIcon::fromTheme("desktop")}, // {1 << NET::Override, i18n("Unmanaged Window")}, deprecated {1 << NET::TopMenu, i18n("Standalone menubar"), QIcon::fromTheme("application-menu")}, {1 << NET::OnScreenDisplay, i18n("On-screen display"), QIcon::fromTheme("osd-duplicate")}}; return modelData; } QList RulesModel::virtualDesktopsModelData() const { QList modelData; modelData << OptionsModel::Data{ QString(), i18n("All desktops"), QIcon::fromTheme("window-pin"), i18nc("@info:tooltip in the virtual desktop list", "Make the window available on all desktops"), OptionsModel::ExclusiveOption, }; for (const DBusDesktopDataStruct &desktop : m_virtualDesktops) { modelData << OptionsModel::Data{ desktop.id, QString::number(desktop.position + 1).rightJustified(2) + QStringLiteral(": ") + desktop.name, QIcon::fromTheme("virtual-desktops")}; } return modelData; } QList RulesModel::activitiesModelData() const { #if KWIN_BUILD_ACTIVITIES QList modelData; modelData << OptionsModel::Data{ Activities::nullUuid(), i18n("All activities"), QIcon::fromTheme("activities"), i18nc("@info:tooltip in the activity list", "Make the window available on all activities"), OptionsModel::ExclusiveOption, }; 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 { static const auto modelData = QList{ {PlacementDefault, i18n("Default")}, {PlacementNone, i18n("No placement")}, {PlacementSmart, i18n("Minimal overlapping")}, {PlacementMaximizing, i18n("Maximized")}, {PlacementCentered, i18n("Centered")}, {PlacementRandom, i18n("Random")}, {PlacementZeroCornered, i18n("In top-left corner")}, {PlacementUnderMouse, i18n("Under mouse")}, {PlacementOnMainWindow, 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; } QList RulesModel::layerModelData() const { static const auto modelData = QList{ {DesktopLayer, i18n("Desktop")}, {BelowLayer, i18n("Below")}, {NormalLayer, i18n("Normal")}, {AboveLayer, i18n("Above")}, {NotificationLayer, i18n("Notification")}, {ActiveLayer, i18n("Fullscreen")}, {PopupLayer, i18n("Popup")}, {CriticalNotificationLayer, i18n("Critical notification")}, {OnScreenDisplayLayer, i18n("On-screen display")}, {OverlayLayer, i18n("Overlay")}, }; return modelData; } void RulesModel::detectWindowProperties(int miliseconds) { QTimer::singleShot(miliseconds, 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()) { if (reply.error().name() == QLatin1String("org.kde.KWin.Error.InvalidWindow")) { Q_EMIT showErrorMessage(i18n("Unmanaged window"), i18n("Could not detect window properties. The window is not managed by KWin.")); } return; } const QVariantMap windowInfo = reply.value(); setSuggestedProperties(windowInfo); Q_EMIT showSuggestions(); }); } void RulesModel::updateVirtualDesktops() { QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.kde.KWin"), QStringLiteral("/VirtualDesktopManager"), QStringLiteral("org.freedesktop.DBus.Properties"), QStringLiteral("Get")); message.setArguments(QVariantList{ QStringLiteral("org.kde.KWin.VirtualDesktopManager"), QStringLiteral("desktops")}); 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; } m_virtualDesktops = qdbus_cast(reply.value()); Q_EMIT virtualDesktopsUpdated(); }); } } // namespace #include "moc_rulesmodel.cpp"