diff --git a/src/gui/element/findpdfui.cpp b/src/gui/element/findpdfui.cpp index 0a7d825d..194b9dbd 100644 --- a/src/gui/element/findpdfui.cpp +++ b/src/gui/element/findpdfui.cpp @@ -1,535 +1,543 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, see . * ***************************************************************************/ #include "findpdfui.h" #include "findpdfui_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "field/fieldlistedit.h" #include "logging_gui.h" class PDFListModel; const int posLabelUrl = 0; const int posLabelPreview = 1; const int posViewButton = 2; const int posRadioNoDownload = 3; const int posRadioDownload = 4; const int posRadioURLonly = 5; /// inspired by KNewStuff3's ItemsViewDelegate PDFItemDelegate::PDFItemDelegate(QListView *itemView, QObject *parent) : KWidgetItemDelegate(itemView, parent), m_parent(itemView) { /// nothing } void PDFItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { QStyle *style = QApplication::style(); style->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, painter, nullptr); painter->save(); if (option.state & QStyle::State_Selected) { painter->setPen(QPen(option.palette.highlightedText().color())); } else { painter->setPen(QPen(option.palette.text().color())); } /// draw icon based on mime-type QPixmap icon = index.data(Qt::DecorationRole).value(); if (!icon.isNull()) { int margin = option.fontMetrics.height() / 3; painter->drawPixmap(margin, margin + option.rect.top(), KIconLoader::SizeMedium, KIconLoader::SizeMedium, icon); } painter->restore(); } QList PDFItemDelegate::createItemWidgets(const QModelIndex &index) const { Q_UNUSED(index) // FIXME really of no use? QList list; /// first, the label with shows the found PDF file's origin (URL) KSqueezedTextLabel *label = new KSqueezedTextLabel(); label->setBackgroundRole(QPalette::NoRole); label->setAlignment(Qt::AlignTop | Qt::AlignLeft); list << label; Q_ASSERT_X(list.count() == posLabelUrl + 1, "QList PDFItemDelegate::createItemWidgets() const", "list.count() != posLabelUrl + 1"); /// a label with shows either the PDF's title or a text snipplet QLabel *previewLabel = new QLabel(); previewLabel->setBackgroundRole(QPalette::NoRole); previewLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft); list << previewLabel; Q_ASSERT_X(list.count() == posLabelPreview + 1, "QList PDFItemDelegate::createItemWidgets() const", "list.count() != posLabelPreview + 1"); /// add a push button to view the PDF file QPushButton *pushButton = new QPushButton(QIcon::fromTheme(QStringLiteral("application-pdf")), i18n("View")); list << pushButton; connect(pushButton, &QPushButton::clicked, this, &PDFItemDelegate::slotViewPDF); Q_ASSERT_X(list.count() == posViewButton + 1, "QList PDFItemDelegate::createItemWidgets() const", "list.count() != posViewButton + 1"); /// a button group to choose what to do with this particular PDF file QButtonGroup *bg = new QButtonGroup(); /// button group's first choice: ignore file (discard it) QRadioButton *radioButton = new QRadioButton(i18n("Ignore")); bg->addButton(radioButton); list << radioButton; connect(radioButton, &QRadioButton::toggled, this, &PDFItemDelegate::slotRadioNoDownloadToggled); Q_ASSERT_X(list.count() == posRadioNoDownload + 1, "QList PDFItemDelegate::createItemWidgets() const", "list.count() != posRadioNoDownload + 1"); /// download this file and store it locally, user will be asked for "Save As" radioButton = new QRadioButton(i18n("Download")); bg->addButton(radioButton); list << radioButton; connect(radioButton, &QRadioButton::toggled, this, &PDFItemDelegate::slotRadioDownloadToggled); Q_ASSERT_X(list.count() == posRadioDownload + 1, "QList PDFItemDelegate::createItemWidgets() const", "list.count() != posRadioDownload + 1"); /// paste URL into BibTeX entry, no local copy is stored radioButton = new QRadioButton(i18n("Use URL only")); bg->addButton(radioButton); list << radioButton; connect(radioButton, &QRadioButton::toggled, this, &PDFItemDelegate::slotRadioURLonlyToggled); Q_ASSERT_X(list.count() == posRadioURLonly + 1, "QList PDFItemDelegate::createItemWidgets() const", "list.count() != posRadioURLonly + 1"); return list; } /// Update the widgets /// Clazy warns: "Missing reference on non-trivial type" for argument 'widgets', /// but KWidgetItemDelegate defines this function this way and cannot be changed. void PDFItemDelegate::updateItemWidgets(const QList widgets, const QStyleOptionViewItem &option, const QPersistentModelIndex &index) const { if (!index.isValid()) return; const PDFListModel *model = qobject_cast(index.model()); if (model == nullptr) { qCDebug(LOG_KBIBTEX_GUI) << "WARNING - INVALID MODEL!"; return; } /// determine some variables used for layout const int margin = option.fontMetrics.height() / 3; const int buttonHeight = option.fontMetrics.height() * 6 / 3; +#if QT_VERSION >= 0x050b00 + const int maxTextWidth = qMax(qMax(option.fontMetrics.horizontalAdvance(i18n("Use URL only")), option.fontMetrics.horizontalAdvance(i18n("Ignore"))), qMax(option.fontMetrics.horizontalAdvance(i18n("Download")), option.fontMetrics.horizontalAdvance(i18n("View")))); +#else // QT_VERSION >= 0x050b00 const int maxTextWidth = qMax(qMax(option.fontMetrics.width(i18n("Use URL only")), option.fontMetrics.width(i18n("Ignore"))), qMax(option.fontMetrics.width(i18n("Download")), option.fontMetrics.width(i18n("View")))); +#endif // QT_VERSION >= 0x050b00 const int buttonWidth = maxTextWidth * 3 / 2; const int labelWidth = option.rect.width() - 3 * margin - KIconLoader::SizeMedium; const int labelHeight = option.fontMetrics.height();//(option.rect.height() - 4 * margin - buttonHeight) / 2; /// Total height = margin + labelHeight + margin + labelHeight + marin + buttonHeight + margin /// = option.fontMetrics.height() * (1/3 + 1 + 1/3 + 1 + 1/3 + 6/3 + 1/3) /// = option.fontMetrics.height() * 16 / 3 /// setup label which will show the PDF file's URL KSqueezedTextLabel *label = qobject_cast(widgets[posLabelUrl]); if (label != nullptr) { const QString text = index.data(PDFListModel::URLRole).toUrl().toDisplayString(); label->setText(text); label->setToolTip(text); label->move(margin * 2 + KIconLoader::SizeMedium, margin); label->resize(labelWidth, labelHeight); } /// setup label which will show the PDF's title or textual beginning QLabel *previewLabel = qobject_cast(widgets[posLabelPreview]); if (previewLabel != nullptr) { previewLabel->setText(index.data(PDFListModel::TextualPreviewRole).toString()); previewLabel->move(margin * 2 + KIconLoader::SizeMedium, margin * 2 + labelHeight); previewLabel->resize(labelWidth, labelHeight); } /// setup the view button QPushButton *viewButton = qobject_cast(widgets[posViewButton]); if (viewButton != nullptr) { const QSize hint = viewButton->sizeHint(); const int h = hint.isValid() ? qMin(buttonHeight, hint.height()) : buttonHeight; viewButton->move(margin * 2 + KIconLoader::SizeMedium, option.rect.height() - margin - h); viewButton->resize(buttonWidth, h); } /// setup each of the three radio buttons for (int i = 0; i < 3; ++i) { QRadioButton *radioButton = qobject_cast(widgets[posRadioNoDownload + i]); if (radioButton != nullptr) { const QSize hint = radioButton->sizeHint(); const int h = hint.isValid() ? qMin(buttonHeight, hint.height()) : buttonHeight; radioButton->move(option.rect.width() - margin - (3 - i) * (buttonWidth + margin), option.rect.height() - margin - h); radioButton->resize(buttonWidth, h); bool ok = false; radioButton->setChecked(i + FindPDF::NoDownload == index.data(PDFListModel::DownloadModeRole).toInt(&ok) && ok); } } } QSize PDFItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &) const { /// set a size that is suiteable QSize size; +#if QT_VERSION >= 0x050b00 + size.setWidth(option.fontMetrics.horizontalAdvance(i18n("Download")) * 6); +#else // QT_VERSION >= 0x050b00 size.setWidth(option.fontMetrics.width(i18n("Download")) * 6); +#endif // QT_VERSION >= 0x050b00 size.setHeight(qMax(option.fontMetrics.height() * 16 / 3, static_cast(KIconLoader::SizeMedium))); ///< KIconLoader::SizeMedium should be 32 return size; } /** * Method is called when the "View PDF" button of a list item is clicked. * Opens the associated URL or its local copy using the system's default viewer. */ void PDFItemDelegate::slotViewPDF() { QModelIndex index = focusedIndex(); if (index.isValid()) { const QString tempfileName = index.data(PDFListModel::TempFileNameRole).toString(); const QUrl url = index.data(PDFListModel::URLRole).toUrl(); if (!tempfileName.isEmpty()) { /// Guess mime type for url to open QUrl tempUrl(tempfileName); QMimeType mimeType = FileInfo::mimeTypeForUrl(tempUrl); const QString mimeTypeName = mimeType.name(); /// Ask KDE subsystem to open url in viewer matching mime type #if KIO_VERSION < 0x051f00 // < 5.31.0 KRun::runUrl(tempUrl, mimeTypeName, itemView(), false, false, url.toDisplayString()); #else // KIO_VERSION < 0x051f00 // >= 5.31.0 KRun::runUrl(tempUrl, mimeTypeName, itemView(), KRun::RunFlags(), url.toDisplayString()); #endif // KIO_VERSION < 0x051f00 } else if (url.isValid()) { /// Guess mime type for url to open QMimeType mimeType = FileInfo::mimeTypeForUrl(url); const QString mimeTypeName = mimeType.name(); /// Ask KDE subsystem to open url in viewer matching mime type #if KIO_VERSION < 0x051f00 // < 5.31.0 KRun::runUrl(url, mimeTypeName, itemView(), false, false); #else // KIO_VERSION < 0x051f00 // >= 5.31.0 KRun::runUrl(url, mimeTypeName, itemView(), KRun::RunFlags()); #endif // KIO_VERSION < 0x051f00 } } } /** * Updated the model when the user selects the radio button for ignoring a PDF file. */ void PDFItemDelegate::slotRadioNoDownloadToggled(bool checked) { QModelIndex index = focusedIndex(); if (index.isValid() && checked) { m_parent->model()->setData(index, FindPDF::NoDownload, PDFListModel::DownloadModeRole); } } /** * Updated the model when the user selects the radio button for downloading a PDF file. */ void PDFItemDelegate::slotRadioDownloadToggled(bool checked) { QModelIndex index = focusedIndex(); if (index.isValid() && checked) { m_parent->model()->setData(index, FindPDF::Download, PDFListModel::DownloadModeRole); } } /** * Updated the model when the user selects the radio button for keeping a PDF file's URL. */ void PDFItemDelegate::slotRadioURLonlyToggled(bool checked) { QModelIndex index = focusedIndex(); if (index.isValid() && checked) { m_parent->model()->setData(index, FindPDF::URLonly, PDFListModel::DownloadModeRole); } } PDFListModel::PDFListModel(QList &resultList, QObject *parent) : QAbstractListModel(parent), m_resultList(resultList) { /// nothing } int PDFListModel::rowCount(const QModelIndex &parent) const { /// row cout depends on number of found PDF references int count = parent == QModelIndex() ? m_resultList.count() : 0; return count; } QVariant PDFListModel::data(const QModelIndex &index, int role) const { if (index != QModelIndex() && index.parent() == QModelIndex() && index.row() < m_resultList.count()) { if (role == Qt::DisplayRole) return m_resultList[index.row()].url.toDisplayString(); else if (role == URLRole) return m_resultList[index.row()].url; else if (role == TextualPreviewRole) return m_resultList[index.row()].textPreview; else if (role == Qt::ToolTipRole) return QStringLiteral("") + m_resultList[index.row()].textPreview + QStringLiteral(""); ///< 'qt' tags required for word wrap else if (role == TempFileNameRole) { if (m_resultList[index.row()].tempFilename != nullptr) return m_resultList[index.row()].tempFilename->fileName(); else return QVariant(); } else if (role == DownloadModeRole) return m_resultList[index.row()].downloadMode; else if (role == Qt::DecorationRole) { /// make an educated guess on the icon, based on URL or path QString iconName = FileInfo::mimeTypeForUrl(m_resultList[index.row()].url).iconName(); iconName = iconName == QStringLiteral("application-octet-stream") ? QStringLiteral("application-pdf") : iconName; return QIcon::fromTheme(iconName).pixmap(KIconLoader::SizeMedium, KIconLoader::SizeMedium); } else return QVariant(); } return QVariant(); } bool PDFListModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (index != QModelIndex() && index.row() < m_resultList.count() && role == DownloadModeRole) { bool ok = false; const FindPDF::DownloadMode downloadMode = static_cast(value.toInt(&ok)); if (ok) { m_resultList[index.row()].downloadMode = downloadMode; return true; } } return false; } QVariant PDFListModel::headerData(int section, Qt::Orientation orientation, int role) const { Q_UNUSED(orientation); if (section == 0) { if (role == Qt::DisplayRole || role == Qt::ToolTipRole) { return i18n("Result"); } else return QVariant(); } return QVariant(); } class FindPDFUI::Private { private: FindPDFUI *p; public: QListView *listViewResult; QLabel *labelMessage; QList resultList; FindPDF *findpdf; Private(FindPDFUI *parent) : p(parent), findpdf(new FindPDF(parent)) { setupGUI(); } void setupGUI() { QGridLayout *layout = new QGridLayout(p); const int minWidth = p->fontMetrics().height() * 40; const int minHeight = p->fontMetrics().height() * 20; p->setMinimumSize(minWidth, minHeight); listViewResult = new QListView(p); layout->addWidget(listViewResult, 0, 0); listViewResult->setEnabled(false); listViewResult->hide(); labelMessage = new QLabel(p); layout->addWidget(labelMessage, 1, 0); labelMessage->setMinimumSize(minWidth, minHeight); labelMessage->setAlignment(Qt::AlignVCenter | Qt::AlignCenter); static_cast(p->parent())->setCursor(Qt::WaitCursor); } }; FindPDFUI::FindPDFUI(Entry &entry, QWidget *parent) : QWidget(parent), d(new Private(this)) { d->labelMessage->show(); d->labelMessage->setText(i18n("Starting to search...")); connect(d->findpdf, &FindPDF::finished, this, &FindPDFUI::searchFinished); connect(d->findpdf, &FindPDF::progress, this, &FindPDFUI::searchProgress); d->findpdf->search(entry); } FindPDFUI::~FindPDFUI() { for (QList::Iterator it = d->resultList.begin(); it != d->resultList.end();) { delete it->tempFilename; it = d->resultList.erase(it); } } void FindPDFUI::interactiveFindPDF(Entry &entry, const File &bibtexFile, QWidget *parent) { QPointer dlg = new QDialog(parent); QPointer widget = new FindPDFUI(entry, dlg); dlg->setWindowTitle(i18n("Find PDF")); QBoxLayout *layout = new QVBoxLayout(dlg); layout->addWidget(widget); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Abort | QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, dlg); layout->addWidget(buttonBox); dlg->setLayout(layout); buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); connect(widget.data(), &FindPDFUI::resultAvailable, buttonBox->button(QDialogButtonBox::Ok), &QWidget::setEnabled); connect(widget.data(), &FindPDFUI::resultAvailable, buttonBox->button(QDialogButtonBox::Abort), &QWidget::setDisabled); connect(buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, dlg.data(), &QDialog::accept); connect(buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, dlg.data(), &QDialog::reject); connect(buttonBox->button(QDialogButtonBox::Abort), &QPushButton::clicked, widget.data(), &FindPDFUI::stopSearch); if (dlg->exec() == QDialog::Accepted) widget->apply(entry, bibtexFile); delete dlg; } void FindPDFUI::apply(Entry &entry, const File &bibtexFile) { QAbstractItemModel *model = d->listViewResult->model(); for (int i = 0; i < model->rowCount(); ++i) { bool ok = false; FindPDF::DownloadMode downloadMode = static_cast(model->data(model->index(i, 0), PDFListModel::DownloadModeRole).toInt(&ok)); if (!ok) { qCDebug(LOG_KBIBTEX_GUI) << "Could not interprete download mode"; downloadMode = FindPDF::NoDownload; } QUrl url = model->data(model->index(i, 0), PDFListModel::URLRole).toUrl(); QString tempfileName = model->data(model->index(i, 0), PDFListModel::TempFileNameRole).toString(); if (downloadMode == FindPDF::URLonly && url.isValid()) { bool alreadyContained = false; for (QMap::ConstIterator it = entry.constBegin(); !alreadyContained && it != entry.constEnd(); ++it) // FIXME this will terribly break if URLs in an entry's URL field are separated with semicolons alreadyContained |= it.key().toLower().startsWith(Entry::ftUrl) && PlainTextValue::text(it.value()) == url.toDisplayString(); if (!alreadyContained) { Value value; value.append(QSharedPointer(new VerbatimText(url.toDisplayString()))); if (!entry.contains(Entry::ftUrl)) entry.insert(Entry::ftUrl, value); else for (int i = 2; i < 256; ++i) { const QString keyName = QString(QStringLiteral("%1%2")).arg(Entry::ftUrl).arg(i); if (!entry.contains(keyName)) { entry.insert(keyName, value); break; } } } } else if (downloadMode == FindPDF::Download && !tempfileName.isEmpty()) { QUrl startUrl = bibtexFile.property(File::Url, QUrl()).toUrl(); const QString absoluteFilename = QFileDialog::getSaveFileName(this, i18n("Save URL '%1'", url.url(QUrl::PreferLocalFile)), startUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path(), QStringLiteral("application/pdf")); if (!absoluteFilename.isEmpty()) { const QString visibleFilename = UrlListEdit::askRelativeOrStaticFilename(this, absoluteFilename, startUrl); qCDebug(LOG_KBIBTEX_GUI) << "Saving PDF from " << url << " to file " << absoluteFilename << " known as " << visibleFilename; // FIXME test for overwrite QFile::copy(tempfileName, absoluteFilename); bool alreadyContained = false; for (QMap::ConstIterator it = entry.constBegin(); !alreadyContained && it != entry.constEnd(); ++it) alreadyContained |= (it.key().toLower().startsWith(Entry::ftFile) || it.key().toLower().startsWith(Entry::ftLocalFile) || it.key().toLower().startsWith(Entry::ftUrl)) && PlainTextValue::text(it.value()) == url.toDisplayString(); if (!alreadyContained) { Value value; value.append(QSharedPointer(new VerbatimText(visibleFilename))); const QString fieldNameStem = Preferences::instance().bibliographySystem() == Preferences::BibTeX ? Entry::ftLocalFile : Entry::ftFile; if (!entry.contains(fieldNameStem)) entry.insert(fieldNameStem, value); else for (int i = 2; i < 256; ++i) { const QString keyName = QString(QStringLiteral("%1%2")).arg(fieldNameStem).arg(i); if (!entry.contains(keyName)) { entry.insert(keyName, value); break; } } } } } } } void FindPDFUI::searchFinished() { d->labelMessage->hide(); d->listViewResult->show(); d->resultList = d->findpdf->results(); d->listViewResult->setModel(new PDFListModel(d->resultList, d->listViewResult)); d->listViewResult->setItemDelegate(new PDFItemDelegate(d->listViewResult, d->listViewResult)); d->listViewResult->setEnabled(true); d->listViewResult->reset(); static_cast(parent())->unsetCursor(); emit resultAvailable(true); } void FindPDFUI::searchProgress(int visitedPages, int runningJobs, int foundDocuments) { d->listViewResult->hide(); d->labelMessage->show(); d->labelMessage->setText(i18n("Number of visited pages: %1
Number of running downloads: %2
Number of found documents: %3
", visitedPages, runningJobs, foundDocuments)); } void FindPDFUI::stopSearch() { d->findpdf->abort(); searchFinished(); } void FindPDFUI::abort() { d->findpdf->abort(); } diff --git a/src/gui/preferences/settingsfileexporterwidget.cpp b/src/gui/preferences/settingsfileexporterwidget.cpp index c97ee105..d264a0a7 100644 --- a/src/gui/preferences/settingsfileexporterwidget.cpp +++ b/src/gui/preferences/settingsfileexporterwidget.cpp @@ -1,208 +1,212 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, see . * ***************************************************************************/ #include "settingsfileexporterwidget.h" #include #include #include #include #include #include #include #include #include #include /// required as KUrlRequester returns it #include #include #include #include "guihelper.h" #include "italictextitemmodel.h" #include "file/clipboard.h" class SettingsFileExporterWidget::SettingsFileExporterWidgetPrivate { private: SettingsFileExporterWidget *p; QComboBox *comboBoxCopyReferenceCmd; static const QString citeCmdToLabel; public: #ifdef QT_LSTAT QCheckBox *checkboxUseAutomaticLyXPipeDetection; #endif // QT_LSTAT QComboBox *comboBoxBackupScope; QSpinBox *spinboxNumberOfBackups; KUrlRequester *lineeditLyXPipePath; #ifdef QT_LSTAT QString lastUserInputLyXPipePath; #endif // QT_LSTAT SettingsFileExporterWidgetPrivate(SettingsFileExporterWidget *parent) : p(parent) { setupGUI(); } void loadState() { int row = GUIHelper::selectValue(comboBoxCopyReferenceCmd->model(), Preferences::instance().copyReferenceCommand(), ItalicTextItemModel::IdentifierRole); comboBoxCopyReferenceCmd->setCurrentIndex(row); const int index = qMax(0, comboBoxBackupScope->findData(static_cast(Preferences::instance().backupScope()))); comboBoxBackupScope->setCurrentIndex(index); spinboxNumberOfBackups->setValue(qMax(0, qMin(spinboxNumberOfBackups->maximum(), Preferences::instance().numberOfBackups()))); #ifndef QT_LSTAT lineeditLyXPipePath->setText(Preferences::instance().lyXPipePath()); #else // QT_LSTAT checkboxUseAutomaticLyXPipeDetection->setChecked(Preferences::instance().lyXUseAutomaticPipeDetection()); lastUserInputLyXPipePath = Preferences::instance().lyXPipePath(); p->automaticLyXDetectionToggled(checkboxUseAutomaticLyXPipeDetection->isChecked()); #endif // QT_LSTAT } void saveState() { Preferences::instance().setCopyReferenceCommand(comboBoxCopyReferenceCmd->itemData(comboBoxCopyReferenceCmd->currentIndex(), ItalicTextItemModel::IdentifierRole).toString()); Preferences::instance().setBackupScope(static_cast(comboBoxBackupScope->itemData(comboBoxBackupScope->currentIndex()).toInt())); Preferences::instance().setNumberOfBackups(spinboxNumberOfBackups->value()); #ifndef QT_LSTAT Preferences::instance().setLyXPipePath(lineeditLyXPipePath->text()); #else // QT_LSTAT Preferences::instance().setLyXUseAutomaticPipeDetection(checkboxUseAutomaticLyXPipeDetection->isChecked()); Preferences::instance().setLyXPipePath(checkboxUseAutomaticLyXPipeDetection->isChecked() ? lastUserInputLyXPipePath : lineeditLyXPipePath->text()); #endif // QT_LSTAT } void resetToDefaults() { int row = GUIHelper::selectValue(comboBoxCopyReferenceCmd->model(), QString(), Qt::UserRole); comboBoxCopyReferenceCmd->setCurrentIndex(row); const int index = qMax(0, comboBoxBackupScope->findData(Preferences::defaultBackupScope)); comboBoxBackupScope->setCurrentIndex(index); spinboxNumberOfBackups->setValue(qMax(0, qMin(spinboxNumberOfBackups->maximum(), Preferences::defaultNumberOfBackups))); #ifndef QT_LSTAT const QString pipe = Preferences::defaultLyXPipePath; #else // QT_LSTAT checkboxUseAutomaticLyXPipeDetection->setChecked(Preferences::defaultLyXUseAutomaticPipeDetection); QString pipe = LyX::guessLyXPipeLocation(); if (pipe.isEmpty()) pipe = Preferences::defaultLyXPipePath; #endif // QT_LSTAT lineeditLyXPipePath->setText(pipe); } void setupGUI() { QFormLayout *layout = new QFormLayout(p); comboBoxCopyReferenceCmd = new QComboBox(p); comboBoxCopyReferenceCmd->setObjectName(QStringLiteral("comboBoxCopyReferenceCmd")); layout->addRow(i18n("Command for 'Copy Reference':"), comboBoxCopyReferenceCmd); ItalicTextItemModel *itim = new ItalicTextItemModel(); itim->addItem(i18n("No command"), QString()); for (const QString &citeCommand : Preferences::availableCopyReferenceCommands) itim->addItem(citeCmdToLabel.arg(citeCommand), citeCommand); comboBoxCopyReferenceCmd->setModel(itim); connect(comboBoxCopyReferenceCmd, static_cast(&QComboBox::currentIndexChanged), p, &SettingsFileExporterWidget::changed); #ifdef QT_LSTAT checkboxUseAutomaticLyXPipeDetection = new QCheckBox(QString(), p); layout->addRow(i18n("Detect LyX pipe automatically:"), checkboxUseAutomaticLyXPipeDetection); connect(checkboxUseAutomaticLyXPipeDetection, &QCheckBox::toggled, p, &SettingsFileExporterWidget::changed); connect(checkboxUseAutomaticLyXPipeDetection, &QCheckBox::toggled, p, &SettingsFileExporterWidget::automaticLyXDetectionToggled); #endif // QT_LSTAT lineeditLyXPipePath = new KUrlRequester(p); layout->addRow(i18n("Manually specified LyX pipe:"), lineeditLyXPipePath); connect(qobject_cast(lineeditLyXPipePath->lineEdit()), &QLineEdit::textEdited, p, &SettingsFileExporterWidget::changed); +#if QT_VERSION >= 0x050b00 + lineeditLyXPipePath->setMinimumWidth(lineeditLyXPipePath->fontMetrics().horizontalAdvance(QChar('W')) * 20); +#else // QT_VERSION >= 0x050b00 lineeditLyXPipePath->setMinimumWidth(lineeditLyXPipePath->fontMetrics().width(QChar('W')) * 20); +#endif // QT_VERSION >= 0x050b00 lineeditLyXPipePath->setFilter(QStringLiteral("inode/fifo")); lineeditLyXPipePath->setMode(KFile::ExistingOnly | KFile::LocalOnly); comboBoxBackupScope = new QComboBox(p); for (const auto &pair : Preferences::availableBackupScopes) comboBoxBackupScope->addItem(pair.second, static_cast(pair.first)); layout->addRow(i18n("Backups when saving:"), comboBoxBackupScope); connect(comboBoxBackupScope, static_cast(&QComboBox::currentIndexChanged), p, &SettingsFileExporterWidget::changed); spinboxNumberOfBackups = new QSpinBox(p); spinboxNumberOfBackups->setMinimum(1); spinboxNumberOfBackups->setMaximum(16); layout->addRow(i18n("Number of Backups:"), spinboxNumberOfBackups); connect(spinboxNumberOfBackups, static_cast(&QSpinBox::valueChanged), p, &SettingsFileExporterWidget::changed); connect(comboBoxBackupScope, static_cast(&QComboBox::currentIndexChanged), p, &SettingsFileExporterWidget::updateGUI); } }; const QString SettingsFileExporterWidget::SettingsFileExporterWidgetPrivate::citeCmdToLabel = QStringLiteral("\\%1{") + QChar(0x2026) + QChar('}'); SettingsFileExporterWidget::SettingsFileExporterWidget(QWidget *parent) : SettingsAbstractWidget(parent), d(new SettingsFileExporterWidgetPrivate(this)) { d->loadState(); } SettingsFileExporterWidget::~SettingsFileExporterWidget() { delete d; } QString SettingsFileExporterWidget::label() const { return i18n("Saving and Exporting"); } QIcon SettingsFileExporterWidget::icon() const { return QIcon::fromTheme(QStringLiteral("document-save")); } void SettingsFileExporterWidget::loadState() { d->loadState(); } void SettingsFileExporterWidget::saveState() { d->saveState(); } void SettingsFileExporterWidget::resetToDefaults() { d->resetToDefaults(); } #ifdef QT_LSTAT void SettingsFileExporterWidget::automaticLyXDetectionToggled(bool isChecked) { d->lineeditLyXPipePath->setEnabled(!isChecked); if (isChecked) { d->lastUserInputLyXPipePath = d->lineeditLyXPipePath->text(); d->lineeditLyXPipePath->setText(LyX::guessLyXPipeLocation()); } else d->lineeditLyXPipePath->setText(d->lastUserInputLyXPipePath); } #endif // QT_LSTAT void SettingsFileExporterWidget::updateGUI() { d->spinboxNumberOfBackups->setEnabled(d->comboBoxBackupScope->itemData(d->comboBoxBackupScope->currentIndex()).toInt() != static_cast(Preferences::NoBackup)); } diff --git a/src/gui/preferences/settingsidsuggestionseditor.cpp b/src/gui/preferences/settingsidsuggestionseditor.cpp index 5906ab8f..504b4dea 100644 --- a/src/gui/preferences/settingsidsuggestionseditor.cpp +++ b/src/gui/preferences/settingsidsuggestionseditor.cpp @@ -1,928 +1,937 @@ /*************************************************************************** * Copyright (C) 2004-2018 by Thomas Fischer * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, see . * ***************************************************************************/ #include "settingsidsuggestionseditor.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "widgets/rangewidget.h" /** * @author Thomas Fischer */ class TokenWidget : public QGroupBox { Q_OBJECT protected: QGridLayout *gridLayout; QFormLayout *formLayout; public: explicit TokenWidget(QWidget *parent) : QGroupBox(parent) { gridLayout = new QGridLayout(this); formLayout = new QFormLayout(); gridLayout->addLayout(formLayout, 0, 0, 4, 1); gridLayout->setColumnStretch(0, 100); } void addButtons(QPushButton *buttonUp, QPushButton *buttonDown, QPushButton *buttonRemove) { gridLayout->setColumnMinimumWidth(1, 32); gridLayout->setColumnStretch(1, 1); gridLayout->setColumnStretch(2, 1); gridLayout->addWidget(buttonUp, 0, 2, 1, 1); buttonUp->setParent(this); gridLayout->addWidget(buttonDown, 1, 2, 1, 1); buttonDown->setParent(this); gridLayout->addWidget(buttonRemove, 2, 2, 1, 1); buttonRemove->setParent(this); } virtual QString toString() const = 0; }; /** * @author Thomas Fischer */ class AuthorWidget : public TokenWidget { Q_OBJECT private: RangeWidget *rangeWidgetAuthor; QCheckBox *checkBoxLastAuthor; QLabel *labelAuthorRange; QComboBox *comboBoxChangeCase; QLineEdit *lineEditTextInBetween; QSpinBox *spinBoxLength; private slots: void updateRangeLabel() { const int lower = rangeWidgetAuthor->lowerValue(); const int upper = rangeWidgetAuthor->upperValue(); const int max = rangeWidgetAuthor->maximum(); labelAuthorRange->setText(IdSuggestions::formatAuthorRange(lower, upper == max ? 0x00ffffff : upper, checkBoxLastAuthor->isChecked())); } public: AuthorWidget(const struct IdSuggestions::IdSuggestionTokenInfo &info, IdSuggestionsEditWidget *isew, QWidget *parent) : TokenWidget(parent) { setTitle(i18n("Authors")); QBoxLayout *boxLayout = new QVBoxLayout(); boxLayout->setMargin(0); formLayout->addRow(i18n("Author Range:"), boxLayout); static const QStringList authorRange {i18n("First author"), i18n("Second author"), i18n("Third author"), i18n("Fourth author"), i18n("Fifth author"), i18n("Sixth author"), i18n("Seventh author"), i18n("Eighth author"), i18n("Ninth author"), i18n("Tenth author"), i18n("|Last author")}; rangeWidgetAuthor = new RangeWidget(authorRange, this); boxLayout->addWidget(rangeWidgetAuthor); rangeWidgetAuthor->setLowerValue(info.startWord); rangeWidgetAuthor->setUpperValue(qMin(authorRange.size() - 1, info.endWord)); checkBoxLastAuthor = new QCheckBox(i18n("... and last author"), this); boxLayout->addWidget(checkBoxLastAuthor); labelAuthorRange = new QLabel(this); boxLayout->addWidget(labelAuthorRange); +#if QT_VERSION >= 0x050b00 + const int maxWidth = qMax(labelAuthorRange->fontMetrics().horizontalAdvance(i18n("From first author to author %1 and last author", 88)), labelAuthorRange->fontMetrics().horizontalAdvance(i18n("From author %1 to author %2 and last author", 88, 88))); +#else // QT_VERSION >= 0x050b00 const int maxWidth = qMax(labelAuthorRange->fontMetrics().width(i18n("From first author to author %1 and last author", 88)), labelAuthorRange->fontMetrics().width(i18n("From author %1 to author %2 and last author", 88, 88))); +#endif // QT_VERSION >= 0x050b00 labelAuthorRange->setMinimumWidth(maxWidth); comboBoxChangeCase = new QComboBox(this); comboBoxChangeCase->addItem(i18n("No change"), IdSuggestions::ccNoChange); comboBoxChangeCase->addItem(i18n("To upper case"), IdSuggestions::ccToUpper); comboBoxChangeCase->addItem(i18n("To lower case"), IdSuggestions::ccToLower); comboBoxChangeCase->addItem(i18n("To CamelCase"), IdSuggestions::ccToCamelCase); formLayout->addRow(i18n("Change casing:"), comboBoxChangeCase); comboBoxChangeCase->setCurrentIndex(static_cast(info.caseChange)); /// enum has numbers assigned to cases and combo box has same indices lineEditTextInBetween = new QLineEdit(this); formLayout->addRow(i18n("Text in between:"), lineEditTextInBetween); lineEditTextInBetween->setText(info.inBetween); spinBoxLength = new QSpinBox(this); formLayout->addRow(i18n("Only first characters:"), spinBoxLength); spinBoxLength->setSpecialValueText(i18n("No limitation")); spinBoxLength->setMinimum(0); spinBoxLength->setMaximum(9); spinBoxLength->setValue(info.len == 0 || info.len > 9 ? 0 : info.len); connect(rangeWidgetAuthor, &RangeWidget::lowerValueChanged, isew, &IdSuggestionsEditWidget::updatePreview); connect(rangeWidgetAuthor, &RangeWidget::upperValueChanged, isew, &IdSuggestionsEditWidget::updatePreview); connect(rangeWidgetAuthor, &RangeWidget::lowerValueChanged, this, &AuthorWidget::updateRangeLabel); connect(rangeWidgetAuthor, &RangeWidget::upperValueChanged, this, &AuthorWidget::updateRangeLabel); connect(checkBoxLastAuthor, &QCheckBox::toggled, isew, &IdSuggestionsEditWidget::updatePreview); connect(checkBoxLastAuthor, &QCheckBox::toggled, this, &AuthorWidget::updateRangeLabel); connect(comboBoxChangeCase, static_cast(&QComboBox::currentIndexChanged), isew, &IdSuggestionsEditWidget::updatePreview); connect(lineEditTextInBetween, &QLineEdit::textEdited, isew, &IdSuggestionsEditWidget::updatePreview); connect(spinBoxLength, static_cast(&QSpinBox::valueChanged), isew, &IdSuggestionsEditWidget::updatePreview); updateRangeLabel(); } QString toString() const override { QString result = QStringLiteral("A"); if (spinBoxLength->value() > 0) result.append(QString::number(spinBoxLength->value())); const IdSuggestions::CaseChange caseChange = static_cast(comboBoxChangeCase->currentIndex()); if (caseChange == IdSuggestions::ccToLower) result.append(QStringLiteral("l")); else if (caseChange == IdSuggestions::ccToUpper) result.append(QStringLiteral("u")); else if (caseChange == IdSuggestions::ccToCamelCase) result.append(QStringLiteral("c")); if (rangeWidgetAuthor->lowerValue() > 0 || rangeWidgetAuthor->upperValue() < rangeWidgetAuthor->maximum()) result.append(QString(QStringLiteral("w%1%2")).arg(rangeWidgetAuthor->lowerValue()).arg(rangeWidgetAuthor->upperValue() < rangeWidgetAuthor->maximum() ? QString::number(rangeWidgetAuthor->upperValue()) : QStringLiteral("I"))); if (checkBoxLastAuthor->isChecked()) result.append(QStringLiteral("L")); const QString text = lineEditTextInBetween->text(); if (!text.isEmpty()) result.append(QStringLiteral("\"")).append(text); return result; } }; /** * @author Thomas Fischer */ class YearWidget : public TokenWidget { Q_OBJECT private: QComboBox *comboBoxDigits; public: YearWidget(int digits, IdSuggestionsEditWidget *isew, QWidget *parent) : TokenWidget(parent) { setTitle(i18n("Year")); comboBoxDigits = new QComboBox(this); comboBoxDigits->addItem(i18n("2 digits"), 2); comboBoxDigits->addItem(i18n("4 digits"), 4); formLayout->addRow(i18n("Digits:"), comboBoxDigits); comboBoxDigits->setCurrentIndex(comboBoxDigits->findData(digits)); connect(comboBoxDigits, static_cast(&QComboBox::currentIndexChanged), isew, &IdSuggestionsEditWidget::updatePreview); } QString toString() const override { const int year = comboBoxDigits->itemData(comboBoxDigits->currentIndex()).toInt(); QString result = year == 4 ? QStringLiteral("Y") : QStringLiteral("y"); return result; } }; /** * @author Thomas Fischer */ class VolumeWidget : public TokenWidget { Q_OBJECT private: QLabel *labelCheckmark; public: VolumeWidget(IdSuggestionsEditWidget *, QWidget *parent) : TokenWidget(parent) { setTitle(i18n("Volume")); labelCheckmark = new QLabel(this); labelCheckmark->setPixmap(KIconLoader::global()->loadMimeTypeIcon(QStringLiteral("dialog-ok-apply"), KIconLoader::Small)); formLayout->addRow(i18n("Volume:"), labelCheckmark); } QString toString() const override { return QStringLiteral("v"); } }; /** * @author Thomas Fischer */ class PageNumberWidget : public TokenWidget { Q_OBJECT private: QLabel *labelCheckmark; public: PageNumberWidget(IdSuggestionsEditWidget *, QWidget *parent) : TokenWidget(parent) { setTitle(i18n("Page Number")); labelCheckmark = new QLabel(this); labelCheckmark->setPixmap(KIconLoader::global()->loadMimeTypeIcon(QStringLiteral("dialog-ok-apply"), KIconLoader::Small)); formLayout->addRow(i18n("First page's number:"), labelCheckmark); } QString toString() const override { return QStringLiteral("p"); } }; /** * @author Thomas Fischer */ class TitleWidget : public TokenWidget { Q_OBJECT private: RangeWidget *rangeWidgetAuthor; QLabel *labelWordsRange; QCheckBox *checkBoxRemoveSmallWords; QComboBox *comboBoxChangeCase; QLineEdit *lineEditTextInBetween; QSpinBox *spinBoxLength; private slots: void updateRangeLabel() { const int lower = rangeWidgetAuthor->lowerValue(); const int upper = rangeWidgetAuthor->upperValue(); const int max = rangeWidgetAuthor->maximum(); if (lower == 0 && upper == 0) labelWordsRange->setText(i18n("First word only")); else if (lower == 1 && upper == max) labelWordsRange->setText(i18n("All but first word")); else if (lower == 0 && upper == max) labelWordsRange->setText(i18n("From first to last word")); else if (lower > 0 && upper == max) labelWordsRange->setText(i18n("From word %1 to last word", lower + 1)); else if (lower == 0 && upper < max) labelWordsRange->setText(i18n("From first word to word %1", upper + 1)); else labelWordsRange->setText(i18n("From word %1 to word %2", lower + 1, upper + 1)); } public: TitleWidget(const struct IdSuggestions::IdSuggestionTokenInfo &info, bool removeSmallWords, IdSuggestionsEditWidget *isew, QWidget *parent) : TokenWidget(parent) { setTitle(i18n("Title")); QBoxLayout *boxLayout = new QVBoxLayout(); boxLayout->setMargin(0); formLayout->addRow(i18n("Word Range:"), boxLayout); static const QStringList wordRange {i18n("First word"), i18n("Second word"), i18n("Third word"), i18n("Fourth word"), i18n("Fifth word"), i18n("Sixth word"), i18n("Seventh word"), i18n("Eighth word"), i18n("Ninth word"), i18n("Tenth word"), i18n("|Last word")}; rangeWidgetAuthor = new RangeWidget(wordRange, this); boxLayout->addWidget(rangeWidgetAuthor); if (info.startWord > 0 || info.endWord < 0xffff) { rangeWidgetAuthor->setLowerValue(info.startWord); rangeWidgetAuthor->setUpperValue(qMin(rangeWidgetAuthor->maximum(), info.endWord)); } else { rangeWidgetAuthor->setLowerValue(0); rangeWidgetAuthor->setUpperValue(rangeWidgetAuthor->maximum()); } labelWordsRange = new QLabel(this); boxLayout->addWidget(labelWordsRange); +#if QT_VERSION >= 0x050b00 + const int a = qMax(labelWordsRange->fontMetrics().horizontalAdvance(i18n("From first to last word")), labelWordsRange->fontMetrics().horizontalAdvance(i18n("From word %1 to last word", 88))); + const int b = qMax(labelWordsRange->fontMetrics().horizontalAdvance(i18n("From first word to word %1", 88)), labelWordsRange->fontMetrics().horizontalAdvance(i18n("From word %1 to word %2", 88, 88))); +#else // QT_VERSION >= 0x050b00 const int a = qMax(labelWordsRange->fontMetrics().width(i18n("From first to last word")), labelWordsRange->fontMetrics().width(i18n("From word %1 to last word", 88))); const int b = qMax(labelWordsRange->fontMetrics().width(i18n("From first word to word %1", 88)), labelWordsRange->fontMetrics().width(i18n("From word %1 to word %2", 88, 88))); +#endif // QT_VERSION >= 0x050b00 labelWordsRange->setMinimumWidth(qMax(a, b)); checkBoxRemoveSmallWords = new QCheckBox(i18n("Remove"), this); formLayout->addRow(i18n("Small words:"), checkBoxRemoveSmallWords); checkBoxRemoveSmallWords->setChecked(removeSmallWords); comboBoxChangeCase = new QComboBox(this); comboBoxChangeCase->addItem(i18n("No change"), IdSuggestions::ccNoChange); comboBoxChangeCase->addItem(i18n("To upper case"), IdSuggestions::ccToUpper); comboBoxChangeCase->addItem(i18n("To lower case"), IdSuggestions::ccToLower); comboBoxChangeCase->addItem(i18n("To CamelCase"), IdSuggestions::ccToCamelCase); formLayout->addRow(i18n("Change casing:"), comboBoxChangeCase); comboBoxChangeCase->setCurrentIndex(static_cast(info.caseChange)); /// enum has numbers assigned to cases and combo box has same indices lineEditTextInBetween = new QLineEdit(this); formLayout->addRow(i18n("Text in between:"), lineEditTextInBetween); lineEditTextInBetween->setText(info.inBetween); spinBoxLength = new QSpinBox(this); formLayout->addRow(i18n("Only first characters:"), spinBoxLength); spinBoxLength->setSpecialValueText(i18n("No limitation")); spinBoxLength->setMinimum(0); spinBoxLength->setMaximum(9); spinBoxLength->setValue(info.len == 0 || info.len > 9 ? 0 : info.len); connect(rangeWidgetAuthor, &RangeWidget::lowerValueChanged, isew, &IdSuggestionsEditWidget::updatePreview); connect(rangeWidgetAuthor, &RangeWidget::upperValueChanged, isew, &IdSuggestionsEditWidget::updatePreview); connect(rangeWidgetAuthor, &RangeWidget::lowerValueChanged, this, &TitleWidget::updateRangeLabel); connect(rangeWidgetAuthor, &RangeWidget::upperValueChanged, this, &TitleWidget::updateRangeLabel); connect(checkBoxRemoveSmallWords, &QCheckBox::toggled, isew, &IdSuggestionsEditWidget::updatePreview); connect(comboBoxChangeCase, static_cast(&QComboBox::currentIndexChanged), isew, &IdSuggestionsEditWidget::updatePreview); connect(lineEditTextInBetween, &QLineEdit::textEdited, isew, &IdSuggestionsEditWidget::updatePreview); connect(spinBoxLength, static_cast(&QSpinBox::valueChanged), isew, &IdSuggestionsEditWidget::updatePreview); updateRangeLabel(); } QString toString() const override { QString result = checkBoxRemoveSmallWords->isChecked() ? QStringLiteral("T") : QStringLiteral("t"); if (spinBoxLength->value() > 0) result.append(QString::number(spinBoxLength->value())); const IdSuggestions::CaseChange caseChange = static_cast(comboBoxChangeCase->currentIndex()); if (caseChange == IdSuggestions::ccToLower) result.append(QStringLiteral("l")); else if (caseChange == IdSuggestions::ccToUpper) result.append(QStringLiteral("u")); else if (caseChange == IdSuggestions::ccToCamelCase) result.append(QStringLiteral("c")); if (rangeWidgetAuthor->lowerValue() > 0 || rangeWidgetAuthor->upperValue() < rangeWidgetAuthor->maximum()) result.append(QString(QStringLiteral("w%1%2")).arg(rangeWidgetAuthor->lowerValue()).arg(rangeWidgetAuthor->upperValue() < rangeWidgetAuthor->maximum() ? QString::number(rangeWidgetAuthor->upperValue()) : QStringLiteral("I"))); const QString text = lineEditTextInBetween->text(); if (!text.isEmpty()) result.append(QStringLiteral("\"")).append(text); return result; } }; /** * @author Thomas Fischer */ class JournalWidget : public TokenWidget { Q_OBJECT private: QCheckBox *checkBoxRemoveSmallWords; QComboBox *comboBoxChangeCase; QLineEdit *lineEditTextInBetween; QSpinBox *spinBoxLength; public: JournalWidget(const struct IdSuggestions::IdSuggestionTokenInfo &info, bool removeSmallWords, IdSuggestionsEditWidget *isew, QWidget *parent) : TokenWidget(parent) { setTitle(i18n("Journal")); QBoxLayout *boxLayout = new QVBoxLayout(); boxLayout->setMargin(0); checkBoxRemoveSmallWords = new QCheckBox(i18n("Remove"), this); formLayout->addRow(i18n("Small words:"), checkBoxRemoveSmallWords); checkBoxRemoveSmallWords->setChecked(removeSmallWords); comboBoxChangeCase = new QComboBox(this); comboBoxChangeCase->addItem(i18n("No change"), IdSuggestions::ccNoChange); comboBoxChangeCase->addItem(i18n("To upper case"), IdSuggestions::ccToUpper); comboBoxChangeCase->addItem(i18n("To lower case"), IdSuggestions::ccToLower); comboBoxChangeCase->addItem(i18n("To CamelCase"), IdSuggestions::ccToCamelCase); formLayout->addRow(i18n("Change casing:"), comboBoxChangeCase); comboBoxChangeCase->setCurrentIndex(static_cast(info.caseChange)); /// enum has numbers assigned to cases and combo box has same indices lineEditTextInBetween = new QLineEdit(this); formLayout->addRow(i18n("Text in between:"), lineEditTextInBetween); lineEditTextInBetween->setText(info.inBetween); spinBoxLength = new QSpinBox(this); formLayout->addRow(i18n("Only first characters:"), spinBoxLength); spinBoxLength->setSpecialValueText(i18n("No limitation")); spinBoxLength->setMinimum(0); spinBoxLength->setMaximum(9); spinBoxLength->setValue(info.len == 0 || info.len > 9 ? 0 : info.len); connect(checkBoxRemoveSmallWords, &QCheckBox::toggled, isew, &IdSuggestionsEditWidget::updatePreview); connect(comboBoxChangeCase, static_cast(&QComboBox::currentIndexChanged), isew, &IdSuggestionsEditWidget::updatePreview); connect(lineEditTextInBetween, &QLineEdit::textEdited, isew, &IdSuggestionsEditWidget::updatePreview); connect(spinBoxLength, static_cast(&QSpinBox::valueChanged), isew, &IdSuggestionsEditWidget::updatePreview); } QString toString() const override { QString result = checkBoxRemoveSmallWords->isChecked() ? QStringLiteral("J") : QStringLiteral("j"); if (spinBoxLength->value() > 0) result.append(QString::number(spinBoxLength->value())); const IdSuggestions::CaseChange caseChange = static_cast(comboBoxChangeCase->currentIndex()); if (caseChange == IdSuggestions::ccToLower) result.append(QStringLiteral("l")); else if (caseChange == IdSuggestions::ccToUpper) result.append(QStringLiteral("u")); else if (caseChange == IdSuggestions::ccToCamelCase) result.append(QStringLiteral("c")); const QString text = lineEditTextInBetween->text(); if (!text.isEmpty()) result.append(QStringLiteral("\"")).append(text); return result; } }; /** * @author Erik Quaeghebeur */ class TypeWidget : public TokenWidget { Q_OBJECT private: QComboBox *comboBoxChangeCase; QSpinBox *spinBoxLength; public: TypeWidget(const struct IdSuggestions::IdSuggestionTokenInfo &info, IdSuggestionsEditWidget *isew, QWidget *parent) : TokenWidget(parent) { setTitle(i18n("Type")); QBoxLayout *boxLayout = new QVBoxLayout(); boxLayout->setMargin(0); comboBoxChangeCase = new QComboBox(this); comboBoxChangeCase->addItem(i18n("No change"), IdSuggestions::ccNoChange); comboBoxChangeCase->addItem(i18n("To upper case"), IdSuggestions::ccToUpper); comboBoxChangeCase->addItem(i18n("To lower case"), IdSuggestions::ccToLower); comboBoxChangeCase->addItem(i18n("To CamelCase"), IdSuggestions::ccToCamelCase); formLayout->addRow(i18n("Change casing:"), comboBoxChangeCase); comboBoxChangeCase->setCurrentIndex(static_cast(info.caseChange)); /// enum has numbers assigned to cases and combo box has same indices spinBoxLength = new QSpinBox(this); formLayout->addRow(i18n("Only first characters:"), spinBoxLength); spinBoxLength->setSpecialValueText(i18n("No limitation")); spinBoxLength->setMinimum(0); spinBoxLength->setMaximum(9); spinBoxLength->setValue(info.len == 0 || info.len > 9 ? 0 : info.len); connect(comboBoxChangeCase, static_cast(&QComboBox::currentIndexChanged), isew, &IdSuggestionsEditWidget::updatePreview); connect(spinBoxLength, static_cast(&QSpinBox::valueChanged), isew, &IdSuggestionsEditWidget::updatePreview); } QString toString() const override { QString result = QStringLiteral("e"); if (spinBoxLength->value() > 0) result.append(QString::number(spinBoxLength->value())); const IdSuggestions::CaseChange caseChange = static_cast(comboBoxChangeCase->currentIndex()); if (caseChange == IdSuggestions::ccToLower) result.append(QStringLiteral("l")); else if (caseChange == IdSuggestions::ccToUpper) result.append(QStringLiteral("u")); else if (caseChange == IdSuggestions::ccToCamelCase) result.append(QStringLiteral("c")); return result; } }; /** * @author Thomas Fischer */ class TextWidget : public TokenWidget { Q_OBJECT private: QLineEdit *lineEditText; public: TextWidget(const QString &text, IdSuggestionsEditWidget *isew, QWidget *parent) : TokenWidget(parent) { setTitle(i18n("Text")); lineEditText = new QLineEdit(this); formLayout->addRow(i18n("Text:"), lineEditText); lineEditText->setText(text); connect(lineEditText, &QLineEdit::textEdited, isew, &IdSuggestionsEditWidget::updatePreview); } QString toString() const override { QString result = QStringLiteral("\"") + lineEditText->text(); return result; } }; class IdSuggestionsEditWidget::IdSuggestionsEditWidgetPrivate { private: IdSuggestionsEditWidget *p; public: enum TokenType {ttTitle, ttAuthor, ttYear, ttJournal, ttType, ttText, ttVolume, ttPageNumber}; QWidget *container; QBoxLayout *containerLayout; QList widgetList; QLabel *labelPreview; QPushButton *buttonAddTokenAtTop, *buttonAddTokenAtBottom; const Entry *previewEntry; QSignalMapper *signalMapperRemove, *signalMapperMoveUp, *signalMapperMoveDown; QScrollArea *area; IdSuggestionsEditWidgetPrivate(const Entry *pe, IdSuggestionsEditWidget *parent) : p(parent), previewEntry(pe) { setupGUI(); } void setupGUI() { QGridLayout *layout = new QGridLayout(p); labelPreview = new QLabel(p); layout->addWidget(labelPreview, 0, 0, 1, 1); layout->setColumnStretch(0, 100); area = new QScrollArea(p); layout->addWidget(area, 1, 0, 1, 1); area->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); area->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); container = new QWidget(area); area->setWidget(container); area->setWidgetResizable(true); containerLayout = new QVBoxLayout(container); area->setMinimumSize(384, 256); buttonAddTokenAtTop = new QPushButton(QIcon::fromTheme(QStringLiteral("list-add")), i18n("Add at top"), container); containerLayout->addWidget(buttonAddTokenAtTop, 0); containerLayout->addStretch(1); buttonAddTokenAtBottom = new QPushButton(QIcon::fromTheme(QStringLiteral("list-add")), i18n("Add at bottom"), container); containerLayout->addWidget(buttonAddTokenAtBottom, 0); QMenu *menuAddToken = new QMenu(p); QSignalMapper *signalMapperAddMenu = new QSignalMapper(p); buttonAddTokenAtTop->setMenu(menuAddToken); QAction *action = menuAddToken->addAction(i18n("Title"), signalMapperAddMenu, static_cast(&QSignalMapper::map)); signalMapperAddMenu->setMapping(action, -ttTitle); action = menuAddToken->addAction(i18n("Author"), signalMapperAddMenu, static_cast(&QSignalMapper::map)); signalMapperAddMenu->setMapping(action, -ttAuthor); action = menuAddToken->addAction(i18n("Year"), signalMapperAddMenu, static_cast(&QSignalMapper::map)); signalMapperAddMenu->setMapping(action, -ttYear); action = menuAddToken->addAction(i18n("Journal"), signalMapperAddMenu, static_cast(&QSignalMapper::map)); signalMapperAddMenu->setMapping(action, -ttJournal); action = menuAddToken->addAction(i18n("Type"), signalMapperAddMenu, static_cast(&QSignalMapper::map)); signalMapperAddMenu->setMapping(action, -ttType); action = menuAddToken->addAction(i18n("Volume"), signalMapperAddMenu, static_cast(&QSignalMapper::map)); signalMapperAddMenu->setMapping(action, -ttVolume); action = menuAddToken->addAction(i18n("Page Number"), signalMapperAddMenu, static_cast(&QSignalMapper::map)); signalMapperAddMenu->setMapping(action, -ttPageNumber); action = menuAddToken->addAction(i18n("Text"), signalMapperAddMenu, static_cast(&QSignalMapper::map)); signalMapperAddMenu->setMapping(action, -ttText); connect(signalMapperAddMenu, static_cast(&QSignalMapper::mapped), p, &IdSuggestionsEditWidget::addToken); menuAddToken = new QMenu(p); signalMapperAddMenu = new QSignalMapper(p); buttonAddTokenAtBottom->setMenu(menuAddToken); action = menuAddToken->addAction(i18n("Title"), signalMapperAddMenu, static_cast(&QSignalMapper::map)); signalMapperAddMenu->setMapping(action, ttTitle); action = menuAddToken->addAction(i18n("Author"), signalMapperAddMenu, static_cast(&QSignalMapper::map)); signalMapperAddMenu->setMapping(action, ttAuthor); action = menuAddToken->addAction(i18n("Year"), signalMapperAddMenu, static_cast(&QSignalMapper::map)); signalMapperAddMenu->setMapping(action, ttYear); action = menuAddToken->addAction(i18n("Journal"), signalMapperAddMenu, static_cast(&QSignalMapper::map)); signalMapperAddMenu->setMapping(action, ttJournal); action = menuAddToken->addAction(i18n("Type"), signalMapperAddMenu, static_cast(&QSignalMapper::map)); signalMapperAddMenu->setMapping(action, ttType); action = menuAddToken->addAction(i18n("Volume"), signalMapperAddMenu, static_cast(&QSignalMapper::map)); signalMapperAddMenu->setMapping(action, ttVolume); action = menuAddToken->addAction(i18n("Page Number"), signalMapperAddMenu, static_cast(&QSignalMapper::map)); signalMapperAddMenu->setMapping(action, ttPageNumber); action = menuAddToken->addAction(i18n("Text"), signalMapperAddMenu, static_cast(&QSignalMapper::map)); signalMapperAddMenu->setMapping(action, ttText); connect(signalMapperAddMenu, static_cast(&QSignalMapper::mapped), p, &IdSuggestionsEditWidget::addToken); signalMapperMoveUp = new QSignalMapper(p); connect(signalMapperMoveUp, static_cast(&QSignalMapper::mapped), p, &IdSuggestionsEditWidget::moveUpToken); signalMapperMoveDown = new QSignalMapper(p); connect(signalMapperMoveDown, static_cast(&QSignalMapper::mapped), p, &IdSuggestionsEditWidget::moveDownToken); signalMapperRemove = new QSignalMapper(p); connect(signalMapperRemove, static_cast(&QSignalMapper::mapped), p, &IdSuggestionsEditWidget::removeToken); } void addManagementButtons(TokenWidget *tokenWidget) { if (tokenWidget != nullptr) { QPushButton *buttonUp = new QPushButton(QIcon::fromTheme(QStringLiteral("go-up")), QString(), tokenWidget); QPushButton *buttonDown = new QPushButton(QIcon::fromTheme(QStringLiteral("go-down")), QString(), tokenWidget); QPushButton *buttonRemove = new QPushButton(QIcon::fromTheme(QStringLiteral("list-remove")), QString(), tokenWidget); tokenWidget->addButtons(buttonUp, buttonDown, buttonRemove); connect(buttonUp, &QPushButton::clicked, signalMapperMoveUp, static_cast(&QSignalMapper::map)); signalMapperMoveUp->setMapping(buttonUp, tokenWidget); connect(buttonDown, &QPushButton::clicked, signalMapperMoveDown, static_cast(&QSignalMapper::map)); signalMapperMoveDown->setMapping(buttonDown, tokenWidget); connect(buttonRemove, &QPushButton::clicked, signalMapperRemove, static_cast(&QSignalMapper::map)); signalMapperRemove->setMapping(buttonRemove, tokenWidget); } } void add(TokenType tokenType, bool atTop) { TokenWidget *tokenWidget = nullptr; switch (tokenType) { case ttTitle: { struct IdSuggestions::IdSuggestionTokenInfo info; info.inBetween = QString(); info.len = -1; info.startWord = 0; info.endWord = 0x00ffffff; info.lastWord = false; info.caseChange = IdSuggestions::ccNoChange; tokenWidget = new TitleWidget(info, true, p, container); } break; case ttAuthor: { struct IdSuggestions::IdSuggestionTokenInfo info; info.inBetween = QString(); info.len = -1; info.startWord = 0; info.endWord = 0x00ffffff; info.lastWord = false; info.caseChange = IdSuggestions::ccNoChange; tokenWidget = new AuthorWidget(info, p, container); } break; case ttYear: tokenWidget = new YearWidget(4, p, container); break; case ttJournal: { struct IdSuggestions::IdSuggestionTokenInfo info; info.inBetween = QString(); info.len = 1; info.startWord = 0; info.endWord = 0x00ffffff; info.lastWord = false; info.caseChange = IdSuggestions::ccNoChange; tokenWidget = new JournalWidget(info, true, p, container); } break; case ttType: { struct IdSuggestions::IdSuggestionTokenInfo info; info.inBetween = QString(); info.len = -1; info.startWord = 0; info.endWord = 0x00ffffff; info.lastWord = false; info.caseChange = IdSuggestions::ccNoChange; tokenWidget = new TypeWidget(info, p, container); } break; case ttText: tokenWidget = new TextWidget(QString(), p, container); break; case ttVolume: tokenWidget = new VolumeWidget(p, container); break; case ttPageNumber: tokenWidget = new PageNumberWidget(p, container); break; } if (tokenWidget != nullptr) { const int pos = atTop ? 1 : containerLayout->count() - 2; atTop ? widgetList.prepend(tokenWidget) : widgetList.append(tokenWidget); containerLayout->insertWidget(pos, tokenWidget, 1); addManagementButtons(tokenWidget); } } void reset(const QString &formatString) { while (!widgetList.isEmpty()) delete widgetList.takeFirst(); const QStringList tokenList = formatString.split(QStringLiteral("|"), QString::SkipEmptyParts); for (const QString &token : tokenList) { TokenWidget *tokenWidget = nullptr; if (token[0] == 'a' || token[0] == 'A' || token[0] == 'z') { struct IdSuggestions::IdSuggestionTokenInfo info = p->evalToken(token.mid(1)); /// Support deprecated 'a' and 'z' cases if (token[0] == 'a') info.startWord = info.endWord = 0; else if (token[0] == 'z') { info.startWord = 1; info.endWord = 0x00ffffff; } tokenWidget = new AuthorWidget(info, p, container); widgetList << tokenWidget; containerLayout->insertWidget(containerLayout->count() - 2, tokenWidget, 1); } else if (token[0] == 'y') { tokenWidget = new YearWidget(2, p, container); widgetList << tokenWidget; containerLayout->insertWidget(containerLayout->count() - 2, tokenWidget, 1); } else if (token[0] == 'Y') { tokenWidget = new YearWidget(4, p, container); widgetList << tokenWidget; containerLayout->insertWidget(containerLayout->count() - 2, tokenWidget, 1); } else if (token[0] == 't' || token[0] == 'T') { struct IdSuggestions::IdSuggestionTokenInfo info = p->evalToken(token.mid(1)); tokenWidget = new TitleWidget(info, token[0].isUpper(), p, container); widgetList << tokenWidget; containerLayout->insertWidget(containerLayout->count() - 2, tokenWidget, 1); } else if (token[0] == 'j' || token[0] == 'J') { struct IdSuggestions::IdSuggestionTokenInfo info = p->evalToken(token.mid(1)); tokenWidget = new JournalWidget(info, token[0].isUpper(), p, container); widgetList << tokenWidget; containerLayout->insertWidget(containerLayout->count() - 2, tokenWidget, 1); } else if (token[0] == 'e') { struct IdSuggestions::IdSuggestionTokenInfo info = p->evalToken(token.mid(1)); tokenWidget = new TypeWidget(info, p, container); widgetList << tokenWidget; containerLayout->insertWidget(containerLayout->count() - 2, tokenWidget, 1); } else if (token[0] == 'v') { tokenWidget = new VolumeWidget(p, container); widgetList << tokenWidget; containerLayout->insertWidget(containerLayout->count() - 2, tokenWidget, 1); } else if (token[0] == 'p') { tokenWidget = new PageNumberWidget(p, container); widgetList << tokenWidget; containerLayout->insertWidget(containerLayout->count() - 2, tokenWidget, 1); } else if (token[0] == '"') { tokenWidget = new TextWidget(token.mid(1), p, container); widgetList << tokenWidget; containerLayout->insertWidget(containerLayout->count() - 2, tokenWidget, 1); } if (tokenWidget != nullptr) addManagementButtons(tokenWidget); } p->updatePreview(); } QString apply() { QStringList result; result.reserve(widgetList.size()); for (TokenWidget *widget : const_cast &>(widgetList)) result << widget->toString(); return result.join(QStringLiteral("|")); } }; IdSuggestionsEditWidget::IdSuggestionsEditWidget(const Entry *previewEntry, QWidget *parent, Qt::WindowFlags f) : QWidget(parent, f), IdSuggestions(), d(new IdSuggestionsEditWidgetPrivate(previewEntry, this)) { /// nothing } IdSuggestionsEditWidget::~IdSuggestionsEditWidget() { // TODO } void IdSuggestionsEditWidget::setFormatString(const QString &formatString) { d->reset(formatString); } QString IdSuggestionsEditWidget::formatString() const { return d->apply(); } void IdSuggestionsEditWidget::updatePreview() { const QString formatString = d->apply(); d->labelPreview->setText(formatId(*d->previewEntry, formatString)); d->labelPreview->setToolTip(i18n("Structure:
  • %1
Example: %2
", formatStrToHuman(formatString).join(QStringLiteral("
  • ")), formatId(*d->previewEntry, formatString))); } void IdSuggestionsEditWidget::moveUpToken(QWidget *widget) { TokenWidget *tokenWidget = static_cast(widget); int curPos = d->widgetList.indexOf(tokenWidget); if (curPos > 0) { d->widgetList.removeAt(curPos); const int layoutPos = d->containerLayout->indexOf(tokenWidget); d->containerLayout->removeWidget(tokenWidget); d->widgetList.insert(curPos - 1, tokenWidget); d->containerLayout->insertWidget(layoutPos - 1, tokenWidget, 1); updatePreview(); } } void IdSuggestionsEditWidget::moveDownToken(QWidget *widget) { TokenWidget *tokenWidget = static_cast(widget); int curPos = d->widgetList.indexOf(tokenWidget); if (curPos < d->widgetList.size() - 1) { d->widgetList.removeAt(curPos); const int layoutPos = d->containerLayout->indexOf(tokenWidget); d->containerLayout->removeWidget(tokenWidget); d->widgetList.insert(curPos + 1, tokenWidget); d->containerLayout->insertWidget(layoutPos + 1, tokenWidget, 1); updatePreview(); } } void IdSuggestionsEditWidget::removeToken(QWidget *widget) { TokenWidget *tokenWidget = static_cast(widget); d->widgetList.removeOne(tokenWidget); d->containerLayout->removeWidget(tokenWidget); tokenWidget->deleteLater(); updatePreview(); } void IdSuggestionsEditWidget::addToken(int cmd) { if (cmd < 0) { d->add(static_cast(-cmd), true); d->area->ensureWidgetVisible(d->buttonAddTokenAtTop); // FIXME does not work as intended } else { d->add(static_cast(cmd), false); d->area->ensureWidgetVisible(d->buttonAddTokenAtBottom); // FIXME does not work as intended } updatePreview(); } IdSuggestionsEditDialog::IdSuggestionsEditDialog(QWidget *parent, Qt::WindowFlags flags) : QDialog(parent, flags) { setWindowTitle(i18n("Edit Id Suggestion")); } IdSuggestionsEditDialog::~IdSuggestionsEditDialog() { /// nothing } QString IdSuggestionsEditDialog::editSuggestion(const Entry *previewEntry, const QString &suggestion, QWidget *parent) { QPointer dlg = new IdSuggestionsEditDialog(parent); QBoxLayout *boxLayout = new QVBoxLayout(dlg); IdSuggestionsEditWidget *widget = new IdSuggestionsEditWidget(previewEntry, dlg); boxLayout->addWidget(widget); QDialogButtonBox *dbb = new QDialogButtonBox(dlg); dbb->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); boxLayout->addWidget(dbb); connect(dbb->button(QDialogButtonBox::Ok), &QPushButton::clicked, dlg.data(), &QDialog::accept); connect(dbb->button(QDialogButtonBox::Cancel), &QPushButton::clicked, dlg.data(), &QDialog::reject); widget->setFormatString(suggestion); if (dlg->exec() == Accepted) { const QString formatString = widget->formatString(); delete dlg; return formatString; } delete dlg; /// Return unmodified original suggestion return suggestion; } #include "settingsidsuggestionseditor.moc" diff --git a/src/gui/preferences/settingsidsuggestionswidget.cpp b/src/gui/preferences/settingsidsuggestionswidget.cpp index f6a1db8d..7a200729 100644 --- a/src/gui/preferences/settingsidsuggestionswidget.cpp +++ b/src/gui/preferences/settingsidsuggestionswidget.cpp @@ -1,397 +1,400 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, see . * ***************************************************************************/ #include "settingsidsuggestionswidget.h" #include #include #include #include #include #include #include #include #include #include "settingsidsuggestionseditor.h" class IdSuggestionsModel : public QAbstractListModel { Q_OBJECT private: QStringList m_formatStringList; int m_defaultFormatStringRow; IdSuggestions *m_idSuggestions; static const QString exampleBibTeXEntryString; static QSharedPointer exampleBibTeXEntry; public: IdSuggestionsModel(QObject *parent = nullptr) : QAbstractListModel(parent) { m_idSuggestions = new IdSuggestions(); m_defaultFormatStringRow = -1; if (exampleBibTeXEntry.isNull()) { FileImporterBibTeX fileImporterBibTeX(this); File *file = fileImporterBibTeX.fromString(exampleBibTeXEntryString); if (file != nullptr) { if (!file->isEmpty()) exampleBibTeXEntry = file->first().dynamicCast(); delete file; } } } ~IdSuggestionsModel() override { delete m_idSuggestions; } enum IdSuggestionsModelRole { /// Raw format string as QString FormatStringRole = Qt::UserRole + 7811, /// Flag whether current index is the default one; boolean value IsDefaultFormatStringRole = Qt::UserRole + 7812 }; QSharedPointer previewEntry() { return exampleBibTeXEntry; } void setFormatStringList(const QStringList &formatStringList, const QString &defaultString = QString()) { beginResetModel(); m_formatStringList = formatStringList; m_defaultFormatStringRow = m_formatStringList.indexOf(defaultString); endResetModel(); } QStringList formatStringList() const { return this->m_formatStringList; } QString defaultFormatString() const { if (m_defaultFormatStringRow >= 0 && m_defaultFormatStringRow < m_formatStringList.length()) return m_formatStringList[m_defaultFormatStringRow]; else return QString(); } int rowCount(const QModelIndex &parent = QModelIndex()) const override { if (parent == QModelIndex()) return m_formatStringList.count(); else return 0; } QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override { if (index.row() < 0 || index.row() >= m_formatStringList.count()) return QVariant(); switch (role) { case Qt::FontRole: { QFont defaultFont = QFontDatabase::systemFont(QFontDatabase::GeneralFont); if (index.row() == m_defaultFormatStringRow) defaultFont.setBold(true); return defaultFont; } case Qt::DecorationRole: if (index.row() == m_defaultFormatStringRow) return QIcon::fromTheme(QStringLiteral("favorites")); else return QIcon::fromTheme(QStringLiteral("view-filter")); case Qt::ToolTipRole: return i18n("Structure:
    • %1
    Example: %2
    ", m_idSuggestions->formatStrToHuman(m_formatStringList[index.row()]).join(QStringLiteral("
  • ")), m_idSuggestions->formatId(*exampleBibTeXEntry, m_formatStringList[index.row()])); case Qt::DisplayRole: return m_idSuggestions->formatId(*exampleBibTeXEntry, m_formatStringList[index.row()]); case Qt::UserRole: case FormatStringRole: return m_formatStringList[index.row()]; case IsDefaultFormatStringRole: return index.row() == m_defaultFormatStringRow; default: return QVariant(); } } bool setData(const QModelIndex &idx, const QVariant &value, int role) override { if (idx.row() < 0 || idx.row() >= m_formatStringList.count()) return false; if (role == IsDefaultFormatStringRole && value.canConvert()) { if (value.toBool()) { if (idx.row() != m_defaultFormatStringRow) { QModelIndex oldDefaultIndex = index(m_defaultFormatStringRow, 0); m_defaultFormatStringRow = idx.row(); emit dataChanged(oldDefaultIndex, oldDefaultIndex); emit dataChanged(idx, idx); } } else { m_defaultFormatStringRow = -1; emit dataChanged(idx, idx); } return true; } else if (role == FormatStringRole && value.canConvert()) { m_formatStringList[idx.row()] = value.toString(); emit dataChanged(idx, idx); return true; } return false; } virtual bool insertRow(int row, const QModelIndex &parent = QModelIndex()) { if (parent != QModelIndex()) return false; beginInsertRows(parent, row, row); m_formatStringList.insert(row, QStringLiteral("T")); endInsertRows(); return true; } QVariant headerData(int section, Qt::Orientation, int role = Qt::DisplayRole) const override { if (role == Qt::DisplayRole && section == 0) return i18n("Id Suggestions"); return QVariant(); } bool moveUp(const QModelIndex &index) { int row = index.row(); if (row < 1 || row >= m_formatStringList.count()) return false; beginMoveColumns(index.parent(), row, row, index.parent(), row - 1); const QString formatString = m_formatStringList[row]; m_formatStringList.removeAt(row); m_formatStringList.insert(row - 1, formatString); if (m_defaultFormatStringRow == row) --m_defaultFormatStringRow; ///< update default id suggestion endMoveRows(); return true; } bool moveDown(const QModelIndex &index) { int row = index.row(); if (row < 0 || row >= m_formatStringList.count() - 1) return false; beginMoveColumns(index.parent(), row + 1, row + 1, index.parent(), row); const QString formatString = m_formatStringList[row]; m_formatStringList.removeAt(row); m_formatStringList.insert(row + 1, formatString); if (m_defaultFormatStringRow == row) ++m_defaultFormatStringRow; ///< update default id suggestion endMoveRows(); return true; } bool remove(const QModelIndex &index) { int row = index.row(); if (row < 0 || row >= m_formatStringList.count()) return false; beginRemoveRows(index.parent(), row, row); m_formatStringList.removeAt(row); if (m_defaultFormatStringRow == row) m_defaultFormatStringRow = -1; ///< update default id suggestion endRemoveRows(); return true; } }; const QString IdSuggestionsModel::exampleBibTeXEntryString = QStringLiteral("@Article{ dijkstra1983terminationdetect,\nauthor = {Edsger W. Dijkstra and W. H. J. Feijen and A. J. M. {van Gasteren}},\ntitle = {{Derivation of a Termination Detection Algorithm for Distributed Computations}},\njournal = {Information Processing Letters},\nvolume = 16,\nnumber = 5,\npages = {217--219},\nmonth = jun,\nyear = 1983\n}"); QSharedPointer IdSuggestionsModel::exampleBibTeXEntry; class SettingsIdSuggestionsWidget::SettingsIdSuggestionsWidgetPrivate { private: SettingsIdSuggestionsWidget *p; public: QTreeView *treeViewSuggestions; IdSuggestionsModel *idSuggestionsModel; QPushButton *buttonNewSuggestion, *buttonEditSuggestion, *buttonDeleteSuggestion, *buttonSuggestionUp, *buttonSuggestionDown, *buttonToggleDefaultString; SettingsIdSuggestionsWidgetPrivate(SettingsIdSuggestionsWidget *parent) : p(parent) { setupGUI(); } void loadState() { idSuggestionsModel->setFormatStringList(Preferences::instance().idSuggestionFormatStrings(), Preferences::instance().activeIdSuggestionFormatString()); } void saveState() { Preferences::instance().setIdSuggestionFormatStrings(idSuggestionsModel->formatStringList()); Preferences::instance().setActiveIdSuggestionFormatString(idSuggestionsModel->defaultFormatString()); } void resetToDefaults() { idSuggestionsModel->setFormatStringList(Preferences::defaultIdSuggestionFormatStrings); } void setupGUI() { QGridLayout *layout = new QGridLayout(p); treeViewSuggestions = new QTreeView(p); layout->addWidget(treeViewSuggestions, 0, 0, 8, 1); idSuggestionsModel = new IdSuggestionsModel(treeViewSuggestions); treeViewSuggestions->setModel(idSuggestionsModel); treeViewSuggestions->setRootIsDecorated(false); connect(treeViewSuggestions->selectionModel(), &QItemSelectionModel::currentChanged, p, &SettingsIdSuggestionsWidget::itemChanged); +#if QT_VERSION >= 0x050b00 + treeViewSuggestions->setMinimumSize(treeViewSuggestions->fontMetrics().horizontalAdvance(QChar('W')) * 25, treeViewSuggestions->fontMetrics().height() * 15); +#else // QT_VERSION >= 0x050b00 treeViewSuggestions->setMinimumSize(treeViewSuggestions->fontMetrics().width(QChar('W')) * 25, treeViewSuggestions->fontMetrics().height() * 15); - +#endif // QT_VERSION >= 0x050b00 buttonNewSuggestion = new QPushButton(QIcon::fromTheme(QStringLiteral("list-add")), i18n("Add..."), p); layout->addWidget(buttonNewSuggestion, 0, 1, 1, 1); buttonEditSuggestion = new QPushButton(QIcon::fromTheme(QStringLiteral("document-edit")), i18n("Edit..."), p); layout->addWidget(buttonEditSuggestion, 1, 1, 1, 1); buttonDeleteSuggestion = new QPushButton(QIcon::fromTheme(QStringLiteral("list-remove")), i18n("Remove"), p); layout->addWidget(buttonDeleteSuggestion, 2, 1, 1, 1); buttonSuggestionUp = new QPushButton(QIcon::fromTheme(QStringLiteral("go-up")), i18n("Up"), p); layout->addWidget(buttonSuggestionUp, 3, 1, 1, 1); buttonSuggestionDown = new QPushButton(QIcon::fromTheme(QStringLiteral("go-down")), i18n("Down"), p); layout->addWidget(buttonSuggestionDown, 4, 1, 1, 1); buttonToggleDefaultString = new QPushButton(QIcon::fromTheme(QStringLiteral("favorites")), i18n("Toggle Default"), p); layout->addWidget(buttonToggleDefaultString, 5, 1, 1, 1); connect(buttonNewSuggestion, &QPushButton::clicked, p, &SettingsIdSuggestionsWidget::buttonClicked); connect(buttonEditSuggestion, &QPushButton::clicked, p, &SettingsIdSuggestionsWidget::buttonClicked); connect(buttonDeleteSuggestion, &QPushButton::clicked, p, &SettingsIdSuggestionsWidget::buttonClicked); connect(buttonSuggestionUp, &QPushButton::clicked, p, &SettingsIdSuggestionsWidget::buttonClicked); connect(buttonSuggestionDown, &QPushButton::clicked, p, &SettingsIdSuggestionsWidget::buttonClicked); connect(buttonToggleDefaultString, &QPushButton::clicked, p, &SettingsIdSuggestionsWidget::toggleDefault); connect(treeViewSuggestions, &QTreeView::doubleClicked, p, &SettingsIdSuggestionsWidget::editItem); } }; SettingsIdSuggestionsWidget::SettingsIdSuggestionsWidget(QWidget *parent) : SettingsAbstractWidget(parent), d(new SettingsIdSuggestionsWidgetPrivate(this)) { d->loadState(); itemChanged(QModelIndex()); } SettingsIdSuggestionsWidget::~SettingsIdSuggestionsWidget() { delete d; } QString SettingsIdSuggestionsWidget::label() const { return i18n("Id Suggestions"); } QIcon SettingsIdSuggestionsWidget::icon() const { return QIcon::fromTheme(QStringLiteral("view-filter")); } void SettingsIdSuggestionsWidget::loadState() { d->loadState(); } void SettingsIdSuggestionsWidget::saveState() { d->saveState(); } void SettingsIdSuggestionsWidget::resetToDefaults() { d->resetToDefaults(); } void SettingsIdSuggestionsWidget::buttonClicked() { QPushButton *button = qobject_cast(sender()); QModelIndex selectedIndex = d->treeViewSuggestions->selectionModel()->currentIndex(); if (button == d->buttonNewSuggestion) { const QString newSuggestion = IdSuggestionsEditDialog::editSuggestion(d->idSuggestionsModel->previewEntry().data(), QString(), this); const int row = d->treeViewSuggestions->model()->rowCount(QModelIndex()); if (!newSuggestion.isEmpty() && d->idSuggestionsModel->insertRow(row)) { QModelIndex index = d->idSuggestionsModel->index(row, 0, QModelIndex()); d->treeViewSuggestions->setCurrentIndex(index); if (d->idSuggestionsModel->setData(index, newSuggestion, IdSuggestionsModel::FormatStringRole)) emit changed(); } } else if (button == d->buttonEditSuggestion) { QModelIndex currIndex = d->treeViewSuggestions->currentIndex(); editItem(currIndex); } else if (button == d->buttonDeleteSuggestion) { if (d->idSuggestionsModel->remove(selectedIndex)) { emit changed(); } } else if (button == d->buttonSuggestionUp) { if (d->idSuggestionsModel->moveUp(selectedIndex)) { d->treeViewSuggestions->selectionModel()->setCurrentIndex(selectedIndex.sibling(selectedIndex.row() - 1, 0), QItemSelectionModel::ClearAndSelect); emit changed(); } } else if (button == d->buttonSuggestionDown) { if (d->idSuggestionsModel->moveDown(selectedIndex)) { d->treeViewSuggestions->selectionModel()->setCurrentIndex(selectedIndex.sibling(selectedIndex.row() + 1, 0), QItemSelectionModel::ClearAndSelect); emit changed(); } } } void SettingsIdSuggestionsWidget::itemChanged(const QModelIndex &index) { bool enableChange = index != QModelIndex(); d->buttonEditSuggestion->setEnabled(enableChange); d->buttonDeleteSuggestion->setEnabled(enableChange); d->buttonSuggestionUp->setEnabled(enableChange && index.row() > 0); d->buttonSuggestionDown->setEnabled(enableChange && index.row() < d->idSuggestionsModel->rowCount() - 1); d->buttonToggleDefaultString->setEnabled(enableChange); } void SettingsIdSuggestionsWidget::editItem(const QModelIndex &index) { QString suggestion; if (index != QModelIndex() && !(suggestion = index.data(IdSuggestionsModel::FormatStringRole).toString()).isEmpty()) { const QString newSuggestion = IdSuggestionsEditDialog::editSuggestion(d->idSuggestionsModel->previewEntry().data(), suggestion, this); if (newSuggestion.isEmpty()) { if (KMessageBox::questionYesNo(this, i18n("All token have been removed from this suggestion. Remove suggestion itself or restore original suggestion?"), i18n("Remove suggestion?"), KGuiItem(i18n("Remove suggestion"), QIcon::fromTheme(QStringLiteral("list-remove"))), KGuiItem(i18n("Revert changes"), QIcon::fromTheme(QStringLiteral("edit-undo")))) == KMessageBox::Yes && d->idSuggestionsModel->remove(index)) { emit changed(); } } else if (newSuggestion != suggestion) { if (d->idSuggestionsModel->setData(index, newSuggestion, IdSuggestionsModel::FormatStringRole)) emit changed(); } } } void SettingsIdSuggestionsWidget::toggleDefault() { QModelIndex curIndex = d->treeViewSuggestions->currentIndex(); bool current = d->idSuggestionsModel->data(curIndex, IdSuggestionsModel::IsDefaultFormatStringRole).toBool(); d->idSuggestionsModel->setData(curIndex, !current, IdSuggestionsModel::IsDefaultFormatStringRole); emit changed(); } #include "settingsidsuggestionswidget.moc" diff --git a/src/gui/valuelistmodel.cpp b/src/gui/valuelistmodel.cpp index d05e524f..e2dee578 100644 --- a/src/gui/valuelistmodel.cpp +++ b/src/gui/valuelistmodel.cpp @@ -1,572 +1,577 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, see . * ***************************************************************************/ #include "valuelistmodel.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "field/fieldlineedit.h" #include "logging_gui.h" QWidget *ValueListDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &sovi, const QModelIndex &index) const { if (index.column() == 0) { const FieldDescription &fd = BibTeXFields::instance().find(m_fieldName); FieldLineEdit *fieldLineEdit = new FieldLineEdit(fd.preferredTypeFlag, fd.typeFlags, false, parent); fieldLineEdit->setAutoFillBackground(true); return fieldLineEdit; } else return QStyledItemDelegate::createEditor(parent, sovi, index); } void ValueListDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const { if (index.column() == 0) { FieldLineEdit *fieldLineEdit = qobject_cast(editor); if (fieldLineEdit != nullptr) fieldLineEdit->reset(index.model()->data(index, Qt::EditRole).value()); } } void ValueListDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const { FieldLineEdit *fieldLineEdit = qobject_cast(editor); if (fieldLineEdit != nullptr) { Value v; fieldLineEdit->apply(v); if (v.count() == 1) /// field should contain exactly one value item (no zero, not two or more) model->setData(index, QVariant::fromValue(v)); } } QSize ValueListDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { QSize size = QStyledItemDelegate::sizeHint(option, index); size.setHeight(qMax(size.height(), option.fontMetrics.height() * 3 / 2)); // TODO calculate height better return size; } void ValueListDelegate::commitAndCloseEditor() { QLineEdit *editor = qobject_cast(sender()); emit commitData(editor); emit closeEditor(editor); } void ValueListDelegate::initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const { QStyledItemDelegate::initStyleOption(option, index); if (option->decorationPosition != QStyleOptionViewItem::Top) { /// remove text from style (do not draw text) option->text.clear(); } } void ValueListDelegate::paint(QPainter *painter, const QStyleOptionViewItem &_option, const QModelIndex &index) const { QStyleOptionViewItem option = _option; /// code heavily inspired by kdepimlibs-4.6.3/akonadi/collectionstatisticsdelegate.cpp /// save painter's state, restored before leaving this function painter->save(); /// first, paint the basic, but without the text. We remove the text /// in initStyleOption(), which gets called by QStyledItemDelegate::paint(). QStyledItemDelegate::paint(painter, option, index); /// now, we retrieve the correct style option by calling intiStyleOption from /// the superclass. QStyledItemDelegate::initStyleOption(&option, index); QString field = option.text; /// now calculate the rectangle for the text QStyle *s = m_parent->style(); const QWidget *widget = option.widget; const QRect textRect = s->subElementRect(QStyle::SE_ItemViewItemText, &option, widget); if (option.state & QStyle::State_Selected) { /// selected lines are drawn with different color painter->setPen(option.palette.highlightedText().color()); } /// count will be empty unless only one column is shown const QString count = index.column() == 0 && index.model()->columnCount() == 1 ? QString(QStringLiteral(" (%1)")).arg(index.data(ValueListModel::CountRole).toInt()) : QString(); /// squeeze the folder text if it is to big and calculate the rectangles /// where the folder text and the unread count will be drawn to - QFontMetrics fm(painter->fontMetrics()); - int countWidth = fm.width(count); + const QFontMetrics fm(painter->fontMetrics()); +#if QT_VERSION >= 0x050b00 + const int countWidth = fm.horizontalAdvance(count); + int fieldWidth = fm.horizontalAdvance(field); +#else // QT_VERSION >= 0x050b00 + const int countWidth = fm.width(count); int fieldWidth = fm.width(field); +#endif // QT_VERSION >= 0x050b00 if (countWidth + fieldWidth > textRect.width()) { /// text plus count is too wide for column, cut text and insert "..." field = fm.elidedText(field, Qt::ElideRight, textRect.width() - countWidth - 8); fieldWidth = textRect.width() - countWidth - 12; } /// determine rects to draw field int top = textRect.top() + (textRect.height() - fm.height()) / 2; QRect fieldRect = textRect; QRect countRect = textRect; fieldRect.setTop(top); fieldRect.setHeight(fm.height()); if (m_parent->header()->visualIndex(index.column()) == 0) { /// left-align text fieldRect.setLeft(fieldRect.left() + 4); ///< hm, indent necessary? fieldRect.setRight(fieldRect.left() + fieldWidth); } else { /// right-align text fieldRect.setRight(fieldRect.right() - 4); ///< hm, indent necessary? fieldRect.setLeft(fieldRect.right() - fieldWidth); ///< hm, indent necessary? } /// draw field name painter->drawText(fieldRect, Qt::AlignLeft, field); if (!count.isEmpty()) { /// determine rects to draw count countRect.setTop(top); countRect.setHeight(fm.height()); countRect.setLeft(fieldRect.right()); /// use bold font QFont font = painter->font(); font.setBold(true); painter->setFont(font); /// determine color for count number const QColor countColor = (option.state & QStyle::State_Selected) ? KColorScheme(QPalette::Active, KColorScheme::Selection).foreground(KColorScheme::LinkText).color() : KColorScheme(QPalette::Active, KColorScheme::View).foreground(KColorScheme::LinkText).color(); painter->setPen(countColor); /// draw count painter->drawText(countRect, Qt::AlignLeft, count); } /// restore painter's state painter->restore(); } ValueListModel::ValueListModel(const File *bibtexFile, const QString &fieldName, QObject *parent) : QAbstractTableModel(parent), file(bibtexFile), fName(fieldName.toLower()), showCountColumn(true), sortBy(SortByText) { readConfiguration(); updateValues(); NotificationHub::registerNotificationListener(this, NotificationHub::EventConfigurationChanged); } int ValueListModel::rowCount(const QModelIndex &parent) const { return parent == QModelIndex() ? values.count() : 0; } int ValueListModel::columnCount(const QModelIndex &parent) const { return parent == QModelIndex() ? (showCountColumn ? 2 : 1) : 0; } QVariant ValueListModel::data(const QModelIndex &index, int role) const { if (index.row() >= values.count() || index.column() >= 2) return QVariant(); if (role == Qt::DisplayRole || role == Qt::ToolTipRole) { if (index.column() == 0) { if (fName == Entry::ftColor) { QString text = values[index.row()].text; if (text.isEmpty()) return QVariant(); QString colorText = colorToLabel[text]; if (colorText.isEmpty()) return QVariant(text); return QVariant(colorText); } else return QVariant(values[index.row()].text); } else return QVariant(values[index.row()].count); } else if (role == SortRole) { static const QRegularExpression ignoredInSorting(QStringLiteral("[{}\\\\]+")); QString buffer = values[index.row()].sortBy.isEmpty() ? values[index.row()].text : values[index.row()].sortBy; buffer = buffer.remove(ignoredInSorting).toLower(); if ((showCountColumn && index.column() == 1) || (!showCountColumn && sortBy == SortByCount)) { /// Sort by string consisting of a zero-padded count and the lower-case text, /// for example "0000000051keyword" /// Used if (a) two columns are shown (showCountColumn is true) and column 1 /// (the count column) is to be sorted or (b) if only one column is shown /// (showCountColumn is false) and this single column is to be sorted by count. return QString(QStringLiteral("%1%2")).arg(values[index.row()].count, 10, 10, QLatin1Char('0')).arg(buffer); } else { /// Otherwise use lower-case text for sorting return QVariant(buffer); } } else if (role == SearchTextRole) { return QVariant(values[index.row()].text); } else if (role == Qt::EditRole) return QVariant::fromValue(values[index.row()].value); else if (role == CountRole) return QVariant(values[index.row()].count); else return QVariant(); } bool ValueListModel::setData(const QModelIndex &index, const QVariant &value, int role) { Q_ASSERT_X(file != nullptr, "ValueListModel::setData", "You cannot set data if there is no BibTeX file associated with this value list."); /// Continue only if in edit role and first column is to be changed if (role == Qt::EditRole && index.column() == 0) { /// Fetch the string as it was shown before the editing started QString origText = data(index, Qt::DisplayRole).toString(); /// Special treatment for colors if (fName == Entry::ftColor) { /// for colors, convert color (RGB) to the associated label QString color = colorToLabel.key(origText); if (!color.isEmpty()) origText = color; } /// Retrieve the Value object containing the user-entered data Value newValue = value.value(); /// nice variable names ... ;-) if (newValue.isEmpty()) { qCWarning(LOG_KBIBTEX_GUI) << "Cannot replace with empty value"; return false; } /// Fetch the string representing the new, user-entered value const QString newText = PlainTextValue::text(newValue); if (newText == origText) { qCWarning(LOG_KBIBTEX_GUI) << "Skipping to replace value with itself"; return false; } bool success = searchAndReplaceValueInEntries(index, newValue) && searchAndReplaceValueInModel(index, newValue); return success; } return false; } Qt::ItemFlags ValueListModel::flags(const QModelIndex &index) const { Qt::ItemFlags result = QAbstractTableModel::flags(index); /// make first column editable if (index.column() == 0) result |= Qt::ItemIsEditable; return result; } QVariant ValueListModel::headerData(int section, Qt::Orientation orientation, int role) const { if (section >= 2 || orientation != Qt::Horizontal || role != Qt::DisplayRole) return QVariant(); else if ((section == 0 && columnCount() == 2) || (columnCount() == 1 && sortBy == SortByText)) return QVariant(i18n("Value")); else return QVariant(i18n("Count")); } void ValueListModel::removeValue(const QModelIndex &index) { removeValueFromEntries(index); removeValueFromModel(index); } void ValueListModel::setShowCountColumn(bool showCountColumn) { beginResetModel(); this->showCountColumn = showCountColumn; endResetModel(); } void ValueListModel::setSortBy(SortBy sortBy) { beginResetModel(); this->sortBy = sortBy; endResetModel(); } void ValueListModel::notificationEvent(int eventId) { if (eventId == NotificationHub::EventConfigurationChanged) { beginResetModel(); readConfiguration(); endResetModel(); } } void ValueListModel::readConfiguration() { /// load mapping from color value to label colorToLabel.clear(); for (QVector>::ConstIterator it = Preferences::instance().colorCodes().constBegin(); it != Preferences::instance().colorCodes().constEnd(); ++it) colorToLabel.insert(it->first.name(), it->second); } void ValueListModel::updateValues() { values.clear(); if (file == nullptr) return; for (const auto &element : const_cast(*file)) { QSharedPointer entry = element.dynamicCast(); if (!entry.isNull()) { for (Entry::ConstIterator eit = entry->constBegin(); eit != entry->constEnd(); ++eit) { QString key = eit.key().toLower(); if (key == fName) { insertValue(eit.value()); break; } if (eit.value().isEmpty()) qCWarning(LOG_KBIBTEX_GUI) << "value for key" << key << "in entry" << entry->id() << "is empty"; } } } } void ValueListModel::insertValue(const Value &value) { for (const QSharedPointer &item : value) { const QString text = PlainTextValue::text(*item); if (text.isEmpty()) continue; ///< skip empty values int index = indexOf(text); if (index < 0) { /// previously unknown text ValueLine newValueLine; newValueLine.text = text; newValueLine.count = 1; newValueLine.value.append(item); /// memorize sorting criterium: /// * for persons, use last name first /// * in any case, use lower case const QSharedPointer person = item.dynamicCast(); newValueLine.sortBy = person.isNull() ? text.toLower() : person->lastName().toLower() + QStringLiteral(" ") + person->firstName().toLower(); values << newValueLine; } else { ++values[index].count; } } } int ValueListModel::indexOf(const QString &text) { QString color; QString cmpText = text; if (fName == Entry::ftColor && !(color = colorToLabel.key(text, QString())).isEmpty()) cmpText = color; if (cmpText.isEmpty()) qCWarning(LOG_KBIBTEX_GUI) << "Should never happen"; int i = 0; /// this is really slow for large data sets: O(n^2) /// maybe use a hash table instead? for (const ValueLine &valueLine : const_cast(values)) { if (valueLine.text == cmpText) return i; ++i; } return -1; } bool ValueListModel::searchAndReplaceValueInEntries(const QModelIndex &index, const Value &newValue) { /// Fetch the string representing the new, user-entered value const QString newText = PlainTextValue::text(newValue); if (newText.isEmpty()) return false; /// Fetch the string as it was shown before the editing started QString origText = data(index, Qt::DisplayRole).toString(); /// Special treatment for colors if (fName == Entry::ftColor) { /// for colors, convert color (RGB) to the associated label QString color = colorToLabel.key(origText); if (!color.isEmpty()) origText = color; } /// Go through all elements in the current file for (const QSharedPointer &element : const_cast(*file)) { QSharedPointer entry = element.dynamicCast(); /// Process only Entry objects if (!entry.isNull()) { /// Go through every key-value pair in entry (author, title, ...) for (Entry::Iterator eit = entry->begin(); eit != entry->end(); ++eit) { /// Fetch key-value pair's key const QString key = eit.key().toLower(); /// Process only key-value pairs that are filtered for (e.g. only keywords) if (key == fName) { eit.value().replace(origText, newValue.first()); break; } } } } return true; } bool ValueListModel::searchAndReplaceValueInModel(const QModelIndex &index, const Value &newValue) { /// Fetch the string representing the new, user-entered value const QString newText = PlainTextValue::text(newValue); if (newText.isEmpty()) return false; const int row = index.row(); /// Test if user-entered text exists already in model's data /// newTextAlreadyInListIndex will be row of duplicate or /// -1 if new text is unique int newTextAlreadyInListIndex = -1; for (int r = values.count() - 1; newTextAlreadyInListIndex < 0 && r >= 0; --r) { if (row != r && values[r].text == newText) newTextAlreadyInListIndex = r; } if (newTextAlreadyInListIndex < 0) { /// User-entered text is unique, so simply replace /// old text with new text values[row].text = newText; values[row].value = newValue; const QSharedPointer person = newValue.first().dynamicCast(); values[row].sortBy = person.isNull() ? QString() : person->lastName() + QStringLiteral(" ") + person->firstName(); } else { /// The user-entered text existed before const int lastRow = values.count() - 1; if (row != lastRow) { /// Unless duplicate is last one in list, /// overwrite edited row with last row's value values[row].text = values[lastRow].text; values[row].value = values[lastRow].value; values[row].sortBy = values[lastRow].sortBy; } /// Remove last row, which is no longer used beginRemoveRows(QModelIndex(), lastRow, lastRow); values.remove(lastRow); endRemoveRows(); } /// Notify Qt about data changed emit dataChanged(index, index); return true; } void ValueListModel::removeValueFromEntries(const QModelIndex &index) { /// Retrieve the Value object containing the user-entered data const Value toBeDeletedValue = values[index.row()].value; if (toBeDeletedValue.isEmpty()) { return; } const QString toBeDeletedText = PlainTextValue::text(toBeDeletedValue); if (toBeDeletedText.isEmpty()) { return; } /// Go through all elements in the current file for (const QSharedPointer &element : const_cast(*file)) { QSharedPointer entry = element.dynamicCast(); /// Process only Entry objects if (!entry.isNull()) { /// Go through every key-value pair in entry (author, title, ...) for (Entry::Iterator eit = entry->begin(); eit != entry->end(); ++eit) { /// Fetch key-value pair's key const QString key = eit.key().toLower(); /// Process only key-value pairs that are filtered for (e.g. only keywords) if (key == fName) { /// Fetch the key-value pair's value's textual representation const QString valueFullText = PlainTextValue::text(eit.value()); if (valueFullText == toBeDeletedText) { /// If the key-value pair's value's textual representation is the same /// as the value to be delted, remove this key-value pair /// This test is usually true for keys like title, year, or edition. entry->remove(key); /// This would break the Iterator, but code "breakes" from loop anyways } else { /// The test above failed, but the delete operation may have /// to be applied to a ValueItem inside the value. /// Possible keys for such a case include author, editor, or keywords. /// Process each ValueItem inside this Value for (Value::Iterator vit = eit.value().begin(); vit != eit.value().end();) { /// Similar procedure as for full values above: /// If a ValueItem's textual representation is the same /// as the shown string which has be deleted, remove the /// ValueItem from this Value. If the Value becomes empty, /// remove Value as well. const QString valueItemText = PlainTextValue::text(* (*vit)); if (valueItemText == toBeDeletedText) { /// Erase old ValueItem from this Value vit = eit.value().erase(vit); } else ++vit; } if (eit.value().isEmpty()) { /// This value does no longer contain any ValueItems. entry->remove(key); /// This would break the Iterator, but code "breakes" from loop anyways } } break; } } } } } void ValueListModel::removeValueFromModel(const QModelIndex &index) { const int row = index.row(); const int lastRow = values.count() - 1; if (row != lastRow) { /// Unless duplicate is last one in list, /// overwrite edited row with last row's value values[row].text = values[lastRow].text; values[row].value = values[lastRow].value; values[row].sortBy = values[lastRow].sortBy; emit dataChanged(index, index); } /// Remove last row, which is no longer used beginRemoveRows(QModelIndex(), lastRow, lastRow); values.remove(lastRow); endRemoveRows(); } diff --git a/src/gui/widgets/filterbar.cpp b/src/gui/widgets/filterbar.cpp index c649d2be..3d689bbd 100644 --- a/src/gui/widgets/filterbar.cpp +++ b/src/gui/widgets/filterbar.cpp @@ -1,324 +1,328 @@ /*************************************************************************** - * Copyright (C) 2004-2018 by Thomas Fischer * + * Copyright (C) 2004-2019 by Thomas Fischer * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, see . * ***************************************************************************/ #include "filterbar.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "delayedexecutiontimer.h" static bool sortStringsLocaleAware(const QString &s1, const QString &s2) { return QString::localeAwareCompare(s1, s2) < 0; } class FilterBar::FilterBarPrivate { private: FilterBar *p; public: KSharedConfigPtr config; const QString configGroupName; QComboBox *comboBoxFilterText; const int maxNumStoredFilterTexts; QComboBox *comboBoxCombination; QComboBox *comboBoxField; QPushButton *buttonSearchPDFfiles; QPushButton *buttonClearAll; DelayedExecutionTimer *delayedTimer; FilterBarPrivate(FilterBar *parent) : p(parent), config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))), configGroupName(QStringLiteral("Filter Bar")), maxNumStoredFilterTexts(12) { delayedTimer = new DelayedExecutionTimer(p); setupGUI(); connect(delayedTimer, &DelayedExecutionTimer::triggered, p, &FilterBar::publishFilter); } ~FilterBarPrivate() { delete delayedTimer; } void setupGUI() { QBoxLayout *layout = new QHBoxLayout(p); layout->setMargin(0); QLabel *label = new QLabel(i18n("Filter:"), p); layout->addWidget(label, 0); comboBoxFilterText = new QComboBox(p); label->setBuddy(comboBoxFilterText); layout->addWidget(comboBoxFilterText, 5); comboBoxFilterText->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred); comboBoxFilterText->setEditable(true); QFontMetrics metrics(comboBoxFilterText->font()); +#if QT_VERSION >= 0x050b00 + comboBoxFilterText->setMinimumWidth(metrics.horizontalAdvance(QStringLiteral("AIWaiw")) * 7); +#else // QT_VERSION >= 0x050b00 comboBoxFilterText->setMinimumWidth(metrics.width(QStringLiteral("AIWaiw")) * 7); +#endif // QT_VERSION >= 0x050b00 QLineEdit *lineEdit = static_cast(comboBoxFilterText->lineEdit()); lineEdit->setClearButtonEnabled(true); lineEdit->setPlaceholderText(i18n("Filter bibliographic entries")); comboBoxCombination = new QComboBox(p); layout->addWidget(comboBoxCombination, 1); comboBoxCombination->addItem(i18n("any word")); /// AnyWord=0 comboBoxCombination->addItem(i18n("every word")); /// EveryWord=1 comboBoxCombination->addItem(i18n("exact phrase")); /// ExactPhrase=2 comboBoxCombination->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); comboBoxField = new QComboBox(p); layout->addWidget(comboBoxField, 1); comboBoxField->addItem(i18n("any field"), QVariant()); comboBoxField->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); /// Use a hash map to get an alphabetically sorted list QHash fielddescs; for (const auto &fd : const_cast(BibTeXFields::instance())) if (fd.upperCamelCaseAlt.isEmpty()) fielddescs.insert(fd.label, fd.upperCamelCase); /// Sort locale-aware QList keys = fielddescs.keys(); std::sort(keys.begin(), keys.end(), sortStringsLocaleAware); for (const QString &key : const_cast &>(keys)) { const QString &value = fielddescs[key]; comboBoxField->addItem(key, value); } buttonSearchPDFfiles = new QPushButton(p); buttonSearchPDFfiles->setIcon(QIcon::fromTheme(QStringLiteral("application-pdf"))); buttonSearchPDFfiles->setToolTip(i18n("Include PDF files in full-text search")); buttonSearchPDFfiles->setCheckable(true); layout->addWidget(buttonSearchPDFfiles, 0); buttonClearAll = new QPushButton(p); buttonClearAll->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-locationbar-rtl"))); buttonClearAll->setToolTip(i18n("Reset filter criteria")); layout->addWidget(buttonClearAll, 0); /// restore history on filter texts /// see addCompletionString for more detailed explanation KConfigGroup configGroup(config, configGroupName); QStringList completionListDate = configGroup.readEntry(QStringLiteral("PreviousSearches"), QStringList()); for (QStringList::Iterator it = completionListDate.begin(); it != completionListDate.end(); ++it) comboBoxFilterText->addItem((*it).mid(12)); comboBoxFilterText->lineEdit()->setText(QString()); comboBoxCombination->setCurrentIndex(configGroup.readEntry("CurrentCombination", 0)); comboBoxField->setCurrentIndex(configGroup.readEntry("CurrentField", 0)); connect(comboBoxFilterText->lineEdit(), &QLineEdit::textChanged, delayedTimer, &DelayedExecutionTimer::trigger); connect(comboBoxFilterText->lineEdit(), &QLineEdit::returnPressed, p, &FilterBar::userPressedEnter); connect(comboBoxCombination, static_cast(&QComboBox::currentIndexChanged), p, &FilterBar::comboboxStatusChanged); connect(comboBoxField, static_cast(&QComboBox::currentIndexChanged), p, &FilterBar::comboboxStatusChanged); connect(buttonSearchPDFfiles, &QPushButton::toggled, p, &FilterBar::comboboxStatusChanged); connect(comboBoxCombination, static_cast(&QComboBox::currentIndexChanged), delayedTimer, &DelayedExecutionTimer::trigger); connect(comboBoxField, static_cast(&QComboBox::currentIndexChanged), delayedTimer, &DelayedExecutionTimer::trigger); connect(buttonSearchPDFfiles, &QPushButton::toggled, delayedTimer, &DelayedExecutionTimer::trigger); connect(buttonClearAll, &QPushButton::clicked, p, &FilterBar::resetState); } SortFilterFileModel::FilterQuery filter() { SortFilterFileModel::FilterQuery result; result.combination = comboBoxCombination->currentIndex() == 0 ? SortFilterFileModel::AnyTerm : SortFilterFileModel::EveryTerm; result.terms.clear(); if (comboBoxCombination->currentIndex() == 2) /// exact phrase result.terms << comboBoxFilterText->lineEdit()->text(); else { /// any or every word static const QRegularExpression sequenceOfSpacesRegExp(QStringLiteral("\\s+")); result.terms = comboBoxFilterText->lineEdit()->text().split(sequenceOfSpacesRegExp, QString::SkipEmptyParts); } result.field = comboBoxField->currentIndex() == 0 ? QString() : comboBoxField->itemData(comboBoxField->currentIndex(), Qt::UserRole).toString(); result.searchPDFfiles = buttonSearchPDFfiles->isChecked(); return result; } void setFilter(const SortFilterFileModel::FilterQuery &fq) { /// Avoid triggering loops of activation comboBoxCombination->blockSignals(true); /// Set check state for action for either "any word", /// "every word", or "exact phrase", respectively const int combinationIndex = fq.combination == SortFilterFileModel::AnyTerm ? 0 : (fq.terms.count() < 2 ? 2 : 1); comboBoxCombination->setCurrentIndex(combinationIndex); /// Reset activation block comboBoxCombination->blockSignals(false); /// Avoid triggering loops of activation comboBoxField->blockSignals(true); /// Find and check action that corresponds to field name ("author", ...) const QString lower = fq.field.toLower(); for (int idx = comboBoxField->count() - 1; idx >= 0; --idx) { if (comboBoxField->itemData(idx, Qt::UserRole).toString().toLower() == lower) { comboBoxField->setCurrentIndex(idx); break; } } /// Reset activation block comboBoxField->blockSignals(false); /// Avoid triggering loops of activation buttonSearchPDFfiles->blockSignals(true); /// Set flag if associated PDF files have to be searched buttonSearchPDFfiles->setChecked(fq.searchPDFfiles); /// Reset activation block buttonSearchPDFfiles->blockSignals(false); /// Avoid triggering loops of activation comboBoxFilterText->lineEdit()->blockSignals(true); /// Set filter text widget's content comboBoxFilterText->lineEdit()->setText(fq.terms.join(QStringLiteral(" "))); /// Reset activation block comboBoxFilterText->lineEdit()->blockSignals(false); } bool modelContainsText(QAbstractItemModel *model, const QString &text) { for (int row = 0; row < model->rowCount(); ++row) if (model->index(row, 0, QModelIndex()).data().toString().contains(text)) return true; return false; } void addCompletionString(const QString &text) { KConfigGroup configGroup(config, configGroupName); /// Previous searches are stored as a string list, where each individual /// string starts with 12 characters for the date and time when this /// search was used. Starting from the 13th character (12th, if you /// start counting from 0) the user's input is stored. /// This approach has several advantages: It does not require a more /// complex data structure, can easily read and written using /// KConfigGroup's functions, and can be sorted lexicographically/ /// chronologically using QStringList's sort. /// Disadvantage is that string fragments have to be managed manually. QStringList completionListDate = configGroup.readEntry(QStringLiteral("PreviousSearches"), QStringList()); for (QStringList::Iterator it = completionListDate.begin(); it != completionListDate.end();) if ((*it).mid(12) == text) it = completionListDate.erase(it); else ++it; completionListDate << (QDateTime::currentDateTime().toString(QStringLiteral("yyyyMMddhhmm")) + text); /// after sorting, discard all but the maxNumStoredFilterTexts most /// recent user-entered filter texts completionListDate.sort(); while (completionListDate.count() > maxNumStoredFilterTexts) completionListDate.removeFirst(); configGroup.writeEntry(QStringLiteral("PreviousSearches"), completionListDate); config->sync(); /// add user-entered filter text to combobox's drop-down list if (!text.isEmpty() && !modelContainsText(comboBoxFilterText->model(), text)) comboBoxFilterText->addItem(text); } void storeComboBoxStatus() { KConfigGroup configGroup(config, configGroupName); configGroup.writeEntry(QStringLiteral("CurrentCombination"), comboBoxCombination->currentIndex()); configGroup.writeEntry(QStringLiteral("CurrentField"), comboBoxField->currentIndex()); configGroup.writeEntry(QStringLiteral("SearchPDFFiles"), buttonSearchPDFfiles->isChecked()); config->sync(); } void restoreState() { KConfigGroup configGroup(config, configGroupName); comboBoxCombination->setCurrentIndex(configGroup.readEntry(QStringLiteral("CurrentCombination"), 0)); comboBoxField->setCurrentIndex(configGroup.readEntry(QStringLiteral("CurrentField"), 0)); buttonSearchPDFfiles->setChecked(configGroup.readEntry(QStringLiteral("SearchPDFFiles"), false)); } void resetState() { comboBoxFilterText->lineEdit()->clear(); comboBoxCombination->setCurrentIndex(0); comboBoxField->setCurrentIndex(0); buttonSearchPDFfiles->setChecked(false); } }; FilterBar::FilterBar(QWidget *parent) : QWidget(parent), d(new FilterBarPrivate(this)) { d->restoreState(); setFocusProxy(d->comboBoxFilterText); QTimer::singleShot(250, this, &FilterBar::buttonHeight); } FilterBar::~FilterBar() { delete d; } void FilterBar::setFilter(const SortFilterFileModel::FilterQuery &fq) { d->setFilter(fq); emit filterChanged(fq); } SortFilterFileModel::FilterQuery FilterBar::filter() { return d->filter(); } void FilterBar::setPlaceholderText(const QString &msg) { QLineEdit *lineEdit = static_cast(d->comboBoxFilterText->lineEdit()); lineEdit->setPlaceholderText(msg); } void FilterBar::comboboxStatusChanged() { d->buttonSearchPDFfiles->setEnabled(d->comboBoxField->currentIndex() == 0); d->storeComboBoxStatus(); } void FilterBar::resetState() { d->resetState(); emit filterChanged(d->filter()); } void FilterBar::userPressedEnter() { /// only store text in auto-completion if user pressed enter d->addCompletionString(d->comboBoxFilterText->lineEdit()->text()); publishFilter(); } void FilterBar::publishFilter() { emit filterChanged(d->filter()); } void FilterBar::buttonHeight() { QSizePolicy sp = d->buttonSearchPDFfiles->sizePolicy(); d->buttonSearchPDFfiles->setSizePolicy(sp.horizontalPolicy(), QSizePolicy::MinimumExpanding); d->buttonClearAll->setSizePolicy(sp.horizontalPolicy(), QSizePolicy::MinimumExpanding); } diff --git a/src/gui/widgets/starrating.cpp b/src/gui/widgets/starrating.cpp index 1fe7321e..11e348ba 100644 --- a/src/gui/widgets/starrating.cpp +++ b/src/gui/widgets/starrating.cpp @@ -1,300 +1,304 @@ /***************************************************************************** - * Copyright (C) 2004-2018 by Thomas Fischer * + * Copyright (C) 2004-2019 by Thomas Fischer * * * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, see . * *****************************************************************************/ #include "starrating.h" #include #include #include #include #include #include #include #include #include class StarRating::Private { private: StarRating *p; public: static const int paintMargin; bool isReadOnly; double percent; int maxNumberOfStars; int spacing; const QString unsetStarsText; QLabel *labelPercent; QPushButton *clearButton; QPoint mouseLocation; Private(int mnos, StarRating *parent) : p(parent), isReadOnly(false), percent(-1.0), maxNumberOfStars(mnos), unsetStarsText(i18n("Not set")) { QHBoxLayout *layout = new QHBoxLayout(p); spacing = qMax(layout->spacing(), 8); layout->setContentsMargins(0, 0, 0, 0); labelPercent = new QLabel(p); layout->addWidget(labelPercent, 0, Qt::AlignRight | Qt::AlignVCenter); - QFontMetrics fm(labelPercent->fontMetrics()); + const QFontMetrics fm(labelPercent->fontMetrics()); +#if QT_VERSION >= 0x050b00 + labelPercent->setFixedWidth(fm.horizontalAdvance(unsetStarsText)); +#else // QT_VERSION >= 0x050b00 labelPercent->setFixedWidth(fm.width(unsetStarsText)); +#endif // QT_VERSION >= 0x050b00 labelPercent->setAlignment(Qt::AlignRight | Qt::AlignVCenter); labelPercent->setText(unsetStarsText); labelPercent->installEventFilter(parent); layout->addStretch(1); clearButton = new QPushButton(QIcon::fromTheme(QStringLiteral("edit-clear-locationbar-rtl")), QString(), p); layout->addWidget(clearButton, 0, Qt::AlignRight | Qt::AlignVCenter); connect(clearButton, &QPushButton::clicked, p, &StarRating::clear); clearButton->installEventFilter(parent); } QRect starsInside() const { const int starRectHeight = qMin(labelPercent->height() * 3 / 2, clearButton->height()); return QRect(QPoint(labelPercent->width() + spacing, (p->height() - starRectHeight) / 2), QSize(p->width() - 2 * spacing - clearButton->width() - labelPercent->width(), starRectHeight)); } double percentForPosition(const QPoint pos, int numTotalStars, const QRect inside) { const int starSize = qMin(inside.height() - 2 * Private::paintMargin, (inside.width() - 2 * Private::paintMargin) / numTotalStars); const int width = starSize * numTotalStars; const int x = pos.x() - Private::paintMargin - inside.left(); const double percent = x * 100.0 / width; return qMax(0.0, qMin(100.0, percent)); } }; const int StarRating::Private::paintMargin = 2; StarRating::StarRating(int maxNumberOfStars, QWidget *parent) : QWidget(parent), d(new Private(maxNumberOfStars, this)) { QTimer::singleShot(250, this, &StarRating::buttonHeight); setMouseTracking(true); } void StarRating::paintEvent(QPaintEvent *ev) { QWidget::paintEvent(ev); QPainter p(this); const QRect r = d->starsInside(); const double percent = d->mouseLocation.isNull() ? d->percent : d->percentForPosition(d->mouseLocation, d->maxNumberOfStars, r); if (percent >= 0.0) { paintStars(&p, KIconLoader::DefaultState, d->maxNumberOfStars, percent, d->starsInside()); if (d->maxNumberOfStars < 10) d->labelPercent->setText(QString::number(percent * d->maxNumberOfStars / 100.0, 'f', 1)); else d->labelPercent->setText(QString::number(percent * d->maxNumberOfStars / 100)); } else { p.setOpacity(0.7); paintStars(&p, KIconLoader::DisabledState, d->maxNumberOfStars, 0.0, d->starsInside()); d->labelPercent->setText(d->unsetStarsText); } ev->accept(); } void StarRating::mouseReleaseEvent(QMouseEvent *ev) { QWidget::mouseReleaseEvent(ev); if (!d->isReadOnly && ev->button() == Qt::LeftButton) { d->mouseLocation = QPoint(); const double newPercent = d->percentForPosition(ev->pos(), d->maxNumberOfStars, d->starsInside()); setValue(newPercent); emit modified(); ev->accept(); } } void StarRating::mouseMoveEvent(QMouseEvent *ev) { QWidget::mouseMoveEvent(ev); if (!d->isReadOnly) { d->mouseLocation = ev->pos(); if (d->mouseLocation.x() < d->labelPercent->width() || d->mouseLocation.x() > width() - d->clearButton->width()) d->mouseLocation = QPoint(); update(); ev->accept(); } } void StarRating::leaveEvent(QEvent *ev) { QWidget::leaveEvent(ev); if (!d->isReadOnly) { d->mouseLocation = QPoint(); update(); ev->accept(); } } bool StarRating::eventFilter(QObject *obj, QEvent *event) { if (obj != d->labelPercent && obj != d->clearButton) return false; if ((event->type() == QEvent::MouseMove || event->type() == QEvent::Enter) && d->mouseLocation != QPoint()) { d->mouseLocation = QPoint(); update(); } return false; } double StarRating::value() const { return d->percent; } void StarRating::setValue(double percent) { if (d->isReadOnly) return; ///< disallow modifications if read-only if (percent >= 0.0 && percent <= 100.0) { d->percent = percent; update(); } } void StarRating::unsetValue() { if (d->isReadOnly) return; ///< disallow modifications if read-only d->mouseLocation = QPoint(); d->percent = -1.0; update(); } void StarRating::setReadOnly(bool isReadOnly) { d->isReadOnly = isReadOnly; d->clearButton->setEnabled(!isReadOnly); setMouseTracking(!isReadOnly); } void StarRating::clear() { if (d->isReadOnly) return; ///< disallow modifications if read-only unsetValue(); emit modified(); } void StarRating::buttonHeight() { const QSizePolicy sp = d->clearButton->sizePolicy(); /// Allow clear button to take as much vertical space as available d->clearButton->setSizePolicy(sp.horizontalPolicy(), QSizePolicy::MinimumExpanding); } void StarRating::paintStars(QPainter *painter, KIconLoader::States defaultState, int numTotalStars, double percent, const QRect inside) { painter->save(); ///< Save the current painter's state; at this function's end restored /// Calculate a single star's width/height /// so that all stars fit into the "inside" rectangle const int starSize = qMin(inside.height() - 2 * Private::paintMargin, (inside.width() - 2 * Private::paintMargin) / numTotalStars); /// First, draw active/golden/glowing stars (on the left side) /// Create a pixmap of a single active/golden/glowing star QPixmap starPixmap = KIconLoader::global()->loadIcon(QStringLiteral("rating"), KIconLoader::Small, starSize, defaultState); /// Calculate vertical position (same for all stars) const int y = inside.top() + (inside.height() - starSize) / 2; /// Number of full golden stars int numActiveStars = static_cast(percent * numTotalStars / 100); /// Number of golden pixels of the star that is /// partially golden and partially grey int coloredPartWidth = static_cast((percent * numTotalStars / 100 - numActiveStars) * starSize); /// Horizontal position of first star int x = inside.left() + Private::paintMargin; int i = 0; ///< start with first star /// Draw active (colored) stars for (; i < numActiveStars; ++i, x += starSize) painter->drawPixmap(x, y, starPixmap); if (coloredPartWidth > 0) { /// One star is partially colored, so draw star's golden left half painter->drawPixmap(x, y, starPixmap, 0, 0, coloredPartWidth, 0); } /// Second, draw grey/disabled stars (on the right side) /// To do so, replace the previously used golden star pixmal with a grey/disabled one starPixmap = KIconLoader::global()->loadIcon(QStringLiteral("rating"), KIconLoader::Small, starSize, KIconLoader::DisabledState); if (coloredPartWidth > 0) { /// One star is partially grey, so draw star's grey right half painter->drawPixmap(x + coloredPartWidth, y, starPixmap, coloredPartWidth, 0, starSize - coloredPartWidth, 0); x += starSize; ++i; } /// Draw the remaining inactive (grey) stars for (; i < numTotalStars; ++i, x += starSize) painter->drawPixmap(x, y, starPixmap); painter->restore(); ///< Restore the painter's state as saved at this function's beginning } bool StarRatingFieldInput::reset(const Value &value) { bool result = false; const QString text = PlainTextValue::text(value); if (text.isEmpty()) { unsetValue(); result = true; } else { const double number = text.toDouble(&result); if (result && number >= 0.0 && number <= 100.0) { setValue(number); result = true; } else { /// Some value provided that cannot be interpreted unsetValue(); } } return result; } bool StarRatingFieldInput::apply(Value &v) const { v.clear(); const double percent = value(); if (percent >= 0.0 && percent <= 100) v.append(QSharedPointer(new PlainText(QString::number(percent, 'f', 2)))); return true; } bool StarRatingFieldInput::validate(QWidget **, QString &) const { return true; } diff --git a/src/io/fileimporterbibtex.cpp b/src/io/fileimporterbibtex.cpp index ba271b40..85993594 100644 --- a/src/io/fileimporterbibtex.cpp +++ b/src/io/fileimporterbibtex.cpp @@ -1,1305 +1,1323 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, see <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "fileimporterbibtex.h" #include <QTextCodec> #include <QIODevice> #include <QRegularExpression> #include <QCoreApplication> #include <QStringList> #include <BibTeXEntries> #include <BibTeXFields> #include <Preferences> #include <File> #include <Comment> #include <Macro> #include <Preamble> #include <Entry> #include <Element> #include <Value> #include "encoder.h" #include "encoderlatex.h" #include "logging_io.h" #define qint64toint(a) (static_cast<int>(qMax(0LL,qMin(0x7fffffffLL,(a))))) FileImporterBibTeX::FileImporterBibTeX(QObject *parent) : FileImporter(parent), m_cancelFlag(false), m_textStream(nullptr), m_commentHandling(IgnoreComments), m_keywordCasing(KBibTeX::cLowerCase), m_lineNo(1) { m_keysForPersonDetection.append(Entry::ftAuthor); m_keysForPersonDetection.append(Entry::ftEditor); m_keysForPersonDetection.append(QStringLiteral("bookauthor")); /// used by JSTOR } File *FileImporterBibTeX::load(QIODevice *iodevice) { m_cancelFlag = false; if (!iodevice->isReadable() && !iodevice->open(QIODevice::ReadOnly)) { qCWarning(LOG_KBIBTEX_IO) << "Input device not readable"; emit message(SeverityError, QStringLiteral("Input device not readable")); return nullptr; } File *result = new File(); /// Used to determine if file prefers quotation marks over /// curly brackets or the other way around m_statistics.countCurlyBrackets = 0; m_statistics.countQuotationMarks = 0; m_statistics.countFirstNameFirst = 0; m_statistics.countLastNameFirst = 0; m_statistics.countNoCommentQuote = 0; m_statistics.countCommentPercent = 0; m_statistics.countCommentCommand = 0; m_statistics.countProtectedTitle = 0; m_statistics.countUnprotectedTitle = 0; m_statistics.mostRecentListSeparator.clear(); m_textStream = new QTextStream(iodevice); m_textStream->setCodec(Preferences::defaultBibTeXEncoding.toLatin1()); ///< unless we learn something else, assume default codec result->setProperty(File::Encoding, Preferences::defaultBibTeXEncoding); QString rawText; rawText.reserve(qint64toint(iodevice->size())); while (!m_textStream->atEnd()) { QString line = m_textStream->readLine(); bool skipline = evaluateParameterComments(m_textStream, line.toLower(), result); // FIXME XML data should be removed somewhere else? onlinesearch ... if (line.startsWith(QStringLiteral("<?xml")) && line.endsWith(QStringLiteral("?>"))) /// Hop over XML declarations skipline = true; if (!skipline) rawText.append(line).append("\n"); } delete m_textStream; /** Remove HTML code from the input source */ // FIXME HTML data should be removed somewhere else? onlinesearch ... const int originalLength = rawText.length(); rawText = rawText.remove(KBibTeX::htmlRegExp); const int afterHTMLremovalLength = rawText.length(); if (originalLength != afterHTMLremovalLength) { qCInfo(LOG_KBIBTEX_IO) << (originalLength - afterHTMLremovalLength) << "characters of HTML tags have been removed"; emit message(SeverityInfo, QString(QStringLiteral("%1 characters of HTML tags have been removed")).arg(originalLength - afterHTMLremovalLength)); } // TODO really necessary to pipe data through several QTextStreams? m_textStream = new QTextStream(&rawText, QIODevice::ReadOnly); m_textStream->setCodec(Preferences::defaultBibTeXEncoding.toLower() == QStringLiteral("latex") ? "us-ascii" : Preferences::defaultBibTeXEncoding.toLatin1()); m_lineNo = 1; m_prevLine = m_currentLine = QString(); m_knownElementIds.clear(); readChar(); while (!m_nextChar.isNull() && !m_cancelFlag && !m_textStream->atEnd()) { emit progress(qint64toint(m_textStream->pos()), rawText.length()); Element *element = nextElement(); if (element != nullptr) { if (m_commentHandling == KeepComments || !Comment::isComment(*element)) result->append(QSharedPointer<Element>(element)); else delete element; } } emit progress(100, 100); if (m_cancelFlag) { qCWarning(LOG_KBIBTEX_IO) << "Loading bibliography data has been canceled"; emit message(SeverityError, QStringLiteral("Loading bibliography data has been canceled")); delete result; result = nullptr; } delete m_textStream; if (result != nullptr) { /// Set the file's preferences for string delimiters /// deduced from statistics built while parsing the file result->setProperty(File::StringDelimiter, m_statistics.countQuotationMarks > m_statistics.countCurlyBrackets ? QStringLiteral("\"\"") : QStringLiteral("{}")); /// Set the file's preferences for name formatting result->setProperty(File::NameFormatting, m_statistics.countFirstNameFirst > m_statistics.countLastNameFirst ? Preferences::personNameFormatFirstLast : Preferences::personNameFormatLastFirst); /// Set the file's preferences for title protected Qt::CheckState triState = (m_statistics.countProtectedTitle > m_statistics.countUnprotectedTitle * 4) ? Qt::Checked : ((m_statistics.countProtectedTitle * 4 < m_statistics.countUnprotectedTitle) ? Qt::Unchecked : Qt::PartiallyChecked); result->setProperty(File::ProtectCasing, static_cast<int>(triState)); /// Set the file's preferences for quoting of comments if (m_statistics.countNoCommentQuote > m_statistics.countCommentCommand && m_statistics.countNoCommentQuote > m_statistics.countCommentPercent) result->setProperty(File::QuoteComment, static_cast<int>(Preferences::qcNone)); else if (m_statistics.countCommentCommand > m_statistics.countNoCommentQuote && m_statistics.countCommentCommand > m_statistics.countCommentPercent) result->setProperty(File::QuoteComment, static_cast<int>(Preferences::qcCommand)); else result->setProperty(File::QuoteComment, static_cast<int>(Preferences::qcPercentSign)); if (!m_statistics.mostRecentListSeparator.isEmpty()) result->setProperty(File::ListSeparator, m_statistics.mostRecentListSeparator); // TODO gather more statistics for keyword casing etc. } iodevice->close(); return result; } bool FileImporterBibTeX::guessCanDecode(const QString &rawText) { static const QRegularExpression bibtexLikeText(QStringLiteral("@\\w+\\{.+\\}")); QString text = EncoderLaTeX::instance().decode(rawText); return bibtexLikeText.match(text).hasMatch(); } void FileImporterBibTeX::cancel() { m_cancelFlag = true; } Element *FileImporterBibTeX::nextElement() { Token token = nextToken(); if (token == tAt) { const QString elementType = readSimpleString(); const QString elementTypeLower = elementType.toLower(); if (elementTypeLower == QStringLiteral("comment")) { ++m_statistics.countCommentCommand; return readCommentElement(); } else if (elementTypeLower == QStringLiteral("string")) return readMacroElement(); else if (elementTypeLower == QStringLiteral("preamble")) return readPreambleElement(); else if (elementTypeLower == QStringLiteral("import")) { qCDebug(LOG_KBIBTEX_IO) << "Skipping potential HTML/JavaScript @import statement near line" << m_lineNo; emit message(SeverityInfo, QString(QStringLiteral("Skipping potential HTML/JavaScript @import statement near line %1")).arg(m_lineNo)); return nullptr; } else if (!elementType.isEmpty()) return readEntryElement(elementType); else { qCWarning(LOG_KBIBTEX_IO) << "Element type after '@' is empty or invalid near line" << m_lineNo; emit message(SeverityError, QString(QStringLiteral("Element type after '@' is empty or invalid near line %1")).arg(m_lineNo)); return nullptr; } } else if (token == tUnknown && m_nextChar == QLatin1Char('%')) { /// do not complain about LaTeX-like comments, just eat them ++m_statistics.countCommentPercent; return readPlainCommentElement(QString()); } else if (token == tUnknown) { if (m_nextChar.isLetter()) { qCDebug(LOG_KBIBTEX_IO) << "Unknown character" << m_nextChar << "near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << ")" << ", treating as comment"; emit message(SeverityInfo, QString(QStringLiteral("Unknown character '%1' near line %2, treating as comment")).arg(m_nextChar).arg(m_lineNo)); } else if (m_nextChar.isPrint()) { qCDebug(LOG_KBIBTEX_IO) << "Unknown character" << m_nextChar << "(" << QString(QStringLiteral("0x%1")).arg(m_nextChar.unicode(), 4, 16, QLatin1Char('0')) << ") near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << ")" << ", treating as comment"; emit message(SeverityInfo, QString(QStringLiteral("Unknown character '%1' (0x%2) near line %3, treating as comment")).arg(m_nextChar).arg(m_nextChar.unicode(), 4, 16, QLatin1Char('0')).arg(m_lineNo)); } else { qCDebug(LOG_KBIBTEX_IO) << "Unknown character" << QString(QStringLiteral("0x%1")).arg(m_nextChar.unicode(), 4, 16, QLatin1Char('0')) << "near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << ")" << ", treating as comment"; emit message(SeverityInfo, QString(QStringLiteral("Unknown character 0x%1 near line %2, treating as comment")).arg(m_nextChar.unicode(), 4, 16, QLatin1Char('0')).arg(m_lineNo)); } ++m_statistics.countNoCommentQuote; return readPlainCommentElement(QString(m_prevChar) + m_nextChar); } if (token != tEOF) { qCWarning(LOG_KBIBTEX_IO) << "Don't know how to parse next token of type" << tokenidToString(token) << "in line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << ")" << endl; emit message(SeverityError, QString(QStringLiteral("Don't know how to parse next token of type %1 in line %2")).arg(tokenidToString(token)).arg(m_lineNo)); } return nullptr; } Comment *FileImporterBibTeX::readCommentElement() { if (!readCharUntil(QStringLiteral("{("))) return nullptr; return new Comment(EncoderLaTeX::instance().decode(readBracketString())); } Comment *FileImporterBibTeX::readPlainCommentElement(const QString &prefix) { QString result = EncoderLaTeX::instance().decode(prefix + readLine()); while (m_nextChar == QLatin1Char('\n') || m_nextChar == QLatin1Char('\r')) readChar(); while (!m_nextChar.isNull() && m_nextChar != QLatin1Char('@')) { const QChar nextChar = m_nextChar; const QString line = readLine(); while (m_nextChar == QLatin1Char('\n') || m_nextChar == QLatin1Char('\r')) readChar(); result.append(EncoderLaTeX::instance().decode((nextChar == QLatin1Char('%') ? QString() : QString(nextChar)) + line)); } if (result.startsWith(QStringLiteral("x-kbibtex"))) { qCWarning(LOG_KBIBTEX_IO) << "Plain comment element starts with 'x-kbibtex', this should not happen"; emit message(SeverityWarning, QStringLiteral("Plain comment element starts with 'x-kbibtex', this should not happen")); /// ignore special comments return nullptr; } return new Comment(result); } Macro *FileImporterBibTeX::readMacroElement() { Token token = nextToken(); while (token != tBracketOpen) { if (token == tEOF) { qCWarning(LOG_KBIBTEX_IO) << "Error in parsing macro near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "): Opening curly brace '{' expected"; emit message(SeverityError, QString(QStringLiteral("Error in parsing macro near line %1: Opening curly brace '{' expected")).arg(m_lineNo)); return nullptr; } token = nextToken(); } QString key = readSimpleString(); if (key.isEmpty()) { /// Cope with empty keys, /// duplicates are handled further below key = QStringLiteral("EmptyId"); } else if (!Encoder::containsOnlyAscii(key)) { /// Try to avoid non-ascii characters in ids const QString newKey = Encoder::instance().convertToPlainAscii(key); qCWarning(LOG_KBIBTEX_IO) << "Macro key" << key << "near line" << m_lineNo << "contains non-ASCII characters, converted to" << newKey; emit message(SeverityWarning, QString(QStringLiteral("Macro key '%1' near line %2 contains non-ASCII characters, converted to '%3'")).arg(key).arg(m_lineNo).arg(newKey)); key = newKey; } /// Check for duplicate entry ids, avoid collisions if (m_knownElementIds.contains(key)) { static const QString newIdPattern = QStringLiteral("%1-%2"); int idx = 2; QString newKey = newIdPattern.arg(key).arg(idx); while (m_knownElementIds.contains(newKey)) newKey = newIdPattern.arg(key).arg(++idx); qCDebug(LOG_KBIBTEX_IO) << "Duplicate macro key" << key << ", using replacement key" << newKey; emit message(SeverityWarning, QString(QStringLiteral("Duplicate macro key '%1', using replacement key '%2'")).arg(key, newKey)); key = newKey; } m_knownElementIds.insert(key); if (nextToken() != tAssign) { qCCritical(LOG_KBIBTEX_IO) << "Error in parsing macro" << key << "near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "): Assign symbol '=' expected"; emit message(SeverityError, QString(QStringLiteral("Error in parsing macro '%1' near line %2: Assign symbol '=' expected")).arg(key).arg(m_lineNo)); return nullptr; } Macro *macro = new Macro(key); do { bool isStringKey = false; QString text = readString(isStringKey); if (text.isNull()) { qCWarning(LOG_KBIBTEX_IO) << "Error in parsing macro" << key << "near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "): Could not read macro's text"; emit message(SeverityError, QString(QStringLiteral("Error in parsing macro '%1' near line %2: Could not read macro's text")).arg(key).arg(m_lineNo)); delete macro; } text = EncoderLaTeX::instance().decode(bibtexAwareSimplify(text)); if (isStringKey) macro->value().append(QSharedPointer<MacroKey>(new MacroKey(text))); else macro->value().append(QSharedPointer<PlainText>(new PlainText(text))); token = nextToken(); } while (token == tDoublecross); return macro; } Preamble *FileImporterBibTeX::readPreambleElement() { Token token = nextToken(); while (token != tBracketOpen) { if (token == tEOF) { qCWarning(LOG_KBIBTEX_IO) << "Error in parsing preamble near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "): Opening curly brace '{' expected"; emit message(SeverityError, QString(QStringLiteral("Error in parsing preamble near line %1: Opening curly brace '{' expected")).arg(m_lineNo)); return nullptr; } token = nextToken(); } Preamble *preamble = new Preamble(); do { bool isStringKey = false; QString text = readString(isStringKey); if (text.isNull()) { qCWarning(LOG_KBIBTEX_IO) << "Error in parsing preamble near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "): Could not read preamble's text"; emit message(SeverityError, QString(QStringLiteral("Error in parsing preamble near line %1: Could not read preamble's text")).arg(m_lineNo)); delete preamble; return nullptr; } /// Remember: strings from preamble do not get encoded, /// may contain raw LaTeX commands and code text = bibtexAwareSimplify(text); if (isStringKey) preamble->value().append(QSharedPointer<MacroKey>(new MacroKey(text))); else preamble->value().append(QSharedPointer<PlainText>(new PlainText(text))); token = nextToken(); } while (token == tDoublecross); return preamble; } Entry *FileImporterBibTeX::readEntryElement(const QString &typeString) { Token token = nextToken(); while (token != tBracketOpen) { if (token == tEOF) { qCWarning(LOG_KBIBTEX_IO) << "Error in parsing entry near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "): Opening curly brace '{' expected"; emit message(SeverityError, QString(QStringLiteral("Error in parsing entry near line %1: Opening curly brace '{' expected")).arg(m_lineNo)); return nullptr; } token = nextToken(); } QString id = readSimpleString(QStringLiteral(",}"), true).trimmed(); if (id.isEmpty()) { if (m_nextChar == QLatin1Char(',') || m_nextChar == QLatin1Char('}')) { /// Cope with empty ids, /// duplicates are handled further below id = QStringLiteral("EmptyId"); } else { qCWarning(LOG_KBIBTEX_IO) << "Error in parsing entry near line" << m_lineNo << ":" << m_prevLine << endl << m_currentLine << "): Could not read entry id"; emit message(SeverityError, QString(QStringLiteral("Error in parsing preambentryle near line %1: Could not read entry id")).arg(m_lineNo)); return nullptr; } } else { if (id.contains(QStringLiteral("\\")) || id.contains(QStringLiteral("{"))) { const QString newId = EncoderLaTeX::instance().decode(id); qCWarning(LOG_KBIBTEX_IO) << "Entry id" << id << "near line" << m_lineNo << "contains backslashes or curly brackets, converted to" << newId; emit message(SeverityWarning, QString(QStringLiteral("Entry id '%1' near line %2 contains backslashes or curly brackets, converted to '%3'")).arg(id).arg(m_lineNo).arg(newId)); id = newId; } if (!Encoder::containsOnlyAscii(id)) { /// Try to avoid non-ascii characters in ids const QString newId = Encoder::instance().convertToPlainAscii(id); qCWarning(LOG_KBIBTEX_IO) << "Entry id" << id << "near line" << m_lineNo << "contains non-ASCII characters, converted to" << newId; emit message(SeverityWarning, QString(QStringLiteral("Entry id '%1' near line %2 contains non-ASCII characters, converted to '%3'")).arg(id).arg(m_lineNo).arg(newId)); id = newId; } } static const QVector<QChar> invalidIdCharacters = {QLatin1Char('{'), QLatin1Char('}'), QLatin1Char(',')}; for (const QChar &invalidIdCharacter : invalidIdCharacters) if (id.contains(invalidIdCharacter)) { qCWarning(LOG_KBIBTEX_IO) << "Entry id" << id << "near line" << m_lineNo << "contains invalid character" << invalidIdCharacter; emit message(SeverityError, QString(QStringLiteral("Entry id '%1' near line %2 contains invalid character '%3'")).arg(id).arg(m_lineNo).arg(invalidIdCharacter)); return nullptr; } /// Check for duplicate entry ids, avoid collisions if (m_knownElementIds.contains(id)) { static const QString newIdPattern = QStringLiteral("%1-%2"); int idx = 2; QString newId = newIdPattern.arg(id).arg(idx); while (m_knownElementIds.contains(newId)) newId = newIdPattern.arg(id).arg(++idx); qCDebug(LOG_KBIBTEX_IO) << "Duplicate id" << id << "near line" << m_lineNo << ", using replacement id" << newId; emit message(SeverityInfo, QString(QStringLiteral("Duplicate id '%1' near line %2, using replacement id '%3'")).arg(id).arg(m_lineNo).arg(newId)); id = newId; } m_knownElementIds.insert(id); Entry *entry = new Entry(BibTeXEntries::instance().format(typeString, m_keywordCasing), id); token = nextToken(); do { if (token == tBracketClose) break; else if (token == tEOF) { qCWarning(LOG_KBIBTEX_IO) << "Unexpected end of data in entry" << id << "near line" << m_lineNo << ":" << m_prevLine << endl << m_currentLine; emit message(SeverityError, QString(QStringLiteral("Unexpected end of data in entry '%1' near line %2")).arg(id).arg(m_lineNo)); delete entry; return nullptr; } else if (token != tComma) { if (m_nextChar.isLetter()) { qCWarning(LOG_KBIBTEX_IO) << "Error in parsing entry" << id << "near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "): Comma symbol ',' expected but got character" << m_nextChar << "(token" << tokenidToString(token) << ")"; emit message(SeverityError, QString(QStringLiteral("Error in parsing entry '%1' near line %2: Comma symbol ',' expected but got character '%3' (token %4)")).arg(id).arg(m_lineNo).arg(m_nextChar).arg(tokenidToString(token))); } else if (m_nextChar.isPrint()) { qCWarning(LOG_KBIBTEX_IO) << "Error in parsing entry" << id << "near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "): Comma symbol ',' expected but got character" << m_nextChar << "(" << QString(QStringLiteral("0x%1")).arg(m_nextChar.unicode(), 4, 16, QLatin1Char('0')) << ", token" << tokenidToString(token) << ")"; emit message(SeverityError, QString(QStringLiteral("Error in parsing entry '%1' near line %2: Comma symbol ',' expected but got character '%3' (0x%4, token %5)")).arg(id).arg(m_lineNo).arg(m_nextChar).arg(m_nextChar.unicode(), 4, 16, QLatin1Char('0')).arg(tokenidToString(token))); } else { qCWarning(LOG_KBIBTEX_IO) << "Error in parsing entry" << id << "near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "): Comma symbol (,) expected but got character" << QString(QStringLiteral("0x%1")).arg(m_nextChar.unicode(), 4, 16, QLatin1Char('0')) << "(token" << tokenidToString(token) << ")"; emit message(SeverityError, QString(QStringLiteral("Error in parsing entry '%1' near line %2: Comma symbol ',' expected but got character 0x%3 (token %4)")).arg(id).arg(m_lineNo).arg(m_nextChar.unicode(), 4, 16, QLatin1Char('0')).arg(tokenidToString(token))); } delete entry; return nullptr; } QString keyName = BibTeXFields::instance().format(readSimpleString(), m_keywordCasing); if (keyName.isEmpty()) { token = nextToken(); if (token == tBracketClose) { /// Most often it is the case that the previous line ended with a comma, /// implying that this entry continues, but instead it gets closed by /// a closing curly bracket. qCDebug(LOG_KBIBTEX_IO) << "Issue while parsing entry" << id << "near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "): Last key-value pair ended with a non-conformant comma, ignoring that"; emit message(SeverityInfo, QString(QStringLiteral("Issue while parsing entry '%1' near line %2: Last key-value pair ended with a non-conformant comma, ignoring that")).arg(id).arg(m_lineNo)); break; } else { /// Something looks terribly wrong qCWarning(LOG_KBIBTEX_IO) << "Error in parsing entry" << id << "near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "): Closing curly bracket expected, but found" << tokenidToString(token); emit message(SeverityError, QString(QStringLiteral("Error in parsing entry '%1' near line %2: Closing curly bracket expected, but found %3")).arg(id).arg(m_lineNo).arg(tokenidToString(token))); delete entry; return nullptr; } } /// Try to avoid non-ascii characters in keys const QString newkeyName = Encoder::instance().convertToPlainAscii(keyName); if (newkeyName != keyName) { qCWarning(LOG_KBIBTEX_IO) << "Field name " << keyName << "near line" << m_lineNo << "contains non-ASCII characters, converted to" << newkeyName; emit message(SeverityWarning, QString(QStringLiteral("Field name '%1' near line %2 contains non-ASCII characters, converted to '%3'")).arg(keyName).arg(m_lineNo).arg(newkeyName)); keyName = newkeyName; } token = nextToken(); if (token != tAssign) { qCWarning(LOG_KBIBTEX_IO) << "Error in parsing entry" << id << ", field name" << keyName << "near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "): Assign symbol '=' expected after field name"; emit message(SeverityError, QString(QStringLiteral("Error in parsing entry '%1', field name '%2' near line %3: Assign symbol '=' expected after field name")).arg(id, keyName).arg(m_lineNo)); delete entry; return nullptr; } Value value; /// check for duplicate fields if (entry->contains(keyName)) { if (keyName.toLower() == Entry::ftKeywords || keyName.toLower() == Entry::ftUrl) { /// Special handling of keywords and URLs: instead of using fallback names /// like "keywords2", "keywords3", ..., append new keywords to /// already existing keyword value value = entry->value(keyName); } else if (m_keysForPersonDetection.contains(keyName.toLower())) { /// Special handling of authors and editors: instead of using fallback names /// like "author2", "author3", ..., append new authors to /// already existing author value value = entry->value(keyName); } else { int i = 2; QString appendix = QString::number(i); while (entry->contains(keyName + appendix)) { ++i; appendix = QString::number(i); } qCDebug(LOG_KBIBTEX_IO) << "Entry" << id << "already contains a key" << keyName << "near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "), using" << (keyName + appendix); emit message(SeverityWarning, QString(QStringLiteral("Entry '%1' already contains a key '%2' near line %4, using '%3'")).arg(id, keyName, keyName + appendix).arg(m_lineNo)); keyName += appendix; } } token = readValue(value, keyName); if (token != tBracketClose && token != tComma) { qCWarning(LOG_KBIBTEX_IO) << "Failed to read value in entry" << id << ", field name" << keyName << "near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << ")"; emit message(SeverityError, QString(QStringLiteral("Failed to read value in entry '%1', field name '%2' near line %3")).arg(id, keyName).arg(m_lineNo)); delete entry; return nullptr; } entry->insert(keyName, value); } while (true); return entry; } FileImporterBibTeX::Token FileImporterBibTeX::nextToken() { if (!skipWhiteChar()) { /// Some error occurred while reading from data stream return tEOF; } Token result = tUnknown; switch (m_nextChar.toLatin1()) { case '@': result = tAt; break; case '{': case '(': result = tBracketOpen; break; case '}': case ')': result = tBracketClose; break; case ',': result = tComma; break; case '=': result = tAssign; break; case '#': result = tDoublecross; break; default: if (m_textStream->atEnd()) result = tEOF; } if (m_nextChar != QLatin1Char('%')) { /// Unclean solution, but necessary for comments /// that have a percent sign as a prefix readChar(); } return result; } QString FileImporterBibTeX::readString(bool &isStringKey) { /// Most often it is not a string key isStringKey = false; if (!skipWhiteChar()) { /// Some error occurred while reading from data stream - return QString::null; + return QString(); ///< return null QString } switch (m_nextChar.toLatin1()) { case '{': case '(': { ++m_statistics.countCurlyBrackets; const QString result = readBracketString(); return result; } case '"': { ++m_statistics.countQuotationMarks; const QString result = readQuotedString(); return result; } default: isStringKey = true; const QString result = readSimpleString(); return result; } } QString FileImporterBibTeX::readSimpleString(const QString &until, const bool readNestedCurlyBrackets) { static const QString extraAlphaNumChars = QString(QStringLiteral("?'`-_:.+/$\\\"&")); QString result; ///< 'result' is Null on purpose: simple strings cannot be empty in contrast to e.g. quoted strings if (!skipWhiteChar()) { /// Some error occurred while reading from data stream - return QString::null; + return QString(); ///< return null QString } QChar prevChar = QChar(0x00); while (!m_nextChar.isNull()) { if (readNestedCurlyBrackets && m_nextChar == QLatin1Char('{') && prevChar != QLatin1Char('\\')) { int depth = 1; while (depth > 0) { result.append(m_nextChar); prevChar = m_nextChar; if (!readChar()) return result; if (m_nextChar == QLatin1Char('{') && prevChar != QLatin1Char('\\')) ++depth; else if (m_nextChar == QLatin1Char('}') && prevChar != QLatin1Char('\\')) --depth; } result.append(m_nextChar); prevChar = m_nextChar; if (!readChar()) return result; } const ushort nextCharUnicode = m_nextChar.unicode(); if (!until.isEmpty()) { /// Variable "until" has user-defined value if (m_nextChar == QLatin1Char('\n') || m_nextChar == QLatin1Char('\r') || until.contains(m_nextChar)) { /// Force break on line-breaks or if one of the "until" chars has been read break; } else { /// Append read character to final result result.append(m_nextChar); } } else if ((nextCharUnicode >= (ushort)'a' && nextCharUnicode <= (ushort)'z') || (nextCharUnicode >= (ushort)'A' && nextCharUnicode <= (ushort)'Z') || (nextCharUnicode >= (ushort)'0' && nextCharUnicode <= (ushort)'9') || extraAlphaNumChars.contains(m_nextChar)) { /// Accept default set of alpha-numeric characters result.append(m_nextChar); } else break; prevChar = m_nextChar; if (!readChar()) break; } return result; } QString FileImporterBibTeX::readQuotedString() { QString result(0, QChar()); ///< Construct an empty but non-null string Q_ASSERT_X(m_nextChar == QLatin1Char('"'), "QString FileImporterBibTeX::readQuotedString()", "m_nextChar is not '\"'"); - if (!readChar()) return QString::null; + if (!readChar()) { + /// Some error occurred while reading from data stream + return QString(); ///< return null QString + } while (!m_nextChar.isNull()) { if (m_nextChar == QLatin1Char('"') && m_prevChar != QLatin1Char('\\') && m_prevChar != QLatin1Char('{')) break; else result.append(m_nextChar); - if (!readChar()) return QString::null; + if (!readChar()) { + /// Some error occurred while reading from data stream + return QString(); ///< return null QString + } } - if (!readChar()) return QString::null; + if (!readChar()) { + /// Some error occurred while reading from data stream + return QString(); ///< return null QString + } /// Remove protection around quotation marks result.replace(QStringLiteral("{\"}"), QStringLiteral("\"")); return result; } QString FileImporterBibTeX::readBracketString() { static const QChar backslash = QLatin1Char('\\'); QString result(0, QChar()); ///< Construct an empty but non-null string const QChar openingBracket = m_nextChar; const QChar closingBracket = openingBracket == QLatin1Char('{') ? QLatin1Char('}') : (openingBracket == QLatin1Char('(') ? QLatin1Char(')') : QChar()); Q_ASSERT_X(!closingBracket.isNull(), "QString FileImporterBibTeX::readBracketString()", "openingBracket==m_nextChar is neither '{' nor '('"); int counter = 1; - if (!readChar()) return QString::null; + if (!readChar()) { + /// Some error occurred while reading from data stream + return QString(); ///< return null QString + } while (!m_nextChar.isNull()) { if (m_nextChar == openingBracket && m_prevChar != backslash) ++counter; else if (m_nextChar == closingBracket && m_prevChar != backslash) --counter; if (counter == 0) { break; } else result.append(m_nextChar); - if (!readChar()) return QString::null; + if (!readChar()) { + /// Some error occurred while reading from data stream + return QString(); ///< return null QString + } } - if (!readChar()) return QString::null; + if (!readChar()) { + /// Some error occurred while reading from data stream + return QString(); ///< return null QString + } return result; } FileImporterBibTeX::Token FileImporterBibTeX::readValue(Value &value, const QString &key) { Token token = tUnknown; const QString iKey = key.toLower(); static const QSet<QString> verbatimKeys {Entry::ftColor.toLower(), Entry::ftCrossRef.toLower(), Entry::ftXData.toLower()}; do { bool isStringKey = false; const QString rawText = readString(isStringKey); if (rawText.isNull()) return tEOF; QString text = EncoderLaTeX::instance().decode(rawText); /// for all entries except for abstracts ... if (iKey != Entry::ftAbstract && !(iKey.startsWith(Entry::ftUrl) && !iKey.startsWith(Entry::ftUrlDate)) && !iKey.startsWith(Entry::ftLocalFile) && !iKey.startsWith(Entry::ftFile)) { /// ... remove redundant spaces including newlines text = bibtexAwareSimplify(text); } /// abstracts will keep their formatting (regarding line breaks) /// as requested by Thomas Jensch via mail (20 October 2010) /// Maintain statistics on if (book) titles are protected /// by surrounding curly brackets if (iKey == Entry::ftTitle || iKey == Entry::ftBookTitle) { if (text[0] == QLatin1Char('{') && text[text.length() - 1] == QLatin1Char('}')) ++m_statistics.countProtectedTitle; else ++m_statistics.countUnprotectedTitle; } if (m_keysForPersonDetection.contains(iKey)) { if (isStringKey) value.append(QSharedPointer<MacroKey>(new MacroKey(text))); else { CommaContainment comma = ccContainsComma; parsePersonList(text, value, &comma, m_lineNo, this); /// Update statistics on name formatting if (comma == ccContainsComma) ++m_statistics.countLastNameFirst; else ++m_statistics.countFirstNameFirst; } } else if (iKey == Entry::ftPages) { static const QRegularExpression rangeInAscii(QStringLiteral("\\s*--?\\s*")); text.replace(rangeInAscii, QChar(0x2013)); if (isStringKey) value.append(QSharedPointer<MacroKey>(new MacroKey(text))); else value.append(QSharedPointer<PlainText>(new PlainText(text))); } else if ((iKey.startsWith(Entry::ftUrl) && !iKey.startsWith(Entry::ftUrlDate)) || iKey.startsWith(Entry::ftLocalFile) || iKey.startsWith(Entry::ftFile) || iKey == QStringLiteral("ee") || iKey == QStringLiteral("biburl")) { if (isStringKey) value.append(QSharedPointer<MacroKey>(new MacroKey(text))); else { /// Assumption: in fields like Url or LocalFile, file names are separated by ; static const QRegularExpression semicolonSpace = QRegularExpression(QStringLiteral("[;]\\s*")); const QStringList fileList = rawText.split(semicolonSpace, QString::SkipEmptyParts); for (const QString &filename : fileList) { value.append(QSharedPointer<VerbatimText>(new VerbatimText(filename))); } } } else if (iKey.startsWith(Entry::ftFile)) { if (isStringKey) value.append(QSharedPointer<MacroKey>(new MacroKey(text))); else { /// Assumption: this field was written by Mendeley, which uses /// a very strange format for file names: /// :C$\backslash$:/Users/BarisEvrim/Documents/Mendeley Desktop/GeversPAMI10.pdf:pdf /// :: /// :Users/Fred/Library/Application Support/Mendeley Desktop/Downloaded/Hasselman et al. - 2011 - (Still) Growing Up What should we be a realist about in the cognitive and behavioural sciences Abstract.pdf:pdf const QRegularExpressionMatch match = KBibTeX::mendeleyFileRegExp.match(rawText); if (match.hasMatch()) { static const QString backslashLaTeX = QStringLiteral("$\\backslash$"); QString filename = match.captured(1).remove(backslashLaTeX); if (filename.startsWith(QStringLiteral("home/")) || filename.startsWith(QStringLiteral("Users/"))) { /// Mendeley doesn't have a slash at the beginning of absolute paths, /// so, insert one /// See bug 19833, comment 5: https://gna.org/bugs/index.php?19833#comment5 filename.prepend(QLatin1Char('/')); } value.append(QSharedPointer<VerbatimText>(new VerbatimText(filename))); } else value.append(QSharedPointer<VerbatimText>(new VerbatimText(text))); } } else if (iKey == Entry::ftMonth) { if (isStringKey) { static const QRegularExpression monthThreeChars(QStringLiteral("^[a-z]{3}"), QRegularExpression::CaseInsensitiveOption); if (monthThreeChars.match(text).hasMatch()) text = text.left(3).toLower(); value.append(QSharedPointer<MacroKey>(new MacroKey(text))); } else value.append(QSharedPointer<PlainText>(new PlainText(text))); } else if (iKey.startsWith(Entry::ftDOI)) { if (isStringKey) value.append(QSharedPointer<MacroKey>(new MacroKey(text))); else { /// Take care of "; " which separates multiple DOIs, but which may baffle the regexp QString preprocessedText = rawText; preprocessedText.replace(QStringLiteral("; "), QStringLiteral(" ")); /// Extract everything that looks like a DOI using a regular expression, /// ignore everything else QRegularExpressionMatchIterator doiRegExpMatchIt = KBibTeX::doiRegExp.globalMatch(preprocessedText); while (doiRegExpMatchIt.hasNext()) { const QRegularExpressionMatch doiRegExpMatch = doiRegExpMatchIt.next(); value.append(QSharedPointer<VerbatimText>(new VerbatimText(doiRegExpMatch.captured(0)))); } } } else if (iKey == Entry::ftKeywords) { if (isStringKey) value.append(QSharedPointer<MacroKey>(new MacroKey(text))); else { char splitChar; const QList<QSharedPointer<Keyword> > keywords = splitKeywords(text, &splitChar); for (const auto &keyword : keywords) value.append(keyword); /// Memorize (some) split characters for later use /// (e.g. when writing file again) if (splitChar == ';') m_statistics.mostRecentListSeparator = QStringLiteral("; "); else if (splitChar == ',') m_statistics.mostRecentListSeparator = QStringLiteral(", "); } } else if (verbatimKeys.contains(iKey)) { if (isStringKey) value.append(QSharedPointer<MacroKey>(new MacroKey(text))); else value.append(QSharedPointer<VerbatimText>(new VerbatimText(rawText))); } else { if (isStringKey) value.append(QSharedPointer<MacroKey>(new MacroKey(text))); else value.append(QSharedPointer<PlainText>(new PlainText(text))); } token = nextToken(); } while (token == tDoublecross); return token; } bool FileImporterBibTeX::readChar() { /// Memorize previous char m_prevChar = m_nextChar; if (m_textStream->atEnd()) { /// At end of data stream m_nextChar = QChar::Null; return false; } /// Read next char *m_textStream >> m_nextChar; /// Test for new line if (m_nextChar == QLatin1Char('\n')) { /// Update variables tracking line numbers and line content ++m_lineNo; m_prevLine = m_currentLine; m_currentLine.clear(); } else { /// Add read char to current line m_currentLine.append(m_nextChar); } return true; } bool FileImporterBibTeX::readCharUntil(const QString &until) { Q_ASSERT_X(!until.isEmpty(), "bool FileImporterBibTeX::readCharUntil(const QString &until)", "\"until\" is empty or invalid"); bool result = true; while (!until.contains(m_nextChar) && (result = readChar())); return result; } bool FileImporterBibTeX::skipWhiteChar() { bool result = true; while ((m_nextChar.isSpace() || m_nextChar == QLatin1Char('\t') || m_nextChar == QLatin1Char('\n') || m_nextChar == QLatin1Char('\r')) && result) result = readChar(); return result; } QString FileImporterBibTeX::readLine() { QString result; while (m_nextChar != QLatin1Char('\n') && m_nextChar != QLatin1Char('\r') && readChar()) result.append(m_nextChar); return result; } QList<QSharedPointer<Keyword> > FileImporterBibTeX::splitKeywords(const QString &text, char *usedSplitChar) { QList<QSharedPointer<Keyword> > result; static const QHash<char, QRegularExpression> splitAlong = { {'\n', QRegularExpression(QStringLiteral("\\s*\n\\s*"))}, {';', QRegularExpression(QStringLiteral("\\s*;\\s*"))}, {',', QRegularExpression(QString("\\s*,\\s*"))} }; if (usedSplitChar != nullptr) *usedSplitChar = '\0'; for (auto it = splitAlong.constBegin(); it != splitAlong.constEnd(); ++it) { /// check if character is contained in text (should be cheap to test) if (text.contains(QLatin1Char(it.key()))) { /// split text along a pattern like spaces-splitchar-spaces /// extract keywords static const QRegularExpression unneccessarySpacing(QStringLiteral("[ \n\r\t]+")); const QStringList keywords = text.split(it.value(), QString::SkipEmptyParts).replaceInStrings(unneccessarySpacing, QStringLiteral(" ")); /// build QList of Keyword objects from keywords for (const QString &keyword : keywords) { result.append(QSharedPointer<Keyword>(new Keyword(keyword))); } /// Memorize (some) split characters for later use /// (e.g. when writing file again) if (usedSplitChar != nullptr) *usedSplitChar = it.key(); /// no more splits necessary break; } } /// no split was performed, so whole text must be a single keyword if (result.isEmpty()) result.append(QSharedPointer<Keyword>(new Keyword(text))); return result; } QList<QSharedPointer<Person> > FileImporterBibTeX::splitNames(const QString &text, const int line_number, QObject *parent) { /// Case: Smith, John and Johnson, Tim /// Case: Smith, John and Fulkerson, Ford and Johnson, Tim /// Case: Smith, John, Fulkerson, Ford, and Johnson, Tim /// Case: John Smith and Tim Johnson /// Case: John Smith and Ford Fulkerson and Tim Johnson /// Case: Smith, John, Johnson, Tim /// Case: Smith, John, Fulkerson, Ford, Johnson, Tim /// Case: John Smith, Tim Johnson /// Case: John Smith, Tim Johnson, Ford Fulkerson /// Case: Smith, John ; Johnson, Tim ; Fulkerson, Ford (IEEE Xplore) /// German case: Robert A. Gehring und Bernd Lutterbeck QString internalText = text; /// Remove invalid characters such as dots or (double) daggers for footnotes static const QList<QChar> invalidChars {QChar(0x00b7), QChar(0x2020), QChar(0x2217), QChar(0x2021), QChar(0x002a), QChar(0x21d1) /** Upwards double arrow */}; for (const auto &invalidChar : invalidChars) /// Replacing daggers with commas ensures that they act as persons' names separator internalText = internalText.replace(invalidChar, QChar(',')); /// Remove numbers to footnotes static const QRegularExpression numberFootnoteRegExp(QStringLiteral("(\\w)\\d+\\b")); internalText = internalText.replace(numberFootnoteRegExp, QStringLiteral("\\1")); /// Remove academic degrees static const QRegularExpression academicDegreesRegExp(QStringLiteral("(,\\s*)?(MA|PhD)\\b")); internalText = internalText.remove(academicDegreesRegExp); /// Remove email addresses static const QRegularExpression emailAddressRegExp(QStringLiteral("\\b[a-zA-Z0-9][a-zA-Z0-9._-]+[a-zA-Z0-9]@[a-z0-9][a-z0-9-]*([.][a-z0-9-]+)*([.][a-z]+)+\\b")); internalText = internalText.remove(emailAddressRegExp); /// Split input string into tokens which are either name components (first or last name) /// or full names (composed of first and last name), depending on the input string's structure static const QRegularExpression split(QStringLiteral("\\s*([,]+|[,]*\\b[au]nd\\b|[;]|&|\\n|\\s{4,})\\s*")); const QStringList authorTokenList = internalText.split(split, QString::SkipEmptyParts); bool containsSpace = true; for (QStringList::ConstIterator it = authorTokenList.constBegin(); containsSpace && it != authorTokenList.constEnd(); ++it) containsSpace = (*it).contains(QChar(' ')); QList<QSharedPointer<Person> > result; result.reserve(authorTokenList.size()); if (containsSpace) { /// Tokens look like "John Smith" for (const QString &authorToken : authorTokenList) { QSharedPointer<Person> person = personFromString(authorToken, nullptr, line_number, parent); if (!person.isNull()) result.append(person); } } else { /// Tokens look like "Smith" or "John" /// Assumption: two consecutive tokens form a name for (QStringList::ConstIterator it = authorTokenList.constBegin(); it != authorTokenList.constEnd(); ++it) { QString lastname = *it; ++it; if (it != authorTokenList.constEnd()) { lastname += QStringLiteral(", ") + (*it); QSharedPointer<Person> person = personFromString(lastname, nullptr, line_number, parent); if (!person.isNull()) result.append(person); } else break; } } return result; } void FileImporterBibTeX::parsePersonList(const QString &text, Value &value, const int line_number, QObject *parent) { parsePersonList(text, value, nullptr, line_number, parent); } void FileImporterBibTeX::parsePersonList(const QString &text, Value &value, CommaContainment *comma, const int line_number, QObject *parent) { static const QString tokenAnd = QStringLiteral("and"); static const QString tokenOthers = QStringLiteral("others"); static QStringList tokens; contextSensitiveSplit(text, tokens); if (tokens.count() > 0) { if (tokens[0] == tokenAnd) { qCInfo(LOG_KBIBTEX_IO) << "Person list starts with" << tokenAnd << "near line" << line_number; if (parent != nullptr) QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, SeverityWarning), Q_ARG(QString, QString(QStringLiteral("Person list starts with 'and' near line %1")).arg(line_number))); } else if (tokens.count() > 1 && tokens[tokens.count() - 1] == tokenAnd) { qCInfo(LOG_KBIBTEX_IO) << "Person list ends with" << tokenAnd << "near line" << line_number; if (parent != nullptr) QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, SeverityWarning), Q_ARG(QString, QString(QStringLiteral("Person list ends with 'and' near line %1")).arg(line_number))); } if (tokens[0] == tokenOthers) { qCInfo(LOG_KBIBTEX_IO) << "Person list starts with" << tokenOthers << "near line" << line_number; if (parent != nullptr) QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, SeverityWarning), Q_ARG(QString, QString(QStringLiteral("Person list starts with 'others' near line %1")).arg(line_number))); } else if (tokens[tokens.count() - 1] == tokenOthers && (tokens.count() < 3 || tokens[tokens.count() - 2] != tokenAnd)) { qCInfo(LOG_KBIBTEX_IO) << "Person list ends with" << tokenOthers << "but is not preceeded with name and" << tokenAnd << "near line" << line_number; if (parent != nullptr) QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, SeverityWarning), Q_ARG(QString, QString(QStringLiteral("Person list ends with 'others' but is not preceeded with name and 'and' near line %1")).arg(line_number))); } } int nameStart = 0; QString prevToken; for (int i = 0; i < tokens.count(); ++i) { if (tokens[i] == tokenAnd) { if (prevToken == tokenAnd) { qCInfo(LOG_KBIBTEX_IO) << "Two subsequent" << tokenAnd << "found in person list near line" << line_number; if (parent != nullptr) QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, SeverityWarning), Q_ARG(QString, QString(QStringLiteral("Two subsequent 'and' found in person list near line %1")).arg(line_number))); } else if (nameStart < i) { const QSharedPointer<Person> person = personFromTokenList(tokens.mid(nameStart, i - nameStart), comma, line_number, parent); if (!person.isNull()) value.append(person); else { qCInfo(LOG_KBIBTEX_IO) << "Text" << tokens.mid(nameStart, i - nameStart).join(' ') << "does not form a name near line" << line_number; if (parent != nullptr) QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, SeverityWarning), Q_ARG(QString, QString(QStringLiteral("Text '%1' does not form a name near line %2")).arg(tokens.mid(nameStart, i - nameStart).join(' ')).arg(line_number))); } } else { qCInfo(LOG_KBIBTEX_IO) << "Found" << tokenAnd << "but no name before it near line" << line_number; if (parent != nullptr) QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, SeverityWarning), Q_ARG(QString, QString(QStringLiteral("Found 'and' but no name before it near line %1")).arg(line_number))); } nameStart = i + 1; } else if (tokens[i] == tokenOthers) { if (i < tokens.count() - 1) { qCInfo(LOG_KBIBTEX_IO) << "Special word" << tokenOthers << "found before last position in person name near line" << line_number; if (parent != nullptr) QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, SeverityWarning), Q_ARG(QString, QString(QStringLiteral("Special word 'others' found before last position in person name near line %1")).arg(line_number))); } else value.append(QSharedPointer<PlainText>(new PlainText(QStringLiteral("others")))); nameStart = tokens.count() + 1; } prevToken = tokens[i]; } if (nameStart < tokens.count()) { const QSharedPointer<Person> person = personFromTokenList(tokens.mid(nameStart), comma, line_number, parent); if (!person.isNull()) value.append(person); else { qCInfo(LOG_KBIBTEX_IO) << "Text" << tokens.mid(nameStart).join(' ') << "does not form a name near line" << line_number; if (parent != nullptr) QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, SeverityWarning), Q_ARG(QString, QString(QStringLiteral("Text '%1' does not form a name near line %2")).arg(tokens.mid(nameStart).join(' ')).arg(line_number))); } } } QSharedPointer<Person> FileImporterBibTeX::personFromString(const QString &name, const int line_number, QObject *parent) { return personFromString(name, nullptr, line_number, parent); } QSharedPointer<Person> FileImporterBibTeX::personFromString(const QString &name, CommaContainment *comma, const int line_number, QObject *parent) { static QStringList tokens; contextSensitiveSplit(name, tokens); return personFromTokenList(tokens, comma, line_number, parent); } QSharedPointer<Person> FileImporterBibTeX::personFromTokenList(const QStringList &tokens, CommaContainment *comma, const int line_number, QObject *parent) { if (comma != nullptr) *comma = ccNoComma; /// Simple case: provided list of tokens is empty, return invalid Person if (tokens.isEmpty()) return QSharedPointer<Person>(); /** * Sequence of tokens may contain somewhere a comma, like * "Tuckwell," "Peter". In this case, fill two string lists: * one with tokens before the comma, one with tokens after the * comma (excluding the comma itself). Example: * partA = ( "Tuckwell" ); partB = ( "Peter" ); partC = ( "Jr." ) * If a comma was found, boolean variable gotComma is set. */ QStringList partA, partB, partC; int commaCount = 0; for (const QString &token : tokens) { /// Position where comma was found, or -1 if no comma in token int p = -1; if (commaCount < 2) { /// Only check if token contains comma /// if no comma was found before int bracketCounter = 0; for (int i = 0; i < token.length(); ++i) { /// Consider opening curly brackets if (token[i] == QChar('{')) ++bracketCounter; /// Consider closing curly brackets else if (token[i] == QChar('}')) --bracketCounter; /// Only if outside any open curly bracket environments /// consider comma characters else if (bracketCounter == 0 && token[i] == QChar(',')) { /// Memorize comma's position and break from loop p = i; break; } else if (bracketCounter < 0) { /// Should never happen: more closing brackets than opening ones qCWarning(LOG_KBIBTEX_IO) << "Opening and closing brackets do not match near line" << line_number; if (parent != nullptr) QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, SeverityWarning), Q_ARG(QString, QString(QStringLiteral("Opening and closing brackets do not match near line %1")).arg(line_number))); } } } if (p >= 0) { if (commaCount == 0) { if (p > 0) partA.append(token.left(p)); if (p < token.length() - 1) partB.append(token.mid(p + 1)); } else if (commaCount == 1) { if (p > 0) partB.append(token.left(p)); if (p < token.length() - 1) partC.append(token.mid(p + 1)); } ++commaCount; } else if (commaCount == 0) partA.append(token); else if (commaCount == 1) partB.append(token); else if (commaCount == 2) partC.append(token); } if (commaCount > 0) { if (comma != nullptr) *comma = ccContainsComma; return QSharedPointer<Person>(new Person(partC.isEmpty() ? partB.join(QChar(' ')) : partC.join(QChar(' ')), partA.join(QChar(' ')), partC.isEmpty() ? QString() : partB.join(QChar(' ')))); } /** * PubMed uses a special writing style for names, where the * last name is followed by single capital letters, each being * the first letter of each first name. Example: Tuckwell P H * So, check how many single capital letters are at the end of * the given token list */ partA.clear(); partB.clear(); bool singleCapitalLetters = true; QStringList::ConstIterator it = tokens.constEnd(); while (it != tokens.constBegin()) { --it; if (singleCapitalLetters && it->length() == 1 && it->at(0).isUpper()) partB.prepend(*it); else { singleCapitalLetters = false; partA.prepend(*it); } } if (!partB.isEmpty()) { /// Name was actually given in PubMed format return QSharedPointer<Person>(new Person(partB.join(QChar(' ')), partA.join(QChar(' ')))); } /** * Normally, the last upper case token in a name is the last name * (last names consisting of multiple space-separated parts *have* * to be protected by {...}), but some languages have fill words * in lower case belonging to the last name as well (example: "van"). * In addition, some languages have capital case letters as well * (example: "Di Cosmo"). * Exception: Special keywords such as "Jr." can be appended to the * name, not counted as part of the last name. */ partA.clear(); partB.clear(); partC.clear(); static const QSet<QString> capitalCaseLastNameFragments {QStringLiteral("Di")}; it = tokens.constEnd(); while (it != tokens.constBegin()) { --it; if (partB.isEmpty() && (it->toLower().startsWith(QStringLiteral("jr")) || it->toLower().startsWith(QStringLiteral("sr")) || it->toLower().startsWith(QStringLiteral("iii")))) /// handle name suffices like "Jr" or "III." partC.prepend(*it); else if (partB.isEmpty() || it->at(0).isLower() || capitalCaseLastNameFragments.contains(*it)) partB.prepend(*it); else partA.prepend(*it); } if (!partB.isEmpty()) { /// Name was actually like "Peter Ole van der Tuckwell", /// split into "Peter Ole" and "van der Tuckwell" return QSharedPointer<Person>(new Person(partA.join(QChar(' ')), partB.join(QChar(' ')), partC.isEmpty() ? QString() : partC.join(QChar(' ')))); } qCWarning(LOG_KBIBTEX_IO) << "Don't know how to handle name" << tokens.join(QLatin1Char(' ')) << "near line" << line_number; if (parent != nullptr) QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, SeverityWarning), Q_ARG(QString, QString(QStringLiteral("Don't know how to handle name '%1' near line %2")).arg(tokens.join(QLatin1Char(' '))).arg(line_number))); return QSharedPointer<Person>(); } void FileImporterBibTeX::contextSensitiveSplit(const QString &text, QStringList &segments) { int bracketCounter = 0; ///< keep track of opening and closing brackets: {...} QString buffer; int len = text.length(); segments.clear(); ///< empty list for results before proceeding for (int pos = 0; pos < len; ++pos) { if (text[pos] == '{') ++bracketCounter; else if (text[pos] == '}') --bracketCounter; if (text[pos].isSpace() && bracketCounter == 0) { if (!buffer.isEmpty()) { segments.append(buffer); buffer.clear(); } } else buffer.append(text[pos]); } if (!buffer.isEmpty()) segments.append(buffer); } QString FileImporterBibTeX::bibtexAwareSimplify(const QString &text) { QString result; int i = 0; /// Consume initial spaces ... while (i < text.length() && text[i].isSpace()) ++i; /// ... but if there have been spaces (i.e. i>0), then record a single space only if (i > 0) result.append(QStringLiteral(" ")); while (i < text.length()) { /// Consume non-spaces while (i < text.length() && !text[i].isSpace()) { result.append(text[i]); ++i; } /// String may end with a non-space if (i >= text.length()) break; /// Consume spaces, ... while (i < text.length() && text[i].isSpace()) ++i; /// ... but record only a single space result.append(QStringLiteral(" ")); } return result; } bool FileImporterBibTeX::evaluateParameterComments(QTextStream *textStream, const QString &line, File *file) { /// Assertion: variable "line" is all lower-case /** check if this file requests a special encoding */ if (line.startsWith(QStringLiteral("@comment{x-kbibtex-encoding=")) && line.endsWith(QLatin1Char('}'))) { const QString encoding = line.mid(28, line.length() - 29).toLower(); textStream->setCodec(encoding.toLower() == QStringLiteral("latex") ? "us-ascii" : encoding.toLatin1()); file->setProperty(File::Encoding, encoding.toLower() == QStringLiteral("latex") ? encoding : QString::fromLatin1(textStream->codec()->name())); return true; } else if (line.startsWith(QStringLiteral("@comment{x-kbibtex-personnameformatting=")) && line.endsWith(QLatin1Char('}'))) { // TODO usage of x-kbibtex-personnameformatting is deprecated, // as automatic detection is in place QString personNameFormatting = line.mid(40, line.length() - 41); file->setProperty(File::NameFormatting, personNameFormatting); return true; } else if (line.startsWith(QStringLiteral("% encoding:"))) { /// Interprete JabRef's encoding information QString encoding = line.mid(12); qCDebug(LOG_KBIBTEX_IO) << "Using JabRef's encoding:" << encoding; textStream->setCodec(encoding.toLatin1()); file->setProperty(File::Encoding, QString::fromLatin1(textStream->codec()->name())); return true; } return false; } QString FileImporterBibTeX::tokenidToString(Token token) { switch (token) { case tAt: return QString(QStringLiteral("At")); case tBracketClose: return QString(QStringLiteral("BracketClose")); case tBracketOpen: return QString(QStringLiteral("BracketOpen")); case tAlphaNumText: return QString(QStringLiteral("AlphaNumText")); case tAssign: return QString(QStringLiteral("Assign")); case tComma: return QString(QStringLiteral("Comma")); case tDoublecross: return QString(QStringLiteral("Doublecross")); case tEOF: return QString(QStringLiteral("EOF")); case tUnknown: return QString(QStringLiteral("Unknown")); default: return QString(QStringLiteral("<Unknown>")); } } void FileImporterBibTeX::setCommentHandling(CommentHandling commentHandling) { m_commentHandling = commentHandling; } diff --git a/src/program/docklets/valuelist.cpp b/src/program/docklets/valuelist.cpp index 5ec37c01..7bcce0ee 100644 --- a/src/program/docklets/valuelist.cpp +++ b/src/program/docklets/valuelist.cpp @@ -1,485 +1,491 @@ /*************************************************************************** - * Copyright (C) 2004-2018 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * + * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, see <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "valuelist.h" #include <typeinfo> #include <QTreeView> #include <QHeaderView> #include <QGridLayout> #include <QStringListModel> #include <QScrollBar> #include <QLineEdit> #include <QComboBox> #include <QTimer> #include <QSortFilterProxyModel> #include <QAction> #include <KConfigGroup> #include <KLocalizedString> #include <KToggleAction> #include <KSharedConfig> #include <BibTeXFields> #include <Entry> #include <file/FileView> #include <ValueListModel> #include <models/FileModel> class ValueList::ValueListPrivate { private: ValueList *p; ValueListDelegate *delegate; public: KSharedConfigPtr config; const QString configGroupName; const QString configKeyFieldName, configKeyShowCountColumn, configKeySortByCountAction, configKeyHeaderState; FileView *fileView; QTreeView *treeviewFieldValues; ValueListModel *model; QSortFilterProxyModel *sortingModel; QComboBox *comboboxFieldNames; QLineEdit *lineeditFilter; const int countWidth; QAction *assignSelectionAction; QAction *removeSelectionAction; KToggleAction *showCountColumnAction; KToggleAction *sortByCountAction; ValueListPrivate(ValueList *parent) : p(parent), config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))), configGroupName(QStringLiteral("Value List Docklet")), configKeyFieldName(QStringLiteral("FieldName")), configKeyShowCountColumn(QStringLiteral("ShowCountColumn")), configKeySortByCountAction(QStringLiteral("SortByCountAction")), configKeyHeaderState(QStringLiteral("HeaderState")), - fileView(nullptr), model(nullptr), sortingModel(nullptr), countWidth(8 + parent->fontMetrics().width(i18n("Count"))) { + fileView(nullptr), model(nullptr), sortingModel(nullptr), +#if QT_VERSION >= 0x050b00 + countWidth(8 + parent->fontMetrics().horizontalAdvance(i18n("Count"))) +#else // QT_VERSION >= 0x050b00 + countWidth(8 + parent->fontMetrics().width(i18n("Count"))) +#endif // QT_VERSION >= 0x050b00 + { setupGUI(); initialize(); } void setupGUI() { QBoxLayout *layout = new QVBoxLayout(p); layout->setMargin(0); comboboxFieldNames = new QComboBox(p); comboboxFieldNames->setEditable(true); layout->addWidget(comboboxFieldNames); lineeditFilter = new QLineEdit(p); layout->addWidget(lineeditFilter); lineeditFilter->setClearButtonEnabled(true); lineeditFilter->setPlaceholderText(i18n("Filter value list")); treeviewFieldValues = new QTreeView(p); layout->addWidget(treeviewFieldValues); treeviewFieldValues->setEditTriggers(QAbstractItemView::EditKeyPressed); treeviewFieldValues->setSortingEnabled(true); treeviewFieldValues->sortByColumn(0, Qt::AscendingOrder); delegate = new ValueListDelegate(treeviewFieldValues); treeviewFieldValues->setItemDelegate(delegate); treeviewFieldValues->setRootIsDecorated(false); treeviewFieldValues->setSelectionMode(QTreeView::ExtendedSelection); treeviewFieldValues->setAlternatingRowColors(true); treeviewFieldValues->header()->setSectionResizeMode(QHeaderView::Fixed); treeviewFieldValues->setContextMenuPolicy(Qt::ActionsContextMenu); /// create context menu item to start renaming QAction *action = new QAction(QIcon::fromTheme(QStringLiteral("edit-rename")), i18n("Replace all occurrences"), p); connect(action, &QAction::triggered, p, &ValueList::startItemRenaming); treeviewFieldValues->addAction(action); /// create context menu item to delete value action = new QAction(QIcon::fromTheme(QStringLiteral("edit-table-delete-row")), i18n("Delete all occurrences"), p); connect(action, &QAction::triggered, p, &ValueList::deleteAllOccurrences); treeviewFieldValues->addAction(action); /// create context menu item to search for multiple selections action = new QAction(QIcon::fromTheme(QStringLiteral("edit-find")), i18n("Search for selected values"), p); connect(action, &QAction::triggered, p, &ValueList::searchSelection); treeviewFieldValues->addAction(action); /// create context menu item to assign value to selected bibliography elements assignSelectionAction = new QAction(QIcon::fromTheme(QStringLiteral("emblem-new")), i18n("Add value to selected entries"), p); connect(assignSelectionAction, &QAction::triggered, p, &ValueList::assignSelection); treeviewFieldValues->addAction(assignSelectionAction); /// create context menu item to remove value from selected bibliography elements removeSelectionAction = new QAction(QIcon::fromTheme(QStringLiteral("list-remove")), i18n("Remove value from selected entries"), p); connect(removeSelectionAction, &QAction::triggered, p, &ValueList::removeSelection); treeviewFieldValues->addAction(removeSelectionAction); p->setEnabled(false); connect(comboboxFieldNames, static_cast<void(QComboBox::*)(int)>(&QComboBox::activated), p, &ValueList::fieldNamesChanged); connect(comboboxFieldNames, static_cast<void(QComboBox::*)(int)>(&QComboBox::activated), lineeditFilter, &QLineEdit::clear); connect(treeviewFieldValues, &QTreeView::activated, p, &ValueList::listItemActivated); connect(delegate, &ValueListDelegate::closeEditor, treeviewFieldValues, &QTreeView::reset); /// add context menu to header treeviewFieldValues->header()->setContextMenuPolicy(Qt::ActionsContextMenu); showCountColumnAction = new KToggleAction(i18n("Show Count Column"), treeviewFieldValues); connect(showCountColumnAction, &QAction::triggered, p, &ValueList::showCountColumnToggled); treeviewFieldValues->header()->addAction(showCountColumnAction); sortByCountAction = new KToggleAction(i18n("Sort by Count"), treeviewFieldValues); connect(sortByCountAction, &QAction::triggered, p, &ValueList::sortByCountToggled); treeviewFieldValues->header()->addAction(sortByCountAction); } void setComboboxFieldNamesCurrentItem(const QString &text) { int index = comboboxFieldNames->findData(text, Qt::UserRole, Qt::MatchExactly); if (index < 0) index = comboboxFieldNames->findData(text, Qt::UserRole, Qt::MatchStartsWith); if (index < 0) index = comboboxFieldNames->findData(text, Qt::UserRole, Qt::MatchContains); if (index >= 0) comboboxFieldNames->setCurrentIndex(index); } void initialize() { lineeditFilter->clear(); comboboxFieldNames->clear(); for (const auto &fd : const_cast<const BibTeXFields &>(BibTeXFields::instance())) { if (!fd.upperCamelCaseAlt.isEmpty()) continue; /// keep only "single" fields and not combined ones like "Author or Editor" if (fd.upperCamelCase.startsWith('^')) continue; /// skip "type" and "id" comboboxFieldNames->addItem(fd.label, fd.upperCamelCase); } /// Sort the combo box locale-aware. Thus we need a SortFilterProxyModel QSortFilterProxyModel *proxy = new QSortFilterProxyModel(comboboxFieldNames); proxy->setSortLocaleAware(true); proxy->setSourceModel(comboboxFieldNames->model()); comboboxFieldNames->model()->setParent(proxy); comboboxFieldNames->setModel(proxy); comboboxFieldNames->model()->sort(0); KConfigGroup configGroup(config, configGroupName); QString fieldName = configGroup.readEntry(configKeyFieldName, QString(Entry::ftAuthor)); setComboboxFieldNamesCurrentItem(fieldName); if (allowsMultipleValues(fieldName)) assignSelectionAction->setText(i18n("Add value to selected entries")); else assignSelectionAction->setText(i18n("Replace value of selected entries")); showCountColumnAction->setChecked(configGroup.readEntry(configKeyShowCountColumn, true)); sortByCountAction->setChecked(configGroup.readEntry(configKeySortByCountAction, false)); sortByCountAction->setEnabled(!showCountColumnAction->isChecked()); QByteArray headerState = configGroup.readEntry(configKeyHeaderState, QByteArray()); treeviewFieldValues->header()->restoreState(headerState); connect(treeviewFieldValues->header(), &QHeaderView::sortIndicatorChanged, p, &ValueList::columnsChanged); } void update() { QString text = comboboxFieldNames->itemData(comboboxFieldNames->currentIndex()).toString(); if (text.isEmpty()) text = comboboxFieldNames->currentText(); delegate->setFieldName(text); model = fileView == nullptr ? nullptr : fileView->valueListModel(text); QAbstractItemModel *usedModel = model; if (usedModel != nullptr) { model->setShowCountColumn(showCountColumnAction->isChecked()); model->setSortBy(sortByCountAction->isChecked() ? ValueListModel::SortByCount : ValueListModel::SortByText); if (sortingModel != nullptr) delete sortingModel; sortingModel = new QSortFilterProxyModel(p); sortingModel->setSourceModel(model); if (treeviewFieldValues->header()->isSortIndicatorShown()) sortingModel->sort(treeviewFieldValues->header()->sortIndicatorSection(), treeviewFieldValues->header()->sortIndicatorOrder()); else sortingModel->sort(1, Qt::DescendingOrder); sortingModel->setSortRole(ValueListModel::SortRole); sortingModel->setFilterKeyColumn(0); sortingModel->setFilterCaseSensitivity(Qt::CaseInsensitive); sortingModel->setFilterRole(ValueListModel::SearchTextRole); connect(lineeditFilter, &QLineEdit::textEdited, sortingModel, &QSortFilterProxyModel::setFilterFixedString); sortingModel->setSortLocaleAware(true); usedModel = sortingModel; } treeviewFieldValues->setModel(usedModel); KConfigGroup configGroup(config, configGroupName); configGroup.writeEntry(configKeyFieldName, text); config->sync(); } bool allowsMultipleValues(const QString &field) const { return (field.compare(Entry::ftAuthor, Qt::CaseInsensitive) == 0 || field.compare(Entry::ftEditor, Qt::CaseInsensitive) == 0 || field.compare(Entry::ftUrl, Qt::CaseInsensitive) == 0 || field.compare(Entry::ftFile, Qt::CaseInsensitive) == 0 || field.compare(Entry::ftLocalFile, Qt::CaseInsensitive) == 0 || field.compare(Entry::ftDOI, Qt::CaseInsensitive) == 0 || field.compare(Entry::ftKeywords, Qt::CaseInsensitive) == 0); } }; ValueList::ValueList(QWidget *parent) : QWidget(parent), d(new ValueListPrivate(this)) { QTimer::singleShot(500, this, &ValueList::delayedResize); } ValueList::~ValueList() { delete d; } void ValueList::setFileView(FileView *fileView) { if (d->fileView != nullptr) disconnect(d->fileView, &FileView::selectedElementsChanged, this, &ValueList::editorSelectionChanged); d->fileView = fileView; if (d->fileView != nullptr) { connect(d->fileView, &FileView::selectedElementsChanged, this, &ValueList::editorSelectionChanged); connect(d->fileView, &FileView::destroyed, this, &ValueList::editorDestroyed); } editorSelectionChanged(); update(); resizeEvent(nullptr); } void ValueList::update() { d->update(); setEnabled(d->fileView != nullptr); } void ValueList::resizeEvent(QResizeEvent *) { int widgetWidth = d->treeviewFieldValues->size().width() - d->treeviewFieldValues->verticalScrollBar()->size().width() - 8; d->treeviewFieldValues->setColumnWidth(0, widgetWidth - d->countWidth); d->treeviewFieldValues->setColumnWidth(1, d->countWidth); } void ValueList::listItemActivated(const QModelIndex &index) { setEnabled(false); QString itemText = d->sortingModel->mapToSource(index).data(ValueListModel::SearchTextRole).toString(); QVariant fieldVar = d->comboboxFieldNames->itemData(d->comboboxFieldNames->currentIndex()); QString fieldText = fieldVar.toString(); if (fieldText.isEmpty()) fieldText = d->comboboxFieldNames->currentText(); SortFilterFileModel::FilterQuery fq; fq.terms << itemText; fq.combination = SortFilterFileModel::EveryTerm; fq.field = fieldText; fq.searchPDFfiles = false; d->fileView->setFilterBarFilter(fq); setEnabled(true); } void ValueList::searchSelection() { QVariant fieldVar = d->comboboxFieldNames->itemData(d->comboboxFieldNames->currentIndex()); QString fieldText = fieldVar.toString(); if (fieldText.isEmpty()) fieldText = d->comboboxFieldNames->currentText(); SortFilterFileModel::FilterQuery fq; fq.combination = SortFilterFileModel::EveryTerm; fq.field = fieldText; const auto selectedIndexes = d->treeviewFieldValues->selectionModel()->selectedIndexes(); for (const QModelIndex &index : selectedIndexes) { if (index.column() == 0) { QString itemText = index.data(ValueListModel::SearchTextRole).toString(); fq.terms << itemText; } } fq.searchPDFfiles = false; if (!fq.terms.isEmpty()) d->fileView->setFilterBarFilter(fq); } void ValueList::assignSelection() { QString field = d->comboboxFieldNames->itemData(d->comboboxFieldNames->currentIndex()).toString(); if (field.isEmpty()) field = d->comboboxFieldNames->currentText(); if (field.isEmpty()) return; ///< empty field is invalid; quit const Value toBeAssignedValue = d->sortingModel->mapToSource(d->treeviewFieldValues->currentIndex()).data(Qt::EditRole).value<Value>(); if (toBeAssignedValue.isEmpty()) return; ///< empty value is invalid; quit const QString toBeAssignedValueText = PlainTextValue::text(toBeAssignedValue); /// Keep track if any modifications were made to the bibliography file bool madeModification = false; /// Go through all selected elements in current editor const QList<QSharedPointer<Element> > &selection = d->fileView->selectedElements(); for (const auto &element : selection) { /// Only entries (not macros or comments) are of interest QSharedPointer<Entry> entry = element.dynamicCast<Entry>(); if (!entry.isNull()) { /// Fields are separated into two categories: /// 1. Where more values can be appended, like authors or URLs /// 2. Where values should be replaced, like title, year, or journal if (d->allowsMultipleValues(field)) { /// Fields for which multiple values are valid bool valueItemAlreadyContained = false; ///< add only if to-be-assigned value is not yet contained Value entrysValueForField = entry->value(field); for (const auto &containedValueItem : const_cast<const Value &>(entrysValueForField)) { valueItemAlreadyContained |= PlainTextValue::text(containedValueItem) == toBeAssignedValueText; if (valueItemAlreadyContained) break; } if (!valueItemAlreadyContained) { /// Add each ValueItem from the to-be-assigned value to the entry's value for this field entrysValueForField.reserve(toBeAssignedValue.size()); for (const auto &newValueItem : toBeAssignedValue) { entrysValueForField.append(newValueItem); } /// "Write back" value to field in entry entry->remove(field); entry->insert(field, entrysValueForField); /// Keep track that bibliography file has been modified madeModification = true; } } else { /// Fields for which only value is valid, thus the old value will be replaced entry->remove(field); entry->insert(field, toBeAssignedValue); /// Keep track that bibliography file has been modified madeModification = true; } } } if (madeModification) { /// Notify main editor about change it its data d->fileView->externalModification(); } } void ValueList::removeSelection() { QString field = d->comboboxFieldNames->itemData(d->comboboxFieldNames->currentIndex()).toString(); if (field.isEmpty()) field = d->comboboxFieldNames->currentText(); if (field.isEmpty()) return; ///< empty field is invalid; quit const Value toBeRemovedValue = d->sortingModel->mapToSource(d->treeviewFieldValues->currentIndex()).data(Qt::EditRole).value<Value>(); if (toBeRemovedValue.isEmpty()) return; ///< empty value is invalid; quit const QString toBeRemovedValueText = PlainTextValue::text(toBeRemovedValue); /// Keep track if any modifications were made to the bibliography file bool madeModification = false; /// Go through all selected elements in current editor const QList<QSharedPointer<Element> > &selection = d->fileView->selectedElements(); for (const auto &element : selection) { /// Only entries (not macros or comments) are of interest QSharedPointer<Entry> entry = element.dynamicCast<Entry>(); if (!entry.isNull()) { Value entrysValueForField = entry->value(field); bool valueModified = false; for (int i = 0; i < entrysValueForField.count(); ++i) { const QString valueItemText = PlainTextValue::text(entrysValueForField[i]); if (valueItemText == toBeRemovedValueText) { valueModified = true; entrysValueForField.remove(i); break; } } if (valueModified) { entry->remove(field); entry->insert(field, entrysValueForField); madeModification = true; } } } if (madeModification) { update(); /// Notify main editor about change it its data d->fileView->externalModification(); } } void ValueList::startItemRenaming() { /// Get current index from sorted model QModelIndex sortedIndex = d->treeviewFieldValues->currentIndex(); /// Make the tree view start and editing delegate on the index d->treeviewFieldValues->edit(sortedIndex); } void ValueList::deleteAllOccurrences() { /// Get current index from sorted model QModelIndex sortedIndex = d->treeviewFieldValues->currentIndex(); /// Get "real" index from original model, but resort to sibling in first column QModelIndex realIndex = d->sortingModel->mapToSource(sortedIndex); realIndex = realIndex.sibling(realIndex.row(), 0); /// Remove current index from data model d->model->removeValue(realIndex); /// Notify main editor about change it its data d->fileView->externalModification(); } void ValueList::showCountColumnToggled() { if (d->model != nullptr) d->model->setShowCountColumn(d->showCountColumnAction->isChecked()); if (d->showCountColumnAction->isChecked()) resizeEvent(nullptr); d->sortByCountAction->setEnabled(!d->showCountColumnAction->isChecked()); KConfigGroup configGroup(d->config, d->configGroupName); configGroup.writeEntry(d->configKeyShowCountColumn, d->showCountColumnAction->isChecked()); d->config->sync(); } void ValueList::sortByCountToggled() { if (d->model != nullptr) d->model->setSortBy(d->sortByCountAction->isChecked() ? ValueListModel::SortByCount : ValueListModel::SortByText); KConfigGroup configGroup(d->config, d->configGroupName); configGroup.writeEntry(d->configKeySortByCountAction, d->sortByCountAction->isChecked()); d->config->sync(); } void ValueList::delayedResize() { resizeEvent(nullptr); } void ValueList::columnsChanged() { QByteArray headerState = d->treeviewFieldValues->header()->saveState(); KConfigGroup configGroup(d->config, d->configGroupName); configGroup.writeEntry(d->configKeyHeaderState, headerState); d->config->sync(); resizeEvent(nullptr); } void ValueList::editorSelectionChanged() { const bool selectedElements = d->fileView == nullptr ? false : d->fileView->selectedElements().count() > 0; d->assignSelectionAction->setEnabled(selectedElements); d->removeSelectionAction->setEnabled(selectedElements); } void ValueList::editorDestroyed() { /// Reset internal variable to NULL to avoid /// accessing invalid pointer/data later d->fileView = nullptr; editorSelectionChanged(); } void ValueList::fieldNamesChanged(int i) { const QString field = d->comboboxFieldNames->itemData(i).toString(); if (d->allowsMultipleValues(field)) d->assignSelectionAction->setText(i18n("Add value to selected entries")); else d->assignSelectionAction->setText(i18n("Replace value of selected entries")); update(); } diff --git a/src/program/openfileinfo.cpp b/src/program/openfileinfo.cpp index bd5065ac..b9018d5f 100644 --- a/src/program/openfileinfo.cpp +++ b/src/program/openfileinfo.cpp @@ -1,717 +1,717 @@ /*************************************************************************** - * Copyright (C) 2004-2018 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * + * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, see <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "openfileinfo.h" #include <QString> #include <QTimer> #include <QFileInfo> #include <QWidget> #include <QUrl> #include <QDebug> #include <QApplication> #include <KLocalizedString> #include <KConfig> #include <KConfigGroup> #include <KSharedConfig> #include <KMimeTypeTrader> #include <KParts/Part> #include <KParts/ReadOnlyPart> #include <KParts/ReadWritePart> #include <FileImporterPDF> #include "logging_program.h" class OpenFileInfo::OpenFileInfoPrivate { private: static int globalCounter; int m_counter; public: static const QString keyLastAccess; static const QString keyURL; static const QString dateTimeFormat; OpenFileInfo *p; KParts::ReadOnlyPart *part; KService::Ptr internalServicePtr; QWidget *internalWidgetParent; QDateTime lastAccessDateTime; StatusFlags flags; OpenFileInfoManager *openFileInfoManager; QString mimeType; QUrl url; OpenFileInfoPrivate(OpenFileInfoManager *openFileInfoManager, const QUrl &url, const QString &mimeType, OpenFileInfo *p) : m_counter(-1), p(p), part(nullptr), internalServicePtr(KService::Ptr()), internalWidgetParent(nullptr), flags(nullptr) { this->openFileInfoManager = openFileInfoManager; this->url = url; if (this->url.isValid() && this->url.scheme().isEmpty()) qCWarning(LOG_KBIBTEX_PROGRAM) << "No scheme specified for URL" << this->url.toDisplayString(); this->mimeType = mimeType; } ~OpenFileInfoPrivate() { if (part != nullptr) { KParts::ReadWritePart *rwp = qobject_cast<KParts::ReadWritePart *>(part); if (rwp != nullptr) rwp->closeUrl(true); delete part; } } KParts::ReadOnlyPart *createPart(QWidget *newWidgetParent, KService::Ptr newServicePtr = KService::Ptr()) { if (!p->flags().testFlag(OpenFileInfo::Open)) { qCWarning(LOG_KBIBTEX_PROGRAM) << "Cannot create part for a file which is not open"; return nullptr; } Q_ASSERT_X(internalWidgetParent == nullptr || internalWidgetParent == newWidgetParent, "KParts::ReadOnlyPart *OpenFileInfo::OpenFileInfoPrivate::createPart(QWidget *newWidgetParent, KService::Ptr newServicePtr = KService::Ptr())", "internal widget should be either NULL or the same one as supplied as \"newWidgetParent\""); /** use cached part for this parent if possible */ if (internalWidgetParent == newWidgetParent && (newServicePtr == KService::Ptr() || internalServicePtr == newServicePtr)) { Q_ASSERT_X(part != nullptr, "KParts::ReadOnlyPart *OpenFileInfo::OpenFileInfoPrivate::createPart(QWidget *newWidgetParent, KService::Ptr newServicePtr = KService::Ptr())", "Part is NULL"); return part; } else if (part != nullptr) { KParts::ReadWritePart *rwp = qobject_cast<KParts::ReadWritePart *>(part); if (rwp != nullptr) rwp->closeUrl(true); part->deleteLater(); part = nullptr; } /// reset to invalid values in case something goes wrong internalServicePtr = KService::Ptr(); internalWidgetParent = nullptr; if (!newServicePtr) { /// no valid KService has been passed /// try to find a read-write part to open file newServicePtr = p->defaultService(); } if (!newServicePtr) { qCDebug(LOG_KBIBTEX_PROGRAM) << "PATH=" << getenv("PATH"); qCDebug(LOG_KBIBTEX_PROGRAM) << "LD_LIBRARY_PATH=" << getenv("LD_LIBRARY_PATH"); qCDebug(LOG_KBIBTEX_PROGRAM) << "XDG_DATA_DIRS=" << getenv("XDG_DATA_DIRS"); qCDebug(LOG_KBIBTEX_PROGRAM) << "QT_PLUGIN_PATH=" << getenv("QT_PLUGIN_PATH"); qCDebug(LOG_KBIBTEX_PROGRAM) << "KDEDIRS=" << getenv("KDEDIRS"); qCCritical(LOG_KBIBTEX_PROGRAM) << "Cannot find service to handle mimetype " << mimeType << endl; return nullptr; } QString errorString; part = newServicePtr->createInstance<KParts::ReadWritePart>(newWidgetParent, (QObject *)newWidgetParent, QVariantList(), &errorString); if (part == nullptr) { qCDebug(LOG_KBIBTEX_PROGRAM) << "PATH=" << getenv("PATH"); qCDebug(LOG_KBIBTEX_PROGRAM) << "LD_LIBRARY_PATH=" << getenv("LD_LIBRARY_PATH"); qCDebug(LOG_KBIBTEX_PROGRAM) << "XDG_DATA_DIRS=" << getenv("XDG_DATA_DIRS"); qCDebug(LOG_KBIBTEX_PROGRAM) << "QT_PLUGIN_PATH=" << getenv("QT_PLUGIN_PATH"); qCDebug(LOG_KBIBTEX_PROGRAM) << "KDEDIRS=" << getenv("KDEDIRS"); qCWarning(LOG_KBIBTEX_PROGRAM) << "Could not instantiate read-write part for service" << newServicePtr->name() << "(mimeType=" << mimeType << ", library=" << newServicePtr->library() << ", error msg=" << errorString << ")"; /// creating a read-write part failed, so maybe it is read-only (like Okular's PDF viewer)? part = newServicePtr->createInstance<KParts::ReadOnlyPart>(newWidgetParent, (QObject *)newWidgetParent, QVariantList(), &errorString); } if (part == nullptr) { /// still cannot create part, must be error qCCritical(LOG_KBIBTEX_PROGRAM) << "Could not instantiate part for service" << newServicePtr->name() << "(mimeType=" << mimeType << ", library=" << newServicePtr->library() << ", error msg=" << errorString << ")"; return nullptr; } if (url.isValid()) { /// open URL in part part->openUrl(url); /// update document list widget accordingly p->addFlags(OpenFileInfo::RecentlyUsed); p->addFlags(OpenFileInfo::HasName); } else { /// initialize part with empty document part->openUrl(QUrl()); } p->addFlags(OpenFileInfo::Open); internalServicePtr = newServicePtr; internalWidgetParent = newWidgetParent; Q_ASSERT_X(part != nullptr, "KParts::ReadOnlyPart *OpenFileInfo::OpenFileInfoPrivate::createPart(QWidget *newWidgetParent, KService::Ptr newServicePtr = KService::Ptr())", "Creation of part failed, is NULL"); /// test should not be necessary, but just to be save ... return part; } int counter() { if (!url.isValid() && m_counter < 0) m_counter = ++globalCounter; else if (url.isValid()) qCWarning(LOG_KBIBTEX_PROGRAM) << "This function should not be called if URL is valid"; return m_counter; } }; int OpenFileInfo::OpenFileInfoPrivate::globalCounter = 0; const QString OpenFileInfo::OpenFileInfoPrivate::dateTimeFormat = QStringLiteral("yyyy-MM-dd-hh-mm-ss-zzz"); const QString OpenFileInfo::OpenFileInfoPrivate::keyLastAccess = QStringLiteral("LastAccess"); const QString OpenFileInfo::OpenFileInfoPrivate::keyURL = QStringLiteral("URL"); OpenFileInfo::OpenFileInfo(OpenFileInfoManager *openFileInfoManager, const QUrl &url) : d(new OpenFileInfoPrivate(openFileInfoManager, url, FileInfo::mimeTypeForUrl(url).name(), this)) { /// nothing } OpenFileInfo::OpenFileInfo(OpenFileInfoManager *openFileInfoManager, const QString &mimeType) : d(new OpenFileInfoPrivate(openFileInfoManager, QUrl(), mimeType, this)) { /// nothing } OpenFileInfo::~OpenFileInfo() { delete d; } void OpenFileInfo::setUrl(const QUrl &url) { Q_ASSERT_X(url.isValid(), "void OpenFileInfo::setUrl(const QUrl&)", "URL is not valid"); d->url = url; if (d->url.scheme().isEmpty()) qCWarning(LOG_KBIBTEX_PROGRAM) << "No scheme specified for URL" << d->url.toDisplayString(); d->mimeType = FileInfo::mimeTypeForUrl(url).name(); addFlags(OpenFileInfo::HasName); } QUrl OpenFileInfo::url() const { return d->url; } bool OpenFileInfo::isModified() const { KParts::ReadWritePart *rwPart = qobject_cast< KParts::ReadWritePart *>(d->part); if (rwPart == nullptr) return false; else return rwPart->isModified(); } bool OpenFileInfo::save() { KParts::ReadWritePart *rwPart = qobject_cast< KParts::ReadWritePart *>(d->part); if (rwPart == nullptr) return true; else return rwPart->save(); } bool OpenFileInfo::close() { if (d->part == nullptr) { /// if there is no part, closing always "succeeds" return true; } KParts::ReadWritePart *rwp = qobject_cast<KParts::ReadWritePart *>(d->part); if (rwp == nullptr || rwp->closeUrl(true)) { d->part->deleteLater(); d->part = nullptr; d->internalWidgetParent = nullptr; return true; } return false; } QString OpenFileInfo::mimeType() const { return d->mimeType; } QString OpenFileInfo::shortCaption() const { if (d->url.isValid()) return d->url.fileName(); else return i18n("Unnamed-%1", d->counter()); } QString OpenFileInfo::fullCaption() const { if (d->url.isValid()) return d->url.url(QUrl::PreferLocalFile); else return shortCaption(); } /// Clazy warns: "Missing reference on non-trivial type" for argument 'servicePtr', /// but type 'KService::Ptr' is actually a pointer (QExplicitlySharedDataPointer). KParts::ReadOnlyPart *OpenFileInfo::part(QWidget *parent, KService::Ptr servicePtr) { return d->createPart(parent, servicePtr); } OpenFileInfo::StatusFlags OpenFileInfo::flags() const { return d->flags; } void OpenFileInfo::setFlags(StatusFlags statusFlags) { /// disallow files without name or valid url to become favorites if (!d->url.isValid() || !d->flags.testFlag(HasName)) statusFlags &= ~Favorite; /// files that got opened are by definition recently used files if (!d->url.isValid() && d->flags.testFlag(Open)) statusFlags &= RecentlyUsed; bool hasChanged = d->flags != statusFlags; d->flags = statusFlags; if (hasChanged) emit flagsChanged(statusFlags); } void OpenFileInfo::addFlags(StatusFlags statusFlags) { /// disallow files without name or valid url to become favorites if (!d->url.isValid() || !d->flags.testFlag(HasName)) statusFlags &= ~Favorite; bool hasChanged = (~d->flags & statusFlags) > 0; d->flags |= statusFlags; if (hasChanged) emit flagsChanged(statusFlags); } void OpenFileInfo::removeFlags(StatusFlags statusFlags) { bool hasChanged = (d->flags & statusFlags) > 0; d->flags &= ~statusFlags; if (hasChanged) emit flagsChanged(statusFlags); } QDateTime OpenFileInfo::lastAccess() const { return d->lastAccessDateTime; } void OpenFileInfo::setLastAccess(const QDateTime &dateTime) { d->lastAccessDateTime = dateTime; emit flagsChanged(OpenFileInfo::RecentlyUsed); } KService::List OpenFileInfo::listOfServices() { const QString mt = mimeType(); /// First, try to locate KPart that can both read and write the queried MIME type KService::List result = KMimeTypeTrader::self()->query(mt, QStringLiteral("KParts/ReadWritePart")); if (result.isEmpty()) { /// Second, if no 'writing' KPart was found, try to locate KPart that can at least read the queried MIME type result = KMimeTypeTrader::self()->query(mt, QStringLiteral("KParts/ReadOnlyPart")); if (result.isEmpty()) { /// If not even a 'reading' KPart was found, something is off, so warn the user and stop here qCWarning(LOG_KBIBTEX_PROGRAM) << "Could not find any KPart that reads or writes mimetype" << mt; return result; } } /// Always include KBibTeX's KPart in list of services: /// First, check if KBibTeX's KPart is already in list as returned by /// KMimeTypeTrader::self()->query(..) bool listIncludesKBibTeXPart = false; for (KService::List::ConstIterator it = result.constBegin(); it != result.constEnd(); ++it) { qCDebug(LOG_KBIBTEX_PROGRAM) << "Found library for" << mt << ":" << (*it)->library(); listIncludesKBibTeXPart |= (*it)->library() == QStringLiteral("kbibtexpart"); } /// Then, if KBibTeX's KPart is not in the list, try to located it by desktop name if (!listIncludesKBibTeXPart) { KService::Ptr kbibtexpartByDesktopName = KService::serviceByDesktopName(QStringLiteral("kbibtexpart")); if (kbibtexpartByDesktopName != nullptr) { result << kbibtexpartByDesktopName; qCDebug(LOG_KBIBTEX_PROGRAM) << "Adding library for" << mt << ":" << kbibtexpartByDesktopName->library(); } else { qCDebug(LOG_KBIBTEX_PROGRAM) << "Could not locate KBibTeX's KPart neither by MIME type search, nor by desktop name"; } } return result; } KService::Ptr OpenFileInfo::defaultService() { const QString mt = mimeType(); KService::Ptr result; if (mt == QStringLiteral("application/pdf") || mt == QStringLiteral("text/x-bibtex")) { /// If either a BibTeX file or a PDF file is to be opened, enforce using /// KBibTeX's part over anything else. /// KBibTeX has a FileImporterPDF which allows it to load .pdf file /// that got generated with KBibTeX and contain the original /// .bib file as an 'attachment'. /// This importer does not work with any other .pdf files!!! result = KService::serviceByDesktopName(QStringLiteral("kbibtexpart")); } if (result == nullptr) { /// First, try to locate KPart that can both read and write the queried MIME type result = KMimeTypeTrader::self()->preferredService(mt, QStringLiteral("KParts/ReadWritePart")); if (result == nullptr) { /// Second, if no 'writing' KPart was found, try to locate KPart that can at least read the queried MIME type result = KMimeTypeTrader::self()->preferredService(mt, QStringLiteral("KParts/ReadOnlyPart")); if (result == nullptr && mt == QStringLiteral("text/x-bibtex")) /// Third, if MIME type is for BibTeX files, try loading KBibTeX part via desktop name result = KService::serviceByDesktopName(QStringLiteral("kbibtexpart")); } } if (result != nullptr) qCDebug(LOG_KBIBTEX_PROGRAM) << "Using service" << result->name() << "(" << result->comment() << ") for mime type" << mt << "through library" << result->library(); else qCWarning(LOG_KBIBTEX_PROGRAM) << "Could not find service for mime type" << mt; return result; } KService::Ptr OpenFileInfo::currentService() { return d->internalServicePtr; } class OpenFileInfoManager::OpenFileInfoManagerPrivate { private: static const QString configGroupNameRecentlyUsed; static const QString configGroupNameFavorites; static const QString configGroupNameOpen; static const int maxNumRecentlyUsedFiles, maxNumFavoriteFiles, maxNumOpenFiles; public: OpenFileInfoManager *p; OpenFileInfoManager::OpenFileInfoList openFileInfoList; OpenFileInfo *currentFileInfo; OpenFileInfoManagerPrivate(OpenFileInfoManager *parent) : p(parent), currentFileInfo(nullptr) { /// nothing } ~OpenFileInfoManagerPrivate() { for (OpenFileInfoManager::OpenFileInfoList::Iterator it = openFileInfoList.begin(); it != openFileInfoList.end();) { OpenFileInfo *ofi = *it; delete ofi; it = openFileInfoList.erase(it); } } static bool byNameLessThan(const OpenFileInfo *left, const OpenFileInfo *right) { return left->shortCaption() < right->shortCaption(); } static bool byLRULessThan(const OpenFileInfo *left, const OpenFileInfo *right) { return left->lastAccess() > right->lastAccess(); /// reverse sorting! } void readConfig() { readConfig(OpenFileInfo::RecentlyUsed, configGroupNameRecentlyUsed, maxNumRecentlyUsedFiles); readConfig(OpenFileInfo::Favorite, configGroupNameFavorites, maxNumFavoriteFiles); readConfig(OpenFileInfo::Open, configGroupNameOpen, maxNumOpenFiles); } void writeConfig() { writeConfig(OpenFileInfo::RecentlyUsed, configGroupNameRecentlyUsed, maxNumRecentlyUsedFiles); writeConfig(OpenFileInfo::Favorite, configGroupNameFavorites, maxNumFavoriteFiles); writeConfig(OpenFileInfo::Open, configGroupNameOpen, maxNumOpenFiles); } void readConfig(OpenFileInfo::StatusFlag statusFlag, const QString &configGroupName, int maxNumFiles) { KSharedConfigPtr config = KSharedConfig::openConfig(QStringLiteral("kbibtexrc")); bool isFirst = true; KConfigGroup cg(config, configGroupName); for (int i = 0; i < maxNumFiles; ++i) { QUrl fileUrl = QUrl(cg.readEntry(QString(QStringLiteral("%1-%2")).arg(OpenFileInfo::OpenFileInfoPrivate::keyURL).arg(i), "")); if (!fileUrl.isValid()) break; if (fileUrl.scheme().isEmpty()) fileUrl.setScheme(QStringLiteral("file")); /// For local files, test if they exist; ignore local files that do not exist if (fileUrl.isLocalFile()) { if (!QFileInfo::exists(fileUrl.toLocalFile())) continue; } OpenFileInfo *ofi = p->contains(fileUrl); if (ofi == nullptr) { ofi = p->open(fileUrl); } ofi->addFlags(statusFlag); ofi->addFlags(OpenFileInfo::HasName); ofi->setLastAccess(QDateTime::fromString(cg.readEntry(QString(QStringLiteral("%1-%2")).arg(OpenFileInfo::OpenFileInfoPrivate::keyLastAccess).arg(i), ""), OpenFileInfo::OpenFileInfoPrivate::dateTimeFormat)); if (isFirst) { isFirst = false; if (statusFlag == OpenFileInfo::Open) p->setCurrentFile(ofi); } } } void writeConfig(OpenFileInfo::StatusFlag statusFlag, const QString &configGroupName, int maxNumFiles) { KSharedConfigPtr config = KSharedConfig::openConfig(QStringLiteral("kbibtexrc")); KConfigGroup cg(config, configGroupName); OpenFileInfoManager::OpenFileInfoList list = p->filteredItems(statusFlag); int i = 0; for (OpenFileInfoManager::OpenFileInfoList::ConstIterator it = list.constBegin(); i < maxNumFiles && it != list.constEnd(); ++it, ++i) { OpenFileInfo *ofi = *it; cg.writeEntry(QString(QStringLiteral("%1-%2")).arg(OpenFileInfo::OpenFileInfoPrivate::keyURL).arg(i), ofi->url().url(QUrl::PreferLocalFile)); cg.writeEntry(QString(QStringLiteral("%1-%2")).arg(OpenFileInfo::OpenFileInfoPrivate::keyLastAccess).arg(i), ofi->lastAccess().toString(OpenFileInfo::OpenFileInfoPrivate::dateTimeFormat)); } for (; i < maxNumFiles; ++i) { cg.deleteEntry(QString(QStringLiteral("%1-%2")).arg(OpenFileInfo::OpenFileInfoPrivate::keyURL).arg(i)); cg.deleteEntry(QString(QStringLiteral("%1-%2")).arg(OpenFileInfo::OpenFileInfoPrivate::keyLastAccess).arg(i)); } config->sync(); } }; const QString OpenFileInfoManager::OpenFileInfoManagerPrivate::configGroupNameRecentlyUsed = QStringLiteral("DocumentList-RecentlyUsed"); const QString OpenFileInfoManager::OpenFileInfoManagerPrivate::configGroupNameFavorites = QStringLiteral("DocumentList-Favorites"); const QString OpenFileInfoManager::OpenFileInfoManagerPrivate::configGroupNameOpen = QStringLiteral("DocumentList-Open"); const int OpenFileInfoManager::OpenFileInfoManagerPrivate::maxNumFavoriteFiles = 256; const int OpenFileInfoManager::OpenFileInfoManagerPrivate::maxNumRecentlyUsedFiles = 8; const int OpenFileInfoManager::OpenFileInfoManagerPrivate::maxNumOpenFiles = 16; OpenFileInfoManager::OpenFileInfoManager(QObject *parent) : QObject(parent), d(new OpenFileInfoManagerPrivate(this)) { QTimer::singleShot(300, this, &OpenFileInfoManager::delayedReadConfig); } OpenFileInfoManager &OpenFileInfoManager::instance() { /// Allocate this singleton on heap not stack like most other singletons /// Supposedly, QCoreApplication will clean this singleton at application's end static OpenFileInfoManager *singleton = new OpenFileInfoManager(QCoreApplication::instance()); return *singleton; } OpenFileInfoManager::~OpenFileInfoManager() { delete d; } OpenFileInfo *OpenFileInfoManager::createNew(const QString &mimeType) { OpenFileInfo *result = new OpenFileInfo(this, mimeType); connect(result, &OpenFileInfo::flagsChanged, this, &OpenFileInfoManager::flagsChanged); d->openFileInfoList << result; result->setLastAccess(); return result; } OpenFileInfo *OpenFileInfoManager::open(const QUrl &url) { Q_ASSERT_X(url.isValid(), "OpenFileInfo *OpenFileInfoManager::open(const QUrl&)", "URL is not valid"); OpenFileInfo *result = contains(url); if (result == nullptr) { /// file not yet open result = new OpenFileInfo(this, url); connect(result, &OpenFileInfo::flagsChanged, this, &OpenFileInfoManager::flagsChanged); d->openFileInfoList << result; } /// else: file was already open, re-use and return existing OpenFileInfo pointer result->setLastAccess(); return result; } OpenFileInfo *OpenFileInfoManager::contains(const QUrl &url) const { if (!url.isValid()) return nullptr; /// can only be unnamed file for (auto *ofi : const_cast<const OpenFileInfoManager::OpenFileInfoList &>(d->openFileInfoList)) { if (ofi->url() == url) return ofi; } return nullptr; } bool OpenFileInfoManager::changeUrl(OpenFileInfo *openFileInfo, const QUrl &url) { OpenFileInfo *previouslyContained = contains(url); /// check if old url differs from new url and old url is valid if (previouslyContained != nullptr && previouslyContained->flags().testFlag(OpenFileInfo::Open) && previouslyContained != openFileInfo) { qCWarning(LOG_KBIBTEX_PROGRAM) << "Open file with same URL already exists, forcefully closing it" << endl; close(previouslyContained); } QUrl oldUrl = openFileInfo->url(); openFileInfo->setUrl(url); if (url != oldUrl && oldUrl.isValid()) { /// current document was most probabily renamed (e.g. due to "Save As") /// add old URL to recently used files, but exclude the open files list OpenFileInfo *ofi = open(oldUrl); // krazy:exclude=syscalls OpenFileInfo::StatusFlags statusFlags = (openFileInfo->flags() & (~OpenFileInfo::Open)) | OpenFileInfo::RecentlyUsed; ofi->setFlags(statusFlags); } if (previouslyContained != nullptr) { /// keep Favorite flag if set in file that have previously same URL if (previouslyContained->flags().testFlag(OpenFileInfo::Favorite)) openFileInfo->setFlags(openFileInfo->flags() | OpenFileInfo::Favorite); /// remove the old entry with the same url has it will be replaced by the new one d->openFileInfoList.remove(d->openFileInfoList.indexOf(previouslyContained)); previouslyContained->deleteLater(); OpenFileInfo::StatusFlags statusFlags = OpenFileInfo::Open; statusFlags |= OpenFileInfo::RecentlyUsed; statusFlags |= OpenFileInfo::Favorite; emit flagsChanged(statusFlags); } if (openFileInfo == d->currentFileInfo) emit currentChanged(openFileInfo, KService::Ptr()); emit flagsChanged(openFileInfo->flags()); return true; } bool OpenFileInfoManager::close(OpenFileInfo *openFileInfo) { if (openFileInfo == nullptr) { qCWarning(LOG_KBIBTEX_PROGRAM) << "void OpenFileInfoManager::close(OpenFileInfo *openFileInfo): Cannot close openFileInfo which is NULL"; return false; } bool isClosing = false; openFileInfo->setLastAccess(); /// remove flag "open" from file to be closed and determine which file to show instead OpenFileInfo *nextCurrent = (d->currentFileInfo == openFileInfo) ? nullptr : d->currentFileInfo; for (OpenFileInfo *ofi : const_cast<const OpenFileInfoManager::OpenFileInfoList &>(d->openFileInfoList)) { if (!isClosing && ofi == openFileInfo && openFileInfo->close()) { isClosing = true; /// Mark file as closed (i.e. not open) openFileInfo->removeFlags(OpenFileInfo::Open); /// If file has a filename, remember as recently used if (openFileInfo->flags().testFlag(OpenFileInfo::HasName)) openFileInfo->addFlags(OpenFileInfo::RecentlyUsed); } else if (nextCurrent == nullptr && ofi->flags().testFlag(OpenFileInfo::Open)) nextCurrent = ofi; } /// If the current document is to be closed, /// switch over to the next available one if (isClosing) setCurrentFile(nextCurrent); return isClosing; } bool OpenFileInfoManager::queryCloseAll() { /// Assume that all closing operations succeed bool isClosing = true; /// For keeping track of files that get closed here OpenFileInfoList restoreLaterList; /// For each file known ... for (OpenFileInfo *openFileInfo : const_cast<const OpenFileInfoManager::OpenFileInfoList &>(d->openFileInfoList)) { /// Check only open file (ignore recently used, favorites, ...) if (openFileInfo->flags().testFlag(OpenFileInfo::Open)) { if (openFileInfo->close()) { /// If file could be closed without user canceling the operation ... /// Mark file as closed (i.e. not open) openFileInfo->removeFlags(OpenFileInfo::Open); /// If file has a filename, remember as recently used if (openFileInfo->flags().testFlag(OpenFileInfo::HasName)) openFileInfo->addFlags(OpenFileInfo::RecentlyUsed); /// Remember file as to be marked as open later restoreLaterList.append(openFileInfo); } else { /// User chose to cancel closing operation, /// stop everything here isClosing = false; break; } } } if (isClosing) { /// Closing operation was not cancelled, therefore mark /// all files that were open before as open now. /// This makes the files to be reopened when KBibTeX is /// restarted again (assuming that this function was /// called when KBibTeX is exiting). for (OpenFileInfo *openFileInfo : const_cast<const OpenFileInfoManager::OpenFileInfoList &>(restoreLaterList)) { openFileInfo->addFlags(OpenFileInfo::Open); } d->writeConfig(); } return isClosing; } OpenFileInfo *OpenFileInfoManager::currentFile() const { return d->currentFileInfo; } /// Clazy warns: "Missing reference on non-trivial type" for argument 'servicePtr', /// but type 'KService::Ptr' is actually a pointer (QExplicitlySharedDataPointer). void OpenFileInfoManager::setCurrentFile(OpenFileInfo *openFileInfo, KService::Ptr servicePtr) { bool hasChanged = d->currentFileInfo != openFileInfo; OpenFileInfo *previous = d->currentFileInfo; d->currentFileInfo = openFileInfo; if (d->currentFileInfo != nullptr) { d->currentFileInfo->addFlags(OpenFileInfo::Open); d->currentFileInfo->setLastAccess(); } if (hasChanged) { if (previous != nullptr) previous->setLastAccess(); emit currentChanged(openFileInfo, servicePtr); } else if (openFileInfo != nullptr && servicePtr != openFileInfo->currentService()) emit currentChanged(openFileInfo, servicePtr); } OpenFileInfoManager::OpenFileInfoList OpenFileInfoManager::filteredItems(OpenFileInfo::StatusFlags required, OpenFileInfo::StatusFlags forbidden) { OpenFileInfoList result; for (OpenFileInfoList::Iterator it = d->openFileInfoList.begin(); it != d->openFileInfoList.end(); ++it) { OpenFileInfo *ofi = *it; if ((ofi->flags() & required) == required && (ofi->flags() & forbidden) == 0) result << ofi; } if (required == OpenFileInfo::RecentlyUsed) - qSort(result.begin(), result.end(), OpenFileInfoManagerPrivate::byLRULessThan); + std::sort(result.begin(), result.end(), OpenFileInfoManagerPrivate::byLRULessThan); else if (required == OpenFileInfo::Favorite || required == OpenFileInfo::Open) - qSort(result.begin(), result.end(), OpenFileInfoManagerPrivate::byNameLessThan); + std::sort(result.begin(), result.end(), OpenFileInfoManagerPrivate::byNameLessThan); return result; } void OpenFileInfoManager::deferredListsChanged() { OpenFileInfo::StatusFlags statusFlags = OpenFileInfo::Open; statusFlags |= OpenFileInfo::RecentlyUsed; statusFlags |= OpenFileInfo::Favorite; emit flagsChanged(statusFlags); } void OpenFileInfoManager::delayedReadConfig() { d->readConfig(); }