File indexing completed on 2024-10-06 03:36:05

0001 /*
0002     SPDX-FileCopyrightText: 2006, 2007 Peter Hedlund <peter.hedlund@kdemail.net>
0003     SPDX-FileCopyrightText: 2007-2008 Frederik Gladhorn <frederik.gladhorn@kdemail.net>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 ///@file vocabularyview.cpp
0009 
0010 #include "vocabularyview.h"
0011 #include "vocabularyheaderview.h"
0012 
0013 #include "vocabularydelegate.h"
0014 #include "vocabularyfilter.h"
0015 #include "vocabularymimedata.h"
0016 #include "vocabularymodel.h"
0017 
0018 #include "documentsettings.h"
0019 #include "editor/editor.h"
0020 #include "prefs.h"
0021 #include "vocabularycolumnsdialog.h"
0022 
0023 #include <KEduVocExpression>
0024 #include <KEduVocTranslation>
0025 
0026 #include <QApplication>
0027 #include <QClipboard>
0028 #include <QHeaderView>
0029 #include <QPainter>
0030 #include <QResizeEvent>
0031 #include <QTimer>
0032 
0033 #include <KActionCollection>
0034 #include <KLocalizedString>
0035 #include <KMessageBox>
0036 #include <KNotification>
0037 #include <KToggleAction>
0038 #include <QAction>
0039 #include <QUrl>
0040 #include <languagesettings.h>
0041 #include <sonnet/backgroundchecker.h>
0042 
0043 using namespace Editor;
0044 
0045 VocabularyView::VocabularyView(EditorWindow *parent)
0046     : QTableView(parent)
0047 {
0048     installEventFilter(this);
0049 
0050     setHorizontalHeader(new VocabularyHeaderView(Qt::Horizontal, this));
0051 
0052     horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive);
0053     horizontalHeader()->setSectionsMovable(true);
0054     setEditTriggers(QAbstractItemView::AnyKeyPressed | QAbstractItemView::EditKeyPressed | QAbstractItemView::DoubleClicked);
0055 
0056     setSortingEnabled(true);
0057     setTabKeyNavigation(true);
0058 
0059     m_vocabularyDelegate = new VocabularyDelegate(this);
0060     setItemDelegate(m_vocabularyDelegate);
0061 
0062     setFrameStyle(QFrame::NoFrame);
0063     setAlternatingRowColors(true);
0064 
0065     // Enable context menus
0066     setContextMenuPolicy(Qt::ActionsContextMenu);
0067     horizontalHeader()->setContextMenuPolicy(Qt::ActionsContextMenu);
0068 
0069     setWordWrap(true);
0070     setDragEnabled(true);
0071 
0072     // smooth scrolling horizontally, otherwise it tries to jump from item to item.
0073     setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
0074 
0075     m_appendEntryAction = new QAction(this);
0076     parent->actionCollection()->addAction(QStringLiteral("edit_append"), m_appendEntryAction);
0077     parent->actionCollection()->setDefaultShortcut(m_appendEntryAction, QKeySequence(Qt::Key_Insert));
0078     m_appendEntryAction->setIcon(QIcon::fromTheme(QStringLiteral("list-add-card")));
0079     m_appendEntryAction->setText(i18n("&Add New Entry"));
0080     connect(m_appendEntryAction, &QAction::triggered, this, &VocabularyView::appendEntry);
0081     m_appendEntryAction->setShortcut(QKeySequence(Qt::Key_Insert));
0082     m_appendEntryAction->setWhatsThis(i18n("Append a new row to the vocabulary"));
0083     m_appendEntryAction->setToolTip(m_appendEntryAction->whatsThis());
0084     m_appendEntryAction->setStatusTip(m_appendEntryAction->whatsThis());
0085     addAction(m_appendEntryAction);
0086 
0087     m_deleteEntriesAction = new QAction(this);
0088     parent->actionCollection()->addAction(QStringLiteral("edit_remove_selected_area"), m_deleteEntriesAction);
0089     parent->actionCollection()->setDefaultShortcut(m_deleteEntriesAction, QKeySequence::Delete);
0090     m_deleteEntriesAction->setIcon(QIcon::fromTheme(QStringLiteral("list-remove-card")));
0091     m_deleteEntriesAction->setText(i18n("&Delete Entry"));
0092     connect(m_deleteEntriesAction, &QAction::triggered, this, &VocabularyView::deleteSelectedEntries);
0093     m_deleteEntriesAction->setShortcut(QKeySequence::Delete);
0094     m_deleteEntriesAction->setWhatsThis(i18n("Delete the selected rows"));
0095     m_deleteEntriesAction->setToolTip(m_deleteEntriesAction->whatsThis());
0096     m_deleteEntriesAction->setStatusTip(m_deleteEntriesAction->whatsThis());
0097     addAction(m_deleteEntriesAction);
0098 
0099     QAction *separator = new QAction(this);
0100     separator->setSeparator(true);
0101     addAction(separator);
0102 
0103     m_copyAction = KStandardAction::copy(this, SLOT(slotEditCopy()), parent->actionCollection());
0104     parent->actionCollection()->setDefaultShortcut(m_copyAction, QKeySequence::Copy);
0105     m_copyAction->setWhatsThis(i18n("Copy"));
0106     m_copyAction->setToolTip(m_copyAction->whatsThis());
0107     m_copyAction->setStatusTip(m_copyAction->whatsThis());
0108     addAction(m_copyAction);
0109 
0110     m_cutAction = KStandardAction::cut(this, SLOT(slotCutEntry()), parent->actionCollection());
0111     parent->actionCollection()->setDefaultShortcut(m_cutAction, QKeySequence::Cut);
0112     m_cutAction->setWhatsThis(i18n("Cut"));
0113     m_cutAction->setToolTip(m_cutAction->whatsThis());
0114     m_cutAction->setStatusTip(m_cutAction->whatsThis());
0115     addAction(m_cutAction);
0116 
0117     m_pasteAction = KStandardAction::paste(this, SLOT(slotEditPaste()), parent->actionCollection());
0118     parent->actionCollection()->setDefaultShortcut(m_pasteAction, QKeySequence::Paste);
0119     m_pasteAction->setWhatsThis(i18n("Paste"));
0120     m_pasteAction->setToolTip(m_pasteAction->whatsThis());
0121     m_pasteAction->setStatusTip(m_pasteAction->whatsThis());
0122     addAction(m_pasteAction);
0123 
0124     m_selectAllAction = KStandardAction::selectAll(this, SLOT(selectAll()), parent->actionCollection());
0125     parent->actionCollection()->setDefaultShortcut(m_selectAllAction, QKeySequence::SelectAll);
0126     m_selectAllAction->setWhatsThis(i18n("Select all rows"));
0127     m_selectAllAction->setToolTip(m_selectAllAction->whatsThis());
0128     m_selectAllAction->setStatusTip(m_selectAllAction->whatsThis());
0129 
0130     m_clearSelectionAction = KStandardAction::deselect(this, SLOT(clearSelection()), parent->actionCollection());
0131     parent->actionCollection()->setDefaultShortcut(m_clearSelectionAction, QKeySequence::Deselect);
0132     m_clearSelectionAction->setWhatsThis(i18n("Deselect all rows"));
0133     m_clearSelectionAction->setToolTip(m_clearSelectionAction->whatsThis());
0134     m_clearSelectionAction->setStatusTip(m_clearSelectionAction->whatsThis());
0135 
0136     // vocabulary columns dialog
0137     QAction *vocabularyColumnsDialogAction = new QAction(this);
0138     parent->actionCollection()->addAction(QStringLiteral("show_vocabulary_columns_dialog"), vocabularyColumnsDialogAction);
0139     vocabularyColumnsDialogAction->setIcon(QIcon::fromTheme(QStringLiteral("view-file-columns")));
0140     vocabularyColumnsDialogAction->setText(i18n("Vocabulary Columns..."));
0141     vocabularyColumnsDialogAction->setWhatsThis(i18n("Toggle display of individual vocabulary columns"));
0142     vocabularyColumnsDialogAction->setToolTip(vocabularyColumnsDialogAction->whatsThis());
0143     vocabularyColumnsDialogAction->setStatusTip(vocabularyColumnsDialogAction->whatsThis());
0144     horizontalHeader()->addAction(vocabularyColumnsDialogAction);
0145     addAction(vocabularyColumnsDialogAction);
0146     connect(vocabularyColumnsDialogAction, &QAction::triggered, this, &VocabularyView::slotShowVocabularyColumnsDialog);
0147 }
0148 
0149 void VocabularyView::setFilter(VocabularyFilter *model)
0150 {
0151     QTableView::setModel(model);
0152 
0153     m_model = model;
0154     connect(selectionModel(), &QItemSelectionModel::currentChanged, this, &VocabularyView::slotCurrentChanged);
0155     connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &VocabularyView::slotSelectionChanged);
0156     slotSelectionChanged(QItemSelection(), QItemSelection());
0157 }
0158 
0159 void VocabularyView::slotCurrentChanged(const QModelIndex &current, const QModelIndex &previous)
0160 {
0161     Q_UNUSED(previous);
0162     KEduVocExpression *entry = nullptr;
0163     if (current.isValid()) {
0164         entry = model()->data(current, VocabularyModel::EntryRole).value<KEduVocExpression *>();
0165     }
0166     Q_EMIT translationChanged(entry, VocabularyModel::translation(current.column()));
0167 }
0168 
0169 void VocabularyView::reset()
0170 {
0171     QTableView::reset();
0172     Q_EMIT translationChanged(nullptr, 0);
0173 
0174     QList<int> visibleColumns;
0175     if (m_doc) {
0176         DocumentSettings ds(m_doc->url().url());
0177         ds.load();
0178         visibleColumns = ds.visibleColumns();
0179 
0180         KConfig parleyConfig(QStringLiteral("parleyrc"));
0181         KConfigGroup documentGroup(&parleyConfig, "Document " + m_doc->url().url());
0182         QByteArray state = documentGroup.readEntry("VocabularyColumns", QByteArray());
0183 
0184         if (!horizontalHeader()->restoreState(state)) {
0185             resizeColumnsToContents();
0186         }
0187     }
0188 
0189     horizontalHeader()->setSectionsMovable(true);
0190 
0191     for (int i = 0; i < model()->columnCount(QModelIndex()); i++) {
0192         if (i < visibleColumns.size()) {
0193             setColumnHidden(i, !visibleColumns.value(i));
0194         } else {
0195             if (VocabularyModel::columnType(i) != VocabularyModel::Translation) {
0196                 setColumnHidden(i, true);
0197             }
0198         }
0199     }
0200 }
0201 
0202 void VocabularyView::saveColumnVisibility() const
0203 {
0204     if (!m_doc) {
0205         return;
0206     }
0207 
0208     // Generate a QList<int> for saving
0209     QList<int> visibleList;
0210     for (int i = 0; i < m_model->columnCount(); ++i) {
0211         visibleList.append(static_cast<int>(!isColumnHidden(i)));
0212     }
0213 
0214     DocumentSettings ds(m_doc->url().url());
0215     ds.setVisibleColumns(visibleList);
0216     ds.save();
0217 
0218     horizontalHeader()->saveState();
0219     KConfig parleyConfig(QStringLiteral("parleyrc"));
0220     KConfigGroup documentGroup(&parleyConfig, "Document " + m_doc->url().url());
0221     documentGroup.writeEntry("VocabularyColumns", horizontalHeader()->saveState());
0222 }
0223 
0224 void VocabularyView::appendEntry()
0225 {
0226     QModelIndex newIndex = m_model->appendEntry();
0227     scrollTo(newIndex);
0228     selectionModel()->clear(); // otherwise proxy mapping gets screwed for some reason
0229     selectionModel()->select(newIndex, QItemSelectionModel::ClearAndSelect);
0230     selectionModel()->setCurrentIndex(newIndex, QItemSelectionModel::ClearAndSelect);
0231     edit(newIndex);
0232 }
0233 
0234 void VocabularyView::appendChar(const QChar &c)
0235 {
0236     const QModelIndex &index = selectionModel()->currentIndex();
0237     m_model->setData(index, QString(m_model->data(index).toString() + c));
0238 }
0239 
0240 void VocabularyView::deleteSelectedEntries(bool askConfirmation)
0241 {
0242     QSet<int> rows;
0243     const QModelIndexList selectedIndexes = selectionModel()->selectedIndexes();
0244     for (const QModelIndex &index : selectedIndexes) {
0245         rows.insert(index.row());
0246     }
0247 
0248     bool del = true;
0249     if (askConfirmation) {
0250         del = KMessageBox::Continue
0251             == KMessageBox::warningContinueCancel(
0252                   this,
0253                   i18np("Do you really want to delete the selected entry?", "Do you really want to delete the selected %1 entries?", rows.count()),
0254                   i18n("Delete"),
0255                   KStandardGuiItem::del());
0256     }
0257 
0258     if (del) {
0259         Q_EMIT translationChanged(nullptr, 0);
0260         while (!selectionModel()->selectedIndexes().isEmpty()) {
0261             m_model->removeRows(selectionModel()->selectedIndexes()[0].row(), 1, QModelIndex());
0262         }
0263     }
0264 }
0265 
0266 void VocabularyView::slotEditCopy()
0267 {
0268     QModelIndexList sortedIndexes = selectionModel()->selectedIndexes();
0269     std::sort(sortedIndexes.begin(), sortedIndexes.end());
0270     QMimeData *mimeData = m_model->mimeData(sortedIndexes);
0271 
0272     QClipboard *clipboard = QApplication::clipboard();
0273     clipboard->setMimeData(mimeData);
0274 }
0275 
0276 void VocabularyView::slotEditPaste()
0277 {
0278     QClipboard *clipboard = QApplication::clipboard();
0279     const QMimeData *mimeData = clipboard->mimeData();
0280     const VocabularyMimeData *vocMimeData = qobject_cast<const VocabularyMimeData *>(mimeData);
0281     if (vocMimeData) {
0282         qDebug() << "Clipboard contains vocabulary mime data.";
0283         QList<VocabularyMimeData::MimeExpression> expressionList = vocMimeData->expressionList();
0284         for (const VocabularyMimeData::MimeExpression &mimeEntry : qAsConst(expressionList)) {
0285             KEduVocExpression *pasteExpression = new KEduVocExpression(mimeEntry.expression);
0286             m_model->appendEntry(pasteExpression);
0287 
0288             // find word type (create if not found)
0289             KEduVocWordType *type = m_doc->wordTypeContainer();
0290             for (auto iter = mimeEntry.wordTypes.cbegin(); iter != mimeEntry.wordTypes.cend(); ++iter) {
0291                 const int translation = iter.key();
0292                 // append if needed
0293                 const QStringList wordType = mimeEntry.wordTypes.value(translation).wordType;
0294                 for (const QString &typeName : wordType) {
0295                     qDebug() << mimeEntry.wordTypes.value(translation).wordType;
0296                     KEduVocContainer *childType = type->childContainer(typeName);
0297                     if (!childType) {
0298                         // the doc does not contain the right word type - create it
0299                         childType = new KEduVocWordType(typeName);
0300                         type->appendChildContainer(childType);
0301                     }
0302                     type = static_cast<KEduVocWordType *>(childType);
0303                 }
0304                 pasteExpression->translation(translation)->setWordType(type);
0305                 // check for special type stuff
0306                 if (type->wordType() != mimeEntry.wordTypes.value(translation).grammarType) {
0307                     if (type->wordType() == KEduVocWordFlag::NoInformation) {
0308                         type->setWordType(mimeEntry.wordTypes.value(translation).grammarType);
0309                     }
0310                 }
0311             }
0312         }
0313     } else if (mimeData->hasText()) {
0314         qDebug() << "Clipboard contains text data.";
0315         // split at newline
0316         QStringList lines = clipboard->text().split('\n');
0317         for (const QString &line : qAsConst(lines)) {
0318             // split at tabs or semicolon:
0319             m_model->appendEntry(new KEduVocExpression(line.split(QRegularExpression(QStringLiteral("[\t;]")), Qt::KeepEmptyParts)));
0320         }
0321     }
0322 }
0323 
0324 void VocabularyView::slotCutEntry()
0325 {
0326     slotEditCopy();
0327     deleteSelectedEntries(false);
0328 }
0329 
0330 void VocabularyView::slotSelectionChanged(const QItemSelection &, const QItemSelection &)
0331 {
0332     bool hasSelection = selectionModel()->hasSelection();
0333     m_deleteEntriesAction->setEnabled(hasSelection);
0334     m_clearSelectionAction->setEnabled(hasSelection);
0335     m_copyAction->setEnabled(hasSelection);
0336     m_cutAction->setEnabled(hasSelection);
0337 }
0338 
0339 void VocabularyView::setDocument(const std::shared_ptr<KEduVocDocument> &doc)
0340 {
0341     m_doc = doc;
0342     m_vocabularyDelegate->setDocument(doc);
0343     QTimer::singleShot(0, this, SLOT(reset()));
0344 }
0345 
0346 void VocabularyView::slotShowVocabularyColumnsDialog()
0347 {
0348     VocabularyColumnsDialog *dialog = new VocabularyColumnsDialog(m_doc.get(), this);
0349 
0350     if (dialog->exec() == QDialog::Accepted) {
0351         reset();
0352     }
0353 }
0354 
0355 void VocabularyView::checkSpelling(int language)
0356 {
0357     if (!m_model->rowCount()) {
0358         KMessageBox::information(this, i18n("Nothing to spell check."));
0359         return;
0360     }
0361 
0362     if (!m_spellChecker) {
0363         m_spellChecker = new Sonnet::BackgroundChecker(this);
0364         m_spellDialog = new Sonnet::Dialog(m_spellChecker, this);
0365         connect(m_spellDialog, SIGNAL(done(QString)), this, SLOT(continueSpelling()));
0366         connect(m_spellDialog, &Sonnet::Dialog::misspelling, this, &VocabularyView::misspelling);
0367         connect(m_spellDialog, &Sonnet::Dialog::replace, this, &VocabularyView::spellingReplace);
0368     }
0369 
0370     m_spellColumn = language * VocabularyModel::EntryColumnsMAX;
0371     m_spellRow = -1;
0372     if (m_spellColumn < 0) {
0373         return;
0374     }
0375 
0376     QString locale = m_doc->identifier(language).locale();
0377     LanguageSettings settings(locale);
0378     QString spellCode = settings.spellChecker().isEmpty() ? locale : settings.spellChecker();
0379     m_spellChecker->changeLanguage(spellCode);
0380     if (!m_spellChecker->speller().isValid()) {
0381         qDebug() << "Invalid Language, popup here!";
0382         KNotification::event(KNotification::Warning,
0383                              i18nc("@title of a popup", "No Spell Checker Available"),
0384                              i18nc("@popupmessage", "Either the language set up is incorrect or no spellchecker was installed for this locale: %1.", locale));
0385     }
0386     m_spellDialog->show();
0387     continueSpelling();
0388 }
0389 
0390 void VocabularyView::continueSpelling()
0391 {
0392     qDebug() << "Check spelling: " << m_spellRow << m_spellColumn;
0393     ++m_spellRow;
0394     while (m_spellRow < m_model->rowCount()) {
0395         QModelIndex index = m_model->index(m_spellRow, m_spellColumn);
0396         qDebug() << "  " << m_model->data(index).toString();
0397         if (!m_model->data(index).toString().isEmpty()) {
0398             m_spellDialog->setBuffer(m_model->data(index).toString());
0399             break;
0400         } else {
0401             ++m_spellRow;
0402         }
0403     }
0404 }
0405 
0406 void VocabularyView::selectIndex(const QModelIndex &newIndex)
0407 {
0408     selectionModel()->select(newIndex, QItemSelectionModel::ClearAndSelect);
0409     selectionModel()->setCurrentIndex(newIndex, QItemSelectionModel::ClearAndSelect);
0410     scrollTo(newIndex);
0411 }
0412 
0413 bool VocabularyView::eventFilter(QObject *obj, QEvent *event)
0414 {
0415     if (event->type() == QEvent::KeyPress && Prefs::smartAppend()) {
0416         QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
0417         if (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter) {
0418             if (selectionModel()->currentIndex().row() == m_model->rowCount() - 1) {
0419                 appendEntry();
0420             }
0421         }
0422     }
0423     // standard event processing
0424     return QObject::eventFilter(obj, event);
0425 }
0426 
0427 void VocabularyView::misspelling(const QString &word, int start)
0428 {
0429     Q_UNUSED(word)
0430     Q_UNUSED(start)
0431     QModelIndex index = m_model->index(m_spellRow, m_spellColumn);
0432     selectIndex(index);
0433 }
0434 
0435 void VocabularyView::spellingReplace(const QString &oldWord, int start, const QString &newWord)
0436 {
0437     qDebug() << oldWord << start << newWord;
0438     QModelIndex index = m_model->index(m_spellRow, m_spellColumn);
0439     QString data = index.data().toString();
0440     QString newData = data.replace(start, oldWord.length(), newWord);
0441     qDebug() << "Changing " << data << " to " << newData;
0442     m_model->setData(index, newData);
0443 }
0444 
0445 QModelIndexList VocabularyView::getSelectedIndexes() const
0446 {
0447     return selectionModel()->selectedIndexes();
0448 }
0449 
0450 #include "moc_vocabularyview.cpp"