File indexing completed on 2024-05-19 05:17:46
0001 /* 0002 SPDX-FileCopyrightText: 2019 Volker Krause <vkrause@kde.org> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "extractoreditorwidget.h" 0008 #include "ui_extractoreditorwidget.h" 0009 0010 #include "metaenumcombobox.h" 0011 0012 #include <KItinerary/ExtractorFilter> 0013 #include <KItinerary/ExtractorRepository> 0014 #include <KItinerary/ScriptExtractor> 0015 0016 #include <KTextEditor/Document> 0017 #include <KTextEditor/Editor> 0018 #include <KTextEditor/View> 0019 0020 #include <KActionCollection> 0021 #include <KLocalizedString> 0022 0023 #include <QAbstractTableModel> 0024 #include <QDebug> 0025 #include <QFile> 0026 #include <QFileInfo> 0027 #include <QFileDialog> 0028 #include <QItemEditorFactory> 0029 #include <QJsonArray> 0030 #include <QJsonDocument> 0031 #include <QJsonObject> 0032 #include <QMessageBox> 0033 #include <QMetaEnum> 0034 #include <QSettings> 0035 #include <QStandardPaths> 0036 #include <QStyledItemDelegate> 0037 0038 using namespace KItinerary; 0039 0040 class ExtractorFilterModel : public QAbstractTableModel 0041 { 0042 Q_OBJECT 0043 public: 0044 explicit ExtractorFilterModel(QObject *parent = nullptr); 0045 ~ExtractorFilterModel() = default; 0046 0047 const std::vector<ExtractorFilter>& filters() const; 0048 void setFilters(std::vector<ExtractorFilter> filters); 0049 void addFilter(); 0050 void removeFilter(int row); 0051 0052 bool isReadOnly() const; 0053 void setReadOnly(bool ro); 0054 0055 int columnCount(const QModelIndex &parent = {}) const override; 0056 int rowCount(const QModelIndex &parent = {}) const override; 0057 Qt::ItemFlags flags(const QModelIndex &index) const override; 0058 QVariant data(const QModelIndex &index, int role) const override; 0059 bool setData(const QModelIndex &index, const QVariant &value, int role) override; 0060 QVariant headerData(int section, Qt::Orientation orientation, int role) const override; 0061 0062 private: 0063 std::vector<ExtractorFilter> m_filters; 0064 bool m_readOnly = false; 0065 }; 0066 0067 ExtractorFilterModel::ExtractorFilterModel(QObject *parent) 0068 : QAbstractTableModel(parent) 0069 { 0070 } 0071 0072 const std::vector<ExtractorFilter>& ExtractorFilterModel::filters() const 0073 { 0074 return m_filters; 0075 } 0076 0077 void ExtractorFilterModel::setFilters(std::vector<ExtractorFilter> filters) 0078 { 0079 beginResetModel(); 0080 m_filters = std::move(filters); 0081 endResetModel(); 0082 } 0083 0084 void ExtractorFilterModel::addFilter() 0085 { 0086 beginInsertRows({}, m_filters.size(), m_filters.size()); 0087 ExtractorFilter f; 0088 f.setMimeType(QStringLiteral("text/plain")); 0089 f.setFieldName(i18n("<field>")); 0090 f.setPattern(i18n("<pattern>")); 0091 m_filters.push_back(f); 0092 endInsertRows(); 0093 } 0094 0095 void ExtractorFilterModel::removeFilter(int row) 0096 { 0097 beginRemoveRows({}, row, row); 0098 m_filters.erase(m_filters.begin() + row); 0099 endRemoveRows(); 0100 } 0101 0102 bool ExtractorFilterModel::isReadOnly() const 0103 { 0104 return m_readOnly; 0105 } 0106 0107 void ExtractorFilterModel::setReadOnly(bool ro) 0108 { 0109 m_readOnly = ro; 0110 if (!m_filters.empty()) { 0111 Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1)); 0112 } 0113 } 0114 0115 int ExtractorFilterModel::columnCount(const QModelIndex &parent) const 0116 { 0117 Q_UNUSED(parent); 0118 return 4; 0119 } 0120 0121 int ExtractorFilterModel::rowCount(const QModelIndex &parent) const 0122 { 0123 if (parent.isValid()) { 0124 return 0; 0125 } 0126 return m_filters.size(); 0127 } 0128 0129 Qt::ItemFlags ExtractorFilterModel::flags(const QModelIndex &index) const 0130 { 0131 if (m_readOnly) { 0132 return QAbstractTableModel::flags(index); 0133 } 0134 return QAbstractTableModel::flags(index) | Qt::ItemIsEditable; 0135 } 0136 0137 QVariant ExtractorFilterModel::data(const QModelIndex &index, int role) const 0138 { 0139 if (role == Qt::DisplayRole) { 0140 const auto &filter = m_filters[index.row()]; 0141 switch (index.column()) { 0142 case 0: return filter.mimeType(); 0143 case 1: return QString::fromUtf8(QMetaEnum::fromType<ExtractorFilter::Scope>().valueToKey(filter.scope())); 0144 case 2: return filter.fieldName(); 0145 case 3: return filter.pattern(); 0146 } 0147 } else if (role == Qt::EditRole) { 0148 const auto &filter = m_filters[index.row()]; 0149 switch (index.column()) { 0150 case 0: return filter.mimeType(); 0151 case 1: return QVariant::fromValue(filter.scope()); 0152 case 2: return filter.fieldName(); 0153 case 3: return filter.pattern(); 0154 } 0155 } 0156 return {}; 0157 } 0158 0159 bool ExtractorFilterModel::setData(const QModelIndex &index, const QVariant &value, int role) 0160 { 0161 if (!index.isValid() || role != Qt::EditRole) { 0162 return false; 0163 } 0164 0165 auto &filter = m_filters[index.row()]; 0166 switch (index.column()) { 0167 case 0: 0168 filter.setMimeType(value.toString()); 0169 break; 0170 case 1: 0171 filter.setScope(static_cast<ExtractorFilter::Scope>(value.toInt())); 0172 break; 0173 case 2: 0174 filter.setFieldName(value.toString()); 0175 break; 0176 case 3: 0177 filter.setPattern(value.toString()); 0178 break; 0179 } 0180 0181 Q_EMIT dataChanged(index, index); 0182 return true; 0183 } 0184 0185 QVariant ExtractorFilterModel::headerData(int section, Qt::Orientation orientation, int role) const 0186 { 0187 if (role == Qt::DisplayRole && orientation == Qt::Horizontal) { 0188 switch (section) { 0189 case 0: return i18n("Type"); 0190 case 1: return i18n("Scope"); 0191 case 2: return i18n("Value"); 0192 case 3: return i18n("Pattern"); 0193 } 0194 } 0195 return QAbstractTableModel::headerData(section, orientation, role); 0196 } 0197 0198 0199 ExtractorEditorWidget::ExtractorEditorWidget(QWidget *parent) 0200 : QWidget(parent) 0201 , ui(new Ui::ExtractorEditorWidget) 0202 , m_filterModel(new ExtractorFilterModel(this)) 0203 { 0204 ui->setupUi(this); 0205 ui->inputType->addItems({ 0206 QStringLiteral("application/ld+json"), 0207 QStringLiteral("application/octet-stream"), 0208 QStringLiteral("application/pdf"), 0209 QStringLiteral("application/vnd.apple.pkpass"), 0210 QStringLiteral("internal/era-elb"), 0211 QStringLiteral("internal/era-ssb"), 0212 QStringLiteral("internal/event"), 0213 QStringLiteral("internal/uic9183"), 0214 QStringLiteral("internal/vdv"), 0215 QStringLiteral("message/rfc822"), 0216 QStringLiteral("text/calendar"), 0217 QStringLiteral("text/html"), 0218 QStringLiteral("text/plain") 0219 }); 0220 ui->filterView->setModel(m_filterModel); 0221 0222 QSettings settings; 0223 settings.beginGroup(QLatin1String("Extractor Repository")); 0224 ExtractorRepository repo; 0225 repo.setAdditionalSearchPaths(settings.value(QLatin1String("SearchPaths"), QStringList()).toStringList()); 0226 repo.reload(); 0227 0228 connect(ui->extractorCombobox, qOverload<int>(&QComboBox::currentIndexChanged), this, [this]() { 0229 ExtractorRepository repo; 0230 const auto extId = ui->extractorCombobox->currentText(); 0231 const auto extractor = dynamic_cast<const ScriptExtractor*>(repo.extractorByName(extId)); 0232 if (!extractor) { 0233 return; 0234 } 0235 ui->scriptEdit->setText(extractor->scriptFileName()); 0236 ui->functionEdit->setText(extractor->scriptFunction()); 0237 ui->inputType->setCurrentIndex(ui->inputType->findText(extractor->mimeType())); 0238 m_filterModel->setFilters(extractor->filters()); 0239 m_scriptDoc->openUrl(QUrl::fromLocalFile(extractor->scriptFileName())); 0240 0241 QFileInfo scriptFi(extractor->fileName()); 0242 m_scriptDoc->setReadWrite(scriptFi.isWritable()); 0243 QFileInfo metaFi(extractor->fileName()); 0244 setMetaDataReadOnly(!metaFi.isWritable()); 0245 validateInput(); 0246 }); 0247 0248 auto editor = KTextEditor::Editor::instance(); 0249 m_scriptDoc = editor->createDocument(nullptr); 0250 m_scriptDoc->setHighlightingMode(QStringLiteral("JavaScript")); 0251 m_scriptView = m_scriptDoc->createView(nullptr); 0252 ui->topLayout->addWidget(m_scriptView); 0253 reloadExtractors(); 0254 0255 connect(m_scriptDoc, &KTextEditor::Document::modifiedChanged, this, [this]() { 0256 if (!m_scriptDoc->isModified()) { // approximation for "document has been saved" 0257 Q_EMIT extractorChanged(); 0258 } 0259 }); 0260 0261 connect(ui->addFilterButton, &QToolButton::clicked, m_filterModel, &ExtractorFilterModel::addFilter); 0262 connect(ui->removeFilterButton, &QToolButton::clicked, this, [this]() { 0263 const auto sel = ui->filterView->selectionModel()->selection(); 0264 if (sel.isEmpty()) { 0265 return; 0266 } 0267 m_filterModel->removeFilter(sel.first().topLeft().row()); 0268 validateInput(); 0269 }); 0270 0271 connect(ui->scriptEdit, &QLineEdit::textChanged, this, &ExtractorEditorWidget::validateInput); 0272 connect(ui->functionEdit, &QLineEdit::textChanged, this, &ExtractorEditorWidget::validateInput); 0273 connect(m_filterModel, &ExtractorFilterModel::rowsInserted, this, &ExtractorEditorWidget::validateInput); 0274 connect(m_filterModel, &ExtractorFilterModel::dataChanged, this, &ExtractorEditorWidget::validateInput); 0275 connect(ui->filterView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &ExtractorEditorWidget::validateInput); 0276 0277 auto factory = new QItemEditorFactory; 0278 factory->registerEditor(qMetaTypeId<ExtractorFilter::Scope>(), new QStandardItemEditorCreator<MetaEnumComboBox>()); 0279 qobject_cast<QStyledItemDelegate*>(ui->filterView->itemDelegate())->setItemEditorFactory(factory); 0280 } 0281 0282 ExtractorEditorWidget::~ExtractorEditorWidget() = default; 0283 0284 void ExtractorEditorWidget::registerActions(KActionCollection *ac) 0285 { 0286 connect(ui->actionFileNewExtractor, &QAction::triggered, this, &ExtractorEditorWidget::create); 0287 connect(ui->actionFileSaveExtractor, &QAction::triggered, this, &ExtractorEditorWidget::save); 0288 0289 ac->addAction(QStringLiteral("file_new_extractor"), ui->actionFileNewExtractor); 0290 ac->addAction(QStringLiteral("file_save_extractor"), ui->actionFileSaveExtractor); 0291 } 0292 0293 void ExtractorEditorWidget::reloadExtractors() 0294 { 0295 ui->extractorCombobox->clear(); 0296 ExtractorRepository repo; 0297 for (const auto &ext : repo.extractors()) { 0298 if (dynamic_cast<ScriptExtractor*>(ext.get())) { 0299 ui->extractorCombobox->addItem(ext->name()); 0300 } 0301 } 0302 } 0303 0304 void ExtractorEditorWidget::showExtractor(const QString &extractorId) 0305 { 0306 const auto idx = ui->extractorCombobox->findText(extractorId); 0307 if (idx >= 0 && idx != ui->extractorCombobox->currentIndex()) { 0308 ui->extractorCombobox->setCurrentIndex(idx); 0309 } 0310 } 0311 0312 void ExtractorEditorWidget::navigateToSource(const QString &fileName, int line) 0313 { 0314 // TODO find the extractor this file belongs to and select it? 0315 if (m_scriptDoc->url().toString() != fileName) { 0316 m_scriptDoc->openUrl(QUrl(fileName)); 0317 } 0318 m_scriptView->setCursorPosition(KTextEditor::Cursor(line - 1, 0)); 0319 } 0320 0321 void ExtractorEditorWidget::setMetaDataReadOnly(bool readOnly) 0322 { 0323 ui->inputType->setEnabled(!readOnly); 0324 ui->scriptEdit->setEnabled(!readOnly); 0325 ui->functionEdit->setEnabled(!readOnly); 0326 m_filterModel->setReadOnly(readOnly); 0327 ui->addFilterButton->setEnabled(!readOnly); 0328 ui->removeFilterButton->setEnabled(!readOnly); 0329 } 0330 0331 void ExtractorEditorWidget::save() 0332 { 0333 ExtractorRepository repo; 0334 const auto extId = ui->extractorCombobox->currentText(); 0335 auto extractor = const_cast<ScriptExtractor*>(dynamic_cast<const ScriptExtractor*>(repo.extractorByName(extId))); 0336 Q_ASSERT(extractor); 0337 0338 extractor->setMimeType(ui->inputType->currentText()); 0339 extractor->setScriptFileName(ui->scriptEdit->text()); 0340 extractor->setScriptFunction(ui->functionEdit->text()); 0341 extractor->setFilters(m_filterModel->filters()); 0342 0343 const auto val = repo.extractorToJson(extractor); 0344 0345 QFile f(extractor->fileName()); 0346 if (!f.open(QFile::WriteOnly)) { 0347 QMessageBox::critical(this, i18n("Saving Failed"), i18n("Failed to open file %1 for saving: %2", f.fileName(), f.errorString())); 0348 return; 0349 } 0350 f.write((val.isArray() ? QJsonDocument(val.toArray()) : QJsonDocument(val.toObject())).toJson()); 0351 f.close(); 0352 m_scriptDoc->save(); 0353 0354 repo.reload(); 0355 } 0356 0357 void ExtractorEditorWidget::create() 0358 { 0359 ExtractorRepository repo; 0360 QString startDir; 0361 if (!repo.additionalSearchPaths().empty()) { 0362 startDir = repo.additionalSearchPaths().at(0); 0363 } else { 0364 startDir = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kitinerary/extractors"), QStandardPaths::LocateDirectory); 0365 } 0366 0367 const auto metaFileName = QFileDialog::getSaveFileName(this, i18n("Create New Extractor"), startDir, i18n("JSON (*.json)"), nullptr, QFileDialog::DontConfirmOverwrite); 0368 if (metaFileName.isEmpty()) { 0369 return; 0370 } 0371 0372 QFileInfo metaFi(metaFileName); 0373 const QString scriptFileName = metaFi.path() + QLatin1Char('/') + metaFi.baseName() + QLatin1String(".js"); 0374 QFile scriptFile(scriptFileName); 0375 if (!scriptFile.open(QFile::WriteOnly | QFile::Append)) { 0376 QMessageBox::critical(this, i18n("Creation Failed"), i18n("Failed to create file %1: %2", scriptFile.fileName(), scriptFile.errorString())); 0377 return; 0378 } 0379 scriptFile.write(R"( 0380 function main(content) { 0381 console.log(content); 0382 } 0383 )"); 0384 scriptFile.close(); 0385 0386 ScriptExtractor extractor; 0387 extractor.load({}, metaFileName, std::numeric_limits<int>::max()); // use a certainly unused index, so this doesn't clash with existing ones in a multi-extractor file 0388 extractor.setScriptFileName(scriptFileName); 0389 extractor.setMimeType(QStringLiteral("text/plain")); 0390 ExtractorFilter filter; 0391 filter.setMimeType(QStringLiteral("message/rfc822")); 0392 filter.setFieldName(QStringLiteral("From")); 0393 filter.setPattern(QStringLiteral("@change-me.com")); 0394 extractor.setFilters({filter}); 0395 QFile metaFile(metaFileName); 0396 if (!metaFile.open(QFile::WriteOnly)) { 0397 QMessageBox::critical(this, i18n("Creation Failed"), i18n("Failed to create file %1: %2", metaFile.fileName(), metaFile.errorString())); 0398 return; 0399 } 0400 const auto json = repo.extractorToJson(&extractor); 0401 metaFile.write((json.isArray() ? QJsonDocument(json.toArray()) : QJsonDocument(json.toObject())).toJson()); 0402 metaFile.close(); 0403 0404 repo.reload(); 0405 reloadExtractors(); 0406 showExtractor(metaFi.baseName()); 0407 } 0408 0409 void ExtractorEditorWidget::validateInput() 0410 { 0411 bool valid = !ui->scriptEdit->text().isEmpty() && !ui->functionEdit->text().isEmpty(); 0412 ui->actionFileSaveExtractor->setEnabled(valid); 0413 ui->removeFilterButton->setEnabled(m_filterModel->rowCount() > 1 && !m_filterModel->isReadOnly()); 0414 } 0415 0416 #include "extractoreditorwidget.moc"