File indexing completed on 2024-05-12 17:07:14

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