File indexing completed on 2024-04-28 09:46:47

0001 /*
0002     SPDX-FileCopyrightText: 2008 Robert Knight <robertknight@gmail.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 // Own
0007 #include "KeyBindingEditor.h"
0008 
0009 // Qt
0010 #include <QDialogButtonBox>
0011 #include <QIcon>
0012 #include <QKeyEvent>
0013 
0014 // KDE
0015 #include <KLocalizedString>
0016 #include <KMessageBox>
0017 
0018 // Konsole
0019 #include "keyboardtranslator/KeyboardTranslator.h"
0020 #include "keyboardtranslator/KeyboardTranslatorManager.h"
0021 #include "keyboardtranslator/KeyboardTranslatorReader.h"
0022 #include "ui_KeyBindingEditor.h"
0023 
0024 #include "widgets/EditProfileDialog.h"
0025 
0026 using namespace Konsole;
0027 
0028 KeyBindingEditor::KeyBindingEditor(QWidget *parent)
0029     : QDialog(parent)
0030     , _ui(nullptr)
0031     , _translator(new KeyboardTranslator(QString()))
0032     , _isNewTranslator(false)
0033 {
0034     auto layout = new QVBoxLayout;
0035 
0036     auto mainWidget = new QWidget(this);
0037     layout->addWidget(mainWidget);
0038 
0039     auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
0040     buttonBox->button(QDialogButtonBox::Cancel)->setDefault(true);
0041     layout->addWidget(buttonBox);
0042 
0043     setLayout(layout);
0044 
0045     connect(buttonBox, &QDialogButtonBox::accepted, this, &Konsole::KeyBindingEditor::accept);
0046     connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
0047 
0048     setAttribute(Qt::WA_DeleteOnClose);
0049 
0050     _ui = new Ui::KeyBindingEditor();
0051     _ui->setupUi(mainWidget);
0052 
0053     // description edit
0054     _ui->descriptionEdit->setPlaceholderText(i18nc("@label:textbox", "Enter descriptive label"));
0055     connect(_ui->descriptionEdit, &QLineEdit::textChanged, this, &Konsole::KeyBindingEditor::setTranslatorDescription);
0056     // filter edit
0057     connect(_ui->filterEdit, &QLineEdit::textChanged, this, &Konsole::KeyBindingEditor::filterRows);
0058 
0059     // key bindings table
0060     _ui->keyBindingTable->setColumnCount(2);
0061 
0062     QStringList labels;
0063     labels << i18n("Key Combination") << i18n("Output");
0064 
0065     _ui->keyBindingTable->setHorizontalHeaderLabels(labels);
0066     _ui->keyBindingTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
0067 
0068     _ui->keyBindingTable->verticalHeader()->hide();
0069     _ui->keyBindingTable->setSelectionBehavior(QAbstractItemView::SelectRows);
0070 
0071     // add and remove buttons
0072     _ui->addEntryButton->setIcon(QIcon::fromTheme(QStringLiteral("list-add")));
0073     _ui->removeEntryButton->setIcon(QIcon::fromTheme(QStringLiteral("list-remove")));
0074 
0075     connect(_ui->removeEntryButton, &QPushButton::clicked, this, &Konsole::KeyBindingEditor::removeSelectedEntry);
0076     connect(_ui->addEntryButton, &QPushButton::clicked, this, &Konsole::KeyBindingEditor::addNewEntry);
0077 
0078     // test area
0079     _ui->testAreaInputEdit->installEventFilter(this);
0080 }
0081 
0082 KeyBindingEditor::~KeyBindingEditor()
0083 {
0084     delete _ui;
0085     delete _translator;
0086 }
0087 
0088 void KeyBindingEditor::filterRows(const QString &text)
0089 {
0090     const int rows = _ui->keyBindingTable->rowCount();
0091 
0092     QList<int> matchedRows;
0093 
0094     for (QTableWidgetItem *matchedItem : _ui->keyBindingTable->findItems(text, Qt::MatchContains)) {
0095         matchedRows.append(matchedItem->row());
0096     }
0097 
0098     for (int i = 0; i < rows; i++) {
0099         if (matchedRows.contains(i) && _ui->keyBindingTable->isRowHidden(i)) {
0100             _ui->keyBindingTable->showRow(i);
0101         } else if (!matchedRows.contains(i)) {
0102             _ui->keyBindingTable->hideRow(i);
0103         }
0104     }
0105 }
0106 
0107 void KeyBindingEditor::removeSelectedEntry()
0108 {
0109     QList<QTableWidgetItem *> uniqueList;
0110     const QList<QTableWidgetItem *> selectedItems = _ui->keyBindingTable->selectedItems();
0111     for (QTableWidgetItem *item : selectedItems) {
0112         if (item->column() == 1) { // Select item at the first column
0113             item = _ui->keyBindingTable->item(item->row(), 0);
0114         }
0115 
0116         if (!uniqueList.contains(item)) {
0117             uniqueList.append(item);
0118         }
0119     }
0120 
0121     for (QTableWidgetItem *item : std::as_const(uniqueList)) {
0122         // get the first item in the row which has the entry
0123 
0124         KeyboardTranslator::Entry existing = item->data(Qt::UserRole).value<KeyboardTranslator::Entry>();
0125 
0126         _translator->removeEntry(existing);
0127 
0128         _ui->keyBindingTable->removeRow(item->row());
0129     }
0130 }
0131 
0132 void KeyBindingEditor::addNewEntry()
0133 {
0134     _ui->keyBindingTable->insertRow(_ui->keyBindingTable->rowCount());
0135 
0136     int newRowCount = _ui->keyBindingTable->rowCount();
0137 
0138     // block signals here to avoid triggering bindingTableItemChanged() slot call
0139     _ui->keyBindingTable->blockSignals(true);
0140 
0141     _ui->keyBindingTable->setItem(newRowCount - 1, 0, new QTableWidgetItem());
0142     _ui->keyBindingTable->setItem(newRowCount - 1, 1, new QTableWidgetItem());
0143 
0144     _ui->keyBindingTable->blockSignals(false);
0145 
0146     // make sure user can see new row
0147     _ui->keyBindingTable->scrollToItem(_ui->keyBindingTable->item(newRowCount - 1, 0));
0148 }
0149 
0150 bool KeyBindingEditor::eventFilter(QObject *watched, QEvent *event)
0151 {
0152     if (watched == _ui->testAreaInputEdit) {
0153         if (event->type() == QEvent::KeyPress) {
0154             auto *keyEvent = static_cast<QKeyEvent *>(event);
0155 
0156             // The state here is currently set to the state that a newly started
0157             // terminal in Konsole will be in ( which is also the same as the
0158             // state just after a reset ), this has 'ANSI' turned on and all other
0159             // states off.
0160             //
0161             // TODO: It may be useful to be able to specify the state in the 'test input'
0162             // area, but preferably not in a way which clutters the UI with lots of
0163             // checkboxes.
0164             //
0165             const KeyboardTranslator::States states = KeyboardTranslator::AnsiState;
0166 
0167             KeyboardTranslator::Entry entry = _translator->findEntry(keyEvent->key(), keyEvent->modifiers(), states);
0168 
0169             if (!entry.isNull()) {
0170                 _ui->testAreaInputEdit->setText(entry.conditionToString());
0171                 _ui->testAreaOutputEdit->setText(entry.resultToString(true, keyEvent->modifiers()));
0172             } else {
0173                 _ui->testAreaInputEdit->setText(keyEvent->text());
0174                 _ui->testAreaOutputEdit->setText(keyEvent->text());
0175             }
0176 
0177             keyEvent->accept();
0178             return true;
0179         }
0180     }
0181     return QDialog::eventFilter(watched, event);
0182 }
0183 
0184 void KeyBindingEditor::setDescription(const QString &description)
0185 {
0186     _ui->descriptionEdit->setText(description);
0187 
0188     setTranslatorDescription(description);
0189 }
0190 
0191 void KeyBindingEditor::setTranslatorDescription(const QString &description)
0192 {
0193     if (_translator != nullptr) {
0194         _translator->setDescription(description);
0195     }
0196 }
0197 
0198 QString KeyBindingEditor::description() const
0199 {
0200     return _ui->descriptionEdit->text();
0201 }
0202 
0203 void KeyBindingEditor::setup(const KeyboardTranslator *translator, const QString &currentProfileTranslator, bool isNewTranslator)
0204 {
0205     delete _translator;
0206 
0207     _isNewTranslator = isNewTranslator;
0208 
0209     _currentProfileTranslator = currentProfileTranslator;
0210 
0211     _translator = new KeyboardTranslator(*translator);
0212 
0213     // setup description edit line
0214     _ui->descriptionEdit->setClearButtonEnabled(true);
0215     // setup filter edit line
0216     _ui->filterEdit->setClearButtonEnabled(true);
0217 
0218     if (_isNewTranslator) {
0219         setDescription(i18n("New Key Binding List"));
0220         setWindowTitle(i18n("New Key Binding List"));
0221     } else {
0222         _ui->descriptionEdit->setText(translator->description());
0223         setWindowTitle(i18n("Edit Key Binding List"));
0224     }
0225 
0226     // setup key binding table
0227     setupKeyBindingTable(translator);
0228 }
0229 
0230 KeyboardTranslator *KeyBindingEditor::translator() const
0231 {
0232     return _translator;
0233 }
0234 
0235 void KeyBindingEditor::bindingTableItemChanged(QTableWidgetItem *item)
0236 {
0237     QTableWidgetItem *key = _ui->keyBindingTable->item(item->row(), 0);
0238     KeyboardTranslator::Entry existing = key->data(Qt::UserRole).value<KeyboardTranslator::Entry>();
0239 
0240     QString condition = key->text();
0241     QString result = _ui->keyBindingTable->item(item->row(), 1)->text();
0242 
0243     KeyboardTranslator::Entry entry = KeyboardTranslatorReader::createEntry(condition, result);
0244     _translator->replaceEntry(existing, entry);
0245 
0246     // block signals to prevent this slot from being called repeatedly
0247     _ui->keyBindingTable->blockSignals(true);
0248 
0249     key->setData(Qt::UserRole, QVariant::fromValue(entry));
0250 
0251     _ui->keyBindingTable->blockSignals(false);
0252 }
0253 
0254 void KeyBindingEditor::setupKeyBindingTable(const KeyboardTranslator *translator)
0255 {
0256     disconnect(_ui->keyBindingTable, &QTableWidget::itemChanged, this, &Konsole::KeyBindingEditor::bindingTableItemChanged);
0257 
0258     QList<KeyboardTranslator::Entry> entries = translator->entries();
0259     _ui->keyBindingTable->setRowCount(entries.count());
0260 
0261     for (int row = 0; row < entries.count(); row++) {
0262         const KeyboardTranslator::Entry &entry = entries.at(row);
0263 
0264         QTableWidgetItem *keyItem = new QTableWidgetItem(entry.conditionToString());
0265         keyItem->setData(Qt::UserRole, QVariant::fromValue(entry));
0266 
0267         QTableWidgetItem *textItem = new QTableWidgetItem(entry.resultToString());
0268 
0269         _ui->keyBindingTable->setItem(row, 0, keyItem);
0270         _ui->keyBindingTable->setItem(row, 1, textItem);
0271     }
0272     _ui->keyBindingTable->sortItems(0);
0273 
0274     connect(_ui->keyBindingTable, &QTableWidget::itemChanged, this, &Konsole::KeyBindingEditor::bindingTableItemChanged);
0275 }
0276 
0277 void KeyBindingEditor::accept()
0278 {
0279     if (_translator == nullptr) {
0280         return;
0281     }
0282 
0283     const auto newTranslator = new KeyboardTranslator(*_translator);
0284 
0285     if (newTranslator->description().isEmpty()) {
0286         KMessageBox::error(this, i18n("A key bindings scheme cannot be saved with an empty description."));
0287         delete newTranslator;
0288         return;
0289     }
0290 
0291     if (_isNewTranslator) {
0292         newTranslator->setName(newTranslator->description());
0293     }
0294 
0295     KeyboardTranslatorManager::instance()->addTranslator(newTranslator);
0296 
0297     const QString &currentTranslatorName = newTranslator->name();
0298 
0299     Q_EMIT updateKeyBindingsListRequest(currentTranslatorName);
0300 
0301     if (currentTranslatorName == _currentProfileTranslator) {
0302         Q_EMIT updateTempProfileKeyBindingsRequest(Profile::KeyBindings, currentTranslatorName);
0303     }
0304 
0305     QDialog::accept();
0306 }
0307 
0308 QSize KeyBindingEditor::sizeHint() const
0309 {
0310     const auto parent = parentWidget();
0311     if (parent != nullptr) {
0312         return {static_cast<int>(parent->width() * 0.9), static_cast<int>(parent->height() * 0.95)};
0313     }
0314 
0315     return {};
0316 }
0317 
0318 #include "moc_KeyBindingEditor.cpp"