File indexing completed on 2024-05-12 05:35:42

0001 /*
0002     SPDX-FileCopyrightText: 2010 Andriy Rysin <rysin@kde.org>
0003     SPDX-FileCopyrightText: 2021 Cyril Rossi <cyril.rossi@enioka.com>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "kcm_keyboard_widget.h"
0009 #include "debug.h"
0010 
0011 #include <KAboutData>
0012 #include <KActionCollection>
0013 #include <KGlobalAccel>
0014 #include <KLocalizedString>
0015 #include <KWindowSystem>
0016 
0017 #include <QCheckBox>
0018 #include <QDebug>
0019 #include <QMessageBox>
0020 #include <QPixmap>
0021 #include <QVBoxLayout>
0022 #include <QWidget>
0023 #include <QtGui/private/qtx11extras_p.h>
0024 
0025 #include "keyboardmiscsettings.h"
0026 #include "workspace_options.h"
0027 
0028 #include "bindings.h"
0029 #include "flags.h"
0030 #include "kcm_add_layout_dialog.h"
0031 #include "kcm_view_models.h"
0032 #include "tastenbrett.h"
0033 #include "x11_helper.h"
0034 #include "xkb_rules.h"
0035 
0036 #include "kcmmisc.h"
0037 #include "ui_kcm_add_layout_dialog.h"
0038 
0039 static const QString GROUP_SWITCH_GROUP_NAME(QStringLiteral("grp"));
0040 static const QString LV3_SWITCH_GROUP_NAME(QStringLiteral("lv3"));
0041 // static const QString RESET_XKB_OPTIONS("-option");
0042 
0043 static const int TAB_HARDWARE = 0;
0044 static const int TAB_LAYOUTS = 1;
0045 static const int TAB_ADVANCED = 2;
0046 
0047 static const int MIN_LOOPING_COUNT = 2;
0048 
0049 KCMKeyboardWidget::KCMKeyboardWidget(Rules *rules_,
0050                                      KeyboardConfig *keyboardConfig_,
0051                                      WorkspaceOptions &workspaceOptions,
0052                                      KCMiscKeyboardWidget *kcmMiscWidget,
0053                                      const QVariantList &args,
0054                                      QWidget * /*parent*/)
0055     : rules(rules_)
0056     , m_workspaceOptions(workspaceOptions)
0057     , actionCollection(nullptr)
0058     , uiUpdating(false)
0059 {
0060     flags = new Flags();
0061     keyboardConfig = keyboardConfig_;
0062 
0063     uiWidget = new Ui::TabWidget;
0064     uiWidget->setupUi(this);
0065     kcmMiscWidget->setParent(uiWidget->lowerHardwareWidget);
0066     uiWidget->lowerHardwareWidget->layout()->addWidget(kcmMiscWidget);
0067 
0068     if (rules != nullptr) {
0069         initializeKeyboardModelUI();
0070         initializeXkbOptionsUI();
0071         initializeLayoutsUI();
0072     } else {
0073         uiWidget->tabLayouts->setEnabled(false);
0074         uiWidget->tabAdvanced->setEnabled(false);
0075         uiWidget->keyboardModelComboBox->setEnabled(false);
0076     }
0077 
0078     handleParameters(args);
0079 }
0080 
0081 KCMKeyboardWidget::~KCMKeyboardWidget()
0082 {
0083     delete uiWidget;
0084     delete flags;
0085 }
0086 
0087 void KCMKeyboardWidget::handleParameters(const QVariantList &args)
0088 {
0089     // TODO: improve parameter handling
0090     setCurrentIndex(TAB_HARDWARE);
0091     for (const auto &arg : args) {
0092         if (arg.typeId() == QMetaType::Type::QString) {
0093             const QString str = arg.toString();
0094             if (str == QLatin1String("--tab=layouts")) {
0095                 setCurrentIndex(TAB_LAYOUTS);
0096             } else if (str == QLatin1String("--tab=advanced")) {
0097                 setCurrentIndex(TAB_ADVANCED);
0098             }
0099         }
0100     }
0101 }
0102 
0103 void KCMKeyboardWidget::save()
0104 {
0105     if (rules == nullptr)
0106         return;
0107 
0108     keyboardConfig->setKeyboardModel(keyboardModelFromUI());
0109     keyboardConfig->setSwitchingPolicy(switchingPolicyFromUI());
0110 
0111     saveXkbOptions();
0112 }
0113 
0114 void KCMKeyboardWidget::defaults()
0115 {
0116     updateHardwareUI(keyboardConfig->defaultKeyboardModelValue());
0117     updateSwitchingPolicyUI(keyboardConfig->defaultSwitchingPolicyValue());
0118     auto *xkbOptionModel = dynamic_cast<XkbOptionsTreeModel *>(uiWidget->xkbOptionsTreeView->model());
0119     xkbOptionModel->setXkbOptions(keyboardConfig->defaultXkbOptionsValue());
0120     keyboardConfig->setDefaults();
0121     if (actionCollection == nullptr) {
0122         actionCollection = new KeyboardLayoutActionCollection(this, true);
0123     }
0124     actionCollection->setLastUsedLayoutShortcut(QKeySequence(Qt::META | Qt::ALT | Qt::Key_L));
0125     updateShortcutsUI();
0126 }
0127 
0128 bool KCMKeyboardWidget::isSaveNeeded() const
0129 {
0130     return keyboardModelFromUI() != keyboardConfig->keyboardModel() || switchingPolicyFromUI() != keyboardConfig->switchingPolicy()
0131         || xkbOptionsFromUI() != keyboardConfig->xkbOptions() || keyboardConfig->layoutsSaveNeeded();
0132 }
0133 
0134 bool KCMKeyboardWidget::isDefault() const
0135 {
0136     return keyboardModelFromUI() == keyboardConfig->defaultKeyboardModelValue() && switchingPolicyFromUI() == keyboardConfig->defaultSwitchingPolicyValue()
0137         && xkbOptionsFromUI() == keyboardConfig->xkbOptions();
0138 }
0139 
0140 void KCMKeyboardWidget::setDefaultIndicator(bool visible)
0141 {
0142     m_highlightVisible = visible;
0143     updateUiDefaultIndicator();
0144 }
0145 
0146 void KCMKeyboardWidget::updateUiDefaultIndicator()
0147 {
0148     setDefaultIndicatorVisible(uiWidget->keyboardModelComboBox, m_highlightVisible && keyboardModelFromUI() != keyboardConfig->defaultKeyboardModelValue());
0149     const auto isDefaultswitchingPolicy = switchingPolicyFromUI() == keyboardConfig->defaultSwitchingPolicyValue();
0150     for (auto button : uiWidget->switchingPolicyButtonGroup->buttons()) {
0151         setDefaultIndicatorVisible(button, m_highlightVisible && !isDefaultswitchingPolicy && uiWidget->switchingPolicyButtonGroup->checkedButton() == button);
0152     }
0153 }
0154 
0155 void KCMKeyboardWidget::updateUI()
0156 {
0157     if (rules == nullptr)
0158         return;
0159 
0160     uiWidget->layoutsTableView->setModel(uiWidget->layoutsTableView->model());
0161     layoutsTableModel->refresh();
0162     uiWidget->layoutsTableView->resizeRowsToContents();
0163 
0164     uiUpdating = true;
0165     updateHardwareUI(keyboardConfig->keyboardModel());
0166     updateSwitchingPolicyUI(keyboardConfig->switchingPolicy());
0167     XkbOptionsTreeModel *model = dynamic_cast<XkbOptionsTreeModel *>(uiWidget->xkbOptionsTreeView->model());
0168     model->setXkbOptions(keyboardConfig->xkbOptions());
0169     updateLayoutsUI();
0170     updateShortcutsUI();
0171     layoutSelectionChanged();
0172     uiUpdating = false;
0173 }
0174 
0175 void KCMKeyboardWidget::uiChanged()
0176 {
0177     if (rules == nullptr) {
0178         return;
0179     }
0180 
0181     ((LayoutsTableModel *)uiWidget->layoutsTableView->model())->refresh();
0182     layoutSelectionChanged();
0183     // this collapses the tree so use more fine-grained updates
0184     //  ((LayoutsTableModel*)uiWidget->xkbOptionsTreeView->model())->refresh();
0185 
0186     if (uiUpdating) {
0187         return;
0188     }
0189 
0190     updateXkbShortcutsButtons();
0191 
0192     updateLoopCount();
0193     int loop = uiWidget->layoutLoopCountSpinBox->text().isEmpty() ? KeyboardConfig::NO_LOOPING : uiWidget->layoutLoopCountSpinBox->value();
0194     keyboardConfig->setLayoutLoopCount(loop);
0195 
0196     layoutsTableModel->refresh();
0197     layoutSelectionChanged();
0198     // Refresh layout shortcuts
0199     switchKeyboardShortcutChanged();
0200 
0201     Q_EMIT changed(true);
0202 }
0203 
0204 void KCMKeyboardWidget::initializeKeyboardModelUI()
0205 {
0206     for (const ModelInfo *modelInfo : std::as_const(rules->modelInfos)) {
0207         QString vendor = modelInfo->vendor;
0208         if (vendor.isEmpty()) {
0209             vendor = i18nc("unknown keyboard model vendor", "Unknown");
0210         }
0211         uiWidget->keyboardModelComboBox->addItem(i18nc("vendor | keyboard model", "%1 | %2", vendor, modelInfo->description), modelInfo->name);
0212     }
0213     uiWidget->keyboardModelComboBox->model()->sort(0);
0214     connect(uiWidget->keyboardModelComboBox, SIGNAL(activated(int)), this, SLOT(uiChanged()));
0215     connect(uiWidget->keyboardModelComboBox, qOverload<int>(&QComboBox::currentIndexChanged), this, &KCMKeyboardWidget::updateUiDefaultIndicator);
0216 }
0217 
0218 void KCMKeyboardWidget::addLayout()
0219 {
0220     AddLayoutDialog dialog(rules, flags, keyboardModelFromUI(), xkbOptionsFromUI(), false, this);
0221     dialog.setModal(true);
0222     if (dialog.exec() == QDialog::Accepted) {
0223         keyboardConfig->layouts.append(dialog.getSelectedLayoutUnit());
0224         layoutsTableModel->refresh();
0225         uiWidget->layoutsTableView->resizeRowsToContents();
0226         uiChanged();
0227     }
0228 
0229     updateLoopCount();
0230 }
0231 
0232 void KCMKeyboardWidget::updateLoopCount()
0233 {
0234     int maxLoop = qMin(X11Helper::MAX_GROUP_COUNT, keyboardConfig->layouts.count() - 1);
0235     uiWidget->layoutLoopCountSpinBox->setMaximum(qMax(MIN_LOOPING_COUNT, maxLoop));
0236 
0237     if (maxLoop < MIN_LOOPING_COUNT) {
0238         uiWidget->layoutLoopingCheckBox->setEnabled(false);
0239         uiWidget->layoutLoopingCheckBox->setChecked(false);
0240     } else if (maxLoop >= X11Helper::MAX_GROUP_COUNT) {
0241         uiWidget->layoutLoopingCheckBox->setEnabled(false);
0242         uiWidget->layoutLoopingCheckBox->setChecked(true);
0243     } else {
0244         uiWidget->layoutLoopingCheckBox->setEnabled(keyboardConfig->configureLayouts());
0245     }
0246 
0247     uiWidget->layoutLoopingGroupBox->setEnabled(keyboardConfig->configureLayouts() && uiWidget->layoutLoopingCheckBox->isChecked());
0248 
0249     if (uiWidget->layoutLoopingCheckBox->isChecked()) {
0250         if (uiWidget->layoutLoopCountSpinBox->text().isEmpty()) {
0251             uiWidget->layoutLoopCountSpinBox->setValue(maxLoop);
0252             keyboardConfig->setLayoutLoopCount(maxLoop);
0253         }
0254     } else {
0255         uiWidget->layoutLoopCountSpinBox->clear();
0256         keyboardConfig->setLayoutLoopCount(KeyboardConfig::NO_LOOPING);
0257     }
0258 }
0259 
0260 void KCMKeyboardWidget::initializeLayoutsUI()
0261 {
0262     layoutsTableModel = new LayoutsTableModel(rules, flags, keyboardConfig, uiWidget->layoutsTableView);
0263     uiWidget->layoutsTableView->setEditTriggers(QAbstractItemView::SelectedClicked | QAbstractItemView::DoubleClicked | QAbstractItemView::EditKeyPressed
0264                                                 | QAbstractItemView::AnyKeyPressed);
0265     uiWidget->layoutsTableView->setModel(layoutsTableModel);
0266     uiWidget->layoutsTableView->setIconSize({22, 22});
0267 
0268     // TODO: do we need to delete this delegate or parent will take care of it?
0269     VariantComboDelegate *variantDelegate = new VariantComboDelegate(keyboardConfig, rules, uiWidget->layoutsTableView);
0270     uiWidget->layoutsTableView->setItemDelegateForColumn(LayoutsTableModel::VARIANT_COLUMN, variantDelegate);
0271 
0272     LabelEditDelegate *labelDelegate = new LabelEditDelegate(keyboardConfig, uiWidget->layoutsTableView);
0273     uiWidget->layoutsTableView->setItemDelegateForColumn(LayoutsTableModel::DISPLAY_NAME_COLUMN, labelDelegate);
0274 
0275     KKeySequenceWidgetDelegate *shortcutDelegate = new KKeySequenceWidgetDelegate(keyboardConfig, uiWidget->layoutsTableView);
0276     uiWidget->layoutsTableView->setItemDelegateForColumn(LayoutsTableModel::SHORTCUT_COLUMN, shortcutDelegate);
0277 
0278     // TODO: is it ok to hardcode sizes? any better approach?
0279     uiWidget->layoutsTableView->setColumnWidth(LayoutsTableModel::MAP_COLUMN, 70);
0280     uiWidget->layoutsTableView->setColumnWidth(LayoutsTableModel::LAYOUT_COLUMN, 200);
0281     uiWidget->layoutsTableView->setColumnWidth(LayoutsTableModel::VARIANT_COLUMN, 200);
0282     uiWidget->layoutsTableView->setColumnWidth(LayoutsTableModel::DISPLAY_NAME_COLUMN, 50);
0283     uiWidget->layoutsTableView->setColumnWidth(LayoutsTableModel::SHORTCUT_COLUMN, 130);
0284 
0285     connect(layoutsTableModel, &LayoutsTableModel::dataChanged, this, [this]() {
0286         Q_EMIT changed(true);
0287     });
0288 
0289     uiWidget->layoutLoopCountSpinBox->setMinimum(MIN_LOOPING_COUNT);
0290 
0291 #ifdef DRAG_ENABLED
0292     uiWidget->layoutsTableView->setDragEnabled(true);
0293     uiWidget->layoutsTableView->setAcceptDrops(true);
0294 #endif
0295 
0296     uiWidget->moveUpBtn->setIcon(QIcon::fromTheme(QStringLiteral("arrow-up")));
0297     uiWidget->moveDownBtn->setIcon(QIcon::fromTheme(QStringLiteral("arrow-down")));
0298     uiWidget->addLayoutBtn->setIcon(QIcon::fromTheme(QStringLiteral("list-add")));
0299     uiWidget->removeLayoutBtn->setIcon(QIcon::fromTheme(QStringLiteral("list-remove")));
0300 
0301     QIcon clearIcon =
0302         qApp->isLeftToRight() ? QIcon::fromTheme(QStringLiteral("edit-clear-locationbar-rtl")) : QIcon::fromTheme(QStringLiteral("edit-clear-locationbar-ltr"));
0303     uiWidget->xkbGrpClearBtn->setIcon(clearIcon);
0304     uiWidget->xkb3rdLevelClearBtn->setIcon(clearIcon);
0305 
0306     QIcon configIcon = QIcon::fromTheme(QStringLiteral("configure"));
0307     uiWidget->xkbGrpShortcutBtn->setIcon(configIcon);
0308     uiWidget->xkb3rdLevelShortcutBtn->setIcon(configIcon);
0309 
0310     uiWidget->kdeKeySequence->setModifierlessAllowed(false);
0311 
0312     uiWidget->toggleLastUsedLayoutKeySequence->setModifierlessAllowed(false);
0313 
0314     uiWidget->kcfg_osdKbdLayoutChangedEnabled->setText(m_workspaceOptions.osdKbdLayoutChangedEnabledItem()->label());
0315     uiWidget->kcfg_osdKbdLayoutChangedEnabled->setToolTip(m_workspaceOptions.osdKbdLayoutChangedEnabledItem()->toolTip());
0316 
0317     connect(uiWidget->addLayoutBtn, &QAbstractButton::clicked, this, &KCMKeyboardWidget::addLayout);
0318     connect(uiWidget->removeLayoutBtn, &QAbstractButton::clicked, this, &KCMKeyboardWidget::removeLayout);
0319     connect(uiWidget->layoutsTableView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &KCMKeyboardWidget::layoutSelectionChanged);
0320     connect(uiWidget->layoutsTableView->model(),
0321             qOverload<const QModelIndex &, const QModelIndex &, const QList<int> &>(&QAbstractItemModel::dataChanged),
0322             this,
0323             &KCMKeyboardWidget::uiChanged);
0324 
0325     connect(uiWidget->moveUpBtn, &QAbstractButton::clicked, this, &KCMKeyboardWidget::moveUp);
0326     connect(uiWidget->moveDownBtn, &QAbstractButton::clicked, this, &KCMKeyboardWidget::moveDown);
0327 
0328     connect(uiWidget->previewButton, &QAbstractButton::clicked, this, &KCMKeyboardWidget::previewLayout);
0329     connect(uiWidget->xkbGrpClearBtn, &QAbstractButton::clicked, this, &KCMKeyboardWidget::clearGroupShortcuts);
0330     connect(uiWidget->xkb3rdLevelClearBtn, &QAbstractButton::clicked, this, &KCMKeyboardWidget::clear3rdLevelShortcuts);
0331 
0332     connect(uiWidget->kdeKeySequence, &KKeySequenceWidget::keySequenceChanged, this, &KCMKeyboardWidget::alternativeShortcutChanged);
0333     connect(uiWidget->toggleLastUsedLayoutKeySequence, &KKeySequenceWidget::keySequenceChanged, this, &KCMKeyboardWidget::lastUsedLayoutShortcutChanged);
0334     connect(uiWidget->switchingPolicyButtonGroup, &QButtonGroup::idClicked, this, &KCMKeyboardWidget::uiChanged);
0335     connect(uiWidget->switchingPolicyButtonGroup, &QButtonGroup::idClicked, this, &KCMKeyboardWidget::updateUiDefaultIndicator);
0336 
0337     connect(uiWidget->xkbGrpShortcutBtn, &QAbstractButton::clicked, this, &KCMKeyboardWidget::scrollToGroupShortcut);
0338     connect(uiWidget->xkb3rdLevelShortcutBtn, &QAbstractButton::clicked, this, &KCMKeyboardWidget::scrollTo3rdLevelShortcut);
0339 
0340     connect(uiWidget->kcfg_configureLayouts, &QGroupBox::toggled, this, &KCMKeyboardWidget::configureLayoutsChanged);
0341 
0342     connect(uiWidget->layoutLoopingCheckBox, &QAbstractButton::clicked, this, &KCMKeyboardWidget::uiChanged);
0343     connect(uiWidget->layoutLoopCountSpinBox, SIGNAL(valueChanged(int)), this, SLOT(uiChanged()));
0344 }
0345 
0346 void KCMKeyboardWidget::previewLayout()
0347 {
0348     QModelIndex index = uiWidget->layoutsTableView->currentIndex();
0349 
0350     QModelIndex idcountry = index.sibling(index.row(), 0);
0351     const QString country = uiWidget->layoutsTableView->model()->data(idcountry).toString();
0352     const QModelIndex idvariant = index.sibling(index.row(), 2);
0353     QString variant = uiWidget->layoutsTableView->model()->data(idvariant).toString();
0354     const QString model = keyboardModelFromUI();
0355     const QStringList options = xkbOptionsFromUI();
0356 
0357     const LayoutInfo *layoutInfo = rules->getLayoutInfo(country);
0358     if (!layoutInfo) {
0359         return;
0360     }
0361 
0362     for (const VariantInfo *variantInfo : layoutInfo->variantInfos) {
0363         if (variant == variantInfo->description) {
0364             variant = variantInfo->name;
0365             break;
0366         }
0367     }
0368 
0369     const QString title = Flags::getLongText(LayoutUnit(country, variant), rules);
0370     Tastenbrett::launch(model, country, variant, options.join(','), title);
0371 }
0372 
0373 void KCMKeyboardWidget::alternativeShortcutChanged(const QKeySequence &seq)
0374 {
0375     Q_UNUSED(seq)
0376 
0377     if (rules == nullptr) {
0378         return;
0379     }
0380 
0381     if (actionCollection == nullptr) {
0382         actionCollection = new KeyboardLayoutActionCollection(this, true);
0383     }
0384     actionCollection->setToggleShortcut(uiWidget->kdeKeySequence->keySequence());
0385 }
0386 
0387 void KCMKeyboardWidget::lastUsedLayoutShortcutChanged(const QKeySequence &seq)
0388 {
0389     Q_UNUSED(seq)
0390 
0391     if (rules == nullptr) {
0392         return;
0393     }
0394 
0395     if (actionCollection == nullptr) {
0396         actionCollection = new KeyboardLayoutActionCollection(this, true);
0397     }
0398     actionCollection->setLastUsedLayoutShortcut(uiWidget->toggleLastUsedLayoutKeySequence->keySequence());
0399 }
0400 
0401 void KCMKeyboardWidget::switchKeyboardShortcutChanged()
0402 {
0403     if (rules == nullptr) {
0404         return;
0405     }
0406 
0407     if (actionCollection == nullptr) {
0408         actionCollection = new KeyboardLayoutActionCollection(this, true);
0409     }
0410     actionCollection->resetLayoutShortcuts();
0411     actionCollection->setLayoutShortcuts(keyboardConfig->layouts, rules);
0412 }
0413 
0414 void KCMKeyboardWidget::configureLayoutsChanged()
0415 {
0416     if (uiWidget->kcfg_configureLayouts->isChecked() && keyboardConfig->layouts.isEmpty()) {
0417         populateWithCurrentLayouts();
0418     } else {
0419         keyboardConfig->layouts.clear();
0420     }
0421     uiChanged();
0422 }
0423 
0424 static QPair<int, int> getSelectedRowRange(const QModelIndexList &selected)
0425 {
0426     if (selected.isEmpty()) {
0427         return QPair<int, int>(-1, -1);
0428     }
0429 
0430     QList<int> rows;
0431     for (const auto &index : selected) {
0432         rows << index.row();
0433     }
0434     std::sort(rows.begin(), rows.end());
0435     return QPair<int, int>(rows[0], rows[rows.size() - 1]);
0436 }
0437 
0438 void KCMKeyboardWidget::layoutSelectionChanged()
0439 {
0440     QModelIndexList selected = uiWidget->layoutsTableView->selectionModel()->selectedIndexes();
0441     uiWidget->removeLayoutBtn->setEnabled(!selected.isEmpty());
0442     QPair<int, int> rowsRange(getSelectedRowRange(selected));
0443     uiWidget->moveUpBtn->setEnabled(!selected.isEmpty() && rowsRange.first > 0);
0444     uiWidget->previewButton->setEnabled(uiWidget->layoutsTableView->selectionModel()->selectedRows().size() == 1);
0445     uiWidget->previewButton->setVisible(Tastenbrett::exists());
0446     uiWidget->moveDownBtn->setEnabled(!selected.isEmpty() && rowsRange.second < keyboardConfig->layouts.size() - 1);
0447 }
0448 
0449 void KCMKeyboardWidget::removeLayout()
0450 {
0451     if (!uiWidget->layoutsTableView->selectionModel()->hasSelection())
0452         return;
0453 
0454     const QModelIndexList selected = uiWidget->layoutsTableView->selectionModel()->selectedIndexes();
0455     QPair<int, int> rowsRange(getSelectedRowRange(selected));
0456     for (const auto &idx : selected) {
0457         if (idx.column() == 0) {
0458             keyboardConfig->layouts.removeAt(rowsRange.first);
0459         }
0460     }
0461     layoutsTableModel->refresh();
0462     uiChanged();
0463 
0464     if (keyboardConfig->layouts.size() > 0) {
0465         int rowToSelect = rowsRange.first;
0466         if (rowToSelect >= keyboardConfig->layouts.size()) {
0467             rowToSelect--;
0468         }
0469 
0470         QModelIndex topLeft = layoutsTableModel->index(rowToSelect, 0, QModelIndex());
0471         QModelIndex bottomRight = layoutsTableModel->index(rowToSelect, layoutsTableModel->columnCount(topLeft) - 1, QModelIndex());
0472         QItemSelection selection(topLeft, bottomRight);
0473         uiWidget->layoutsTableView->selectionModel()->select(selection, QItemSelectionModel::SelectCurrent);
0474         uiWidget->layoutsTableView->setFocus();
0475     }
0476 
0477     layoutSelectionChanged();
0478 
0479     updateLoopCount();
0480 }
0481 
0482 void KCMKeyboardWidget::moveUp()
0483 {
0484     moveSelectedLayouts(-1);
0485 }
0486 
0487 void KCMKeyboardWidget::moveDown()
0488 {
0489     moveSelectedLayouts(1);
0490 }
0491 
0492 void KCMKeyboardWidget::moveSelectedLayouts(int shift)
0493 {
0494     QItemSelectionModel *selectionModel = uiWidget->layoutsTableView->selectionModel();
0495     if (selectionModel == nullptr || !selectionModel->hasSelection())
0496         return;
0497 
0498     const QModelIndexList selected = selectionModel->selectedRows();
0499     if (selected.count() < 1)
0500         return;
0501 
0502     int newFirstRow = selected[0].row() + shift;
0503     int newLastRow = selected[selected.size() - 1].row() + shift;
0504 
0505     if (newFirstRow >= 0 && newLastRow <= keyboardConfig->layouts.size() - 1) {
0506         QList<int> selectionRows;
0507         for (const QModelIndex &index : selected) {
0508             int newRowIndex = index.row() + shift;
0509             keyboardConfig->layouts.move(index.row(), newRowIndex);
0510             selectionRows << newRowIndex;
0511         }
0512         uiChanged();
0513 
0514         QItemSelection selection;
0515         for (const int row : std::as_const(selectionRows)) {
0516             QModelIndex topLeft = layoutsTableModel->index(row, 0, QModelIndex());
0517             QModelIndex bottomRight = layoutsTableModel->index(row, layoutsTableModel->columnCount(topLeft) - 1, QModelIndex());
0518             selection << QItemSelectionRange(topLeft, bottomRight);
0519         }
0520         uiWidget->layoutsTableView->selectionModel()->select(selection, QItemSelectionModel::SelectCurrent);
0521         uiWidget->layoutsTableView->setFocus();
0522     }
0523 }
0524 
0525 void KCMKeyboardWidget::scrollToGroupShortcut()
0526 {
0527     this->setCurrentIndex(TAB_ADVANCED);
0528     if (!uiWidget->kcfg_resetOldXkbOptions->isChecked()) {
0529         uiWidget->kcfg_resetOldXkbOptions->setChecked(true);
0530     }
0531     ((XkbOptionsTreeModel *)uiWidget->xkbOptionsTreeView->model())->gotoGroup(GROUP_SWITCH_GROUP_NAME, uiWidget->xkbOptionsTreeView);
0532 }
0533 
0534 void KCMKeyboardWidget::scrollTo3rdLevelShortcut()
0535 {
0536     this->setCurrentIndex(TAB_ADVANCED);
0537     if (!uiWidget->kcfg_resetOldXkbOptions->isChecked()) {
0538         uiWidget->kcfg_resetOldXkbOptions->setChecked(true);
0539     }
0540     ((XkbOptionsTreeModel *)uiWidget->xkbOptionsTreeView->model())->gotoGroup(LV3_SWITCH_GROUP_NAME, uiWidget->xkbOptionsTreeView);
0541 }
0542 
0543 void KCMKeyboardWidget::clearGroupShortcuts()
0544 {
0545     clearXkbGroup(GROUP_SWITCH_GROUP_NAME);
0546 }
0547 
0548 void KCMKeyboardWidget::clear3rdLevelShortcuts()
0549 {
0550     clearXkbGroup(LV3_SWITCH_GROUP_NAME);
0551 }
0552 
0553 void KCMKeyboardWidget::clearXkbGroup(const QString &groupName)
0554 {
0555     auto *xkbOptionModel = dynamic_cast<XkbOptionsTreeModel *>(uiWidget->xkbOptionsTreeView->model());
0556     QStringList xkbOptions = xkbOptionModel->xkbOptions();
0557     for (int ii = xkbOptions.count() - 1; ii >= 0; ii--) {
0558         if (xkbOptions.at(ii).startsWith(groupName + Rules::XKB_OPTION_GROUP_SEPARATOR)) {
0559             xkbOptions.removeAt(ii);
0560         }
0561     }
0562     xkbOptionModel->setXkbOptions(xkbOptions);
0563 
0564     xkbOptionModel->reset();
0565     uiWidget->xkbOptionsTreeView->update();
0566     updateXkbShortcutsButtons();
0567     Q_EMIT changed(true);
0568 }
0569 
0570 static bool xkbOptionGroupLessThan(const OptionGroupInfo *og1, const OptionGroupInfo *og2)
0571 {
0572     return og1->description.toLower() < og2->description.toLower();
0573 }
0574 static bool xkbOptionLessThan(const OptionInfo *o1, const OptionInfo *o2)
0575 {
0576     return o1->description.toLower() < o2->description.toLower();
0577 }
0578 
0579 void KCMKeyboardWidget::initializeXkbOptionsUI()
0580 {
0581     std::sort(rules->optionGroupInfos.begin(), rules->optionGroupInfos.end(), xkbOptionGroupLessThan);
0582     for (OptionGroupInfo *optionGroupInfo : std::as_const(rules->optionGroupInfos)) {
0583         std::sort(optionGroupInfo->optionInfos.begin(), optionGroupInfo->optionInfos.end(), xkbOptionLessThan);
0584     }
0585 
0586     XkbOptionsTreeModel *model = new XkbOptionsTreeModel(rules, uiWidget->xkbOptionsTreeView);
0587     uiWidget->xkbOptionsTreeView->setModel(model);
0588     connect(model, &QAbstractItemModel::dataChanged, this, &KCMKeyboardWidget::uiChanged);
0589 
0590     connect(uiWidget->kcfg_resetOldXkbOptions, &QAbstractButton::toggled, this, &KCMKeyboardWidget::configureXkbOptionsChanged);
0591     connect(uiWidget->kcfg_resetOldXkbOptions, &QAbstractButton::toggled, uiWidget->xkbOptionsTreeView, &QWidget::setEnabled);
0592 }
0593 
0594 void KCMKeyboardWidget::configureXkbOptionsChanged()
0595 {
0596     if (uiWidget->kcfg_resetOldXkbOptions->isChecked() && keyboardConfig->xkbOptions().isEmpty()) {
0597         populateWithCurrentXkbOptions();
0598     }
0599     ((XkbOptionsTreeModel *)uiWidget->xkbOptionsTreeView->model())->reset();
0600     uiChanged();
0601 }
0602 
0603 void KCMKeyboardWidget::saveXkbOptions()
0604 {
0605     QStringList options;
0606 
0607     if (uiWidget->kcfg_resetOldXkbOptions->isChecked()) {
0608         options = xkbOptionsFromUI();
0609 
0610         // QStringLists with a single empty string are serialized as "\\0", avoid that
0611         // by saving them as an empty list instead. This way it can be passed as-is to
0612         // libxkbcommon/setxkbmap. Before KConfigXT it used QStringList::join(",").
0613         if (options.size() == 1 && options.constFirst().isEmpty()) {
0614             options.clear();
0615         }
0616     }
0617 
0618     keyboardConfig->setXkbOptions(options);
0619 }
0620 
0621 QStringList KCMKeyboardWidget::xkbOptionsFromUI() const
0622 {
0623     XkbOptionsTreeModel *model = dynamic_cast<XkbOptionsTreeModel *>(uiWidget->xkbOptionsTreeView->model());
0624     return model->xkbOptions();
0625 }
0626 
0627 void KCMKeyboardWidget::updateSwitchingPolicyUI(KeyboardConfig::SwitchingPolicy policy)
0628 {
0629     switch (policy) {
0630     case KeyboardConfig::SWITCH_POLICY_DESKTOP:
0631         uiWidget->switchByDesktopRadioBtn->setChecked(true);
0632         break;
0633     case KeyboardConfig::SWITCH_POLICY_APPLICATION:
0634         uiWidget->switchByApplicationRadioBtn->setChecked(true);
0635         break;
0636     case KeyboardConfig::SWITCH_POLICY_WINDOW:
0637         uiWidget->switchByWindowRadioBtn->setChecked(true);
0638         break;
0639     default:
0640     case KeyboardConfig::SWITCH_POLICY_GLOBAL:
0641         uiWidget->switchByGlobalRadioBtn->setChecked(true);
0642     }
0643 }
0644 
0645 void KCMKeyboardWidget::updateXkbShortcutButton(const QString &groupName, QPushButton *button)
0646 {
0647     QStringList grpOptions;
0648     if (uiWidget->kcfg_resetOldXkbOptions->isChecked()) {
0649         QRegularExpression regexp("^" + groupName + Rules::XKB_OPTION_GROUP_SEPARATOR);
0650         XkbOptionsTreeModel *model = dynamic_cast<XkbOptionsTreeModel *>(uiWidget->xkbOptionsTreeView->model());
0651         grpOptions = model->xkbOptions().filter(regexp);
0652     }
0653     switch (grpOptions.size()) {
0654     case 0:
0655         button->setText(i18nc("no shortcuts defined", "None"));
0656         break;
0657     case 1: {
0658         const QString &option = grpOptions.first();
0659         const OptionGroupInfo *optionGroupInfo = rules->getOptionGroupInfo(groupName);
0660         const OptionInfo *optionInfo = optionGroupInfo->getOptionInfo(option);
0661         if (optionInfo == nullptr || optionInfo->description == nullptr) {
0662             qCDebug(KCM_KEYBOARD) << "Could not find option info for " << option;
0663             button->setText(grpOptions.first());
0664         } else {
0665             button->setText(optionInfo->description);
0666         }
0667     } break;
0668     default:
0669         button->setText(i18np("%1 shortcut", "%1 shortcuts", grpOptions.size()));
0670     }
0671 }
0672 
0673 void KCMKeyboardWidget::updateXkbShortcutsButtons()
0674 {
0675     updateXkbShortcutButton(GROUP_SWITCH_GROUP_NAME, uiWidget->xkbGrpShortcutBtn);
0676     updateXkbShortcutButton(LV3_SWITCH_GROUP_NAME, uiWidget->xkb3rdLevelShortcutBtn);
0677 }
0678 
0679 void KCMKeyboardWidget::updateShortcutsUI()
0680 {
0681     updateXkbShortcutsButtons();
0682 
0683     delete actionCollection;
0684     actionCollection = new KeyboardLayoutActionCollection(this, true);
0685     QAction *toggleAction = actionCollection->getToggleAction();
0686     const auto shortcuts = KGlobalAccel::self()->shortcut(toggleAction);
0687     uiWidget->kdeKeySequence->setKeySequence(shortcuts.isEmpty() ? QKeySequence() : shortcuts.first());
0688 
0689     QAction *lastUsedLayoutAction = actionCollection->getLastUsedLayoutAction();
0690     const auto lastUsedLayoutShortcuts = KGlobalAccel::self()->shortcut(lastUsedLayoutAction);
0691     uiWidget->toggleLastUsedLayoutKeySequence->setKeySequence(lastUsedLayoutShortcuts.isEmpty() ? QKeySequence() : lastUsedLayoutShortcuts.first());
0692 
0693     actionCollection->loadLayoutShortcuts(keyboardConfig->layouts, rules);
0694     layoutsTableModel->refresh();
0695 }
0696 
0697 void KCMKeyboardWidget::updateLayoutsUI()
0698 {
0699     bool loopingOn = keyboardConfig->configureLayouts() && keyboardConfig->layoutLoopCount() != KeyboardConfig::NO_LOOPING;
0700     uiWidget->layoutLoopingCheckBox->setChecked(loopingOn);
0701     uiWidget->layoutLoopingGroupBox->setEnabled(loopingOn);
0702     if (loopingOn) {
0703         // Set maximum to 99 to make sure following setValue succeeds
0704         // Correct maximum value will be set in updateLoopCount()
0705         uiWidget->layoutLoopCountSpinBox->setMaximum(99);
0706         uiWidget->layoutLoopCountSpinBox->setValue(keyboardConfig->layoutLoopCount());
0707     } else {
0708         uiWidget->layoutLoopCountSpinBox->clear();
0709     }
0710 
0711     updateLoopCount();
0712 }
0713 
0714 void KCMKeyboardWidget::updateHardwareUI(const QString &model)
0715 {
0716     int idx = uiWidget->keyboardModelComboBox->findData(model);
0717     if (idx != -1) {
0718         uiWidget->keyboardModelComboBox->setCurrentIndex(idx);
0719     }
0720 }
0721 
0722 void KCMKeyboardWidget::populateWithCurrentLayouts()
0723 {
0724     const QList<LayoutUnit> layouts = X11Helper::getLayoutsList();
0725     for (const auto &layoutUnit : layouts) {
0726         keyboardConfig->layouts.append(layoutUnit);
0727     }
0728 }
0729 
0730 void KCMKeyboardWidget::populateWithCurrentXkbOptions()
0731 {
0732     if (!KWindowSystem::isPlatformX11()) {
0733         // TODO: implement for Wayland - query dbus maybe?
0734         return;
0735     }
0736     XkbConfig xkbConfig;
0737     QStringList xkbOptions;
0738     if (X11Helper::getGroupNames(QX11Info::display(), &xkbConfig, X11Helper::ALL)) {
0739         xkbOptions = xkbConfig.options;
0740     }
0741     auto *xkbOptionModel = dynamic_cast<XkbOptionsTreeModel *>(uiWidget->xkbOptionsTreeView->model());
0742     xkbOptionModel->setXkbOptions(xkbOptions);
0743     keyboardConfig->setXkbOptions(xkbOptions);
0744 }
0745 
0746 QString KCMKeyboardWidget::keyboardModelFromUI() const
0747 {
0748     return uiWidget->keyboardModelComboBox->itemData(uiWidget->keyboardModelComboBox->currentIndex()).toString();
0749 }
0750 
0751 KeyboardConfig::SwitchingPolicy KCMKeyboardWidget::switchingPolicyFromUI() const
0752 {
0753     if (uiWidget->switchByDesktopRadioBtn->isChecked()) {
0754         return KeyboardConfig::SWITCH_POLICY_DESKTOP;
0755     } else if (uiWidget->switchByApplicationRadioBtn->isChecked()) {
0756         return KeyboardConfig::SWITCH_POLICY_APPLICATION;
0757     } else if (uiWidget->switchByWindowRadioBtn->isChecked()) {
0758         return KeyboardConfig::SWITCH_POLICY_WINDOW;
0759     } else {
0760         return KeyboardConfig::SWITCH_POLICY_GLOBAL;
0761     }
0762 }
0763 
0764 void KCMKeyboardWidget::setDefaultIndicatorVisible(QWidget *widget, bool visible)
0765 {
0766     widget->setProperty("_kde_highlight_neutral", visible);
0767     widget->update();
0768 }