diff --git a/src/gui/element/elementeditor.cpp b/src/gui/element/elementeditor.cpp index 7479d437..8a0c64e1 100644 --- a/src/gui/element/elementeditor.cpp +++ b/src/gui/element/elementeditor.cpp @@ -1,698 +1,687 @@ /*************************************************************************** * 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 "elementeditor.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 "elementwidgets.h" #include "widgets/hidingtabwidget.h" #include "widgets/menulineedit.h" class ElementEditor::ElementEditorPrivate : public ElementEditor::ApplyElementInterface { private: const File *file; QSharedPointer internalEntry; QSharedPointer internalMacro; QSharedPointer internalPreamble; QSharedPointer internalComment; ElementEditor *p; ElementWidget *previousWidget; ReferenceWidget *referenceWidget; QPushButton *buttonCheckWithBibTeX; /// Settings management through a push button with menu KSharedConfigPtr config; QPushButton *buttonOptions; QAction *actionForceShowAllWidgets, *actionLimitKeyboardTabStops; public: typedef QVector WidgetList; QSharedPointer element; HidingTabWidget *tab; WidgetList widgets; SourceWidget *sourceWidget; FilesWidget *filesWidget; bool elementChanged, elementUnapplied; ElementEditorPrivate(bool scrollable, ElementEditor *parent) : file(nullptr), p(parent), previousWidget(nullptr), config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))), elementChanged(false), elementUnapplied(false) { internalEntry = QSharedPointer(); internalMacro = QSharedPointer(); internalComment = QSharedPointer(); internalPreamble = QSharedPointer(); createGUI(scrollable); } ~ElementEditorPrivate() override { clearWidgets(); } void clearWidgets() { for (int i = widgets.count() - 1; i >= 0; --i) { QWidget *w = widgets[i]; w->deleteLater(); } widgets.clear(); } void setElement(QSharedPointer element, const File *file) { this->element = element; this->file = file; referenceWidget->setOriginalElement(element); updateTabVisibility(); } void addTabWidgets() { for (const auto &etl : EntryLayout::instance()) { EntryConfiguredWidget *widget = new EntryConfiguredWidget(etl, tab); connect(widget, &ElementWidget::modified, p, &ElementEditor::childModified); connect(widget, &EntryConfiguredWidget::requestingTabChange, p, &ElementEditor::switchToTab); widgets << widget; if (previousWidget == nullptr) previousWidget = widget; ///< memorize the first tab int index = tab->addTab(widget, widget->icon(), widget->label()); tab->hideTab(index); } ElementWidget *widget = new PreambleWidget(tab); connect(widget, &ElementWidget::modified, p, &ElementEditor::childModified); widgets << widget; int index = tab->addTab(widget, widget->icon(), widget->label()); tab->hideTab(index); widget = new MacroWidget(tab); connect(widget, &ElementWidget::modified, p, &ElementEditor::childModified); widgets << widget; index = tab->addTab(widget, widget->icon(), widget->label()); tab->hideTab(index); filesWidget = new FilesWidget(tab); connect(filesWidget, &FilesWidget::modified, p, &ElementEditor::childModified); widgets << filesWidget; index = tab->addTab(filesWidget, filesWidget->icon(), filesWidget->label()); tab->hideTab(index); QStringList blacklistedFields; /// blacklist fields covered by EntryConfiguredWidget for (const auto &etl : EntryLayout::instance()) for (const auto &sfl : const_cast &>(etl->singleFieldLayouts)) blacklistedFields << sfl.bibtexLabel; /// blacklist fields covered by FilesWidget blacklistedFields << QString(Entry::ftUrl) << QString(Entry::ftLocalFile) << QString(Entry::ftFile) << QString(Entry::ftDOI) << QStringLiteral("ee") << QStringLiteral("biburl") << QStringLiteral("postscript"); for (int i = 2; i < 256; ++i) // FIXME replace number by constant blacklistedFields << QString(Entry::ftUrl) + QString::number(i) << QString(Entry::ftLocalFile) + QString::number(i) << QString(Entry::ftFile) + QString::number(i) << QString(Entry::ftDOI) + QString::number(i) << QStringLiteral("ee") + QString::number(i) << QStringLiteral("postscript") + QString::number(i); widget = new OtherFieldsWidget(blacklistedFields, tab); connect(widget, &ElementWidget::modified, p, &ElementEditor::childModified); widgets << widget; index = tab->addTab(widget, widget->icon(), widget->label()); tab->hideTab(index); sourceWidget = new SourceWidget(tab); connect(sourceWidget, &ElementWidget::modified, p, &ElementEditor::childModified); widgets << sourceWidget; index = tab->addTab(sourceWidget, sourceWidget->icon(), sourceWidget->label()); tab->hideTab(index); } void createGUI(bool scrollable) { /// load configuration for options push button static const QString configGroupName = QStringLiteral("User Interface"); static const QString keyEnableAllWidgets = QStringLiteral("EnableAllWidgets"); KConfigGroup configGroup(config, configGroupName); const bool showAll = configGroup.readEntry(keyEnableAllWidgets, true); const bool limitKeyboardTabStops = configGroup.readEntry(MenuLineEdit::keyLimitKeyboardTabStops, false); QBoxLayout *vLayout = new QVBoxLayout(p); referenceWidget = new ReferenceWidget(p); referenceWidget->setApplyElementInterface(this); connect(referenceWidget, &ElementWidget::modified, p, &ElementEditor::childModified); - connect(referenceWidget, &ReferenceWidget::entryTypeChanged, p, &ElementEditor::updateReqOptWidgets); + connect(referenceWidget, &ReferenceWidget::entryTypeChanged, p, [this]() { + updateReqOptWidgets(); + }); vLayout->addWidget(referenceWidget, 0); widgets << referenceWidget; if (scrollable) { QScrollArea *sa = new QScrollArea(p); tab = new HidingTabWidget(sa); sa->setFrameStyle(0); sa->setWidget(tab); sa->setWidgetResizable(true); sa->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); sa->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); vLayout->addWidget(sa, 10); } else { tab = new HidingTabWidget(p); vLayout->addWidget(tab, 10); } QBoxLayout *hLayout = new QHBoxLayout(); vLayout->addLayout(hLayout, 0); /// Push button with menu to toggle various options buttonOptions = new QPushButton(QIcon::fromTheme(QStringLiteral("configure")), i18n("Options"), p); hLayout->addWidget(buttonOptions, 0); QMenu *menuOptions = new QMenu(buttonOptions); buttonOptions->setMenu(menuOptions); /// Option to show all fields or only those require for current entry type actionForceShowAllWidgets = menuOptions->addAction(i18n("Show all fields"), p, SLOT(updateReqOptWidgets())); actionForceShowAllWidgets->setCheckable(true); actionForceShowAllWidgets->setChecked(showAll); /// Option to disable tab key focus to reach/visit various non-editable widgets actionLimitKeyboardTabStops = menuOptions->addAction(i18n("Tab key visits only editable fields"), p, SLOT(limitKeyboardTabStops())); actionLimitKeyboardTabStops->setCheckable(true); actionLimitKeyboardTabStops->setChecked(limitKeyboardTabStops); hLayout->addStretch(10); buttonCheckWithBibTeX = new QPushButton(QIcon::fromTheme(QStringLiteral("tools-check-spelling")), i18n("Check with BibTeX"), p); hLayout->addWidget(buttonCheckWithBibTeX, 0); - connect(buttonCheckWithBibTeX, &QPushButton::clicked, p, &ElementEditor::checkBibTeX); + connect(buttonCheckWithBibTeX, &QPushButton::clicked, p, [this]() { + checkBibTeX(); + }); addTabWidgets(); } void updateTabVisibility() { disconnect(tab, &HidingTabWidget::currentChanged, p, &ElementEditor::tabChanged); if (element.isNull()) { p->setEnabled(false); } else { p->setEnabled(true); int firstEnabledTab = 1024; for (ElementWidget *widget : const_cast(widgets)) { const int index = tab->indexOf(widget); const bool canEdit = widget->canEdit(element.data()); if (widget == referenceWidget) { /// Reference widget widget->setVisible(canEdit); widget->setEnabled(canEdit); } else { if (canEdit) tab->showTab(widget); else if (index >= 0) tab->hideTab(index); if (canEdit && index >= 0 && index < firstEnabledTab) firstEnabledTab = index; } } if (firstEnabledTab < 1024) tab->setCurrentIndex(firstEnabledTab); } connect(tab, &HidingTabWidget::currentChanged, p, &ElementEditor::tabChanged); } /** * If this element editor makes use of a reference widget * (e.g. where entry type and entry id/macro key can be edited), * then return the current value of the entry id/macro key * editing widget. * Otherwise, return an empty string. * * @return Current value of entry id/macro key if any, otherwise empty string */ QString currentId() const { if (referenceWidget != nullptr) return referenceWidget->currentId(); return QString(); } void setCurrentId(const QString &newId) { if (referenceWidget != nullptr) return referenceWidget->setCurrentId(newId); } /** * Return the current File object set for this element editor. * May be NULL if nothing has been set or if it has been cleared. * * @return Current File object, may be nullptr */ const File *currentFile() const { return file; } void apply() { elementChanged = true; elementUnapplied = false; apply(element); } void apply(QSharedPointer element) override { QSharedPointer e = element.dynamicCast(); QSharedPointer m = e.isNull() ? element.dynamicCast() : QSharedPointer(); QSharedPointer c = e.isNull() && m.isNull() ? element.dynamicCast() : QSharedPointer(); QSharedPointer p = e.isNull() && m.isNull() && c.isNull() ? element.dynamicCast() : QSharedPointer(); if (tab->currentWidget() == sourceWidget) { /// Very simple if source view is active: BibTeX code contains /// all necessary data if (!e.isNull()) sourceWidget->setElementClass(SourceWidget::elementEntry); else if (!m.isNull()) sourceWidget->setElementClass(SourceWidget::elementMacro); else if (!p.isNull()) sourceWidget->setElementClass(SourceWidget::elementPreamble); else sourceWidget->setElementClass(SourceWidget::elementInvalid); sourceWidget->apply(element); } else { /// Start by assigning the current internal element's /// data to the output element if (!e.isNull()) *e = *internalEntry; else { if (!m.isNull()) *m = *internalMacro; else { if (!c.isNull()) *c = *internalComment; else { if (!p.isNull()) *p = *internalPreamble; else Q_ASSERT_X(element.isNull(), "ElementEditor::ElementEditorPrivate::apply(QSharedPointer element)", "element is not NULL but could not be cast on a valid Element sub-class"); } } } /// The internal element may be outdated (only updated on tab switch), /// so apply the reference widget's data on the output element if (referenceWidget != nullptr) referenceWidget->apply(element); /// The internal element may be outdated (only updated on tab switch), /// so apply the current widget's data on the output element ElementWidget *currentElementWidget = qobject_cast(tab->currentWidget()); if (currentElementWidget != nullptr) currentElementWidget->apply(element); } } bool validate(QWidget **widgetWithIssue, QString &message) const override { if (tab->currentWidget() == sourceWidget) { /// Source widget must check its textual content for being valid BibTeX code return sourceWidget->validate(widgetWithIssue, message); } else { /// All widgets except for the source widget must validate their values for (WidgetList::ConstIterator it = widgets.begin(); it != widgets.end(); ++it) { if ((*it) == sourceWidget) continue; const bool v = (*it)->validate(widgetWithIssue, message); /// A single widget failing to validate lets the whole validation fail if (!v) return false; } return true; } } void reset() { elementChanged = false; elementUnapplied = false; reset(element); /// show checkbox to enable all fields only if editing an entry actionForceShowAllWidgets->setVisible(!internalEntry.isNull()); /// Disable widgets if necessary if (!actionForceShowAllWidgets->isChecked()) updateReqOptWidgets(); } void reset(QSharedPointer element) { for (WidgetList::Iterator it = widgets.begin(); it != widgets.end(); ++it) { (*it)->setFile(file); (*it)->reset(element); (*it)->setModified(false); } QSharedPointer e = element.dynamicCast(); if (!e.isNull()) { internalEntry = QSharedPointer(new Entry(*e.data())); sourceWidget->setElementClass(SourceWidget::elementEntry); } else { QSharedPointer m = element.dynamicCast(); if (!m.isNull()) { internalMacro = QSharedPointer(new Macro(*m.data())); sourceWidget->setElementClass(SourceWidget::elementMacro); } else { QSharedPointer c = element.dynamicCast(); if (!c.isNull()) { internalComment = QSharedPointer(new Comment(*c.data())); sourceWidget->setElementClass(SourceWidget::elementComment); } else { QSharedPointer p = element.dynamicCast(); if (!p.isNull()) { internalPreamble = QSharedPointer(new Preamble(*p.data())); sourceWidget->setElementClass(SourceWidget::elementPreamble); } else Q_ASSERT_X(element.isNull(), "ElementEditor::ElementEditorPrivate::reset(QSharedPointer element)", "element is not NULL but could not be cast on a valid Element sub-class"); } } } buttonCheckWithBibTeX->setEnabled(!internalEntry.isNull()); } void setReadOnly(bool isReadOnly) { for (WidgetList::Iterator it = widgets.begin(); it != widgets.end(); ++it) (*it)->setReadOnly(isReadOnly); } void updateReqOptWidgets() { /// this function is only relevant if editing an entry (and not e.g. a comment) if (internalEntry.isNull()) return; /// quick-and-dirty test if editing an entry /// make a temporary snapshot of the current state QSharedPointer tempEntry = QSharedPointer(new Entry()); apply(tempEntry); /// update the enabled/disabled state of required and optional widgets/fields bool forceVisible = actionForceShowAllWidgets->isChecked(); for (ElementWidget *elementWidget : const_cast(widgets)) { elementWidget->showReqOptWidgets(forceVisible, tempEntry->type()); } /// save configuration static const QString configGroupName = QStringLiteral("User Interface"); static const QString keyEnableAllWidgets = QStringLiteral("EnableAllWidgets"); KConfigGroup configGroup(config, configGroupName); configGroup.writeEntry(keyEnableAllWidgets, actionForceShowAllWidgets->isChecked()); config->sync(); } void limitKeyboardTabStops() { /// save configuration static const QString configGroupName = QStringLiteral("User Interface"); KConfigGroup configGroup(config, configGroupName); configGroup.writeEntry(MenuLineEdit::keyLimitKeyboardTabStops, actionLimitKeyboardTabStops->isChecked()); config->sync(); /// notify all listening MenuLineEdit widgets to change their behavior NotificationHub::publishEvent(MenuLineEdit::MenuLineConfigurationChangedEvent); } void switchTo(QWidget *futureTab) { /// Switched from source widget to another widget? const bool isToSourceWidget = futureTab == sourceWidget; /// Switch from some widget to the source widget? const bool isFromSourceWidget = previousWidget == sourceWidget; /// Interprete future widget as an ElementWidget ElementWidget *futureWidget = qobject_cast(futureTab); /// Past and future ElementWidget values are valid? if (previousWidget != nullptr && futureWidget != nullptr) { /// Assign to temp wihch internal variable holds current state QSharedPointer temp; if (!internalEntry.isNull()) temp = internalEntry; else if (!internalMacro.isNull()) temp = internalMacro; else if (!internalComment.isNull()) temp = internalComment; else if (!internalPreamble.isNull()) temp = internalPreamble; Q_ASSERT_X(!temp.isNull(), "void ElementEditor::ElementEditorPrivate::switchTo(QWidget *newTab)", "temp is NULL"); /// Past widget writes its state to the internal state previousWidget->apply(temp); /// Before switching to source widget, store internally reference widget's state if (isToSourceWidget && referenceWidget != nullptr) referenceWidget->apply(temp); /// Tell future widget to initialize itself based on internal state futureWidget->reset(temp); /// When switchin from source widget to another widget, initialize reference widget if (isFromSourceWidget && referenceWidget != nullptr) referenceWidget->reset(temp); } previousWidget = futureWidget; /// Enable/disable tabs for (WidgetList::Iterator it = widgets.begin(); it != widgets.end(); ++it) (*it)->setEnabled(!isToSourceWidget || *it == futureTab); } /** * Test current entry if it compiles with BibTeX. * Show warnings and errors in message box. */ void checkBibTeX() { /// disable GUI under process p->setEnabled(false); QSharedPointer entry = QSharedPointer(new Entry()); apply(entry); CheckBibTeX::checkBibTeX(entry, file, p); p->setEnabled(true); } void setModified(bool newIsModified) { for (WidgetList::Iterator it = widgets.begin(); it != widgets.end(); ++it) (*it)->setModified(newIsModified); } void referenceWidgetSetEntryIdByDefault() { referenceWidget->setEntryIdByDefault(); } }; ElementEditor::ElementEditor(bool scrollable, QWidget *parent) : QWidget(parent), d(new ElementEditorPrivate(scrollable, this)) { connect(d->tab, &HidingTabWidget::currentChanged, this, &ElementEditor::tabChanged); } ElementEditor::~ElementEditor() { disconnect(d->tab, &HidingTabWidget::currentChanged, this, &ElementEditor::tabChanged); delete d; } void ElementEditor::apply() { /// The prime problem to tackle in this function is to cope with /// invalid/problematic entry ids or macro keys, respectively: /// - empty ids/keys /// - ids/keys that are duplicates of already used ids/keys QSharedPointer entry = d->element.dynamicCast(); QSharedPointer macro = d->element.dynamicCast(); /// Only for entry or macro bother with duplicate ids (but not for preamble or comment) if (!entry.isNull() || !macro.isNull()) { /// Determine id/key as it was set before the current editing started const QString originalId = !entry.isNull() ? entry->id() : (!macro.isNull() ? macro->key() : QString()); /// Get the id/key as it is in the editing widget right now const QString newId = d->currentId(); /// Keep track whether the 'original' id/key or the 'new' id/key will eventually be used enum IdToUse {UseOriginalId, UseNewId}; IdToUse idToUse = UseNewId; if (newId.isEmpty() && !originalId.isEmpty()) { /// New id/key is empty (invalid by definition), so just notify use and revert back to original id/key /// (assuming that original id/key is valid) KMessageBox::sorry(this, i18n("No id was entered, so the previous id '%1' will be restored.", originalId), i18n("No id given")); idToUse = UseOriginalId; } else if (!newId.isEmpty()) { // FIXME test if !originalId.isEmpty() ? /// If new id/key is not empty, then check if it is identical to another entry/macro in the current file const QSharedPointer knownElementWithSameId = d->currentFile() != nullptr ? d->currentFile()->containsKey(newId) : QSharedPointer(); if (!knownElementWithSameId.isNull() && d->element != knownElementWithSameId) { /// Some other, different element (entry or macro) uses same id/key, so ask user how to proceed const int msgBoxResult = KMessageBox::warningContinueCancel(this, i18n("The entered id '%1' is already in use for another element.\n\nKeep original id '%2' instead?", newId, originalId), i18n("Id already in use"), KGuiItem(i18n("Keep duplicate ids")), KGuiItem(i18n("Restore original id"))); idToUse = msgBoxResult == KMessageBox::Continue ? UseNewId : UseOriginalId; } } if (idToUse == UseOriginalId) { /// As 'apply()' above set the 'new' id/key but the 'original' id/key is to be used, /// now UI must be updated accordingly. Changes will propagate to the entry id or /// macro key, respectively, when invoking apply() further down d->setCurrentId(originalId); } /// Case idToUse == UseNewId does not need to get handled as newId == d->currentId() } d->apply(); d->setModified(false); emit modified(false); } void ElementEditor::reset() { d->reset(); emit modified(false); } bool ElementEditor::validate() { QWidget *widgetWithIssue = nullptr; QString message; if (!validate(&widgetWithIssue, message)) { const QString msgBoxMessage = message.isEmpty() ? i18n("Validation for the current element failed.") : i18n("Validation for the current element failed:\n%1", message); KMessageBox::error(this, msgBoxMessage, i18n("Element validation failed")); if (widgetWithIssue != nullptr) { /// Probe if widget with issue is inside a QTabWiget; if yes, make parenting tab the current tab QWidget *cur = widgetWithIssue; do { QTabWidget *tabWidget = cur->parent() != nullptr && cur->parent()->parent() != nullptr ? qobject_cast(cur->parent()->parent()) : nullptr; if (tabWidget != nullptr) { tabWidget->setCurrentWidget(cur); break; } cur = qobject_cast(cur->parent()); } while (cur != nullptr); /// Set focus to widget with issue widgetWithIssue->setFocus(); } return false; } return true; } void ElementEditor::setElement(QSharedPointer element, const File *file) { d->setElement(element, file); d->reset(); emit modified(false); } void ElementEditor::setElement(QSharedPointer element, const File *file) { QSharedPointer clone; QSharedPointer entry = element.dynamicCast(); if (!entry.isNull()) clone = QSharedPointer(new Entry(*entry.data())); else { QSharedPointer macro = element.dynamicCast(); if (!macro.isNull()) clone = QSharedPointer(new Macro(*macro.data())); else { QSharedPointer preamble = element.dynamicCast(); if (!preamble.isNull()) clone = QSharedPointer(new Preamble(*preamble.data())); else { QSharedPointer comment = element.dynamicCast(); if (!comment.isNull()) clone = QSharedPointer(new Comment(*comment.data())); else Q_ASSERT_X(element == nullptr, "ElementEditor::ElementEditor(const Element *element, QWidget *parent)", "element is not NULL but could not be cast on a valid Element sub-class"); } } } d->setElement(clone, file); d->reset(); } void ElementEditor::setReadOnly(bool isReadOnly) { d->setReadOnly(isReadOnly); } bool ElementEditor::elementChanged() { return d->elementChanged; } bool ElementEditor::elementUnapplied() { return d->elementUnapplied; } bool ElementEditor::validate(QWidget **widgetWithIssue, QString &message) { return d->validate(widgetWithIssue, message); } QWidget *ElementEditor::currentPage() const { return d->tab->currentWidget(); } void ElementEditor::setCurrentPage(QWidget *page) { if (d->tab->indexOf(page) >= 0) d->tab->setCurrentWidget(page); } void ElementEditor::tabChanged() { d->switchTo(d->tab->currentWidget()); } void ElementEditor::switchToTab(const QString &tabIdentifier) { if (tabIdentifier == QStringLiteral("source")) setCurrentPage(d->sourceWidget); else if (tabIdentifier == QStringLiteral("external")) setCurrentPage(d->filesWidget); else { for (ElementWidget *widget : d->widgets) { EntryConfiguredWidget *ecw = qobject_cast(widget); if (ecw != nullptr && ecw->identifier() == tabIdentifier) { setCurrentPage(ecw); break; } } } } -void ElementEditor::checkBibTeX() -{ - d->checkBibTeX(); -} - void ElementEditor::childModified(bool m) { if (m) { d->elementUnapplied = true; d->referenceWidgetSetEntryIdByDefault(); } emit modified(m); } - -void ElementEditor::updateReqOptWidgets() -{ - d->updateReqOptWidgets(); -} - -void ElementEditor::limitKeyboardTabStops() -{ - d->limitKeyboardTabStops(); -} diff --git a/src/gui/element/elementeditor.h b/src/gui/element/elementeditor.h index 61631808..14f88f2a 100644 --- a/src/gui/element/elementeditor.h +++ b/src/gui/element/elementeditor.h @@ -1,79 +1,76 @@ /*************************************************************************** * 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 . * ***************************************************************************/ #ifndef KBIBTEX_GUI_ELEMENTEDITOR_H #define KBIBTEX_GUI_ELEMENTEDITOR_H #include #include "kbibtexgui_export.h" class Element; class File; /** @author Thomas Fischer */ class KBIBTEXGUI_EXPORT ElementEditor : public QWidget { Q_OBJECT public: class ApplyElementInterface { public: virtual ~ApplyElementInterface() { /** nothing */ } virtual void apply(QSharedPointer) = 0; virtual bool validate(QWidget **widgetWithIssue, QString &message) const = 0; }; ElementEditor(bool scrollable, QWidget *parent); ~ElementEditor() override; void setElement(QSharedPointer element, const File *file); void setElement(QSharedPointer element, const File *file); void setReadOnly(bool isReadOnly = true); bool elementChanged(); bool elementUnapplied(); bool validate(QWidget **widgetWithIssue, QString &message); QWidget *currentPage() const; void setCurrentPage(QWidget *tab); signals: void modified(bool); public slots: void apply(); void reset(); bool validate(); private slots: void tabChanged(); void switchToTab(const QString &tabIdentifier); - void checkBibTeX(); void childModified(bool); - void updateReqOptWidgets(); - void limitKeyboardTabStops(); private: class ElementEditorPrivate; ElementEditorPrivate *d; }; #endif // KBIBTEX_GUI_ELEMENTEDITOR_H diff --git a/src/gui/element/findpdfui.cpp b/src/gui/element/findpdfui.cpp index 1f892ff7..f4584e20 100644 --- a/src/gui/element/findpdfui.cpp +++ b/src/gui/element/findpdfui.cpp @@ -1,534 +1,530 @@ /*************************************************************************** * 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 "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 KRun::runUrl(tempUrl, mimeTypeName, itemView(), KRun::RunFlags(), url.toDisplayString()); } 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 KRun::runUrl(url, mimeTypeName, itemView(), KRun::RunFlags()); } } } /** * 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/element/findpdfui.h b/src/gui/element/findpdfui.h index 87cf106e..58338a07 100644 --- a/src/gui/element/findpdfui.h +++ b/src/gui/element/findpdfui.h @@ -1,77 +1,76 @@ /*************************************************************************** - * Copyright (C) 2004-2017 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 . * ***************************************************************************/ #ifndef KBIBTEX_GUI_FINDPDFUI_H #define KBIBTEX_GUI_FINDPDFUI_H #include #include #include #include "kbibtexgui_export.h" class QListView; /** * A user interface too @see FindPDF * @author Thomas Fischer */ class KBIBTEXGUI_EXPORT FindPDFUI : public QWidget { Q_OBJECT public: ~FindPDFUI() override; /** * Show a modal dialog that allows the user to start searching for PDF files, * and from the set of found results to select (1) which PDF files to download * and keep, (2) which URLs to PDF files to keep (no file downloading) and (3) * which PDF files to ignore. * * @param entry * @param bibtexFile * @param parent */ static void interactiveFindPDF(Entry &entry, const File &bibtexFile, QWidget *parent); signals: void resultAvailable(bool); public slots: /** * Abort a running search. */ void stopSearch(); - void abort(); protected: FindPDFUI(Entry &entry, QWidget *parent); void apply(Entry &entry, const File &bibtexFile); private: class Private; Private *const d; private slots: void searchFinished(); void searchProgress(int visitedPages, int runningJobs, int foundDocuments); }; #endif // KBIBTEX_GUI_FINDPDFUI_H diff --git a/src/gui/field/fieldlineedit.cpp b/src/gui/field/fieldlineedit.cpp index 60c465d5..710fd912 100644 --- a/src/gui/field/fieldlineedit.cpp +++ b/src/gui/field/fieldlineedit.cpp @@ -1,556 +1,550 @@ /*************************************************************************** * 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 "fieldlineedit.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 "logging_gui.h" class FieldLineEdit::FieldLineEditPrivate { private: FieldLineEdit *parent; Value currentValue; KBibTeX::TypeFlag preferredTypeFlag; KBibTeX::TypeFlags typeFlags; QPushButton *buttonOpenUrl; public: QMenu *menuTypes; QUrl urlToOpen; const File *file; QString fieldKey; KBibTeX::TypeFlag typeFlag; FieldLineEditPrivate(KBibTeX::TypeFlag ptf, KBibTeX::TypeFlags tf, FieldLineEdit *p) : parent(p), preferredTypeFlag(ptf), typeFlags(tf), file(nullptr) { menuTypes = new QMenu(parent); setupMenu(); buttonOpenUrl = new QPushButton(QIcon::fromTheme(QStringLiteral("document-open-remote")), QString(), parent); buttonOpenUrl->setVisible(false); buttonOpenUrl->setProperty("isConst", true); parent->appendWidget(buttonOpenUrl); - connect(buttonOpenUrl, &QPushButton::clicked, parent, &FieldLineEdit::slotOpenUrl); + connect(buttonOpenUrl, &QPushButton::clicked, parent, [this]() { + openUrl(); + }); - connect(p, &FieldLineEdit::textChanged, p, &FieldLineEdit::slotTextChanged); + connect(p, &FieldLineEdit::textChanged, p, [this](const QString & text) { + textChanged(text); + }); Value value; typeFlag = determineTypeFlag(value, preferredTypeFlag, typeFlags); updateGUI(typeFlag); } bool reset(const Value &value) { bool result = false; QString text; typeFlag = determineTypeFlag(value, typeFlag, typeFlags); updateGUI(typeFlag); if (!value.isEmpty()) { if (typeFlag == KBibTeX::tfSource) { /// simple case: field's value is to be shown as BibTeX code, including surrounding curly braces FileExporterBibTeX exporter(parent); text = exporter.valueToBibTeX(value); result = true; } else { /// except for the source view type flag, type flag views do not support composed values, /// therefore only the first value will be shown const QSharedPointer first = value.first(); const QSharedPointer plainText = first.dynamicCast<PlainText>(); if (typeFlag == KBibTeX::tfPlainText && !plainText.isNull()) { text = plainText->text(); result = true; } else { const QSharedPointer<Person> person = first.dynamicCast<Person>(); if (typeFlag == KBibTeX::tfPerson && !person.isNull()) { text = Person::transcribePersonName(person.data(), Preferences::instance().personNameFormat()); result = true; } else { const QSharedPointer<MacroKey> macroKey = first.dynamicCast<MacroKey>(); if (typeFlag == KBibTeX::tfReference && !macroKey.isNull()) { text = macroKey->text(); result = true; } else { const QSharedPointer<Keyword> keyword = first.dynamicCast<Keyword>(); if (typeFlag == KBibTeX::tfKeyword && !keyword.isNull()) { text = keyword->text(); result = true; } else { const QSharedPointer<VerbatimText> verbatimText = first.dynamicCast<VerbatimText>(); if (typeFlag == KBibTeX::tfVerbatim && !verbatimText.isNull()) { text = verbatimText->text(); result = true; } else qCWarning(LOG_KBIBTEX_GUI) << "Could not reset: " << typeFlag << "(" << (typeFlag == KBibTeX::tfSource ? "Source" : (typeFlag == KBibTeX::tfReference ? "Reference" : (typeFlag == KBibTeX::tfPerson ? "Person" : (typeFlag == KBibTeX::tfPlainText ? "PlainText" : (typeFlag == KBibTeX::tfKeyword ? "Keyword" : (typeFlag == KBibTeX::tfVerbatim ? "Verbatim" : "???")))))) << ") " << (typeFlags.testFlag(KBibTeX::tfPerson) ? "Person" : "") << (typeFlags.testFlag(KBibTeX::tfPlainText) ? "PlainText" : "") << (typeFlags.testFlag(KBibTeX::tfReference) ? "Reference" : "") << (typeFlags.testFlag(KBibTeX::tfVerbatim) ? "Verbatim" : "") << " " << typeid((void)*first).name() << " : " << PlainTextValue::text(value); } } } } } } updateURL(text); parent->setText(text); return result; } bool apply(Value &value) const { value.clear(); /// Remove unnecessary white space from input /// Exception: source and verbatim content is kept unmodified const QString text = typeFlag == KBibTeX::tfSource || typeFlag == KBibTeX::tfVerbatim ? parent->text() : parent->text().simplified(); if (text.isEmpty()) return true; const EncoderLaTeX &encoder = EncoderLaTeX::instance(); const QString encodedText = encoder.decode(text); static const QRegularExpression invalidCharsForReferenceRegExp(QStringLiteral("[^-_:/a-zA-Z0-9]")); if (encodedText.isEmpty()) return true; else if (typeFlag == KBibTeX::tfPlainText) { value.append(QSharedPointer<PlainText>(new PlainText(encodedText))); return true; } else if (typeFlag == KBibTeX::tfReference && !encodedText.contains(invalidCharsForReferenceRegExp)) { value.append(QSharedPointer<MacroKey>(new MacroKey(encodedText))); return true; } else if (typeFlag == KBibTeX::tfPerson) { QSharedPointer<Person> person = FileImporterBibTeX::personFromString(encodedText); if (!person.isNull()) value.append(person); return true; } else if (typeFlag == KBibTeX::tfKeyword) { const QList<QSharedPointer<Keyword> > keywords = FileImporterBibTeX::splitKeywords(encodedText); for (const auto &keyword : keywords) value.append(keyword); return true; } else if (typeFlag == KBibTeX::tfSource) { const QString key = typeFlags.testFlag(KBibTeX::tfPerson) ? QStringLiteral("author") : QStringLiteral("title"); FileImporterBibTeX importer(parent); const QString fakeBibTeXFile = QString(QStringLiteral("@article{dummy, %1=%2}")).arg(key, encodedText); const QScopedPointer<const File> file(importer.fromString(fakeBibTeXFile)); if (!file.isNull() && file->count() == 1) { QSharedPointer<Entry> entry = file->first().dynamicCast<Entry>(); if (!entry.isNull()) { value = entry->value(key); return !value.isEmpty(); } else qCWarning(LOG_KBIBTEX_GUI) << "Parsing " << fakeBibTeXFile << " did not result in valid entry"; } } else if (typeFlag == KBibTeX::tfVerbatim) { value.append(QSharedPointer<VerbatimText>(new VerbatimText(text))); return true; } return false; } bool validate(QWidget **widgetWithIssue, QString &message) const { message.clear(); /// Remove unnecessary white space from input /// Exception: source and verbatim content is kept unmodified const QString text = typeFlag == KBibTeX::tfSource || typeFlag == KBibTeX::tfVerbatim ? parent->text() : parent->text().simplified(); if (text.isEmpty()) return true; const EncoderLaTeX &encoder = EncoderLaTeX::instance(); const QString encodedText = encoder.decode(text); if (encodedText.isEmpty()) return true; bool result = false; if (typeFlag == KBibTeX::tfPlainText || typeFlag == KBibTeX::tfPerson || typeFlag == KBibTeX::tfKeyword) { result = KBibTeX::validateCurlyBracketContext(text) == 0; if (!result) message = i18n("Opening and closing curly brackets do not match."); } else if (typeFlag == KBibTeX::tfReference) { static const QRegularExpression validReferenceRegExp(QStringLiteral("^[-_:/a-zA-Z0-9]+$")); const QRegularExpressionMatch validReferenceMatch = validReferenceRegExp.match(text); result = validReferenceMatch.hasMatch() && validReferenceMatch.captured() == text; if (!result) message = i18n("Reference contains characters outside of the allowed set."); } else if (typeFlag == KBibTeX::tfSource) { const QString key = typeFlags.testFlag(KBibTeX::tfPerson) ? QStringLiteral("author") : QStringLiteral("title"); FileImporterBibTeX importer(parent); const QString fakeBibTeXFile = QString(QStringLiteral("@article{dummy, %1=%2}")).arg(key, encodedText); const QScopedPointer<const File> file(importer.fromString(fakeBibTeXFile)); if (file.isNull() || file->count() != 1) return false; QSharedPointer<Entry> entry = file->first().dynamicCast<Entry>(); result = !entry.isNull() && entry->count() == 1; if (!result) message = i18n("Source code could not be parsed correctly."); } else if (typeFlag == KBibTeX::tfVerbatim) { result = KBibTeX::validateCurlyBracketContext(text) == 0; if (!result) message = i18n("Opening and closing curly brackets do not match."); } if (!result && widgetWithIssue != nullptr) *widgetWithIssue = parent; return result; } KBibTeX::TypeFlag determineTypeFlag(const Value &value, KBibTeX::TypeFlag preferredTypeFlag, KBibTeX::TypeFlags availableTypeFlags) { KBibTeX::TypeFlag result = KBibTeX::tfSource; if (availableTypeFlags.testFlag(preferredTypeFlag) && typeFlagSupported(value, preferredTypeFlag)) result = preferredTypeFlag; else if (value.count() == 1) { int p = 1; for (int i = 1; i < 8; ++i, p <<= 1) { const KBibTeX::TypeFlag flag = static_cast<KBibTeX::TypeFlag>(p); if (availableTypeFlags.testFlag(flag) && typeFlagSupported(value, flag)) { result = flag; break; } } } return result; } bool typeFlagSupported(const Value &value, KBibTeX::TypeFlag typeFlag) { if (value.isEmpty() || typeFlag == KBibTeX::tfSource) return true; const QSharedPointer<ValueItem> first = value.first(); if (value.count() > 1) return typeFlag == KBibTeX::tfSource; else if (typeFlag == KBibTeX::tfKeyword && Keyword::isKeyword(*first)) return true; else if (typeFlag == KBibTeX::tfPerson && Person::isPerson(*first)) return true; else if (typeFlag == KBibTeX::tfPlainText && PlainText::isPlainText(*first)) return true; else if (typeFlag == KBibTeX::tfReference && MacroKey::isMacroKey(*first)) return true; else if (typeFlag == KBibTeX::tfVerbatim && VerbatimText::isVerbatimText(*first)) return true; else return false; } void setupMenu() { menuTypes->clear(); if (typeFlags.testFlag(KBibTeX::tfPlainText)) { QAction *action = menuTypes->addAction(iconForTypeFlag(KBibTeX::tfPlainText), i18n("Plain Text")); connect(action, &QAction::triggered, parent, [this]() { typeChanged(KBibTeX::tfPlainText); }); } if (typeFlags.testFlag(KBibTeX::tfReference)) { QAction *action = menuTypes->addAction(iconForTypeFlag(KBibTeX::tfReference), i18n("Reference")); connect(action, &QAction::triggered, parent, [this]() { typeChanged(KBibTeX::tfReference); }); } if (typeFlags.testFlag(KBibTeX::tfPerson)) { QAction *action = menuTypes->addAction(iconForTypeFlag(KBibTeX::tfPerson), i18n("Person")); connect(action, &QAction::triggered, parent, [this]() { typeChanged(KBibTeX::tfPerson); }); } if (typeFlags.testFlag(KBibTeX::tfKeyword)) { QAction *action = menuTypes->addAction(iconForTypeFlag(KBibTeX::tfKeyword), i18n("Keyword")); connect(action, &QAction::triggered, parent, [this]() { typeChanged(KBibTeX::tfKeyword); }); } if (typeFlags.testFlag(KBibTeX::tfSource)) { QAction *action = menuTypes->addAction(iconForTypeFlag(KBibTeX::tfSource), i18n("Source Code")); connect(action, &QAction::triggered, parent, [this]() { typeChanged(KBibTeX::tfSource); }); } if (typeFlags.testFlag(KBibTeX::tfVerbatim)) { QAction *action = menuTypes->addAction(iconForTypeFlag(KBibTeX::tfVerbatim), i18n("Verbatim Text")); connect(action, &QAction::triggered, parent, [this]() { typeChanged(KBibTeX::tfVerbatim); }); } } QIcon iconForTypeFlag(KBibTeX::TypeFlag typeFlag) { switch (typeFlag) { case KBibTeX::tfInvalid: return QIcon(); case KBibTeX::tfPlainText: return QIcon::fromTheme(QStringLiteral("draw-text")); case KBibTeX::tfReference: return QIcon::fromTheme(QStringLiteral("emblem-symbolic-link")); case KBibTeX::tfPerson: return QIcon::fromTheme(QStringLiteral("user-identity")); case KBibTeX::tfKeyword: return QIcon::fromTheme(QStringLiteral("edit-find")); case KBibTeX::tfSource: return QIcon::fromTheme(QStringLiteral("code-context")); case KBibTeX::tfVerbatim: return QIcon::fromTheme(QStringLiteral("preferences-desktop-keyboard")); } return QIcon(); //< should never happen as switch above covers all cases } void updateGUI(KBibTeX::TypeFlag typeFlag) { parent->setFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont)); parent->setIcon(iconForTypeFlag(typeFlag)); switch (typeFlag) { case KBibTeX::tfInvalid: parent->setButtonToolTip(QString()); break; case KBibTeX::tfPlainText: parent->setButtonToolTip(i18n("Plain Text")); break; case KBibTeX::tfReference: parent->setButtonToolTip(i18n("Reference")); break; case KBibTeX::tfPerson: parent->setButtonToolTip(i18n("Person")); break; case KBibTeX::tfKeyword: parent->setButtonToolTip(i18n("Keyword")); break; case KBibTeX::tfSource: parent->setButtonToolTip(i18n("Source Code")); parent->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); break; case KBibTeX::tfVerbatim: parent->setButtonToolTip(i18n("Verbatim Text")); break; } } void openUrl() { if (urlToOpen.isValid()) { /// Guess mime type for url to open QMimeType mimeType = FileInfo::mimeTypeForUrl(urlToOpen); const QString mimeTypeName = mimeType.name(); /// Ask KDE subsystem to open url in viewer matching mime type KRun::runUrl(urlToOpen, mimeTypeName, parent, KRun::RunFlags()); } } bool convertValueType(Value &value, KBibTeX::TypeFlag destType) { if (value.isEmpty()) return true; /// simple case if (destType == KBibTeX::tfSource) return true; /// simple case bool result = true; const EncoderLaTeX &encoder = EncoderLaTeX::instance(); QString rawText; const QSharedPointer<ValueItem> first = value.first(); const QSharedPointer<PlainText> plainText = first.dynamicCast<PlainText>(); if (!plainText.isNull()) rawText = encoder.encode(plainText->text(), Encoder::TargetEncodingASCII); else { const QSharedPointer<VerbatimText> verbatimText = first.dynamicCast<VerbatimText>(); if (!verbatimText.isNull()) rawText = verbatimText->text(); else { const QSharedPointer<MacroKey> macroKey = first.dynamicCast<MacroKey>(); if (!macroKey.isNull()) rawText = macroKey->text(); else { const QSharedPointer<Person> person = first.dynamicCast<Person>(); if (!person.isNull()) rawText = encoder.encode(QString(QStringLiteral("%1 %2")).arg(person->firstName(), person->lastName()), Encoder::TargetEncodingASCII); // FIXME proper name conversion else { const QSharedPointer<Keyword> keyword = first.dynamicCast<Keyword>(); if (!keyword.isNull()) rawText = encoder.encode(keyword->text(), Encoder::TargetEncodingASCII); else { // TODO case missed? result = false; } } } } } switch (destType) { case KBibTeX::tfPlainText: value.clear(); value.append(QSharedPointer<PlainText>(new PlainText(encoder.decode(rawText)))); break; case KBibTeX::tfVerbatim: value.clear(); value.append(QSharedPointer<VerbatimText>(new VerbatimText(rawText))); break; case KBibTeX::tfPerson: value.clear(); value.append(QSharedPointer<Person>(FileImporterBibTeX::splitName(encoder.decode(rawText)))); break; case KBibTeX::tfReference: { MacroKey *macroKey = new MacroKey(rawText); if (macroKey->isValid()) { value.clear(); value.append(QSharedPointer<MacroKey>(macroKey)); } else { delete macroKey; result = false; } } break; case KBibTeX::tfKeyword: value.clear(); value.append(QSharedPointer<Keyword>(new Keyword(encoder.decode(rawText)))); break; default: { // TODO result = false; } } return result; } void updateURL(const QString &text) { QSet<QUrl> urls; FileInfo::urlsInText(text, FileInfo::TestExistenceYes, file != nullptr && file->property(File::Url).toUrl().isValid() ? QUrl(file->property(File::Url).toUrl()).path() : QString(), urls); QSet<QUrl>::ConstIterator urlsIt = urls.constBegin(); if (urlsIt != urls.constEnd() && (*urlsIt).isValid()) urlToOpen = (*urlsIt); else urlToOpen = QUrl(); /// set special "open URL" button visible if URL (or file or DOI) found buttonOpenUrl->setVisible(urlToOpen.isValid()); buttonOpenUrl->setToolTip(i18n("Open '%1'", urlToOpen.url(QUrl::PreferLocalFile))); } void textChanged(const QString &text) { updateURL(text); } void typeChanged(const KBibTeX::TypeFlag newTypeFlag) { Value value; apply(value); if (convertValueType(value, newTypeFlag)) { typeFlag = newTypeFlag; reset(value); } else KMessageBox::error(parent, i18n("The current text cannot be used as value of type '%1'.\n\nSwitching back to type '%2'.", BibTeXFields::typeFlagToString(newTypeFlag), BibTeXFields::typeFlagToString(typeFlag))); } }; FieldLineEdit::FieldLineEdit(KBibTeX::TypeFlag preferredTypeFlag, KBibTeX::TypeFlags typeFlags, bool isMultiLine, QWidget *parent) : MenuLineEdit(isMultiLine, parent), d(new FieldLineEdit::FieldLineEditPrivate(preferredTypeFlag, typeFlags, this)) { setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); setObjectName(QStringLiteral("FieldLineEdit")); setMenu(d->menuTypes); setChildAcceptDrops(false); setAcceptDrops(true); } FieldLineEdit::~FieldLineEdit() { delete d; } bool FieldLineEdit::apply(Value &value) const { return d->apply(value); } bool FieldLineEdit::reset(const Value &value) { return d->reset(value); } bool FieldLineEdit::validate(QWidget **widgetWithIssue, QString &message) const { return d->validate(widgetWithIssue, message); } void FieldLineEdit::setReadOnly(bool isReadOnly) { MenuLineEdit::setReadOnly(isReadOnly); } void FieldLineEdit::setFile(const File *file) { d->file = file; } void FieldLineEdit::setElement(const Element *element) { Q_UNUSED(element) } void FieldLineEdit::setFieldKey(const QString &fieldKey) { d->fieldKey = fieldKey; } -void FieldLineEdit::slotOpenUrl() -{ - d->openUrl(); -} - -void FieldLineEdit::slotTextChanged(const QString &text) -{ - d->textChanged(text); -} - void FieldLineEdit::dragEnterEvent(QDragEnterEvent *event) { if (event->mimeData()->hasFormat(QStringLiteral("text/plain")) || event->mimeData()->hasFormat(QStringLiteral("text/x-bibtex"))) event->acceptProposedAction(); } void FieldLineEdit::dropEvent(QDropEvent *event) { const QString clipboardText = event->mimeData()->text(); if (clipboardText.isEmpty()) return; bool success = false; if (!d->fieldKey.isEmpty() && clipboardText.startsWith(QStringLiteral("@"))) { FileImporterBibTeX importer(this); QScopedPointer<File> file(importer.fromString(clipboardText)); const QSharedPointer<Entry> entry = (!file.isNull() && file->count() == 1) ? file->first().dynamicCast<Entry>() : QSharedPointer<Entry>(); if (!entry.isNull() && d->fieldKey == Entry::ftCrossRef) { /// handle drop on crossref line differently (use dropped entry's id) Value v; v.append(QSharedPointer<VerbatimText>(new VerbatimText(entry->id()))); reset(v); emit textChanged(entry->id()); success = true; } else if (!entry.isNull() && entry->contains(d->fieldKey)) { /// case for "normal" fields like for journal, pages, ... reset(entry->value(d->fieldKey)); emit textChanged(text()); success = true; } } if (!success) { /// fall-back case: just copy whole text into edit widget setText(clipboardText); emit textChanged(clipboardText); } } diff --git a/src/gui/field/fieldlineedit.h b/src/gui/field/fieldlineedit.h index 1d2aac0e..1b0e3edf 100644 --- a/src/gui/field/fieldlineedit.h +++ b/src/gui/field/fieldlineedit.h @@ -1,75 +1,71 @@ /*************************************************************************** * 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/>. * ***************************************************************************/ #ifndef KBIBTEX_GUI_FIELDLINEEDIT_H #define KBIBTEX_GUI_FIELDLINEEDIT_H #include "kbibtexgui_export.h" #include <QIcon> #include <KBibTeX> #include <Value> #include <widgets/MenuLineEdit> class QMenu; class Element; /** * @author Thomas Fischer <fischer@unix-ag.uni-kl.de> */ class KBIBTEXGUI_EXPORT FieldLineEdit : public MenuLineEdit { Q_OBJECT public: FieldLineEdit(KBibTeX::TypeFlag preferredTypeFlag, KBibTeX::TypeFlags typeFlags, bool isMultiLine = false, QWidget *parent = nullptr); ~FieldLineEdit() override; bool reset(const Value &value); bool apply(Value &value) const; bool validate(QWidget **widgetWithIssue, QString &message) const; void setReadOnly(bool) override; void setFile(const File *file); void setElement(const Element *element); void setFieldKey(const QString &fieldKey); protected: void dragEnterEvent(QDragEnterEvent *event) override; void dropEvent(QDropEvent *event) override; private: bool m_incompleteRepresentation; KBibTeX::TypeFlag typeFlag(); KBibTeX::TypeFlag setTypeFlag(KBibTeX::TypeFlag typeFlag); void setupMenu(); void updateGUI(); class FieldLineEditPrivate; FieldLineEdit::FieldLineEditPrivate *d; - -private slots: - void slotTextChanged(const QString &); - void slotOpenUrl(); }; #endif // KBIBTEX_GUI_FIELDLINEEDIT_H diff --git a/src/gui/file/basicfileview.cpp b/src/gui/file/basicfileview.cpp index 374eb751..329b9dc9 100644 --- a/src/gui/file/basicfileview.cpp +++ b/src/gui/file/basicfileview.cpp @@ -1,276 +1,273 @@ /*************************************************************************** * 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 "basicfileview.h" #include <QHeaderView> #include <QScrollBar> #include <QKeyEvent> #include <QAction> #include <QMenu> #include <KLocalizedString> #include <BibTeXFields> #include <models/FileModel> #include "file/sortfilterfilemodel.h" #include "logging_gui.h" class BasicFileView::Private { private: BasicFileView *p; const QString name; public: FileModel *fileModel; QSortFilterProxyModel *sortFilterProxyModel; Private(const QString &n, BasicFileView *parent) : p(parent), name(n), fileModel(nullptr), sortFilterProxyModel(nullptr) { /// nothing } ~Private() { saveColumnProperties(); } void balanceColumns() { if (p->header()->count() != BibTeXFields::instance().count()) { qCWarning(LOG_KBIBTEX_GUI) << "Number of columns in file view does not match number of bibliography fields:" << p->header()->count() << "!=" << BibTeXFields::instance().count(); return; } int defaultWidthSumVisible = 0; int col = 0; for (const auto &fd : const_cast<const BibTeXFields &>(BibTeXFields::instance())) { if (!p->header()->isSectionHidden(col)) defaultWidthSumVisible += fd.defaultWidth; ++col; } if (defaultWidthSumVisible == 0) return; col = 0; for (const auto &fd : const_cast<const BibTeXFields &>(BibTeXFields::instance())) { if (!p->header()->isSectionHidden(col)) p->header()->resizeSection(col, p->header()->width() * fd.defaultWidth / defaultWidthSumVisible); ++col; } } void resetColumnProperties() { if (p->header()->count() != BibTeXFields::instance().count()) { qCWarning(LOG_KBIBTEX_GUI) << "Number of columns in file view does not match number of bibliography fields:" << p->header()->count() << "!=" << BibTeXFields::instance().count(); return; } int col = 0; for (BibTeXFields::Iterator it = BibTeXFields::instance().begin(), endIt = BibTeXFields::instance().end(); it != endIt; ++it) { auto &fd = *it; fd.visible.remove(name); const bool visibility = fd.defaultVisible; p->header()->setSectionHidden(col, !visibility); ++col; } BibTeXFields::instance().save(); balanceColumns(); } void loadColumnProperties() { if (p->header()->count() != BibTeXFields::instance().count()) { qCWarning(LOG_KBIBTEX_GUI) << "Number of columns in file view does not match number of bibliography fields:" << p->header()->count() << "!=" << BibTeXFields::instance().count(); return; } int col = 0; for (const auto &fd : const_cast<const BibTeXFields &>(BibTeXFields::instance())) { const bool visibility = fd.visible.contains(name) ? fd.visible[name] : fd.defaultVisible; p->header()->setSectionHidden(col, !visibility); ++col; } balanceColumns(); } void saveColumnProperties() { if (p->header()->count() != BibTeXFields::instance().count()) { qCWarning(LOG_KBIBTEX_GUI) << "Number of columns in file view does not match number of bibliography fields:" << p->header()->count() << "!=" << BibTeXFields::instance().count(); return; } int col = 0; for (BibTeXFields::Iterator it = BibTeXFields::instance().begin(), endIt = BibTeXFields::instance().end(); it != endIt; ++it) { auto &fd = *it; fd.visible[name] = !p->header()->isSectionHidden(col); ++col; } BibTeXFields::instance().save(); } }; BasicFileView::BasicFileView(const QString &name, QWidget *parent) : QTreeView(parent), d(new Private(name, this)) { /// general visual appearance and behaviour setSelectionMode(QAbstractItemView::ExtendedSelection); setSelectionBehavior(QAbstractItemView::SelectRows); setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setFrameStyle(QFrame::NoFrame); setAlternatingRowColors(true); setAllColumnsShowFocus(true); setRootIsDecorated(false); /// header appearance and behaviour header()->setSectionsClickable(true); header()->setSortIndicatorShown(true); header()->setSortIndicator(-1, Qt::AscendingOrder); header()->setSectionsMovable(false); header()->setSectionResizeMode(QHeaderView::Fixed); connect(header(), &QHeaderView::sortIndicatorChanged, this, &BasicFileView::sort); header()->setContextMenuPolicy(Qt::CustomContextMenu); connect(header(), &QHeaderView::customContextMenuRequested, this, &BasicFileView::showHeaderContextMenu); } BasicFileView::~BasicFileView() { delete d; } void BasicFileView::setModel(QAbstractItemModel *model) { d->sortFilterProxyModel = nullptr; d->fileModel = dynamic_cast<FileModel *>(model); if (d->fileModel == nullptr) { d->sortFilterProxyModel = qobject_cast<QSortFilterProxyModel *>(model); if (d->sortFilterProxyModel == nullptr) qCWarning(LOG_KBIBTEX_GUI) << "Failed to dynamically cast model to QSortFilterProxyModel*"; else d->fileModel = dynamic_cast<FileModel *>(d->sortFilterProxyModel->sourceModel()); } if (d->fileModel == nullptr) qCWarning(LOG_KBIBTEX_GUI) << "Failed to dynamically cast model to FileModel*"; QTreeView::setModel(model); /// sort according to session if (header()->isSortIndicatorShown()) sort(header()->sortIndicatorSection(), header()->sortIndicatorOrder()); d->loadColumnProperties(); } FileModel *BasicFileView::fileModel() { return d->fileModel; } QSortFilterProxyModel *BasicFileView::sortFilterProxyModel() { return d->sortFilterProxyModel; } void BasicFileView::keyPressEvent(QKeyEvent *event) { if (event->modifiers() == Qt::NoModifier) { if ((event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) && currentIndex() != QModelIndex()) { emit doubleClicked(currentIndex()); event->accept(); } else if (!event->text().isEmpty() && event->text().at(0).isLetterOrNumber()) { emit searchFor(event->text()); event->accept(); } } QTreeView::keyPressEvent(event); } void BasicFileView::resizeEvent(QResizeEvent *event) { Q_UNUSED(event) const int w = qMax(width() - 20, 0); header()->setMinimumWidth(w); header()->setMaximumWidth(w); d->balanceColumns(); } void BasicFileView::headerActionToggled() { QAction *action = qobject_cast<QAction *>(sender()); if (action == nullptr) return; bool ok = false; const int col = action->data().toInt(&ok); if (!ok) return; header()->setSectionHidden(col, !header()->isSectionHidden(col)); d->balanceColumns(); } -void BasicFileView::headerResetToDefaults() -{ - d->resetColumnProperties(); -} - void BasicFileView::sort(int t, Qt::SortOrder s) { SortFilterFileModel *sortedModel = qobject_cast<SortFilterFileModel *>(model()); if (sortedModel != nullptr) sortedModel->sort(t, s); } void BasicFileView::noSorting() { SortFilterFileModel *sortedModel = qobject_cast<SortFilterFileModel *>(model()); if (sortedModel != nullptr) { sortedModel->sort(-1); header()->setSortIndicator(-1, Qt::AscendingOrder); } } void BasicFileView::showHeaderContextMenu(const QPoint &pos) { const QPoint globalPos = viewport()->mapToGlobal(pos); QMenu menu; int col = 0; for (const auto &fd : const_cast<const BibTeXFields &>(BibTeXFields::instance())) { QAction *action = new QAction(fd.label, &menu); action->setData(col); action->setCheckable(true); action->setChecked(!header()->isSectionHidden(col)); connect(action, &QAction::triggered, this, &BasicFileView::headerActionToggled); menu.addAction(action); ++col; } /// Add separator to header's context menu QAction *action = new QAction(&menu); action->setSeparator(true); menu.addAction(action); /// Add action to reset to defaults (regarding column visibility) to header's context menu action = new QAction(i18n("Reset to defaults"), &menu); - connect(action, &QAction::triggered, this, &BasicFileView::headerResetToDefaults); + connect(action, &QAction::triggered, this, [this]() { + d->resetColumnProperties(); + }); menu.addAction(action); /// Add separator to header's context menu action = new QAction(&menu); action->setSeparator(true); menu.addAction(action); /// Add action to disable any sorting action = new QAction(i18n("No sorting"), &menu); connect(action, &QAction::triggered, this, &BasicFileView::noSorting); menu.addAction(action); menu.exec(globalPos); } diff --git a/src/gui/file/basicfileview.h b/src/gui/file/basicfileview.h index 65271275..681329dc 100644 --- a/src/gui/file/basicfileview.h +++ b/src/gui/file/basicfileview.h @@ -1,62 +1,61 @@ /*************************************************************************** - * 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/>. * ***************************************************************************/ #ifndef KBIBTEX_GUI_BASICFILEVIEW_H #define KBIBTEX_GUI_BASICFILEVIEW_H #include <QTreeView> #include "kbibtexgui_export.h" class FileModel; class QSortFilterProxyModel; /** * @author Thomas Fischer <fischer@unix-ag.uni-kl.de> */ class KBIBTEXGUI_EXPORT BasicFileView : public QTreeView { Q_OBJECT public: explicit BasicFileView(const QString &name, QWidget *parent = nullptr); ~BasicFileView() override; void setModel(QAbstractItemModel *model) override; FileModel *fileModel(); QSortFilterProxyModel *sortFilterProxyModel(); signals: void searchFor(const QString &); protected: void keyPressEvent(QKeyEvent *event) override; void resizeEvent(QResizeEvent *event) override; private: class Private; Private *d; private slots: void headerActionToggled(); - void headerResetToDefaults(); void sort(int, Qt::SortOrder); void noSorting(); void showHeaderContextMenu(const QPoint &pos); }; #endif // KBIBTEX_GUI_BASICFILEVIEW_H diff --git a/src/gui/widgets/filesettingswidget.cpp b/src/gui/widgets/filesettingswidget.cpp index 774c99a4..35811fb9 100644 --- a/src/gui/widgets/filesettingswidget.cpp +++ b/src/gui/widgets/filesettingswidget.cpp @@ -1,180 +1,175 @@ /***************************************************************************** * 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 "filesettingswidget.h" #include <QCheckBox> #include <QFormLayout> #include <QComboBox> #include <KLocalizedString> #include <Preferences> #include <File> #include "italictextitemmodel.h" #include "guihelper.h" #define createDelimiterString(a, b) (QString(QStringLiteral("%1%2%3")).arg(a).arg(QChar(8230)).arg(b)) FileSettingsWidget::FileSettingsWidget(QWidget *parent) : QWidget(parent), dummyPerson(Person(i18n("John"), i18n("Doe"), i18n("Jr."))), m_file(nullptr) { setupGUI(); } void FileSettingsWidget::resetToLoadedProperties() { loadProperties(m_file); } void FileSettingsWidget::loadProperties(File *file) { m_file = file; if (m_file == nullptr) return; /// Nothing to do if (file->hasProperty(File::Encoding)) { m_comboBoxEncodings->blockSignals(true); const QString encoding = file->property(File::Encoding).toString(); const int row = GUIHelper::selectValue(m_comboBoxEncodings->model(), encoding); m_comboBoxEncodings->setCurrentIndex(row); m_comboBoxEncodings->blockSignals(false); } if (file->hasProperty(File::StringDelimiter)) { m_comboBoxStringDelimiters->blockSignals(true); const QString stringDelimiter = file->property(File::StringDelimiter).toString(); const int row = GUIHelper::selectValue(m_comboBoxStringDelimiters->model(), createDelimiterString(stringDelimiter[0], stringDelimiter[1])); m_comboBoxStringDelimiters->setCurrentIndex(row); m_comboBoxStringDelimiters->blockSignals(false); } if (file->hasProperty(File::QuoteComment)) { m_comboBoxQuoteComment->blockSignals(true); const Preferences::QuoteComment quoteComment = static_cast<Preferences::QuoteComment>(file->property(File::QuoteComment).toInt()); const int row = qMax(0, GUIHelper::selectValue(m_comboBoxQuoteComment->model(), static_cast<int>(quoteComment), Qt::UserRole)); m_comboBoxQuoteComment->setCurrentIndex(row); m_comboBoxQuoteComment->blockSignals(false); } if (file->hasProperty(File::KeywordCasing)) { m_comboBoxKeywordCasing->blockSignals(true); const KBibTeX::Casing keywordCasing = static_cast<KBibTeX::Casing>(file->property(File::KeywordCasing).toInt()); m_comboBoxKeywordCasing->setCurrentIndex(static_cast<int>(keywordCasing)); m_comboBoxKeywordCasing->blockSignals(false); } if (file->hasProperty(File::ProtectCasing)) { m_checkBoxProtectCasing->blockSignals(true); m_checkBoxProtectCasing->setCheckState(static_cast<Qt::CheckState>(file->property(File::ProtectCasing).toInt())); m_checkBoxProtectCasing->blockSignals(false); } if (file->hasProperty(File::NameFormatting)) { m_comboBoxPersonNameFormatting->blockSignals(true); const int row = GUIHelper::selectValue(m_comboBoxPersonNameFormatting->model(), file->property(File::NameFormatting).toString(), ItalicTextItemModel::IdentifierRole); m_comboBoxPersonNameFormatting->setCurrentIndex(row); m_comboBoxPersonNameFormatting->blockSignals(false); } if (file->hasProperty(File::ListSeparator)) { m_comboBoxListSeparator->blockSignals(true); m_comboBoxListSeparator->setCurrentIndex(m_comboBoxListSeparator->findData(file->property(File::ListSeparator))); m_comboBoxListSeparator->blockSignals(false); } } -void FileSettingsWidget::applyProperties() -{ - saveProperties(m_file); -} - void FileSettingsWidget::saveProperties(File *file) { m_file = file; if (m_file == nullptr) return; file->setProperty(File::Encoding, m_comboBoxEncodings->currentText()); const QString stringDelimiter = m_comboBoxStringDelimiters->currentText(); file->setProperty(File::StringDelimiter, QString(stringDelimiter[0]) + stringDelimiter[stringDelimiter.length() - 1]); bool ok = false; const Preferences::QuoteComment quoteComment = static_cast<Preferences::QuoteComment>(m_comboBoxQuoteComment->currentData().toInt(&ok)); if (ok) file->setProperty(File::QuoteComment, static_cast<int>(quoteComment)); const KBibTeX::Casing keywordCasing = static_cast<KBibTeX::Casing>(m_comboBoxKeywordCasing->currentIndex()); file->setProperty(File::KeywordCasing, static_cast<int>(keywordCasing)); file->setProperty(File::ProtectCasing, static_cast<int>(m_checkBoxProtectCasing->checkState())); file->setProperty(File::NameFormatting, m_comboBoxPersonNameFormatting->itemData(m_comboBoxPersonNameFormatting->currentIndex(), ItalicTextItemModel::IdentifierRole)); file->setProperty(File::ListSeparator, m_comboBoxListSeparator->itemData(m_comboBoxListSeparator->currentIndex()).toString()); } void FileSettingsWidget::resetToDefaults() { if (m_file != nullptr) { m_file->setPropertiesToDefault(); loadProperties(m_file); } } void FileSettingsWidget::setupGUI() { QFormLayout *layout = new QFormLayout(this); m_comboBoxEncodings = new QComboBox(this); m_comboBoxEncodings->setObjectName(QStringLiteral("comboBoxEncodings")); layout->addRow(i18n("Encoding:"), m_comboBoxEncodings); m_comboBoxEncodings->addItems(Preferences::availableBibTeXEncodings); connect(m_comboBoxEncodings, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &FileSettingsWidget::widgetsChanged); m_comboBoxStringDelimiters = new QComboBox(this); m_comboBoxStringDelimiters->setObjectName(QStringLiteral("comboBoxStringDelimiters")); layout->addRow(i18n("String Delimiters:"), m_comboBoxStringDelimiters); m_comboBoxStringDelimiters->addItem(createDelimiterString('"', '"')); m_comboBoxStringDelimiters->addItem(createDelimiterString('{', '}')); m_comboBoxStringDelimiters->addItem(createDelimiterString('(', ')')); connect(m_comboBoxStringDelimiters, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &FileSettingsWidget::widgetsChanged); m_comboBoxQuoteComment = new QComboBox(this); layout->addRow(i18n("Comment Quoting:"), m_comboBoxQuoteComment); for (QVector<QPair<Preferences::QuoteComment, QString>>::ConstIterator it = Preferences::availableBibTeXQuoteComments.constBegin(); it != Preferences::availableBibTeXQuoteComments.constEnd(); ++it) m_comboBoxQuoteComment->addItem(it->second, static_cast<int>(it->first)); connect(m_comboBoxQuoteComment, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &FileSettingsWidget::widgetsChanged); m_comboBoxKeywordCasing = new QComboBox(this); layout->addRow(i18n("Keyword Casing:"), m_comboBoxKeywordCasing); m_comboBoxKeywordCasing->addItem(i18nc("Keyword Casing", "lowercase")); m_comboBoxKeywordCasing->addItem(i18nc("Keyword Casing", "Initial capital")); m_comboBoxKeywordCasing->addItem(i18nc("Keyword Casing", "UpperCamelCase")); m_comboBoxKeywordCasing->addItem(i18nc("Keyword Casing", "lowerCamelCase")); m_comboBoxKeywordCasing->addItem(i18nc("Keyword Casing", "UPPERCASE")); connect(m_comboBoxKeywordCasing, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &FileSettingsWidget::widgetsChanged); m_checkBoxProtectCasing = new QCheckBox(i18n("Protect Titles"), this); m_checkBoxProtectCasing->setTristate(true); layout->addRow(i18n("Protect Casing?"), m_checkBoxProtectCasing); connect(m_checkBoxProtectCasing, &QCheckBox::stateChanged, this, &FileSettingsWidget::widgetsChanged); m_comboBoxPersonNameFormatting = new QComboBox(this); m_comboBoxPersonNameFormatting->setObjectName(QStringLiteral("comboBoxPersonNameFormatting")); layout->addRow(i18n("Person Names Formatting:"), m_comboBoxPersonNameFormatting); connect(m_comboBoxPersonNameFormatting, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &FileSettingsWidget::widgetsChanged); ItalicTextItemModel *itim = new ItalicTextItemModel(this); itim->addItem(i18n("Use global settings"), QString(QString())); itim->addItem(Person::transcribePersonName(&dummyPerson, Preferences::personNameFormatFirstLast), Preferences::personNameFormatFirstLast); itim->addItem(Person::transcribePersonName(&dummyPerson, Preferences::personNameFormatLastFirst), Preferences::personNameFormatLastFirst); m_comboBoxPersonNameFormatting->setModel(itim); m_comboBoxListSeparator = new QComboBox(this); layout->addRow(i18n("List Separator"), m_comboBoxListSeparator); connect(m_comboBoxListSeparator, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &FileSettingsWidget::widgetsChanged); m_comboBoxListSeparator->addItem(QStringLiteral(";"), QVariant::fromValue<QString>(QStringLiteral("; "))); m_comboBoxListSeparator->addItem(QStringLiteral(","), QVariant::fromValue<QString>(QStringLiteral(", "))); } diff --git a/src/gui/widgets/filesettingswidget.h b/src/gui/widgets/filesettingswidget.h index 136ca578..70b9adb5 100644 --- a/src/gui/widgets/filesettingswidget.h +++ b/src/gui/widgets/filesettingswidget.h @@ -1,69 +1,68 @@ /***************************************************************************** - * 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/>. * *****************************************************************************/ #ifndef KBIBTEX_GUI_FILESETTINGSWIDGET_H #define KBIBTEX_GUI_FILESETTINGSWIDGET_H #include <QWidget> #include <Value> #include "kbibtexgui_export.h" class QCheckBox; class QComboBox; class File; /** * @author Thomas Fischer */ class KBIBTEXGUI_EXPORT FileSettingsWidget : public QWidget { Q_OBJECT public: explicit FileSettingsWidget(QWidget *parent); void loadProperties(File *file); void saveProperties(File *file); public slots: void resetToLoadedProperties(); - void applyProperties(); void resetToDefaults(); signals: void widgetsChanged(); private: QComboBox *m_comboBoxEncodings; QComboBox *m_comboBoxStringDelimiters; QComboBox *m_comboBoxQuoteComment; QComboBox *m_comboBoxKeywordCasing; QCheckBox *m_checkBoxProtectCasing; QComboBox *m_comboBoxPersonNameFormatting; QComboBox *m_comboBoxListSeparator; const Person dummyPerson; File *m_file; void setupGUI(); }; #endif // KBIBTEX_GUI_FILESETTINGSWIDGET_H diff --git a/src/gui/widgets/menulineedit.cpp b/src/gui/widgets/menulineedit.cpp index e8f0d5fa..ef067eb6 100644 --- a/src/gui/widgets/menulineedit.cpp +++ b/src/gui/widgets/menulineedit.cpp @@ -1,321 +1,318 @@ /*************************************************************************** - * 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 "menulineedit.h" #include <QLayout> #include <QApplication> #include <QMenu> #include <QTimer> #include <QPushButton> #include <KLineEdit> #include <KTextEdit> #include <KConfigGroup> #include <KSharedConfig> #include <NotificationHub> const int MenuLineEdit::MenuLineConfigurationChangedEvent = NotificationHub::EventUserDefined + 1861; const QString MenuLineEdit::keyLimitKeyboardTabStops = QStringLiteral("LimitKeyboardTabStops"); class MenuLineEdit::MenuLineEditPrivate : public NotificationListener { private: MenuLineEdit *p; bool isMultiLine; bool m_isReadOnly; QHBoxLayout *hLayout; static const QString transparentStyleSheet, normalStyleSheet; bool makeInnerWidgetsTransparent; public: QPushButton *m_pushButtonType; KLineEdit *m_singleLineEditText; KTextEdit *m_multiLineEditText; MenuLineEditPrivate(bool isMultiLine, MenuLineEdit *parent) : p(parent), m_isReadOnly(false), makeInnerWidgetsTransparent(false), m_singleLineEditText(nullptr), m_multiLineEditText(nullptr) { this->isMultiLine = isMultiLine; /// listen to configuration change events specifically concerning a MenuLineEdit widget NotificationHub::registerNotificationListener(this, MenuLineEdit::MenuLineConfigurationChangedEvent); setupUI(); } ~MenuLineEditPrivate() override { for (int i = hLayout->count() - 1; i >= 0; --i) { QWidget *w = hLayout->itemAt(i)->widget(); w->deleteLater(); } } void notificationEvent(int eventId) override { if (eventId == MenuLineEdit::MenuLineConfigurationChangedEvent) { /// load setting limitKeyboardTabStops KSharedConfigPtr config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))); static QString const configGroupName = QStringLiteral("User Interface"); KConfigGroup configGroup(config, configGroupName); const bool limitKeyboardTabStops = configGroup.readEntry(MenuLineEdit::keyLimitKeyboardTabStops, false); /// check each widget inside MenuLineEdit for (int i = hLayout->count() - 1; i >= 0; --i) { QWidget *w = hLayout->itemAt(i)->widget(); if (w != nullptr && w != m_singleLineEditText && w != m_multiLineEditText) { /// for all widgets except the main editing widget: change tab focus policy w->setFocusPolicy(limitKeyboardTabStops ? Qt::ClickFocus : Qt::StrongFocus); } } } } void setupUI() { p->setObjectName(QStringLiteral("FieldLineEdit")); hLayout = new QHBoxLayout(p); hLayout->setMargin(0); hLayout->setSpacing(2); m_pushButtonType = new QPushButton(p); appendWidget(m_pushButtonType); hLayout->setStretchFactor(m_pushButtonType, 0); m_pushButtonType->setObjectName(QStringLiteral("FieldLineEditButton")); if (isMultiLine) { m_multiLineEditText = new KTextEdit(p); appendWidget(m_multiLineEditText); connect(m_multiLineEditText, &KTextEdit::textChanged, p, &MenuLineEdit::slotTextChanged); m_multiLineEditText->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); p->setFocusProxy(m_multiLineEditText); m_multiLineEditText->setAcceptRichText(false); } else { m_singleLineEditText = new KLineEdit(p); appendWidget(m_singleLineEditText); hLayout->setStretchFactor(m_singleLineEditText, 100); m_singleLineEditText->setClearButtonEnabled(true); m_singleLineEditText->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); m_singleLineEditText->setCompletionMode(KCompletion::CompletionPopup); m_singleLineEditText->completionObject()->setIgnoreCase(true); p->setFocusProxy(m_singleLineEditText); connect(m_singleLineEditText, &QLineEdit::textEdited, p, &MenuLineEdit::textChanged); } p->setFocusPolicy(Qt::StrongFocus); // FIXME improve focus handling p->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); } void prependWidget(QWidget *widget) { widget->setParent(p); hLayout->insertWidget(0, widget); widget->setStyleSheet(makeInnerWidgetsTransparent ? transparentStyleSheet : normalStyleSheet); setWidgetReadOnly(widget, m_isReadOnly); fixTabOrder(); } void appendWidget(QWidget *widget) { widget->setParent(p); hLayout->addWidget(widget); widget->setStyleSheet(makeInnerWidgetsTransparent ? transparentStyleSheet : normalStyleSheet); setWidgetReadOnly(widget, m_isReadOnly); fixTabOrder(); } void fixTabOrder() { QWidget *cur = nullptr; if (hLayout->count() > 0) p->setTabOrder(p, (cur = hLayout->itemAt(0)->widget())); for (int i = 1; i < hLayout->count(); ++i) { QWidget *next = hLayout->itemAt(i)->widget(); p->setTabOrder(cur, next); cur = next; } } void verticallyStretchButtons() { /// do not vertically stretch if using transparent style sheet if (makeInnerWidgetsTransparent) return; /// check each widget inside MenuLineEdit for (int i = hLayout->count() - 1; i >= 0; --i) { QWidget *w = hLayout->itemAt(i)->widget(); if (w != nullptr && w != m_singleLineEditText && w != m_multiLineEditText) { /// for all widgets except the main editing widget: change tab focus policy QSizePolicy sp = w->sizePolicy(); w->setSizePolicy(sp.horizontalPolicy(), QSizePolicy::MinimumExpanding); } } } void setStyleSheet(bool makeInnerWidgetsTransparent) { this->makeInnerWidgetsTransparent = makeInnerWidgetsTransparent; for (int i = hLayout->count() - 1; i >= 0; --i) { QWidget *w = hLayout->itemAt(i)->widget(); if (w != nullptr) w->setStyleSheet(makeInnerWidgetsTransparent ? transparentStyleSheet : normalStyleSheet); } } void setWidgetReadOnly(QWidget *w, bool isReadOnly) { if (m_singleLineEditText == w) m_singleLineEditText->setReadOnly(isReadOnly); else if (m_multiLineEditText == w) m_multiLineEditText->setReadOnly(isReadOnly); else if (!w->property("isConst").isValid() && !w->property("isConst").toBool()) w->setEnabled(!isReadOnly); } void setReadOnly(bool isReadOnly) { m_isReadOnly = isReadOnly; for (int i = hLayout->count() - 1; i >= 0; --i) { QWidget *w = hLayout->itemAt(i)->widget(); setWidgetReadOnly(w, isReadOnly); } } }; const QString MenuLineEdit::MenuLineEditPrivate::transparentStyleSheet = QStringLiteral("KTextEdit { border-style: none; background-color: transparent; }\nQLineEdit { border-style: none; background-color: transparent; }\nKPushButton { border-style: none; background-color: transparent; padding: 0px; margin-left: 2px; margin-right:2px; text-align: left; }"); const QString MenuLineEdit::MenuLineEditPrivate::normalStyleSheet = QStringLiteral("QPushButton { padding:4px; margin:0px; text-align: left; }\nQPushButton::menu-indicator {subcontrol-position: right center; subcontrol-origin: content;}"); MenuLineEdit::MenuLineEdit(bool isMultiLine, QWidget *parent) : QFrame(parent), d(new MenuLineEditPrivate(isMultiLine, this)) { if (d->m_singleLineEditText != nullptr) { /// Only for single-line variants stretch buttons vertically - QTimer::singleShot(250, this, &MenuLineEdit::slotVerticallyStretchButtons); + QTimer::singleShot(250, this, [this]() { + d->verticallyStretchButtons(); + }); } } MenuLineEdit::~MenuLineEdit() { delete d; } void MenuLineEdit::setMenu(QMenu *menu) { d->m_pushButtonType->setMenu(menu); } void MenuLineEdit::setReadOnly(bool readOnly) { d->setReadOnly(readOnly); } QString MenuLineEdit::text() const { if (d->m_singleLineEditText != nullptr) return d->m_singleLineEditText->text(); if (d->m_multiLineEditText != nullptr) return d->m_multiLineEditText->document()->toPlainText(); return QString(); } void MenuLineEdit::setText(const QString &text) { if (d->m_singleLineEditText != nullptr) { d->m_singleLineEditText->setText(text); d->m_singleLineEditText->setCursorPosition(0); } else if (d->m_multiLineEditText != nullptr) { d->m_multiLineEditText->document()->setPlainText(text); QTextCursor tc = d->m_multiLineEditText->textCursor(); tc.setPosition(0); d->m_multiLineEditText->setTextCursor(tc); } } void MenuLineEdit::setIcon(const QIcon &icon) { d->m_pushButtonType->setIcon(icon); } void MenuLineEdit::setFont(const QFont &font) { if (d->m_singleLineEditText != nullptr) d->m_singleLineEditText->setFont(font); if (d->m_multiLineEditText != nullptr) d->m_multiLineEditText->document()->setDefaultFont(font); } void MenuLineEdit::setButtonToolTip(const QString &text) { d->m_pushButtonType->setToolTip(text); } void MenuLineEdit::setChildAcceptDrops(bool acceptDrops) { if (d->m_singleLineEditText != nullptr) d->m_singleLineEditText->setAcceptDrops(acceptDrops); if (d->m_multiLineEditText != nullptr) d->m_multiLineEditText->setAcceptDrops(acceptDrops); } QWidget *MenuLineEdit::buddy() { if (d->m_singleLineEditText != nullptr) return d->m_singleLineEditText; if (d->m_multiLineEditText != nullptr) return d->m_multiLineEditText; return nullptr; } void MenuLineEdit::prependWidget(QWidget *widget) { d->prependWidget(widget); } void MenuLineEdit::appendWidget(QWidget *widget) { d->appendWidget(widget); } void MenuLineEdit::setInnerWidgetsTransparency(bool makeInnerWidgetsTransparent) { d->setStyleSheet(makeInnerWidgetsTransparent); } bool MenuLineEdit::isModified() const { if (d->m_singleLineEditText != nullptr) return d->m_singleLineEditText->isModified(); if (d->m_multiLineEditText != nullptr) return d->m_multiLineEditText->document()->isModified(); return false; } void MenuLineEdit::setCompletionItems(const QStringList &items) { if (d->m_singleLineEditText != nullptr) d->m_singleLineEditText->completionObject()->setItems(items); } void MenuLineEdit::focusInEvent(QFocusEvent *) { if (d->m_singleLineEditText != nullptr) d->m_singleLineEditText->setFocus(); else if (d->m_multiLineEditText != nullptr) d->m_multiLineEditText->setFocus(); } void MenuLineEdit::slotTextChanged() { Q_ASSERT_X(d->m_multiLineEditText != nullptr, "MenuLineEdit::slotTextChanged", "d->m_multiLineEditText is NULL"); emit textChanged(d->m_multiLineEditText->toPlainText()); } - -void MenuLineEdit::slotVerticallyStretchButtons() -{ - d->verticallyStretchButtons(); -} diff --git a/src/gui/widgets/menulineedit.h b/src/gui/widgets/menulineedit.h index e40ac5f3..7c3f9f34 100644 --- a/src/gui/widgets/menulineedit.h +++ b/src/gui/widgets/menulineedit.h @@ -1,84 +1,83 @@ /*************************************************************************** - * Copyright (C) 2004-2017 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/>. * ***************************************************************************/ #ifndef KBIBTEX_GUI_MENULINEEDIT_H #define KBIBTEX_GUI_MENULINEEDIT_H #include <QFrame> #include "kbibtexgui_export.h" class QMenu; class QIcon; /** @author Thomas Fischer */ class MenuLineEdit : public QFrame { Q_OBJECT public: /** * @brief MenuLineConfigurationChangedEvent * Event id for use with @c NotificationHub. * All MenuLineWidgets register to this event. */ static const int MenuLineConfigurationChangedEvent; /** * @brief keyLimitKeyboardTabStops * Configuration key in group "UserInterface" to access * the setting for limited keyboard tab stops. */ static const QString keyLimitKeyboardTabStops; MenuLineEdit(bool isMultiLine, QWidget *parent); ~MenuLineEdit() override; void setMenu(QMenu *menu); virtual void setReadOnly(bool); QString text() const; void setText(const QString &); void setIcon(const QIcon &icon); void setFont(const QFont &font); void setButtonToolTip(const QString &); void setChildAcceptDrops(bool acceptDrops); QWidget *buddy(); void prependWidget(QWidget *widget); void appendWidget(QWidget *widget); void setInnerWidgetsTransparency(bool makeInnerWidgetsTransparent); bool isModified() const; void setCompletionItems(const QStringList &items); protected: void focusInEvent(QFocusEvent *event) override; signals: void textChanged(const QString &); private slots: void slotTextChanged(); - void slotVerticallyStretchButtons(); private: class MenuLineEditPrivate; MenuLineEditPrivate *const d; }; #endif // KBIBTEX_GUI_MENULINEEDIT_H diff --git a/src/networking/onlinesearch/onlinesearchabstract.cpp b/src/networking/onlinesearch/onlinesearchabstract.cpp index 9742627a..f50ea4f2 100644 --- a/src/networking/onlinesearch/onlinesearchabstract.cpp +++ b/src/networking/onlinesearch/onlinesearchabstract.cpp @@ -1,702 +1,699 @@ /*************************************************************************** * 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 "onlinesearchabstract.h" #include <QFileInfo> #include <QNetworkReply> #include <QDir> #include <QTimer> #include <QStandardPaths> #include <QRegularExpression> #include <QDBusConnection> #include <QDBusConnectionInterface> #ifdef HAVE_QTWIDGETS #include <QListWidgetItem> #endif // HAVE_QTWIDGETS #ifdef HAVE_KF5 #include <KLocalizedString> #include <KMessageBox> #endif // HAVE_KF5 #include <KBibTeX> #include <Encoder> #include "internalnetworkaccessmanager.h" #include "onlinesearchabstract_p.h" #include "logging_networking.h" const QString OnlineSearchAbstract::queryKeyFreeText = QStringLiteral("free"); const QString OnlineSearchAbstract::queryKeyTitle = QStringLiteral("title"); const QString OnlineSearchAbstract::queryKeyAuthor = QStringLiteral("author"); const QString OnlineSearchAbstract::queryKeyYear = QStringLiteral("year"); const int OnlineSearchAbstract::resultNoError = 0; const int OnlineSearchAbstract::resultCancelled = 0; /// may get redefined in the future! const int OnlineSearchAbstract::resultUnspecifiedError = 1; const int OnlineSearchAbstract::resultAuthorizationRequired = 2; const int OnlineSearchAbstract::resultNetworkError = 3; const int OnlineSearchAbstract::resultInvalidArguments = 4; const char *OnlineSearchAbstract::httpUnsafeChars = "%:/=+$?&\0"; #ifdef HAVE_QTWIDGETS OnlineSearchAbstract::Form::Private::Private() : config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))) { /// nothing } QStringList OnlineSearchAbstract::Form::Private::authorLastNames(const Entry &entry) { const Encoder &encoder = Encoder::instance(); const Value v = entry[Entry::ftAuthor]; QStringList result; result.reserve(v.size()); for (const QSharedPointer<ValueItem> &vi : v) { QSharedPointer<const Person> p = vi.dynamicCast<const Person>(); if (!p.isNull()) result.append(encoder.convertToPlainAscii(p->lastName())); } return result; } QString OnlineSearchAbstract::Form::Private::guessFreeText(const Entry &entry) { /// If there is a DOI value in this entry, use it as free text static const QStringList doiKeys = {Entry::ftDOI, Entry::ftUrl}; for (const QString &doiKey : doiKeys) if (!entry.value(doiKey).isEmpty()) { const QString text = PlainTextValue::text(entry[doiKey]); const QRegularExpressionMatch doiRegExpMatch = KBibTeX::doiRegExp.match(text); if (doiRegExpMatch.hasMatch()) return doiRegExpMatch.captured(0); } /// If there is no free text yet (e.g. no DOI number), try to identify an arXiv eprint number static const QStringList arxivKeys = {QStringLiteral("eprint"), Entry::ftNumber}; for (const QString &arxivKey : arxivKeys) if (!entry.value(arxivKey).isEmpty()) { const QString text = PlainTextValue::text(entry[arxivKey]); const QRegularExpressionMatch arXivRegExpMatch = KBibTeX::arXivRegExpWithPrefix.match(text); if (arXivRegExpMatch.hasMatch()) return arXivRegExpMatch.captured(1); } return QString(); } #endif // HAVE_QTWIDGETS OnlineSearchAbstract::OnlineSearchAbstract(QObject *parent) : QObject(parent), m_hasBeenCanceled(false), numSteps(0), curStep(0), m_previousBusyState(false), m_delayedStoppedSearchReturnCode(0) { m_parent = parent; } #ifdef HAVE_QTWIDGETS QIcon OnlineSearchAbstract::icon(QListWidgetItem *listWidgetItem) { static const QRegularExpression invalidChars(QStringLiteral("[^-a-z0-9_]"), QRegularExpression::CaseInsensitiveOption); const QString cacheDirectory = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QStringLiteral("/favicons/"); QDir().mkpath(cacheDirectory); const QString fileNameStem = cacheDirectory + QString(favIconUrl()).remove(invalidChars); const QStringList fileNameExtensions {QStringLiteral(".ico"), QStringLiteral(".png"), QString()}; for (const QString &extension : fileNameExtensions) { const QString fileName = fileNameStem + extension; if (QFileInfo::exists(fileName)) return QIcon(fileName); } QNetworkRequest request(favIconUrl()); QNetworkReply *reply = InternalNetworkAccessManager::instance().get(request); reply->setObjectName(fileNameStem); if (listWidgetItem != nullptr) m_iconReplyToListWidgetItem.insert(reply, listWidgetItem); connect(reply, &QNetworkReply::finished, this, &OnlineSearchAbstract::iconDownloadFinished); return QIcon::fromTheme(QStringLiteral("applications-internet")); } OnlineSearchAbstract::Form *OnlineSearchAbstract::customWidget(QWidget *) { return nullptr; } void OnlineSearchAbstract::startSearchFromForm() { m_hasBeenCanceled = false; curStep = numSteps = 0; delayedStoppedSearch(resultNoError); } #endif // HAVE_QTWIDGETS QString OnlineSearchAbstract::name() { if (m_name.isEmpty()) { static const QRegularExpression invalidChars(QStringLiteral("[^-a-z0-9]"), QRegularExpression::CaseInsensitiveOption); m_name = label().remove(invalidChars); } return m_name; } bool OnlineSearchAbstract::busy() const { return numSteps > 0 && curStep < numSteps; } void OnlineSearchAbstract::cancel() { m_hasBeenCanceled = true; curStep = numSteps = 0; refreshBusyProperty(); } QStringList OnlineSearchAbstract::splitRespectingQuotationMarks(const QString &text) { int p1 = 0, p2, max = text.length(); QStringList result; while (p1 < max) { while (text[p1] == ' ') ++p1; p2 = p1; if (text[p2] == '"') { ++p2; while (p2 < max && text[p2] != '"') ++p2; } else while (p2 < max && text[p2] != ' ') ++p2; result << text.mid(p1, p2 - p1 + 1).simplified(); p1 = p2 + 1; } return result; } bool OnlineSearchAbstract::handleErrors(QNetworkReply *reply) { QUrl url; return handleErrors(reply, url); } bool OnlineSearchAbstract::handleErrors(QNetworkReply *reply, QUrl &newUrl) { /// The URL to be shown or logged shall not contain any API key const QUrl urlToShow = InternalNetworkAccessManager::removeApiKey(reply->url()); newUrl = QUrl(); if (m_hasBeenCanceled) { stopSearch(resultCancelled); return false; } else if (reply->error() != QNetworkReply::NoError) { m_hasBeenCanceled = true; const QString errorString = reply->errorString(); qCWarning(LOG_KBIBTEX_NETWORKING) << "Search using" << label() << "failed (error code" << reply->error() << "," << errorString << "), HTTP code" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() << ":" << reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toByteArray() << ") for URL" << urlToShow.toDisplayString(); const QNetworkRequest &request = reply->request(); /// Dump all HTTP headers that were sent with the original request (except for API keys) const QList<QByteArray> rawHeadersSent = request.rawHeaderList(); for (const QByteArray &rawHeaderName : rawHeadersSent) { if (rawHeaderName.toLower().contains("apikey") || rawHeaderName.toLower().contains("api-key")) continue; ///< skip dumping header values containing an API key qCDebug(LOG_KBIBTEX_NETWORKING) << "SENT " << rawHeaderName << ":" << request.rawHeader(rawHeaderName); } /// Dump all HTTP headers that were received const QList<QByteArray> rawHeadersReceived = reply->rawHeaderList(); for (const QByteArray &rawHeaderName : rawHeadersReceived) { if (rawHeaderName.toLower().contains("apikey") || rawHeaderName.toLower().contains("api-key")) continue; ///< skip dumping header values containing an API key qCDebug(LOG_KBIBTEX_NETWORKING) << "RECVD " << rawHeaderName << ":" << reply->rawHeader(rawHeaderName); } #ifdef HAVE_KF5 sendVisualNotification(errorString.isEmpty() ? i18n("Searching '%1' failed for unknown reason.", label()) : i18n("Searching '%1' failed with error message:\n\n%2", label(), errorString), label(), QStringLiteral("kbibtex"), 7 * 1000); #endif // HAVE_KF5 int resultCode = resultUnspecifiedError; if (reply->error() == QNetworkReply::AuthenticationRequiredError || reply->error() == QNetworkReply::ProxyAuthenticationRequiredError) resultCode = resultAuthorizationRequired; else if (reply->error() == QNetworkReply::HostNotFoundError || reply->error() == QNetworkReply::TimeoutError) resultCode = resultNetworkError; stopSearch(resultCode); return false; } /** * Check the reply for various problems that might point to * more severe issues. Remember: those are only indicators * to problems which have to be handled elsewhere (therefore, * returning 'true' is totally ok here). */ if (reply->attribute(QNetworkRequest::RedirectionTargetAttribute).isValid()) { newUrl = reply->url().resolved(reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl()); } else if (reply->size() == 0) qCWarning(LOG_KBIBTEX_NETWORKING) << "Search using" << label() << "on url" << urlToShow.toDisplayString() << "returned no data"; return true; } QString OnlineSearchAbstract::htmlAttribute(const QString &htmlCode, const int startPos, const QString &attribute) const { const int endPos = htmlCode.indexOf(QLatin1Char('>'), startPos); if (endPos < 0) return QString(); ///< no closing angle bracket found const QString attributePattern = QString(QStringLiteral(" %1=")).arg(attribute); const int attributePatternPos = htmlCode.indexOf(attributePattern, startPos, Qt::CaseInsensitive); if (attributePatternPos < 0 || attributePatternPos > endPos) return QString(); ///< attribute not found within limits const int attributePatternLen = attributePattern.length(); const int openingQuotationMarkPos = attributePatternPos + attributePatternLen; const QChar quotationMark = htmlCode[openingQuotationMarkPos]; if (quotationMark != QLatin1Char('"') && quotationMark != QLatin1Char('\'')) { /// No valid opening quotation mark found int spacePos = openingQuotationMarkPos; while (spacePos < endPos && !htmlCode[spacePos].isSpace()) ++spacePos; if (spacePos > endPos) return QString(); ///< no closing space found return htmlCode.mid(openingQuotationMarkPos, spacePos - openingQuotationMarkPos); } else { /// Attribute has either single or double quotation marks const int closingQuotationMarkPos = htmlCode.indexOf(quotationMark, openingQuotationMarkPos + 1); if (closingQuotationMarkPos < 0 || closingQuotationMarkPos > endPos) return QString(); ///< closing quotation mark not found within limits return htmlCode.mid(openingQuotationMarkPos + 1, closingQuotationMarkPos - openingQuotationMarkPos - 1); } } bool OnlineSearchAbstract::htmlAttributeIsSelected(const QString &htmlCode, const int startPos, const QString &attribute) const { const int endPos = htmlCode.indexOf(QLatin1Char('>'), startPos); if (endPos < 0) return false; ///< no closing angle bracket found const QString attributePattern = QStringLiteral(" ") + attribute; const int attributePatternPos = htmlCode.indexOf(attributePattern, startPos, Qt::CaseInsensitive); if (attributePatternPos < 0 || attributePatternPos > endPos) return false; ///< attribute not found within limits const int attributePatternLen = attributePattern.length(); const QChar nextAfterAttributePattern = htmlCode[attributePatternPos + attributePatternLen]; if (nextAfterAttributePattern.isSpace() || nextAfterAttributePattern == QLatin1Char('>') || nextAfterAttributePattern == QLatin1Char('/')) /// No value given for attribute (old-style HTML), so assuming it means checked/selected return true; else if (nextAfterAttributePattern == QLatin1Char('=')) { /// Expecting value to attribute, so retrieve it and check for 'selected' or 'checked' const QString attributeValue = htmlAttribute(htmlCode, attributePatternPos, attribute).toLower(); return attributeValue == QStringLiteral("selected") || attributeValue == QStringLiteral("checked"); } /// Reaching this point only if HTML code is invalid return false; } #ifdef HAVE_KF5 /** * Display a passive notification popup using the D-Bus interface. * Copied from KDialog with modifications. */ void OnlineSearchAbstract::sendVisualNotification(const QString &text, const QString &title, const QString &icon, int timeout) { static const QString dbusServiceName = QStringLiteral("org.freedesktop.Notifications"); static const QString dbusInterfaceName = QStringLiteral("org.freedesktop.Notifications"); static const QString dbusPath = QStringLiteral("/org/freedesktop/Notifications"); // check if service already exists on plugin instantiation QDBusConnectionInterface *interface = QDBusConnection::sessionBus().interface(); if (interface == nullptr || !interface->isServiceRegistered(dbusServiceName)) { return; } if (timeout <= 0) timeout = 10 * 1000; QDBusMessage m = QDBusMessage::createMethodCall(dbusServiceName, dbusPath, dbusInterfaceName, QStringLiteral("Notify")); const QList<QVariant> args {QStringLiteral("kdialog"), 0U, icon, title, text, QStringList(), QVariantMap(), timeout}; m.setArguments(args); QDBusMessage replyMsg = QDBusConnection::sessionBus().call(m); if (replyMsg.type() == QDBusMessage::ReplyMessage) { if (!replyMsg.arguments().isEmpty()) { return; } // Not displaying any error messages as this is optional for kdialog // and KPassivePopup is a perfectly valid fallback. //else { // qCDebug(LOG_KBIBTEX_NETWORKING) << "Error: received reply with no arguments."; //} } else if (replyMsg.type() == QDBusMessage::ErrorMessage) { //qCDebug(LOG_KBIBTEX_NETWORKING) << "Error: failed to send D-Bus message"; //qCDebug(LOG_KBIBTEX_NETWORKING) << replyMsg; } else { //qCDebug(LOG_KBIBTEX_NETWORKING) << "Unexpected reply type"; } } #endif // HAVE_KF5 QString OnlineSearchAbstract::encodeURL(QString rawText) { const char *cur = httpUnsafeChars; while (*cur != '\0') { rawText = rawText.replace(QChar(*cur), '%' + QString::number(*cur, 16)); ++cur; } rawText = rawText.replace(QLatin1Char(' '), QLatin1Char('+')); return rawText; } QString OnlineSearchAbstract::decodeURL(QString rawText) { static const QRegularExpression mimeRegExp(QStringLiteral("%([0-9A-Fa-f]{2})")); QRegularExpressionMatch mimeRegExpMatch; while ((mimeRegExpMatch = mimeRegExp.match(rawText)).hasMatch()) { bool ok = false; QChar c(mimeRegExpMatch.captured(1).toInt(&ok, 16)); if (ok) rawText = rawText.replace(mimeRegExpMatch.captured(0), c); } rawText = rawText.replace(QStringLiteral("&amp;"), QStringLiteral("&")).replace(QLatin1Char('+'), QStringLiteral(" ")); return rawText; } QMap<QString, QString> OnlineSearchAbstract::formParameters(const QString &htmlText, int startPos) const { /// how to recognize HTML tags static const QString formTagEnd = QStringLiteral("</form>"); static const QString inputTagBegin = QStringLiteral("<input "); static const QString selectTagBegin = QStringLiteral("<select "); static const QString selectTagEnd = QStringLiteral("</select>"); static const QString optionTagBegin = QStringLiteral("<option "); /// initialize result map QMap<QString, QString> result; /// determined boundaries of (only) "form" tag int endPos = htmlText.indexOf(formTagEnd, startPos, Qt::CaseInsensitive); if (startPos < 0 || endPos < 0) { qCWarning(LOG_KBIBTEX_NETWORKING) << "Could not locate form in text"; return result; } /// search for "input" tags within form int p = htmlText.indexOf(inputTagBegin, startPos, Qt::CaseInsensitive); while (p > startPos && p < endPos) { /// get "type", "name", and "value" attributes const QString inputType = htmlAttribute(htmlText, p, QStringLiteral("type")).toLower(); const QString inputName = htmlAttribute(htmlText, p, QStringLiteral("name")); const QString inputValue = htmlAttribute(htmlText, p, QStringLiteral("value")); if (!inputName.isEmpty()) { /// get value of input types if (inputType == QStringLiteral("hidden") || inputType == QStringLiteral("text") || inputType == QStringLiteral("submit")) result[inputName] = inputValue; else if (inputType == QStringLiteral("radio")) { /// must be selected if (htmlAttributeIsSelected(htmlText, p, QStringLiteral("checked"))) { result[inputName] = inputValue; } } else if (inputType == QStringLiteral("checkbox")) { /// must be checked if (htmlAttributeIsSelected(htmlText, p, QStringLiteral("checked"))) { /// multiple checkbox values with the same name are possible result.insertMulti(inputName, inputValue); } } } /// ignore input type "image" p = htmlText.indexOf(inputTagBegin, p + 1, Qt::CaseInsensitive); } /// search for "select" tags within form p = htmlText.indexOf(selectTagBegin, startPos, Qt::CaseInsensitive); while (p > startPos && p < endPos) { /// get "name" attribute from "select" tag const QString selectName = htmlAttribute(htmlText, p, QStringLiteral("name")); /// "select" tag contains one or several "option" tags, search all int popt = htmlText.indexOf(optionTagBegin, p, Qt::CaseInsensitive); int endSelect = htmlText.indexOf(selectTagEnd, p, Qt::CaseInsensitive); while (popt > p && popt < endSelect) { /// get "value" attribute from "option" tag const QString optionValue = htmlAttribute(htmlText, popt, QStringLiteral("value")); if (!selectName.isEmpty() && !optionValue.isEmpty()) { /// if this "option" tag is "selected", store value if (htmlAttributeIsSelected(htmlText, popt, QStringLiteral("selected"))) { result[selectName] = optionValue; } } popt = htmlText.indexOf(optionTagBegin, popt + 1, Qt::CaseInsensitive); } p = htmlText.indexOf(selectTagBegin, p + 1, Qt::CaseInsensitive); } return result; } #ifdef HAVE_QTWIDGETS void OnlineSearchAbstract::iconDownloadFinished() { QNetworkReply *reply = static_cast<QNetworkReply *>(sender()); if (reply->error() == QNetworkReply::NoError) { const QUrl redirUrl = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl(); if (redirUrl.isValid()) { QNetworkRequest request(redirUrl); QNetworkReply *newReply = InternalNetworkAccessManager::instance().get(request); newReply->setObjectName(reply->objectName()); QListWidgetItem *listWidgetItem = m_iconReplyToListWidgetItem.value(reply, nullptr); m_iconReplyToListWidgetItem.remove(reply); if (listWidgetItem != nullptr) m_iconReplyToListWidgetItem.insert(newReply, listWidgetItem); connect(newReply, &QNetworkReply::finished, this, &OnlineSearchAbstract::iconDownloadFinished); return; } const QByteArray iconData = reply->readAll(); if (iconData.size() < 10) { /// Unlikely that an icon's data is less than 10 bytes, /// must be an error. qCWarning(LOG_KBIBTEX_NETWORKING) << "Received invalid icon data from " << InternalNetworkAccessManager::removeApiKey(reply->url()).toDisplayString(); return; } QString extension; if (iconData[1] == 'P' && iconData[2] == 'N' && iconData[3] == 'G') { /// PNG files have string "PNG" at second to fourth byte extension = QStringLiteral(".png"); } else if (iconData[0] == static_cast<char>(0x00) && iconData[1] == static_cast<char>(0x00) && iconData[2] == static_cast<char>(0x01) && iconData[3] == static_cast<char>(0x00)) { /// Microsoft Icon have first two bytes always 0x0000, /// third and fourth byte is 0x0001 (for .ico) extension = QStringLiteral(".ico"); } else if (iconData[0] == '<') { /// HTML or XML code const QString htmlCode = QString::fromUtf8(iconData); qCDebug(LOG_KBIBTEX_NETWORKING) << "Received XML or HTML data from " << InternalNetworkAccessManager::removeApiKey(reply->url()).toDisplayString() << ": " << htmlCode.left(128); return; } else { qCWarning(LOG_KBIBTEX_NETWORKING) << "Favicon is of unknown format: " << InternalNetworkAccessManager::removeApiKey(reply->url()).toDisplayString(); return; } const QString filename = reply->objectName() + extension; QFile iconFile(filename); if (iconFile.open(QFile::WriteOnly)) { iconFile.write(iconData); iconFile.close(); QListWidgetItem *listWidgetItem = m_iconReplyToListWidgetItem.value(reply, nullptr); if (listWidgetItem != nullptr) { /// Disable signals while updating the widget and its items blockSignals(true); listWidgetItem->setIcon(QIcon(filename)); /// Re-enable signals after updating the widget and its items blockSignals(false); } } else { qCWarning(LOG_KBIBTEX_NETWORKING) << "Could not save icon data from URL" << InternalNetworkAccessManager::removeApiKey(reply->url()).toDisplayString() << "to file" << filename; return; } } else qCWarning(LOG_KBIBTEX_NETWORKING) << "Could not download icon from URL " << InternalNetworkAccessManager::removeApiKey(reply->url()).toDisplayString() << ": " << reply->errorString(); } #endif // HAVE_QTWIDGETS void OnlineSearchAbstract::dumpToFile(const QString &filename, const QString &text) { const QString usedFilename = QDir::tempPath() + QLatin1Char('/') + filename; QFile f(usedFilename); if (f.open(QFile::WriteOnly)) { qCDebug(LOG_KBIBTEX_NETWORKING) << "Dumping text" << KBibTeX::squeezeText(text, 96) << "to" << usedFilename; f.write(text.toUtf8()); f.close(); } } void OnlineSearchAbstract::delayedStoppedSearch(int returnCode) { m_delayedStoppedSearchReturnCode = returnCode; - QTimer::singleShot(500, this, &OnlineSearchAbstract::delayedStoppedSearchTimer); -} - -void OnlineSearchAbstract::delayedStoppedSearchTimer() -{ - stopSearch(m_delayedStoppedSearchReturnCode); + QTimer::singleShot(500, this, [this]() { + stopSearch(m_delayedStoppedSearchReturnCode); + }); } void OnlineSearchAbstract::sanitizeEntry(QSharedPointer<Entry> entry) { if (entry.isNull()) return; /// Sometimes, there is no identifier, so set a random one if (entry->id().isEmpty()) entry->setId(QString(QStringLiteral("entry-%1")).arg(QString::number(qrand(), 36))); /// Missing entry type? Set it to 'misc' if (entry->type().isEmpty()) entry->setType(Entry::etMisc); static const QString ftIssue = QStringLiteral("issue"); if (entry->contains(ftIssue)) { /// ACM's Digital Library uses "issue" instead of "number" -> fix that Value v = entry->value(ftIssue); entry->remove(ftIssue); entry->insert(Entry::ftNumber, v); } /// If entry contains a description field but no abstract, /// rename description field to abstract static const QString ftDescription = QStringLiteral("description"); if (!entry->contains(Entry::ftAbstract) && entry->contains(ftDescription)) { Value v = entry->value(ftDescription); entry->remove(ftDescription); entry->insert(Entry::ftAbstract, v); } /// Remove "dblp" artifacts in abstracts and keywords if (entry->contains(Entry::ftAbstract)) { const QString abstract = PlainTextValue::text(entry->value(Entry::ftAbstract)); if (abstract == QStringLiteral("dblp")) entry->remove(Entry::ftAbstract); } if (entry->contains(Entry::ftKeywords)) { const QString keywords = PlainTextValue::text(entry->value(Entry::ftKeywords)); if (keywords == QStringLiteral("dblp")) entry->remove(Entry::ftKeywords); } if (entry->contains(Entry::ftMonth)) { /// Fix strings for months: "September" -> "sep" Value monthValue = entry->value(Entry::ftMonth); bool updated = false; for (Value::Iterator it = monthValue.begin(); it != monthValue.end(); ++it) { const QString valueItem = PlainTextValue::text(*it); static const QRegularExpression longMonth = QRegularExpression(QStringLiteral("(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*"), QRegularExpression::CaseInsensitiveOption); const QRegularExpressionMatch longMonthMatch = longMonth.match(valueItem); if (longMonthMatch.hasMatch()) { it = monthValue.erase(it); it = monthValue.insert(it, QSharedPointer<MacroKey>(new MacroKey(longMonthMatch.captured(1).toLower()))); updated = true; } } if (updated) entry->insert(Entry::ftMonth, monthValue); } if (entry->contains(Entry::ftDOI) && entry->contains(Entry::ftUrl)) { /// Remove URL from entry if contains a DOI and the DOI field matches the DOI in the URL const Value &doiValue = entry->value(Entry::ftDOI); for (const auto &doiValueItem : doiValue) { const QString doi = PlainTextValue::text(doiValueItem); Value v = entry->value(Entry::ftUrl); bool gotChanged = false; for (Value::Iterator it = v.begin(); it != v.end();) { const QSharedPointer<ValueItem> &vi = (*it); if (vi->containsPattern(QStringLiteral("/") + doi)) { it = v.erase(it); gotChanged = true; } else ++it; } if (v.isEmpty()) entry->remove(Entry::ftUrl); else if (gotChanged) entry->insert(Entry::ftUrl, v); } } else if (!entry->contains(Entry::ftDOI) && entry->contains(Entry::ftUrl)) { /// If URL looks like a DOI, remove URL and add a DOI field QSet<QString> doiSet; ///< using a QSet here to keep only unique DOIs Value v = entry->value(Entry::ftUrl); bool gotChanged = false; for (Value::Iterator it = v.begin(); it != v.end();) { const QString viText = PlainTextValue::text(*it); const QRegularExpressionMatch doiRegExpMatch = KBibTeX::doiRegExp.match(viText); if (doiRegExpMatch.hasMatch()) { doiSet.insert(doiRegExpMatch.captured()); it = v.erase(it); gotChanged = true; } else ++it; } if (v.isEmpty()) entry->remove(Entry::ftUrl); else if (gotChanged) entry->insert(Entry::ftUrl, v); if (!doiSet.isEmpty()) { Value doiValue; /// Rewriting QSet<QString> doiSet into a (sorted) list for reproducibility /// (required for automated test in KBibTeXNetworkingTest) QStringList list; for (const QString &doi : doiSet) list.append(doi); list.sort(); for (const QString &doi : const_cast<const QStringList &>(list)) doiValue.append(QSharedPointer<PlainText>(new PlainText(doi))); entry->insert(Entry::ftDOI, doiValue); } } else if (!entry->contains(Entry::ftDOI)) { const QRegularExpressionMatch doiRegExpMatch = KBibTeX::doiRegExp.match(entry->id()); if (doiRegExpMatch.hasMatch()) { /// If entry id looks like a DOI, add a DOI field Value doiValue; doiValue.append(QSharedPointer<PlainText>(new PlainText(doiRegExpMatch.captured()))); entry->insert(Entry::ftDOI, doiValue); } } /// Referenced strings or entries do not exist in the search result /// and BibTeX breaks if it finds a reference to a non-existing string or entry entry->remove(Entry::ftCrossRef); } bool OnlineSearchAbstract::publishEntry(QSharedPointer<Entry> entry) { if (entry.isNull()) return false; Value v; v.append(QSharedPointer<PlainText>(new PlainText(label()))); entry->insert(QStringLiteral("x-fetchedfrom"), v); sanitizeEntry(entry); emit foundEntry(entry); return true; } void OnlineSearchAbstract::stopSearch(int errorCode) { if (errorCode == resultNoError) curStep = numSteps; else curStep = numSteps = 0; emit progress(curStep, numSteps); emit stoppedSearch(errorCode); } void OnlineSearchAbstract::refreshBusyProperty() { const bool newBusyState = busy(); if (newBusyState != m_previousBusyState) { m_previousBusyState = newBusyState; emit busyChanged(); } } #ifdef HAVE_QTWIDGETS OnlineSearchAbstract::Form::Form(QWidget *parent) : QWidget(parent), d(new OnlineSearchAbstract::Form::Private()) { /// nothing } OnlineSearchAbstract::Form::~Form() { delete d; } #endif // HAVE_QTWIDGETS diff --git a/src/networking/onlinesearch/onlinesearchabstract.h b/src/networking/onlinesearch/onlinesearchabstract.h index 18a3dfd4..9c43ffe8 100644 --- a/src/networking/onlinesearch/onlinesearchabstract.h +++ b/src/networking/onlinesearch/onlinesearchabstract.h @@ -1,218 +1,217 @@ /*************************************************************************** * 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/>. * ***************************************************************************/ #ifndef KBIBTEX_NETWORKING_ONLINESEARCHABSTRACT_H #define KBIBTEX_NETWORKING_ONLINESEARCHABSTRACT_H #include <QObject> #include <QMap> #include <QString> #ifdef HAVE_QTWIDGETS #include <QWidget> #endif // HAVE_QTWIDGETS #include <QMetaType> #include <QIcon> #include <QUrl> #include <Entry> #ifdef HAVE_KF5 #include "kbibtexnetworking_export.h" #endif // HAVE_KF5 class QNetworkReply; class QNetworkRequest; class QListWidgetItem; /** * @author Thomas Fischer <fischer@unix-ag.uni-kl.de> */ class KBIBTEXNETWORKING_EXPORT OnlineSearchAbstract : public QObject { Q_OBJECT Q_PROPERTY(bool busy READ busy NOTIFY busyChanged) public: explicit OnlineSearchAbstract(QObject *parent); #ifdef HAVE_QTWIDGETS class Form; #endif // HAVE_QTWIDGETS static const QString queryKeyFreeText; static const QString queryKeyTitle; static const QString queryKeyAuthor; static const QString queryKeyYear; static const int resultCancelled; static const int resultNoError; static const int resultUnspecifiedError; static const int resultAuthorizationRequired; static const int resultNetworkError; static const int resultInvalidArguments; #ifdef HAVE_QTWIDGETS virtual void startSearchFromForm(); #endif // HAVE_QTWIDGETS virtual void startSearch(const QMap<QString, QString> &query, int numResults) = 0; virtual QString label() const = 0; QString name(); #ifdef HAVE_QTWIDGETS virtual QIcon icon(QListWidgetItem *listWidgetItem = nullptr); virtual OnlineSearchAbstract::Form *customWidget(QWidget *parent); #endif // HAVE_QTWIDGETS virtual QUrl homepage() const = 0; virtual bool busy() const; public slots: void cancel(); protected: QObject *m_parent; bool m_hasBeenCanceled; int numSteps, curStep; virtual QString favIconUrl() const = 0; /** * Split a string along spaces, but keep text in quotation marks together */ static QStringList splitRespectingQuotationMarks(const QString &text); /** * Will check for common problems with downloads via QNetworkReply. It will return true * if there is no problem and you may process this job result. If there is a problem, * this function will notify the user if necessary (KMessageBox), emit a * "stoppedSearch" signal (by invoking "stopSearch"), and return false. * @see handleErrors(KJob*) */ bool handleErrors(QNetworkReply *reply); /** * Will check for common problems with downloads via QNetworkReply. It will return true * if there is no problem and you may process this job result. If there is a problem, * this function will notify the user if necessary (KMessageBox), emit a * "stoppedSearch" signal (by invoking "stopSearch"), and return false. * @see handleErrors(KJob*) * @param newUrl will be set to the new URL if reply contains a redirection, otherwise reply's original URL */ bool handleErrors(QNetworkReply *reply, QUrl &newUrl); /** * Encode a text to be HTTP URL save, e.g. replace '=' by '%3D'. */ static QString encodeURL(QString rawText); static QString decodeURL(QString rawText); QMap<QString, QString> formParameters(const QString &htmlText, int startPos) const; void dumpToFile(const QString &filename, const QString &text); /** * Delay sending of stop signal by a few milliseconds. * Necessary if search stops (is cancelled) already in one * of the startSearch functions. */ void delayedStoppedSearch(int returnCode); /** * Correct the most common problems encountered in fetched entries. * This function should be specialized in each descendant of this class. * @param entry Entry to sanitize */ virtual void sanitizeEntry(QSharedPointer<Entry> entry); /** * Perform the following steps: (1) sanitize entry, (2) add name * of search engine that found the entry, (3) send it back to search * result list. * Returns true if a valid entry was passed to this function and all * steps could be performed. * @param entry Entry to publish * @return returns true if a valid entry was passed to this function and all steps could be performed. */ bool publishEntry(QSharedPointer<Entry> entry); void stopSearch(int errorCode); /** * Allows an online search to notify about a change of its busy state, * i.e. that the public function @see busy may return a different value * than before. If the actual busy state has changed compared to previous * invocations of this function, the signal @see busyChanged will be * emitted. * This function here may be called both on changes from active to * inactive as well as vice versa. */ void refreshBusyProperty(); #ifdef HAVE_KF5 void sendVisualNotification(const QString &text, const QString &title, const QString &icon, int timeout); #endif // HAVE_KF5 private: bool m_previousBusyState; QString m_name; static const char *httpUnsafeChars; #ifdef HAVE_QTWIDGETS QMap<QNetworkReply *, QListWidgetItem *> m_iconReplyToListWidgetItem; #endif // HAVE_QTWIDGETS int m_delayedStoppedSearchReturnCode; QString htmlAttribute(const QString &htmlCode, const int startPos, const QString &attribute) const; bool htmlAttributeIsSelected(const QString &htmlCode, const int startPos, const QString &attribute) const; private slots: #ifdef HAVE_QTWIDGETS void iconDownloadFinished(); #endif // HAVE_QTWIDGETS - void delayedStoppedSearchTimer(); signals: void foundEntry(QSharedPointer<Entry>); void stoppedSearch(int); void progress(int, int); void busyChanged(); }; #ifdef HAVE_QTWIDGETS /** * @author Thomas Fischer <fischer@unix-ag.uni-kl.de> */ class KBIBTEXNETWORKING_EXPORT OnlineSearchAbstract::Form : public QWidget { Q_OBJECT public: explicit Form(QWidget *parent); ~Form(); virtual bool readyToStart() const = 0; virtual void copyFromEntry(const Entry &) = 0; signals: void returnPressed(); protected: class Private; Private *d; }; #endif // HAVE_QTWIDGETS #endif // KBIBTEX_NETWORKING_ONLINESEARCHABSTRACT_H diff --git a/src/networking/zotero/collectionmodel.cpp b/src/networking/zotero/collectionmodel.cpp index 858e52b2..6068b8cf 100644 --- a/src/networking/zotero/collectionmodel.cpp +++ b/src/networking/zotero/collectionmodel.cpp @@ -1,141 +1,138 @@ /*************************************************************************** * 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 "collectionmodel.h" #include <QHash> #include <QIcon> #include "zotero/collection.h" using namespace Zotero; const int Zotero::CollectionModel::CollectionIdRole = Qt::UserRole + 6681; class Zotero::CollectionModel::Private { public: Private(Collection *c, Zotero::CollectionModel *parent) : collection(c) { Q_UNUSED(parent) } Collection *collection; QHash<QString, QModelIndex> collectionIdToModelIndex; }; CollectionModel::CollectionModel(Collection *collection, QObject *parent) : QAbstractItemModel(parent), d(new Zotero::CollectionModel::Private(collection, this)) { beginResetModel(); - connect(collection, &Collection::finishedLoading, this, &CollectionModel::fetchingDone); + connect(collection, &Collection::finishedLoading, this, [this]() { + endResetModel(); + }); } QVariant CollectionModel::data(const QModelIndex &index, int role) const { if (!d->collection->initialized()) return QVariant(); if (index == QModelIndex()) return QVariant(); if (role == Qt::DecorationRole) { /// Fancy icons for collections if (index.internalId() == 0) /// root node return QIcon::fromTheme(QStringLiteral("folder-orange")); else /// any other node return QIcon::fromTheme(QStringLiteral("folder-yellow")); } else if (role == Qt::DisplayRole) { /// Show collections' human-readable description const QString collectionId = d->collection->collectionFromNumericId(index.internalId()); return d->collection->collectionLabel(collectionId); } else if (role == CollectionIdRole) { if (index.internalId() == 0) /// root node return QString(); else /// any other node return d->collection->collectionFromNumericId(index.internalId()); } return QVariant(); } QModelIndex CollectionModel::index(int row, int column, const QModelIndex &parent) const { if (!d->collection->initialized()) return QModelIndex(); if (parent == QModelIndex()) { ///< root node const QModelIndex result = createIndex(row, column, static_cast<quintptr>(0)); d->collectionIdToModelIndex.insert(d->collection->collectionFromNumericId(0), result); return result; } const QString collectionId = d->collection->collectionFromNumericId(parent.internalId()); QVector<QString> children = d->collection->collectionChildren(collectionId); if (row < children.count()) { const QModelIndex result = createIndex(row, column, d->collection->collectionNumericId(children[row])); d->collectionIdToModelIndex.insert(children[row], result); return result; } return QModelIndex(); } QModelIndex CollectionModel::parent(const QModelIndex &index) const { if (!d->collection->initialized()) return QModelIndex(); if (index == QModelIndex() || index.internalId() == 0) return QModelIndex(); ///< invalid index or root node const QString parentId = d->collection->collectionParent(d->collection->collectionFromNumericId(index.internalId())); return d->collectionIdToModelIndex[parentId]; } int CollectionModel::rowCount(const QModelIndex &parent) const { if (!d->collection->initialized()) return 0; if (parent == QModelIndex()) return 1; ///< just one single root node const QString collectionId = d->collection->collectionFromNumericId(parent.internalId()); QVector<QString> children = d->collection->collectionChildren(collectionId); return children.count(); } int CollectionModel::columnCount(const QModelIndex &) const { /// Singe column design; return 1; } bool CollectionModel::hasChildren(const QModelIndex &parent) const { if (!d->collection->initialized()) return false; return rowCount(parent) > 0; } - -void CollectionModel::fetchingDone() -{ - endResetModel(); -} diff --git a/src/networking/zotero/collectionmodel.h b/src/networking/zotero/collectionmodel.h index 0c9e2a4b..b9b1f8f0 100644 --- a/src/networking/zotero/collectionmodel.h +++ b/src/networking/zotero/collectionmodel.h @@ -1,63 +1,60 @@ /*************************************************************************** - * Copyright (C) 2004-2017 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/>. * ***************************************************************************/ #ifndef KBIBTEX_NETWORKING_ZOTERO_COLLECTIONMODEL_H #define KBIBTEX_NETWORKING_ZOTERO_COLLECTIONMODEL_H #include <QAbstractItemModel> #include <QHash> #include <QVector> #include <QSet> #ifdef HAVE_KF5 #include "kbibtexnetworking_export.h" #endif // HAVE_KF5 namespace Zotero { class Collection; /** * @author Thomas Fischer <fischer@unix-ag.uni-kl.de> */ class KBIBTEXNETWORKING_EXPORT CollectionModel : public QAbstractItemModel { Q_OBJECT public: static const int CollectionIdRole; explicit CollectionModel(Zotero::Collection *collection, QObject *parent = nullptr); QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QModelIndex index(int row, int column, const QModelIndex &) const override; QModelIndex parent(const QModelIndex &) const override; int rowCount(const QModelIndex &) const override; int columnCount(const QModelIndex &) const override; bool hasChildren(const QModelIndex &parent = QModelIndex()) const override; -private slots: - void fetchingDone(); - private: class Private; Private *const d; }; } // end of namespace Zotero #endif // KBIBTEX_NETWORKING_ZOTERO_COLLECTIONMODEL_H diff --git a/src/networking/zotero/tagmodel.cpp b/src/networking/zotero/tagmodel.cpp index f0177930..c96a68b4 100644 --- a/src/networking/zotero/tagmodel.cpp +++ b/src/networking/zotero/tagmodel.cpp @@ -1,118 +1,115 @@ /*************************************************************************** - * 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 "tagmodel.h" #include <QHash> #include <QIcon> #include "zotero/tags.h" using namespace Zotero; class Zotero::TagModel::Private { public: Private(Tags *t, Zotero::TagModel *parent) : tags(t) { Q_UNUSED(parent) } Tags *tags; QHash<QString, QModelIndex> tagToModelIndex; }; TagModel::TagModel(Tags *tags, QObject *parent) : QAbstractItemModel(parent), d(new Zotero::TagModel::Private(tags, this)) { beginResetModel(); - connect(tags, &Tags::finishedLoading, this, &TagModel::fetchingDone); + connect(tags, &Tags::finishedLoading, this, [this]() { + endResetModel(); + }); } QVariant TagModel::data(const QModelIndex &index, int role) const { if (!d->tags->initialized()) return QVariant(); if (index == QModelIndex()) return QVariant(); const QMap<QString, int> data = d->tags->tags(); if (index.row() < 0 || index.row() >= data.count()) return QVariant(); const QList<QString> dataKeys = data.keys(); if (role == Qt::DisplayRole) { if (index.column() == 0) return dataKeys[index.row()]; else if (index.column() == 1) return data.value(dataKeys[index.row()]); } else if (role == Qt::DecorationRole) { if (index.column() == 0) return QIcon::fromTheme(QStringLiteral("mail-tagged")); } else if (role == TagRole) return dataKeys[index.row()]; else if (role == TagCountRole) return data.value(dataKeys[index.row()]); return QVariant(); } QModelIndex TagModel::index(int row, int column, const QModelIndex &parent) const { if (!d->tags->initialized() || parent != QModelIndex()) return QModelIndex(); const QMap<QString, int> data = d->tags->tags(); if (row < 0 || column < 0 || row >= data.count() || column >= 2) return QModelIndex(); const QList<QString> dataKeys = data.keys(); return createIndex(row, column, qHash(dataKeys[row])); } QModelIndex TagModel::parent(const QModelIndex &) const { /// This is a flat list return QModelIndex(); } int TagModel::rowCount(const QModelIndex &parent) const { if (!d->tags->initialized() || parent != QModelIndex()) return 0; const QMap<QString, int> data = d->tags->tags(); return data.count(); } int TagModel::columnCount(const QModelIndex &) const { /// Double column design; return 2; } bool TagModel::hasChildren(const QModelIndex &) const { return false; } - -void TagModel::fetchingDone() -{ - endResetModel(); -} diff --git a/src/networking/zotero/tagmodel.h b/src/networking/zotero/tagmodel.h index 64381e93..d7399f84 100644 --- a/src/networking/zotero/tagmodel.h +++ b/src/networking/zotero/tagmodel.h @@ -1,63 +1,60 @@ /*************************************************************************** - * Copyright (C) 2004-2017 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/>. * ***************************************************************************/ #ifndef KBIBTEX_NETWORKING_ZOTERO_TAGMODEL_H #define KBIBTEX_NETWORKING_ZOTERO_TAGMODEL_H #include <QAbstractItemModel> #include <QHash> #include <QVector> #include <QSet> #ifdef HAVE_KF5 #include "kbibtexnetworking_export.h" #endif // HAVE_KF5 namespace Zotero { class Tags; /** * @author Thomas Fischer <fischer@unix-ag.uni-kl.de> */ class KBIBTEXNETWORKING_EXPORT TagModel : public QAbstractItemModel { Q_OBJECT public: enum Roles { TagRole = Qt::UserRole + 6685, TagCountRole = Qt::UserRole + 6686 }; explicit TagModel(Zotero::Tags *tags, QObject *parent = nullptr); QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QModelIndex index(int row, int column, const QModelIndex &) const override; QModelIndex parent(const QModelIndex &) const override; int rowCount(const QModelIndex &) const override; int columnCount(const QModelIndex &) const override; bool hasChildren(const QModelIndex &parent = QModelIndex()) const override; -private slots: - void fetchingDone(); - private: class Private; Private *const d; }; } // end of namespace Zotero #endif // KBIBTEX_NETWORKING_ZOTERO_TAGMODEL_H diff --git a/src/program/docklets/documentpreview.cpp b/src/program/docklets/documentpreview.cpp index 0213c1c8..131e712a 100644 --- a/src/program/docklets/documentpreview.cpp +++ b/src/program/docklets/documentpreview.cpp @@ -1,700 +1,691 @@ /*************************************************************************** * 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 "documentpreview.h" #include <typeinfo> #include <QDomDocument> #include <QDomElement> #include <QList> #include <QLayout> #include <QMap> #include <QFileInfo> #include <QResizeEvent> #include <QCheckBox> #include <QMenuBar> #include <QStackedWidget> #include <QDockWidget> #include <QPushButton> #include <QComboBox> #include <QMutex> #include <QMimeType> #include <QIcon> #ifdef HAVE_WEBENGINEWIDGETS #include <QWebEngineView> #else // HAVE_WEBENGINEWIDGETS #ifdef HAVE_WEBKITWIDGETS #include <QWebView> #endif // HAVE_WEBKITWIDGETS #endif // HAVE_WEBENGINEWIDGETS #include <KLocalizedString> #include <KJobWidgets> #include <KRun> #include <KMimeTypeTrader> #include <KService> #include <KParts/Part> #include <KParts/ReadOnlyPart> #include <kio/jobclasses.h> #include <kio/job.h> #include <kio/jobuidelegate.h> #include <KToolBar> #include <KActionCollection> #include <KSharedConfig> #include <KConfigGroup> #include <kio_version.h> #include <KBibTeX> #include <Element> #include <Entry> #include <File> #include <FileInfo> #include "logging_program.h" ImageLabel::ImageLabel(const QString &text, QWidget *parent, Qt::WindowFlags f) : QLabel(text, parent, f) { /// nothing } void ImageLabel::setPixmap(const QPixmap &pixmap) { m_pixmap = pixmap; if (!m_pixmap.isNull()) { setCursor(Qt::WaitCursor); QPixmap scaledPixmap = m_pixmap.width() <= width() && m_pixmap.height() <= height() ? m_pixmap : pixmap.scaled(width(), height(), Qt::KeepAspectRatio, Qt::SmoothTransformation); QLabel::setPixmap(scaledPixmap); setMinimumSize(100, 100); unsetCursor(); } else QLabel::setPixmap(m_pixmap); } void ImageLabel::resizeEvent(QResizeEvent *event) { QLabel::resizeEvent(event); if (!m_pixmap.isNull()) { setCursor(Qt::WaitCursor); QPixmap scaledPixmap = m_pixmap.width() <= event->size().width() && m_pixmap.height() <= event->size().height() ? m_pixmap : m_pixmap.scaled(event->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); QLabel::setPixmap(scaledPixmap); setMinimumSize(100, 100); unsetCursor(); } } class DocumentPreview::DocumentPreviewPrivate { public: struct UrlInfo { QUrl url; QString mimeType; QIcon icon; }; private: DocumentPreview *p; KSharedConfigPtr config; static const QString configGroupName; static const QString onlyLocalFilesCheckConfig; QPushButton *externalViewerButton; QStackedWidget *stackedWidget; ImageLabel *message; QMap<int, struct UrlInfo> cbxEntryToUrlInfo; QMutex addingUrlMutex; static const QString arXivPDFUrlStart; bool anyLocal; QMenuBar *menuBar; KToolBar *toolBar; KParts::ReadOnlyPart *okularPart; #ifdef HAVE_WEBENGINEWIDGETS QWebEngineView *htmlWidget; #else // HAVE_WEBENGINEWIDGETS #ifdef HAVE_WEBKITWIDGETS QWebView *htmlWidget; #else // HAVE_WEBKITWIDGETS KParts::ReadOnlyPart *htmlPart; #endif // HAVE_WEBKITWIDGETS #endif // HAVE_WEBENGINEWIDGETS int swpMessage, swpOkular, swpHTML; public: QComboBox *urlComboBox; QPushButton *onlyLocalFilesButton; QList<KIO::StatJob *> runningJobs; QSharedPointer<const Entry> entry; QUrl baseUrl; bool anyRemote; KParts::ReadOnlyPart *locatePart(const QString &mimeType, QWidget *parentWidget) { KService::Ptr service = KMimeTypeTrader::self()->preferredService(mimeType, QStringLiteral("KParts/ReadOnlyPart")); if (service) { KParts::ReadOnlyPart *part = service->createInstance<KParts::ReadOnlyPart>(parentWidget, p); connect(part, static_cast<void(KParts::ReadOnlyPart::*)()>(&KParts::ReadOnlyPart::completed), p, &DocumentPreview::loadingFinished); return part; } else return nullptr; } DocumentPreviewPrivate(DocumentPreview *parent) : p(parent), config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))), anyLocal(false), entry(nullptr), anyRemote(false) { setupGUI(); } /** * Create user interface for this widget. * It consists of some controlling widget on the top, * but the most space is consumed by KPart widgets * inside a QStackedWidget to show the external content * (PDF file, web page, ...). */ void setupGUI() { QVBoxLayout *layout = new QVBoxLayout(p); layout->setMargin(0); /// some widgets on the top to control the view QHBoxLayout *innerLayout = new QHBoxLayout(); layout->addLayout(innerLayout, 0); onlyLocalFilesButton = new QPushButton(QIcon::fromTheme(QStringLiteral("applications-internet")), QString(), p); onlyLocalFilesButton->setToolTip(i18n("Toggle between local files only and all documents including remote ones")); innerLayout->addWidget(onlyLocalFilesButton, 0); onlyLocalFilesButton->setCheckable(true); QSizePolicy sp = onlyLocalFilesButton->sizePolicy(); sp.setVerticalPolicy(QSizePolicy::MinimumExpanding); onlyLocalFilesButton->setSizePolicy(sp); urlComboBox = new QComboBox(p); innerLayout->addWidget(urlComboBox, 1); externalViewerButton = new QPushButton(QIcon::fromTheme(QStringLiteral("document-open")), QString(), p); externalViewerButton->setToolTip(i18n("Open in external program")); innerLayout->addWidget(externalViewerButton, 0); sp = externalViewerButton->sizePolicy(); sp.setVerticalPolicy(QSizePolicy::MinimumExpanding); externalViewerButton->setSizePolicy(sp); menuBar = new QMenuBar(p); menuBar->setBackgroundRole(QPalette::Window); menuBar->setVisible(false); layout->addWidget(menuBar, 0); toolBar = new KToolBar(p); toolBar->setToolButtonStyle(Qt::ToolButtonIconOnly); toolBar->setBackgroundRole(QPalette::Window); toolBar->setVisible(false); layout->addWidget(toolBar, 0); /// main part of the widget stackedWidget = new QStackedWidget(p); layout->addWidget(stackedWidget, 1); /// default widget if no preview is available message = new ImageLabel(i18n("No preview available"), stackedWidget); message->setAlignment(Qt::AlignCenter); message->setWordWrap(true); swpMessage = stackedWidget->addWidget(message); connect(message, &QLabel::linkActivated, p, &DocumentPreview::linkActivated); /// add parts to stackedWidget okularPart = locatePart(QStringLiteral("application/pdf"), stackedWidget); swpOkular = (okularPart == nullptr) ? -1 : stackedWidget->addWidget(okularPart->widget()); if (okularPart == nullptr || swpOkular < 0) { qCWarning(LOG_KBIBTEX_PROGRAM) << "No 'KDE Framworks 5'-based Okular part for PDF or PostScript document preview available."; } #ifdef HAVE_WEBENGINEWIDGETS qCDebug(LOG_KBIBTEX_PROGRAM) << "WebEngine is available, using it instead of WebKit or HTML KPart (both neither considered nor tested for) for HTML/Web preview."; /// To make DrKonqi handle crashes in Chromium-based QtWebEngine, /// set a certain environment variable. For details, see here: /// https://www.dvratil.cz/2018/10/drkonqi-and-qtwebengine/ /// https://phabricator.kde.org/D16004 const auto chromiumFlags = qgetenv("QTWEBENGINE_CHROMIUM_FLAGS"); if (!chromiumFlags.contains("disable-in-process-stack-traces")) { qputenv("QTWEBENGINE_CHROMIUM_FLAGS", chromiumFlags + " --disable-in-process-stack-traces"); } htmlWidget = new QWebEngineView(stackedWidget); swpHTML = stackedWidget->addWidget(htmlWidget); connect(htmlWidget, &QWebEngineView::loadFinished, p, &DocumentPreview::loadingFinished); #else // HAVE_WEBENGINEWIDGETS #ifdef HAVE_WEBKITWIDGETS qCDebug(LOG_KBIBTEX_PROGRAM) << "WebKit is available, using it instead of WebEngine (missing) or HTML KPart (not considered) for HTML/Web preview."; htmlWidget = new QWebView(stackedWidget); swpHTML = stackedWidget->addWidget(htmlWidget); connect(htmlWidget, &QWebView::loadFinished, p, &DocumentPreview::loadingFinished); #else // HAVE_WEBKITWIDGETS htmlPart = locatePart(QStringLiteral("text/html"), stackedWidget); if (htmlPart != nullptr) { qCDebug(LOG_KBIBTEX_PROGRAM) << "HTML KPart is available, using it instead of WebEngine or WebKit (neither available) for HTML/Web preview."; swpHTML = stackedWidget->addWidget(htmlPart->widget()); } else { qCDebug(LOG_KBIBTEX_PROGRAM) << "No HTML viewing component is available, disabling HTML/Web preview."; swpHTML = -1; } #endif // HAVE_WEBKITWIDGETS #endif // HAVE_WEBENGINEWIDGETS loadState(); - connect(externalViewerButton, &QPushButton::clicked, p, &DocumentPreview::openExternally); - connect(urlComboBox, static_cast<void(QComboBox::*)(int)>(&QComboBox::activated), p, &DocumentPreview::comboBoxChanged); + connect(externalViewerButton, &QPushButton::clicked, p, [this]() { + openExternally(); + }); + connect(urlComboBox, static_cast<void(QComboBox::*)(int)>(&QComboBox::activated), p, [this](int index) { + comboBoxChanged(index); + }); connect(onlyLocalFilesButton, &QPushButton::toggled, p, &DocumentPreview::onlyLocalFilesChanged); } bool addUrl(const struct UrlInfo &urlInfo) { bool isLocal = KBibTeX::isLocalOrRelative(urlInfo.url); anyLocal |= isLocal; if (!onlyLocalFilesButton->isChecked() && !isLocal) return true; ///< ignore URL if only local files are allowed if (isLocal) { /// create a drop-down list entry if file is a local file /// (based on patch by Luis Silva) QString fn = urlInfo.url.fileName(); QString full = urlInfo.url.url(QUrl::PreferLocalFile); QString dir = urlInfo.url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path(); QString text = fn.isEmpty() ? full : (dir.isEmpty() ? fn : QString(QStringLiteral("%1 [%2]")).arg(fn, dir)); urlComboBox->addItem(urlInfo.icon, text); } else { /// create a drop-down list entry if file is a remote file urlComboBox->addItem(urlInfo.icon, urlInfo.url.toDisplayString()); } urlComboBox->setEnabled(true); cbxEntryToUrlInfo.insert(urlComboBox->count() - 1, urlInfo); externalViewerButton->setEnabled(true); if (urlComboBox->count() == 1 || ///< first entry in combobox isLocal || ///< local files always preferred over URLs /// prefer arXiv summary URLs over other URLs (!anyLocal && urlInfo.url.host().contains(QStringLiteral("arxiv.org/abs")))) { showUrl(urlInfo); } return true; } void update() { p->setCursor(Qt::WaitCursor); /// reset and clear all controls if (swpOkular >= 0 && okularPart != nullptr) okularPart->closeUrl(); #ifdef HAVE_WEBENGINEWIDGETS htmlWidget->stop(); #else // HAVE_WEBENGINEWIDGETS #ifdef HAVE_WEBKITWIDGETS htmlWidget->stop(); #else // HAVE_WEBKITWIDGETS if (swpHTML >= 0 && htmlPart != nullptr) htmlPart->closeUrl(); #endif // HAVE_WEBKITWIDGETS #endif // HAVE_WEBENGINEWIDGETS urlComboBox->setEnabled(false); urlComboBox->clear(); cbxEntryToUrlInfo.clear(); externalViewerButton->setEnabled(false); showMessage(i18n("Refreshing...")); // krazy:exclude=qmethods /// cancel/kill all running jobs auto it = runningJobs.begin(); while (it != runningJobs.end()) { (*it)->kill(); it = runningJobs.erase(it); } /// clear flag that memorizes if any local file was referenced anyLocal = false; anyRemote = false; /// do not load external reference if widget is hidden if (isVisible()) { const auto urlList = FileInfo::entryUrls(entry, baseUrl, FileInfo::TestExistenceYes); for (const QUrl &url : urlList) { bool isLocal = KBibTeX::isLocalOrRelative(url); anyRemote |= !isLocal; if (!onlyLocalFilesButton->isChecked() && !isLocal) continue; KIO::StatJob *job = KIO::stat(url, KIO::StatJob::SourceSide, 3, KIO::HideProgressInfo); runningJobs << job; KJobWidgets::setWindow(job, p); connect(job, &KIO::StatJob::result, p, &DocumentPreview::statFinished); } if (urlList.isEmpty()) { /// Case no URLs associated with this entry. /// For-loop above was never executed. showMessage(i18n("No documents to show.")); // krazy:exclude=qmethods p->setCursor(Qt::ArrowCursor); } else if (runningJobs.isEmpty()) { /// Case no stat jobs are running. As there were URLs (tested in /// previous condition), this implies that there were remote /// references that were ignored by executing "continue" above. /// Give user hint that by enabling remote files, more can be shown. showMessage(i18n("<qt>No documents to show.<br/><a href=\"disableonlylocalfiles\">Disable the restriction</a> to local files to see remote documents.</qt>")); // krazy:exclude=qmethods p->setCursor(Qt::ArrowCursor); } } else p->setCursor(Qt::ArrowCursor); } void showMessage(const QString &msgText) { stackedWidget->setCurrentIndex(swpMessage); message->setPixmap(QPixmap()); message->setText(msgText); if (swpOkular >= 0) stackedWidget->widget(swpOkular)->setEnabled(false); if (swpHTML >= 0) stackedWidget->widget(swpHTML)->setEnabled(false); menuBar->setVisible(false); toolBar->setVisible(true); menuBar->clear(); toolBar->clear(); } void setupToolMenuBarForPart(const KParts::ReadOnlyPart *part) { /* KAction *printAction = KStandardAction::print(part, SLOT(slotPrint()), part->actionCollection()); printAction->setEnabled(false); connect(part, SIGNAL(enablePrintAction(bool)), printAction, SLOT(setEnabled(bool))); */ QDomDocument doc = part->domDocument(); QDomElement docElem = doc.documentElement(); QDomNodeList toolbarNodes = docElem.elementsByTagName(QStringLiteral("ToolBar")); for (int i = 0; i < toolbarNodes.count(); ++i) { QDomNodeList toolbarItems = toolbarNodes.at(i).childNodes(); for (int j = 0; j < toolbarItems.count(); ++j) { QDomNode toolbarItem = toolbarItems.at(j); if (toolbarItem.nodeName() == QStringLiteral("Action")) { QString actionName = toolbarItem.attributes().namedItem(QStringLiteral("name")).nodeValue(); toolBar->addAction(part->actionCollection()->action(actionName)); } else if (toolbarItem.nodeName() == QStringLiteral("Separator")) { toolBar->addSeparator(); } } } QDomNodeList menubarNodes = docElem.elementsByTagName(QStringLiteral("MenuBar")); for (int i = 0; i < menubarNodes.count(); ++i) { QDomNodeList menubarNode = menubarNodes.at(i).childNodes(); for (int j = 0; j < menubarNode.count(); ++j) { QDomNode menubarItem = menubarNode.at(j); if (menubarItem.nodeName() == QStringLiteral("Menu")) { QDomNodeList menuNode = menubarItem.childNodes(); QString text; for (int k = 0; k < menuNode.count(); ++k) { QDomNode menuItem = menuNode.at(k); if (menuItem.nodeName() == QStringLiteral("text")) { text = menuItem.firstChild().toText().data(); break; } } QMenu *menu = menuBar->addMenu(text); for (int k = 0; k < menuNode.count(); ++k) { QDomNode menuItem = menuNode.at(k); if (menuItem.nodeName() == QStringLiteral("Action")) { QString actionName = menuItem.attributes().namedItem(QStringLiteral("name")).nodeValue(); menu->addAction(part->actionCollection()->action(actionName)); } else if (menuItem.nodeName() == QStringLiteral("Separator")) { menu->addSeparator(); } } } } } QDomNodeList actionPropertiesList = docElem.elementsByTagName(QStringLiteral("ActionProperties")); for (int i = 0; i < actionPropertiesList.count(); ++i) { QDomNodeList actionProperties = actionPropertiesList.at(i).childNodes(); for (int j = 0; j < actionProperties.count(); ++j) { QDomNode actionNode = actionProperties.at(j); if (actionNode.nodeName() == QStringLiteral("Action")) { const QString actionName = actionNode.attributes().namedItem(QStringLiteral("name")).toAttr().nodeValue(); const QString actionShortcut = actionNode.attributes().namedItem(QStringLiteral("shortcut")).toAttr().value(); QAction *action = part->actionCollection()->action(actionName); if (action != nullptr) { action->setShortcut(QKeySequence(actionShortcut)); } } } } menuBar->setVisible(true); toolBar->setVisible(true); } void showPart(const KParts::ReadOnlyPart *part, QWidget *widget) { menuBar->setVisible(false); toolBar->setVisible(false); menuBar->clear(); toolBar->clear(); if (okularPart != nullptr && part == okularPart && swpOkular >= 0) { stackedWidget->setCurrentIndex(swpOkular); stackedWidget->widget(swpOkular)->setEnabled(true); setupToolMenuBarForPart(okularPart); #ifdef HAVE_WEBENGINEWIDGETS } else if (widget == htmlWidget) { stackedWidget->setCurrentIndex(swpHTML); stackedWidget->widget(swpHTML)->setEnabled(true); #else // HAVE_WEBENGINEWIDGETS #ifdef HAVE_WEBKITWIDGETS } else if (widget == htmlWidget) { stackedWidget->setCurrentIndex(swpHTML); stackedWidget->widget(swpHTML)->setEnabled(true); #else // HAVE_WEBKITWIDGETS } else if (htmlPart != nullptr && part == htmlPart && swpHTML >= 0) { stackedWidget->setCurrentIndex(swpHTML); stackedWidget->widget(swpHTML)->setEnabled(true); setupToolMenuBarForPart(htmlPart); #endif // HAVE_WEBKITWIDGETS #endif // HAVE_WEBENGINEWIDGETS } else if (widget == message) { stackedWidget->setCurrentIndex(swpMessage); } else showMessage(i18n("Cannot show requested part")); // krazy:exclude=qmethods } bool showUrl(const struct UrlInfo &urlInfo) { static const QStringList okularMimetypes {QStringLiteral("application/x-pdf"), QStringLiteral("application/pdf"), QStringLiteral("application/x-gzpdf"), QStringLiteral("application/x-bzpdf"), QStringLiteral("application/x-wwf"), QStringLiteral("image/vnd.djvu"), QStringLiteral("image/vnd.djvu+multipage"), QStringLiteral("application/postscript"), QStringLiteral("image/x-eps"), QStringLiteral("application/x-gzpostscript"), QStringLiteral("application/x-bzpostscript"), QStringLiteral("image/x-gzeps"), QStringLiteral("image/x-bzeps")}; static const QStringList htmlMimetypes {QStringLiteral("text/html"), QStringLiteral("application/xml"), QStringLiteral("application/xhtml+xml")}; static const QStringList imageMimetypes {QStringLiteral("image/jpeg"), QStringLiteral("image/png"), QStringLiteral("image/gif"), QStringLiteral("image/tiff")}; if (swpHTML >= 0) stackedWidget->widget(swpHTML)->setEnabled(false); if (swpOkular >= 0 && okularPart != nullptr) { stackedWidget->widget(swpOkular)->setEnabled(false); okularPart->closeUrl(); } #ifdef HAVE_WEBENGINEWIDGETS htmlWidget->stop(); #else // HAVE_WEBENGINEWIDGETS #ifdef HAVE_WEBKITWIDGETS htmlWidget->stop(); #else // HAVE_WEBKITWIDGETS if (swpHTML >= 0 && htmlPart != nullptr) htmlPart->closeUrl(); #endif // HAVE_WEBKITWIDGETS #endif // HAVE_WEBENGINEWIDGETS if (swpOkular >= 0 && okularPart != nullptr && okularMimetypes.contains(urlInfo.mimeType)) { p->setCursor(Qt::BusyCursor); showMessage(i18n("Loading...")); // krazy:exclude=qmethods return okularPart->openUrl(urlInfo.url); } else if (htmlMimetypes.contains(urlInfo.mimeType)) { p->setCursor(Qt::BusyCursor); showMessage(i18n("Loading...")); // krazy:exclude=qmethods #ifdef HAVE_WEBENGINEWIDGETS htmlWidget->load(urlInfo.url); return true; #else // HAVE_WEBENGINEWIDGETS #ifdef HAVE_WEBKITWIDGETS htmlWidget->load(urlInfo.url); return true; #else // HAVE_WEBKITWIDGETS return (swpHTML >= 0 && htmlPart != nullptr) ? htmlPart->openUrl(urlInfo.url) : false; #endif // HAVE_WEBKITWIDGETS #endif // HAVE_WEBENGINEWIDGETS } else if (imageMimetypes.contains(urlInfo.mimeType)) { p->setCursor(Qt::BusyCursor); message->setPixmap(QPixmap(urlInfo.url.url(QUrl::PreferLocalFile))); showPart(nullptr, message); p->unsetCursor(); return true; } else { QString additionalInformation; if (urlInfo.mimeType == QStringLiteral("application/pdf")) additionalInformation = i18nc("Additional information in case there is not KPart available for mime type 'application/pdf'", "<br/><br/>Please install <a href=\"https://userbase.kde.org/Okular\">Okular</a> for KDE Frameworks&nbsp;5 to make use of its PDF viewing component.<br/>Okular for KDE&nbsp;4 will not work."); showMessage(i18nc("First parameter is mime type, second parameter is optional information (may be empty)", "<qt>Don't know how to show mimetype '%1'.%2</qt>", urlInfo.mimeType, additionalInformation)); // krazy:exclude=qmethods } return false; } void openExternally() { QUrl url(cbxEntryToUrlInfo[urlComboBox->currentIndex()].url); /// 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 KRun::runUrl(url, mimeTypeName, p, KRun::RunFlags()); } UrlInfo urlMetaInfo(const QUrl &url) { UrlInfo result; result.url = url; if (!KBibTeX::isLocalOrRelative(url) && url.fileName().isEmpty()) { /// URLs not pointing to a specific file should be opened with a web browser component result.icon = QIcon::fromTheme(QStringLiteral("text-html")); result.mimeType = QStringLiteral("text/html"); return result; } const QMimeType mimeType = FileInfo::mimeTypeForUrl(url); result.mimeType = mimeType.name(); result.icon = QIcon::fromTheme(mimeType.iconName()); if (result.mimeType == QStringLiteral("application/octet-stream")) { /// application/octet-stream is a fall-back if KDE did not know better result.icon = QIcon::fromTheme(QStringLiteral("text-html")); result.mimeType = QStringLiteral("text/html"); } else if ((result.mimeType.isEmpty() || result.mimeType == QStringLiteral("inode/directory")) && (result.url.scheme() == QStringLiteral("http") || result.url.scheme() == QStringLiteral("https"))) { /// directory via http means normal webpage (not browsable directory) result.icon = QIcon::fromTheme(QStringLiteral("text-html")); result.mimeType = QStringLiteral("text/html"); } if (url.url(QUrl::PreferLocalFile).startsWith(arXivPDFUrlStart)) { result.icon = QIcon::fromTheme(QStringLiteral("application-pdf")); result.mimeType = QStringLiteral("application/pdf"); } return result; } void comboBoxChanged(int index) { showUrl(cbxEntryToUrlInfo[index]); } bool isVisible() { /// get dock where this widget is inside /// static cast is save as constructor requires parent to be QDockWidget QDockWidget *pp = static_cast<QDockWidget *>(p->parent()); return pp != nullptr && !pp->isHidden(); } void loadState() { KConfigGroup configGroup(config, configGroupName); onlyLocalFilesButton->setChecked(!configGroup.readEntry(onlyLocalFilesCheckConfig, true)); } void saveState() { KConfigGroup configGroup(config, configGroupName); configGroup.writeEntry(onlyLocalFilesCheckConfig, !onlyLocalFilesButton->isChecked()); config->sync(); } }; const QString DocumentPreview::DocumentPreviewPrivate::arXivPDFUrlStart = QStringLiteral("http://arxiv.org/pdf/"); const QString DocumentPreview::DocumentPreviewPrivate::configGroupName = QStringLiteral("URL Preview"); const QString DocumentPreview::DocumentPreviewPrivate::onlyLocalFilesCheckConfig = QStringLiteral("OnlyLocalFiles"); DocumentPreview::DocumentPreview(QDockWidget *parent) : QWidget(parent), d(new DocumentPreviewPrivate(this)) { - connect(parent, &QDockWidget::visibilityChanged, this, &DocumentPreview::visibilityChanged); + connect(parent, &QDockWidget::visibilityChanged, this, [this]() { + d->update(); + }); } DocumentPreview::~DocumentPreview() { delete d; } void DocumentPreview::setElement(QSharedPointer<Element> element, const File *) { d->entry = element.dynamicCast<const Entry>(); d->update(); } -void DocumentPreview::openExternally() -{ - d->openExternally(); -} - void DocumentPreview::setBibTeXUrl(const QUrl &url) { d->baseUrl = url; } void DocumentPreview::onlyLocalFilesChanged() { d->saveState(); d->update(); } -void DocumentPreview::visibilityChanged(bool) -{ - d->update(); -} - -void DocumentPreview::comboBoxChanged(int index) -{ - d->comboBoxChanged(index); -} - void DocumentPreview::statFinished(KJob *kjob) { KIO::StatJob *job = static_cast<KIO::StatJob *>(kjob); d->runningJobs.removeOne(job); if (!job->error()) { const QUrl url = job->mostLocalUrl(); DocumentPreviewPrivate::UrlInfo urlInfo = d->urlMetaInfo(url); setCursor(d->runningJobs.isEmpty() ? Qt::ArrowCursor : Qt::BusyCursor); d->addUrl(urlInfo); } else { qCWarning(LOG_KBIBTEX_PROGRAM) << job->error() << job->errorString(); } if (d->runningJobs.isEmpty()) { /// If this was the last background stat job ... setCursor(Qt::ArrowCursor); if (d->urlComboBox->count() < 1) { /// In case that no valid references were found by the stat jobs ... if (d->anyRemote && !d->onlyLocalFilesButton->isChecked()) { /// There are some remote URLs to probe, /// but user was only looking for local files d->showMessage(i18n("<qt>No documents to show.<br/><a href=\"disableonlylocalfiles\">Disable the restriction</a> to local files to see remote documents.</qt>")); // krazy:exclude=qmethods } else { /// No stat job at all succeeded. Show message to user. d->showMessage(i18n("No documents to show.\nSome URLs or files could not be retrieved.")); // krazy:exclude=qmethods } } } } void DocumentPreview::loadingFinished() { setCursor(Qt::ArrowCursor); d->showPart(qobject_cast<KParts::ReadOnlyPart *>(sender()), qobject_cast<QWidget *>(sender())); } void DocumentPreview::linkActivated(const QString &link) { if (link == QStringLiteral("disableonlylocalfiles")) d->onlyLocalFilesButton->setChecked(true); else if (link.startsWith(QStringLiteral("http://")) || link.startsWith(QStringLiteral("https://"))) { const QUrl urlToOpen = QUrl::fromUserInput(link); if (urlToOpen.isValid()) { /// Guess mime type for url to open QMimeType mimeType = FileInfo::mimeTypeForUrl(urlToOpen); const QString mimeTypeName = mimeType.name(); /// Ask KDE subsystem to open url in viewer matching mime type KRun::runUrl(urlToOpen, mimeTypeName, this, KRun::RunFlags()); } } } diff --git a/src/program/docklets/documentpreview.h b/src/program/docklets/documentpreview.h index e8baef9f..c7f6f56a 100644 --- a/src/program/docklets/documentpreview.h +++ b/src/program/docklets/documentpreview.h @@ -1,80 +1,77 @@ /*************************************************************************** - * Copyright (C) 2004-2017 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/>. * ***************************************************************************/ #ifndef KBIBTEX_PROGRAM_DOCKLET_DOCUMENTPREVIEW_H #define KBIBTEX_PROGRAM_DOCKLET_DOCUMENTPREVIEW_H #include <QWidget> #include <QLabel> #include <QPixmap> #include <QUrl> class QDockWidget; class QResizeEvent; class KJob; namespace KIO { class Job; } class Element; class File; class ImageLabel : public QLabel { Q_OBJECT public: explicit ImageLabel(const QString &text, QWidget *parent = nullptr, Qt::WindowFlags f = 0); void setPixmap(const QPixmap &pixmap); protected: void resizeEvent(QResizeEvent *event) override; private: QPixmap m_pixmap; }; class DocumentPreview : public QWidget { Q_OBJECT public: explicit DocumentPreview(QDockWidget *parent); ~DocumentPreview() override; public slots: void setElement(QSharedPointer<Element>, const File *); void setBibTeXUrl(const QUrl &); private: class DocumentPreviewPrivate; DocumentPreviewPrivate *d; QString mimeType(const QUrl &url); private slots: - void openExternally(); void onlyLocalFilesChanged(); - void visibilityChanged(bool); - void comboBoxChanged(int); void statFinished(KJob *); void loadingFinished(); void linkActivated(const QString &link); }; #endif // KBIBTEX_PROGRAM_DOCKLET_DOCUMENTPREVIEW_H diff --git a/src/program/docklets/elementform.cpp b/src/program/docklets/elementform.cpp index a9242836..e7c2c1d7 100644 --- a/src/program/docklets/elementform.cpp +++ b/src/program/docklets/elementform.cpp @@ -1,270 +1,259 @@ /*************************************************************************** - * 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 "elementform.h" #include <QLayout> #include <QDockWidget> #include <QLabel> #include <QCheckBox> #include <QPushButton> #include <KLocalizedString> #include <KIconLoader> #include <KConfigGroup> #include <KSharedConfig> #include <KMessageBox> #include <Entry> #include <element/ElementEditor> #include "mdiwidget.h" class ElementForm::ElementFormPrivate { private: ElementForm *p; QGridLayout *layout; const File *file; public: ElementEditor *elementEditor; MDIWidget *mdiWidget; QCheckBox *checkBoxAutoApply; QPushButton *buttonApply, *buttonReset; QWidget *widgetUnmodifiedChanges; bool gotModified; QSharedPointer<Element> element; KSharedConfigPtr config; /// Group name in configuration file for all settings for this form static const QString configGroupName; /// Key to store/retrieve setting whether changes in form should be automatically applied to element or not static const QString configKeyAutoApply; ElementFormPrivate(MDIWidget *_mdiWidget, ElementForm *parent) : p(parent), file(nullptr), mdiWidget(_mdiWidget), gotModified(false), config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))) { KConfigGroup configGroup(config, configGroupName); layout = new QGridLayout(p); layout->setColumnStretch(0, 10); layout->setColumnStretch(1, 0); layout->setColumnStretch(2, 0); layout->setColumnStretch(3, 0); elementEditor = new ElementEditor(true, p); layout->addWidget(elementEditor, 0, 0, 1, 4); elementEditor->setEnabled(false); elementEditor->layout()->setMargin(0); connect(elementEditor, &ElementEditor::modified, p, &ElementForm::modified); /// Checkbox enabling/disabling setting to automatically apply changes in form to element checkBoxAutoApply = new QCheckBox(i18n("Automatically apply changes"), p); checkBoxAutoApply->setChecked(configGroup.readEntry(configKeyAutoApply, false)); layout->addWidget(checkBoxAutoApply, 1, 0, 1, 1); /// Create a special widget that shows a small icon and a text /// stating that there are unsaved changes. It will be shown /// simultaneously when the Apply and Reset buttons are enabled. // TODO nearly identical code as in SearchResultsPrivate constructor, create common class widgetUnmodifiedChanges = new QWidget(p); layout->addWidget(widgetUnmodifiedChanges, 1, 1, 1, 1); QBoxLayout *layoutUnmodifiedChanges = new QHBoxLayout(widgetUnmodifiedChanges); layoutUnmodifiedChanges->addSpacing(32); QLabel *label = new QLabel(widgetUnmodifiedChanges); label->setAlignment(Qt::AlignCenter | Qt::AlignVCenter); label->setPixmap(KIconLoader::global()->loadIcon(QStringLiteral("dialog-information"), KIconLoader::Dialog, KIconLoader::SizeSmall)); layoutUnmodifiedChanges->addWidget(label); label = new QLabel(i18n("There are unsaved changes. Please press either 'Apply' or 'Reset'."), widgetUnmodifiedChanges); label->setAlignment(Qt::AlignRight | Qt::AlignVCenter); layoutUnmodifiedChanges->addWidget(label); buttonApply = new QPushButton(QIcon::fromTheme(QStringLiteral("dialog-ok-apply")), i18n("Apply"), p); layout->addWidget(buttonApply, 1, 2, 1, 1); buttonReset = new QPushButton(QIcon::fromTheme(QStringLiteral("edit-undo")), i18n("Reset"), p); layout->addWidget(buttonReset, 1, 3, 1, 1); connect(checkBoxAutoApply, &QCheckBox::toggled, p, &ElementForm::autoApplyToggled); connect(buttonApply, &QPushButton::clicked, p, &ElementForm::validateAndOnlyThenApply); - connect(buttonReset, &QPushButton::clicked, p, &ElementForm::reset); + connect(buttonReset, &QPushButton::clicked, p, [this]() { + reset(); + }); } ~ElementFormPrivate() { delete elementEditor; } void refreshElement() { loadElement(element, file); } void loadElement(QSharedPointer<Element> element, const File *file) { /// store both element and file for later refresh this->element = element; this->file = file; /// skip whole process of loading an element if not visible if (isVisible()) p->setEnabled(true); else { p->setEnabled(false); return; } elementEditor->setElement(element, file); elementEditor->setEnabled(!element.isNull()); /// make apply and reset buttons aware of new element editor buttonApply->setEnabled(false); buttonReset->setEnabled(false); widgetUnmodifiedChanges->setVisible(false); gotModified = false; } bool isVisible() { /// get dock where this widget is inside /// static cast is save as constructor requires parent to be QDockWidget QDockWidget *pp = static_cast<QDockWidget *>(p->parent()); return pp != nullptr && !pp->isHidden(); } void apply() { elementEditor->apply(); buttonApply->setEnabled(false); buttonReset->setEnabled(false); gotModified = false; widgetUnmodifiedChanges->setVisible(false); } void reset() { elementEditor->reset(); buttonApply->setEnabled(false); buttonReset->setEnabled(false); gotModified = false; widgetUnmodifiedChanges->setVisible(false); } }; const QString ElementForm::ElementFormPrivate::configGroupName = QStringLiteral("ElementForm"); const QString ElementForm::ElementFormPrivate::configKeyAutoApply = QStringLiteral("AutoApply"); ElementForm::ElementForm(MDIWidget *mdiWidget, QDockWidget *parent) : QWidget(parent), d(new ElementFormPrivate(mdiWidget, this)) { - connect(parent, &QDockWidget::visibilityChanged, this, &ElementForm::visibilityChanged); + connect(parent, &QDockWidget::visibilityChanged, this, [this]() { + d->refreshElement(); + }); } ElementForm::~ElementForm() { delete d; } void ElementForm::setElement(QSharedPointer<Element> element, const File *file) { /// Test if previous element (1) got modified, (2) the new element isn't /// the same as the new one, and (3) the user confirms to apply those /// changes rather than to discard them -> apply changes in previous element. /// FIXME If the previous element got delete from the file and therefore a different /// element gets set, changes will be still applied to the element to-be-deleted. if (d->gotModified && element != d->element && KMessageBox::questionYesNo(this, i18n("The current element got modified.\nApply or discard changes?"), i18n("Element modified"), KGuiItem(i18n("Apply changes"), QIcon::fromTheme(QStringLiteral("dialog-ok-apply"))), KGuiItem(i18n("Discard changes"), QIcon::fromTheme(QStringLiteral("edit-undo")))) == KMessageBox::Yes) { d->apply(); } if (element != d->element) { /// Ignore loading the same element again d->loadElement(element, file); } } -void ElementForm::refreshElement() -{ - d->refreshElement(); -} - /** * Fetch the modified signal from the editing widget. * @param gotModified true if widget was modified by user, false if modified status was reset by e.g. apply operation */ void ElementForm::modified(bool gotModified) { /// Only interested in modifications, not resets of modified status if (!gotModified) return; if (d->checkBoxAutoApply->isChecked()) { /// User wants to automatically apply changes, so do it // FIXME validateAndOnlyThenApply(); apply(); } else { /// No automatic apply, therefore enable buttons where user can /// apply or reset changes, plus show warning label about unsaved changes d->buttonApply->setEnabled(true); d->buttonReset->setEnabled(true); d->widgetUnmodifiedChanges->setVisible(true); d->gotModified = true; } } void ElementForm::apply() { d->apply(); /// Notify rest of program (esp. main list) about changes emit elementModified(); } bool ElementForm::validateAndOnlyThenApply() { const bool isValid = d->elementEditor->validate(); if (isValid) apply(); return isValid; } -void ElementForm::reset() -{ - d->reset(); -} - -void ElementForm::visibilityChanged(bool) -{ - d->refreshElement(); -} - /** * React on toggles of checkbox for auto-apply. * @param isChecked true if checkbox got checked, false if checkbox got unchecked */ void ElementForm::autoApplyToggled(bool isChecked) { if (isChecked) { /// Got toggled to check state if (!d->element.isNull()) { validateAndOnlyThenApply(); } else { /// The following settings would happen when calling apply(), /// but as no valid element is edited, perform settings here instead d->buttonApply->setEnabled(false); d->buttonReset->setEnabled(false); d->widgetUnmodifiedChanges->setVisible(false); d->gotModified = false; } } /// Save changed status of checkbox in configuration settings KConfigGroup configGroup(d->config, ElementFormPrivate::configGroupName); configGroup.writeEntry(ElementFormPrivate::configKeyAutoApply, d->checkBoxAutoApply->isChecked()); configGroup.sync(); } diff --git a/src/program/docklets/elementform.h b/src/program/docklets/elementform.h index c357f48d..21825b35 100644 --- a/src/program/docklets/elementform.h +++ b/src/program/docklets/elementform.h @@ -1,57 +1,54 @@ /*************************************************************************** - * 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/>. * ***************************************************************************/ #ifndef KBIBTEX_PROGRAM_DOCKLET_ELEMENTFORM_H #define KBIBTEX_PROGRAM_DOCKLET_ELEMENTFORM_H #include <QWidget> class QDockWidget; class MDIWidget; class Element; class File; class ElementForm : public QWidget { Q_OBJECT public: ElementForm(MDIWidget *mdiWidget, QDockWidget *parent); ~ElementForm() override; public slots: void setElement(QSharedPointer<Element>, const File *); - void refreshElement(); signals: void elementModified(); private: class ElementFormPrivate; ElementFormPrivate *d; private slots: void modified(bool); void apply(); bool validateAndOnlyThenApply(); - void reset(); - void visibilityChanged(bool); void autoApplyToggled(bool); }; #endif // KBIBTEX_PROGRAM_DOCKLET_ELEMENTFORM_H diff --git a/src/program/docklets/searchform.cpp b/src/program/docklets/searchform.cpp index 1abb5a38..e45c9e0b 100644 --- a/src/program/docklets/searchform.cpp +++ b/src/program/docklets/searchform.cpp @@ -1,508 +1,487 @@ /*************************************************************************** * 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 "searchform.h" #include <QLayout> #include <QMap> #include <QLabel> #include <QListWidget> #include <QLineEdit> #include <QSpinBox> #include <QStackedWidget> #include <QTabWidget> #include <QProgressBar> #include <QMimeType> #include <QTimer> #include <QSet> #include <QAction> #include <QScrollArea> #include <QIcon> #include <QPushButton> #include <KLocalizedString> #include <KRun> #include <KMessageBox> #include <KParts/Part> #include <KParts/ReadOnlyPart> #include <KConfigGroup> #include <KSharedConfig> #include <kio_version.h> #include <Element> #include <File> #include <Comment> #include <FileExporterBibTeX> #include <onlinesearch/OnlineSearchAbstract> #include <onlinesearch/OnlineSearchGeneral> #include <onlinesearch/OnlineSearchBibsonomy> #include <onlinesearch/onlinesearchgooglescholar.h> #include <onlinesearch/OnlineSearchPubMed> #include <onlinesearch/OnlineSearchIEEEXplore> #include <onlinesearch/OnlineSearchAcmPortal> #include <onlinesearch/OnlineSearchScienceDirect> #include <onlinesearch/OnlineSearchSpringerLink> #include <onlinesearch/OnlineSearchArXiv> #include <onlinesearch/OnlineSearchJStor> #include <onlinesearch/OnlineSearchMathSciNet> #include <onlinesearch/OnlineSearchMRLookup> #include <onlinesearch/OnlineSearchInspireHep> #include <onlinesearch/OnlineSearchCERNDS> #include <onlinesearch/OnlineSearchIngentaConnect> #include <onlinesearch/OnlineSearchSOANASAADS> #include <onlinesearch/OnlineSearchIDEASRePEc> #include <onlinesearch/OnlineSearchDOI> #include <onlinesearch/OnlineSearchBioRxiv> #include <onlinesearch/OnlineSearchSemanticScholar> #include <file/FileView> #include <models/FileModel> #include "openfileinfo.h" #include "searchresults.h" #include "logging_program.h" class SearchForm::SearchFormPrivate { private: SearchForm *p; QStackedWidget *queryTermsStack; QWidget *listContainer; QListWidget *enginesList; QLabel *whichEnginesLabel; QAction *actionOpenHomepage; public: KSharedConfigPtr config; const QString configGroupName; SearchResults *sr; QMap<QListWidgetItem *, OnlineSearchAbstract *> itemToOnlineSearch; QSet<OnlineSearchAbstract *> runningSearches; QPushButton *searchButton; QPushButton *useEntryButton; OnlineSearchGeneral::Form *generalQueryTermsForm; QTabWidget *tabWidget; QSharedPointer<const Entry> currentEntry; QProgressBar *progressBar; QMap<OnlineSearchAbstract *, int> progressMap; QMap<OnlineSearchAbstract::Form *, QScrollArea *> formToScrollArea; enum SearchFormPrivateRole { /// Homepage of a search engine HomepageRole = Qt::UserRole + 5, /// Special widget for a search engine WidgetRole = Qt::UserRole + 6, /// Name of a search engine NameRole = Qt::UserRole + 7 }; SearchFormPrivate(SearchResults *searchResults, SearchForm *parent) : p(parent), whichEnginesLabel(nullptr), config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))), configGroupName(QStringLiteral("Search Engines Docklet")), sr(searchResults), searchButton(nullptr), useEntryButton(nullptr), currentEntry(nullptr) { createGUI(); } OnlineSearchAbstract::Form *currentQueryForm() { QScrollArea *area = qobject_cast<QScrollArea *>(queryTermsStack->currentWidget()); return formToScrollArea.key(area, nullptr); } QScrollArea *wrapInScrollArea(OnlineSearchAbstract::Form *form, QWidget *parent) { QScrollArea *scrollArea = new QScrollArea(parent); form->setParent(scrollArea); scrollArea->setWidget(form); scrollArea->setWidgetResizable(true); scrollArea->setFrameShape(QFrame::NoFrame); scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); formToScrollArea.insert(form, scrollArea); return scrollArea; } QWidget *createQueryTermsStack(QWidget *parent) { QWidget *container = new QWidget(parent); QVBoxLayout *vLayout = new QVBoxLayout(container); whichEnginesLabel = new QLabel(container); whichEnginesLabel->setWordWrap(true); vLayout->addWidget(whichEnginesLabel); vLayout->setStretchFactor(whichEnginesLabel, 0); - connect(whichEnginesLabel, &QLabel::linkActivated, p, &SearchForm::switchToEngines); + connect(whichEnginesLabel, &QLabel::linkActivated, p, [this]() { + switchToEngines(); + }); vLayout->addSpacing(8); queryTermsStack = new QStackedWidget(container); vLayout->addWidget(queryTermsStack); vLayout->setStretchFactor(queryTermsStack, 5); QScrollArea *scrollArea = wrapInScrollArea(createGeneralQueryTermsForm(queryTermsStack), queryTermsStack); queryTermsStack->addWidget(scrollArea); return container; } OnlineSearchGeneral::Form *createGeneralQueryTermsForm(QWidget *parent = nullptr) { generalQueryTermsForm = new OnlineSearchGeneral::Form(parent); return generalQueryTermsForm; } QWidget *createEnginesGUI(QWidget *parent) { listContainer = new QWidget(parent); QGridLayout *layout = new QGridLayout(listContainer); layout->setRowStretch(0, 1); layout->setRowStretch(1, 0); enginesList = new QListWidget(listContainer); layout->addWidget(enginesList, 0, 0, 1, 1); connect(enginesList, &QListWidget::itemChanged, p, &SearchForm::itemCheckChanged); - connect(enginesList, &QListWidget::currentItemChanged, p, &SearchForm::enginesListCurrentChanged); + connect(enginesList, &QListWidget::currentItemChanged, p, [this](QListWidgetItem * current, QListWidgetItem *) { + enginesListCurrentChanged(current); + }); enginesList->setSelectionMode(QAbstractItemView::NoSelection); actionOpenHomepage = new QAction(QIcon::fromTheme(QStringLiteral("internet-web-browser")), i18n("Go to Homepage"), p); - connect(actionOpenHomepage, &QAction::triggered, p, &SearchForm::openHomepage); + connect(actionOpenHomepage, &QAction::triggered, p, [this]() { + openHomepage(); + }); enginesList->addAction(actionOpenHomepage); enginesList->setContextMenuPolicy(Qt::ActionsContextMenu); return listContainer; } void createGUI() { QGridLayout *layout = new QGridLayout(p); layout->setMargin(0); layout->setRowStretch(0, 1); layout->setRowStretch(1, 0); layout->setColumnStretch(0, 0); layout->setColumnStretch(1, 1); layout->setColumnStretch(2, 0); tabWidget = new QTabWidget(p); tabWidget->setDocumentMode(true); layout->addWidget(tabWidget, 0, 0, 1, 3); QWidget *widget = createQueryTermsStack(tabWidget); tabWidget->addTab(widget, QIcon::fromTheme(QStringLiteral("edit-rename")), i18n("Query Terms")); QWidget *listContainer = createEnginesGUI(tabWidget); tabWidget->addTab(listContainer, QIcon::fromTheme(QStringLiteral("applications-engineering")), i18n("Engines")); - connect(tabWidget, &QTabWidget::currentChanged, p, &SearchForm::tabSwitched); + connect(tabWidget, &QTabWidget::currentChanged, p, [this]() { + updateGUI(); + }); useEntryButton = new QPushButton(QIcon::fromTheme(QStringLiteral("go-up")), i18n("Use Entry"), p); layout->addWidget(useEntryButton, 1, 0, 1, 1); useEntryButton->setEnabled(false); connect(useEntryButton, &QPushButton::clicked, p, &SearchForm::copyFromEntry); progressBar = new QProgressBar(p); layout->addWidget(progressBar, 1, 1, 1, 1); progressBar->setMaximum(1000); progressBar->hide(); searchButton = new QPushButton(QIcon::fromTheme(QStringLiteral("edit-find")), i18n("Search"), p); layout->addWidget(searchButton, 1, 2, 1, 1); connect(generalQueryTermsForm, &OnlineSearchAbstract::Form::returnPressed, searchButton, &QPushButton::click); updateGUI(); } void loadEngines() { enginesList->clear(); addEngine(new OnlineSearchAcmPortal(p)); addEngine(new OnlineSearchArXiv(p)); addEngine(new OnlineSearchBioRxiv(p)); addEngine(new OnlineSearchBibsonomy(p)); addEngine(new OnlineSearchGoogleScholar(p)); addEngine(new OnlineSearchIEEEXplore(p)); addEngine(new OnlineSearchIngentaConnect(p)); addEngine(new OnlineSearchJStor(p)); addEngine(new OnlineSearchMathSciNet(p)); addEngine(new OnlineSearchMRLookup(p)); addEngine(new OnlineSearchInspireHep(p)); addEngine(new OnlineSearchCERNDS(p)); addEngine(new OnlineSearchPubMed(p)); addEngine(new OnlineSearchScienceDirect(p)); addEngine(new OnlineSearchSpringerLink(p)); addEngine(new OnlineSearchSOANASAADS(p)); /// addEngine(new OnlineSearchIsbnDB(p)); /// disabled as provider switched to a paid model on 2017-12-26 addEngine(new OnlineSearchIDEASRePEc(p)); addEngine(new OnlineSearchDOI(p)); addEngine(new OnlineSearchSemanticScholar(p)); p->itemCheckChanged(nullptr); updateGUI(); } void addEngine(OnlineSearchAbstract *engine) { KConfigGroup configGroup(config, configGroupName); /// Disable signals while updating the widget and its items enginesList->blockSignals(true); QListWidgetItem *item = new QListWidgetItem(engine->label(), enginesList); static const QSet<QString> enginesEnabledByDefault {QStringLiteral("GoogleScholar"), QStringLiteral("Bibsonomy")}; item->setCheckState(configGroup.readEntry(engine->name(), enginesEnabledByDefault.contains(engine->name())) ? Qt::Checked : Qt::Unchecked); item->setIcon(engine->icon(item)); item->setToolTip(engine->label()); item->setData(HomepageRole, engine->homepage()); item->setData(NameRole, engine->name()); OnlineSearchAbstract::Form *widget = engine->customWidget(queryTermsStack); item->setData(WidgetRole, QVariant::fromValue<OnlineSearchAbstract::Form *>(widget)); if (widget != nullptr) { connect(widget, &OnlineSearchAbstract::Form::returnPressed, searchButton, &QPushButton::click); QScrollArea *scrollArea = wrapInScrollArea(widget, queryTermsStack); queryTermsStack->addWidget(scrollArea); } itemToOnlineSearch.insert(item, engine); - connect(engine, &OnlineSearchAbstract::foundEntry, p, &SearchForm::foundEntry); + connect(engine, &OnlineSearchAbstract::foundEntry, p, [this](QSharedPointer<Entry> entry) { + sr->insertElement(entry); + }); connect(engine, &OnlineSearchAbstract::stoppedSearch, p, &SearchForm::stoppedSearch); connect(engine, &OnlineSearchAbstract::progress, p, &SearchForm::updateProgress); /// Re-enable signals after updating the widget and its items enginesList->blockSignals(false); } void switchToSearch() { for (QMap<QListWidgetItem *, OnlineSearchAbstract *>::ConstIterator it = itemToOnlineSearch.constBegin(); it != itemToOnlineSearch.constEnd(); ++it) disconnect(searchButton, &QPushButton::clicked, it.value(), &OnlineSearchAbstract::cancel); connect(searchButton, &QPushButton::clicked, p, &SearchForm::startSearch); searchButton->setText(i18n("Search")); searchButton->setIcon(QIcon::fromTheme(QStringLiteral("media-playback-start"))); for (int i = tabWidget->count() - 1; i >= 0; --i) tabWidget->widget(i)->setEnabled(true); tabWidget->unsetCursor(); } void switchToCancel() { disconnect(searchButton, &QPushButton::clicked, p, &SearchForm::startSearch); for (QMap<QListWidgetItem *, OnlineSearchAbstract *>::ConstIterator it = itemToOnlineSearch.constBegin(); it != itemToOnlineSearch.constEnd(); ++it) connect(searchButton, &QPushButton::clicked, it.value(), &OnlineSearchAbstract::cancel); searchButton->setText(i18n("Stop")); searchButton->setIcon(QIcon::fromTheme(QStringLiteral("media-playback-stop"))); for (int i = tabWidget->count() - 1; i >= 0; --i) tabWidget->widget(i)->setEnabled(false); tabWidget->setCursor(Qt::WaitCursor); } void switchToEngines() { tabWidget->setCurrentWidget(listContainer); } void updateGUI() { if (whichEnginesLabel == nullptr) return; QStringList checkedEngines; QListWidgetItem *cursor = nullptr; for (QMap<QListWidgetItem *, OnlineSearchAbstract *>::ConstIterator it = itemToOnlineSearch.constBegin(); it != itemToOnlineSearch.constEnd(); ++it) if (it.key()->checkState() == Qt::Checked) { checkedEngines << it.key()->text(); cursor = it.key(); } switch (checkedEngines.size()) { case 0: whichEnginesLabel->setText(i18n("No search engine selected (<a href=\"changeEngine\">change</a>).")); break; case 1: whichEnginesLabel->setText(i18n("Search engine <b>%1</b> is selected (<a href=\"changeEngine\">change</a>).", checkedEngines.first())); break; case 2: whichEnginesLabel->setText(i18n("Search engines <b>%1</b> and <b>%2</b> are selected (<a href=\"changeEngine\">change</a>).", checkedEngines.first(), checkedEngines.at(1))); break; case 3: whichEnginesLabel->setText(i18n("Search engines <b>%1</b>, <b>%2</b>, and <b>%3</b> are selected (<a href=\"changeEngine\">change</a>).", checkedEngines.first(), checkedEngines.at(1), checkedEngines.at(2))); break; default: whichEnginesLabel->setText(i18n("Search engines <b>%1</b>, <b>%2</b>, and more are selected (<a href=\"changeEngine\">change</a>).", checkedEngines.first(), checkedEngines.at(1))); break; } OnlineSearchAbstract::Form *currentQueryWidget = nullptr; if (cursor != nullptr && checkedEngines.size() == 1) currentQueryWidget = cursor->data(WidgetRole).value<OnlineSearchAbstract::Form *>(); if (currentQueryWidget == nullptr) currentQueryWidget = generalQueryTermsForm; QScrollArea *area = formToScrollArea.value(currentQueryWidget, nullptr); if (area != nullptr) queryTermsStack->setCurrentWidget(area); if (useEntryButton != nullptr) useEntryButton->setEnabled(!currentEntry.isNull() && tabWidget->currentIndex() == 0); } void openHomepage() { QListWidgetItem *item = enginesList->currentItem(); if (item != nullptr) { QUrl url = item->data(HomepageRole).toUrl(); /// 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 KRun::runUrl(url, mimeTypeName, p, KRun::RunFlags()); } } void enginesListCurrentChanged(QListWidgetItem *current) { actionOpenHomepage->setEnabled(current != nullptr); } }; SearchForm::SearchForm(SearchResults *searchResults, QWidget *parent) : QWidget(parent), d(new SearchFormPrivate(searchResults, this)) { d->loadEngines(); d->switchToSearch(); } SearchForm::~SearchForm() { delete d; } -void SearchForm::updatedConfiguration() -{ - d->loadEngines(); -} - void SearchForm::setElement(QSharedPointer<Element> element, const File *) { d->currentEntry = element.dynamicCast<const Entry>(); d->useEntryButton->setEnabled(!d->currentEntry.isNull() && d->tabWidget->currentIndex() == 0); } -void SearchForm::switchToEngines() -{ - d->switchToEngines(); -} - void SearchForm::startSearch() { OnlineSearchAbstract::Form *currentForm = d->currentQueryForm(); if (!currentForm->readyToStart()) { KMessageBox::sorry(this, i18n("Could not start searching the Internet:\nThe search terms are not complete or invalid."), i18n("Searching the Internet")); return; } d->runningSearches.clear(); d->sr->clear(); d->progressBar->setValue(0); d->progressMap.clear(); d->useEntryButton->hide(); d->progressBar->show(); if (currentForm == d->generalQueryTermsForm) { /// start search using the general-purpose form's values QMap<QString, QString> queryTerms = d->generalQueryTermsForm->getQueryTerms(); int numResults = d->generalQueryTermsForm->getNumResults(); for (QMap<QListWidgetItem *, OnlineSearchAbstract *>::ConstIterator it = d->itemToOnlineSearch.constBegin(); it != d->itemToOnlineSearch.constEnd(); ++it) if (it.key()->checkState() == Qt::Checked) { it.value()->startSearch(queryTerms, numResults); d->runningSearches.insert(it.value()); } if (d->runningSearches.isEmpty()) { /// if no search engine has been checked (selected), something went wrong return; } } else { /// use the single selected search engine's specific form for (QMap<QListWidgetItem *, OnlineSearchAbstract *>::ConstIterator it = d->itemToOnlineSearch.constBegin(); it != d->itemToOnlineSearch.constEnd(); ++it) if (it.key()->checkState() == Qt::Checked) { it.value()->startSearchFromForm(); d->runningSearches.insert(it.value()); } if (d->runningSearches.isEmpty()) { /// if no search engine has been checked (selected), something went wrong return; } } d->switchToCancel(); } -void SearchForm::foundEntry(QSharedPointer<Entry> entry) -{ - d->sr->insertElement(entry); -} - void SearchForm::stoppedSearch(int) { OnlineSearchAbstract *engine = static_cast<OnlineSearchAbstract *>(sender()); if (d->runningSearches.remove(engine)) { if (d->runningSearches.isEmpty()) { /// last search engine stopped d->switchToSearch(); emit doneSearching(); QTimer::singleShot(1000, d->progressBar, &QProgressBar::hide); QTimer::singleShot(1100, d->useEntryButton, &QPushButton::show); } else { QStringList remainingEngines; remainingEngines.reserve(d->runningSearches.size()); for (OnlineSearchAbstract *running : const_cast<const QSet<OnlineSearchAbstract *> &>(d->runningSearches)) { remainingEngines.append(running->label()); } if (!remainingEngines.isEmpty()) qCDebug(LOG_KBIBTEX_PROGRAM) << "Remaining running engines:" << remainingEngines.join(QStringLiteral(", ")); } } } -void SearchForm::tabSwitched(int newTab) -{ - Q_UNUSED(newTab) - d->updateGUI(); -} - void SearchForm::itemCheckChanged(QListWidgetItem *item) { int numCheckedEngines = 0; for (QMap<QListWidgetItem *, OnlineSearchAbstract *>::ConstIterator it = d->itemToOnlineSearch.constBegin(); it != d->itemToOnlineSearch.constEnd(); ++it) if (it.key()->checkState() == Qt::Checked) ++numCheckedEngines; d->searchButton->setEnabled(numCheckedEngines > 0); if (item != nullptr) { KConfigGroup configGroup(d->config, d->configGroupName); QString name = item->data(SearchForm::SearchFormPrivate::NameRole).toString(); configGroup.writeEntry(name, item->checkState() == Qt::Checked); d->config->sync(); } } -void SearchForm::openHomepage() -{ - d->openHomepage(); -} - -void SearchForm::enginesListCurrentChanged(QListWidgetItem *current, QListWidgetItem *) -{ - d->enginesListCurrentChanged(current); -} - void SearchForm::copyFromEntry() { Q_ASSERT_X(!d->currentEntry.isNull(), "SearchForm::copyFromEntry", "d->currentEntry is NULL"); d->currentQueryForm()->copyFromEntry(*(d->currentEntry)); } void SearchForm::updateProgress(int cur, int total) { OnlineSearchAbstract *ws = static_cast<OnlineSearchAbstract *>(sender()); d->progressMap[ws] = total > 0 ? cur * 1000 / total : 0; int progress = 0, count = 0; for (QMap<OnlineSearchAbstract *, int>::ConstIterator it = d->progressMap.constBegin(); it != d->progressMap.constEnd(); ++it, ++count) progress += it.value(); d->progressBar->setValue(count >= 1 ? progress / count : 0); } diff --git a/src/program/docklets/searchform.h b/src/program/docklets/searchform.h index ce566d7e..08f52ff1 100644 --- a/src/program/docklets/searchform.h +++ b/src/program/docklets/searchform.h @@ -1,92 +1,81 @@ /*************************************************************************** - * Copyright (C) 2004-2017 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/>. * ***************************************************************************/ #ifndef KBIBTEX_PROGRAM_DOCKLET_SEARCHFORM_H #define KBIBTEX_PROGRAM_DOCKLET_SEARCHFORM_H #include <QWidget> class QListWidgetItem; class Element; class File; class Entry; class MDIWidget; class SearchResults; /** * Widget for a dock widget where users can select search engines and * enter search queries (title, author, number of hits, ...) * * @author Thomas Fischer <fischer@unix-ag.uni-kl.de> */ class SearchForm : public QWidget { Q_OBJECT public: /** * Create a new search form, which has to know where to store results * and which (dock) widget is its parent. * @param searchResults SearchResults object where to send results to * @param parent parent widget for this widget */ SearchForm(SearchResults *searchResults, QWidget *parent); ~SearchForm() override; signals: /** * This signal gets emitted once the last of possibly several parallel * online searches is done. */ void doneSearching(); public slots: - /** - * Notify this widget about changes in the configuration settings, - * e.g. to update its list of search engines. - */ - void updatedConfiguration(); - /** * Notify this widget about a new current element selected in the * main list view. Allows the widget to put use in the "Use Entry" * button, i.e. copy title, author, etc from the entry to the search * form. * @param element BibTeX element, which will be cast to an Entry internally * @param file BibTeX file where entry belongs to */ void setElement(QSharedPointer<Element> element, const File *file); private: class SearchFormPrivate; SearchFormPrivate *d; private slots: - void switchToEngines(); void startSearch(); - void foundEntry(QSharedPointer<Entry> entry); void stoppedSearch(int resultCode); - void tabSwitched(int newTab); void itemCheckChanged(QListWidgetItem *); - void openHomepage(); - void enginesListCurrentChanged(QListWidgetItem *, QListWidgetItem *); void copyFromEntry(); void updateProgress(int, int); }; #endif // KBIBTEX_PROGRAM_DOCKLET_SEARCHFORM_H diff --git a/src/program/docklets/valuelist.cpp b/src/program/docklets/valuelist.cpp index 7bcce0ee..6c583e64 100644 --- a/src/program/docklets/valuelist.cpp +++ b/src/program/docklets/valuelist.cpp @@ -1,491 +1,488 @@ /*************************************************************************** * 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), #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); + QTimer::singleShot(500, this, [this]() { + resizeEvent(nullptr); + }); } 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/docklets/valuelist.h b/src/program/docklets/valuelist.h index 3deba564..c3fcd827 100644 --- a/src/program/docklets/valuelist.h +++ b/src/program/docklets/valuelist.h @@ -1,64 +1,63 @@ /*************************************************************************** - * Copyright (C) 2004-2017 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/>. * ***************************************************************************/ #ifndef KBIBTEX_PROGRAM_DOCKLET_VALUELIST_H #define KBIBTEX_PROGRAM_DOCKLET_VALUELIST_H #include <QWidget> #include <QModelIndex> class Element; class File; class FileView; class ValueList : public QWidget { Q_OBJECT public: explicit ValueList(QWidget *parent); ~ValueList() override; void setFileView(FileView *fileView); public slots: void update(); protected slots: void resizeEvent(QResizeEvent *e) override; private slots: void listItemActivated(const QModelIndex &); void searchSelection(); void assignSelection(); void removeSelection(); void startItemRenaming(); void deleteAllOccurrences(); void showCountColumnToggled(); void sortByCountToggled(); - void delayedResize(); void columnsChanged(); void editorSelectionChanged(); void editorDestroyed(); void fieldNamesChanged(int); private: class ValueListPrivate; ValueListPrivate *d; }; #endif // KBIBTEX_PROGRAM_DOCKLET_VALUELIST_H diff --git a/src/program/docklets/zoterobrowser.cpp b/src/program/docklets/zoterobrowser.cpp index 4489de9f..23733d45 100644 --- a/src/program/docklets/zoterobrowser.cpp +++ b/src/program/docklets/zoterobrowser.cpp @@ -1,441 +1,443 @@ /*************************************************************************** * 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 "zoterobrowser.h" #include <QTreeView> #include <QTabWidget> #include <QListView> #include <QLayout> #include <QFormLayout> #include <QAbstractItemModel> #include <QRadioButton> #include <QPushButton> #include <QPointer> #include <QLineEdit> #include <QComboBox> #include <KLocalizedString> #include <KWallet/KWallet> #include <KMessageBox> #include <Element> #include "searchresults.h" #include "zotero/collectionmodel.h" #include "zotero/collection.h" #include "zotero/items.h" #include "zotero/groups.h" #include "zotero/tags.h" #include "zotero/tagmodel.h" #include "zotero/api.h" #include "zotero/oauthwizard.h" #include "logging_program.h" using KWallet::Wallet; class ZoteroBrowser::Private { private: ZoteroBrowser *p; public: Zotero::Items *items; Zotero::Groups *groups; Zotero::Tags *tags; Zotero::TagModel *tagModel; Zotero::Collection *collection; Zotero::CollectionModel *collectionModel; QSharedPointer<Zotero::API> api; bool needToApplyCredentials; SearchResults *searchResults; QTabWidget *tabWidget; QTreeView *collectionBrowser; QListView *tagBrowser; QLineEdit *lineEditNumericUserId; QLineEdit *lineEditApiKey; QRadioButton *radioPersonalLibrary; QRadioButton *radioGroupLibrary; bool comboBoxGroupListInitialized; QComboBox *comboBoxGroupList; QCursor nonBusyCursor; Wallet *wallet; static const QString walletFolderOAuth, walletEntryKBibTeXZotero, walletKeyZoteroId, walletKeyZoteroApiKey; Private(SearchResults *sr, ZoteroBrowser *parent) : p(parent), items(nullptr), groups(nullptr), tags(nullptr), tagModel(nullptr), collection(nullptr), collectionModel(nullptr), needToApplyCredentials(true), searchResults(sr), comboBoxGroupListInitialized(false), nonBusyCursor(p->cursor()), wallet(nullptr) { setupGUI(); } ~Private() { if (wallet != nullptr) delete wallet; if (items != nullptr) delete items; if (groups != nullptr) delete groups; if (tags != nullptr) delete tags; if (tagModel != nullptr) delete tagModel; if (collection != nullptr) delete collection; if (collectionModel != nullptr) delete collectionModel; api.clear(); } void setupGUI() { QBoxLayout *layout = new QVBoxLayout(p); tabWidget = new QTabWidget(p); layout->addWidget(tabWidget); QWidget *container = new QWidget(tabWidget); tabWidget->addTab(container, QIcon::fromTheme(QStringLiteral("preferences-web-browser-identification")), i18n("Library")); connect(tabWidget, &QTabWidget::currentChanged, p, &ZoteroBrowser::tabChanged); QBoxLayout *containerLayout = new QVBoxLayout(container); /// Personal or Group Library QGridLayout *gridLayout = new QGridLayout(); containerLayout->addLayout(gridLayout); gridLayout->setMargin(0); gridLayout->setColumnMinimumWidth(0, 16); // TODO determine size of a radio button radioPersonalLibrary = new QRadioButton(i18n("Personal library"), container); gridLayout->addWidget(radioPersonalLibrary, 0, 0, 1, 2); radioGroupLibrary = new QRadioButton(i18n("Group library"), container); gridLayout->addWidget(radioGroupLibrary, 1, 0, 1, 2); comboBoxGroupList = new QComboBox(container); gridLayout->addWidget(comboBoxGroupList, 2, 1, 1, 1); QSizePolicy sizePolicy = comboBoxGroupList->sizePolicy(); sizePolicy.setHorizontalPolicy(QSizePolicy::MinimumExpanding); comboBoxGroupList->setSizePolicy(sizePolicy); radioPersonalLibrary->setChecked(true); comboBoxGroupList->setEnabled(false); comboBoxGroupList->addItem(i18n("No groups available")); connect(radioGroupLibrary, &QRadioButton::toggled, p, &ZoteroBrowser::radioButtonsToggled); connect(radioPersonalLibrary, &QRadioButton::toggled, p, &ZoteroBrowser::radioButtonsToggled); - connect(comboBoxGroupList, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), p, &ZoteroBrowser::groupListChanged); + connect(comboBoxGroupList, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), p, [this]() { + needToApplyCredentials = true; + }); containerLayout->addStretch(10); /// Credentials QFormLayout *containerForm = new QFormLayout(); containerLayout->addLayout(containerForm, 1); containerForm->setMargin(0); lineEditNumericUserId = new QLineEdit(container); lineEditNumericUserId->setSizePolicy(sizePolicy); lineEditNumericUserId->setReadOnly(true); containerForm->addRow(i18n("Numeric user id:"), lineEditNumericUserId); connect(lineEditNumericUserId, &QLineEdit::textChanged, p, &ZoteroBrowser::invalidateGroupList); lineEditApiKey = new QLineEdit(container); lineEditApiKey->setSizePolicy(sizePolicy); lineEditApiKey->setReadOnly(true); containerForm->addRow(i18n("API key:"), lineEditApiKey); connect(lineEditApiKey, &QLineEdit::textChanged, p, &ZoteroBrowser::invalidateGroupList); QBoxLayout *containerButtonLayout = new QHBoxLayout(); containerLayout->addLayout(containerButtonLayout, 0); containerButtonLayout->setMargin(0); QPushButton *buttonGetOAuthCredentials = new QPushButton(QIcon::fromTheme(QStringLiteral("preferences-web-browser-identification")), i18n("Get New Credentials"), container); containerButtonLayout->addWidget(buttonGetOAuthCredentials, 0); connect(buttonGetOAuthCredentials, &QPushButton::clicked, p, &ZoteroBrowser::getOAuthCredentials); containerButtonLayout->addStretch(1); /// Collection browser collectionBrowser = new QTreeView(tabWidget); tabWidget->addTab(collectionBrowser, QIcon::fromTheme(QStringLiteral("folder-yellow")), i18n("Collections")); collectionBrowser->setHeaderHidden(true); collectionBrowser->setExpandsOnDoubleClick(false); connect(collectionBrowser, &QTreeView::doubleClicked, p, &ZoteroBrowser::collectionDoubleClicked); /// Tag browser tagBrowser = new QListView(tabWidget); tabWidget->addTab(tagBrowser, QIcon::fromTheme(QStringLiteral("mail-tagged")), i18n("Tags")); connect(tagBrowser, &QListView::doubleClicked, p, &ZoteroBrowser::tagDoubleClicked); } void queueReadOAuthCredentials() { if (wallet != nullptr && wallet->isOpen()) p->readOAuthCredentials(true); else { /// Wallet is closed or not initialized if (wallet != nullptr) /// Delete existing but closed wallet, will be replaced by new, open wallet soon delete wallet; p->setEnabled(false); p->setCursor(Qt::WaitCursor); wallet = Wallet::openWallet(Wallet::NetworkWallet(), p->winId(), Wallet::Asynchronous); connect(wallet, &Wallet::walletOpened, p, &ZoteroBrowser::readOAuthCredentials); } } void queueWriteOAuthCredentials() { if (wallet != nullptr && wallet->isOpen()) p->writeOAuthCredentials(true); else { /// Wallet is closed or not initialized if (wallet != nullptr) /// Delete existing but closed wallet, will be replaced by new, open wallet soon delete wallet; p->setEnabled(false); p->setCursor(Qt::WaitCursor); wallet = Wallet::openWallet(Wallet::NetworkWallet(), p->winId(), Wallet::Asynchronous); connect(wallet, &Wallet::walletOpened, p, &ZoteroBrowser::writeOAuthCredentials); } } }; const QString ZoteroBrowser::Private::walletFolderOAuth = QStringLiteral("OAuth"); const QString ZoteroBrowser::Private::walletEntryKBibTeXZotero = QStringLiteral("KBibTeX/Zotero"); const QString ZoteroBrowser::Private::walletKeyZoteroId = QStringLiteral("UserId"); const QString ZoteroBrowser::Private::walletKeyZoteroApiKey = QStringLiteral("ApiKey"); ZoteroBrowser::ZoteroBrowser(SearchResults *searchResults, QWidget *parent) : QWidget(parent), d(new ZoteroBrowser::Private(searchResults, this)) { /// Forece GUI update updateButtons(); radioButtonsToggled(); } ZoteroBrowser::~ZoteroBrowser() { delete d; } void ZoteroBrowser::visibiltyChanged(bool v) { if (v && d->lineEditApiKey->text().isEmpty()) /// If Zotero dock became visible and no API key is set, check KWallet for credentials d->queueReadOAuthCredentials(); } void ZoteroBrowser::modelReset() { if (!d->collection->busy() && !d->tags->busy()) { setCursor(d->nonBusyCursor); setEnabled(true); } else { setCursor(Qt::WaitCursor); setEnabled(false); } if (!d->tags->busy() && !d->collection->busy() && !(d->collection->initialized() && d->tags->initialized())) KMessageBox::information(this, i18n("KBibTeX failed to retrieve the bibliography from Zotero. Please check that the provided user id and API key are valid."), i18n("Failed to retrieve data from Zotero")); } void ZoteroBrowser::collectionDoubleClicked(const QModelIndex &index) { setCursor(Qt::WaitCursor); setEnabled(false); ///< will be re-enabled when item retrieve got finished (slot reenableWidget) const QString collectionId = index.data(Zotero::CollectionModel::CollectionIdRole).toString(); d->searchResults->clear(); d->items->retrieveItemsByCollection(collectionId); } void ZoteroBrowser::tagDoubleClicked(const QModelIndex &index) { setCursor(Qt::WaitCursor); setEnabled(false); ///< will be re-enabled when item retrieve got finished (slot reenableWidget) const QString tag = index.data(Zotero::TagModel::TagRole).toString(); d->searchResults->clear(); d->items->retrieveItemsByTag(tag); } void ZoteroBrowser::showItem(QSharedPointer<Element> e) { d->searchResults->insertElement(e); emit itemToShow(); } void ZoteroBrowser::reenableWidget() { setCursor(d->nonBusyCursor); setEnabled(true); } void ZoteroBrowser::updateButtons() { const bool validNumericIdAndApiKey = !d->lineEditNumericUserId->text().isEmpty() && !d->lineEditApiKey->text().isEmpty(); d->radioGroupLibrary->setEnabled(validNumericIdAndApiKey); d->radioPersonalLibrary->setEnabled(validNumericIdAndApiKey); d->needToApplyCredentials = true; } bool ZoteroBrowser::applyCredentials() { bool ok = false; const int userId = d->lineEditNumericUserId->text().toInt(&ok); const QString apiKey = d->lineEditApiKey->text(); if (ok && !apiKey.isEmpty()) { setCursor(Qt::WaitCursor); setEnabled(false); ok = false; int groupId = d->comboBoxGroupList->itemData(d->comboBoxGroupList->currentIndex()).toInt(&ok); if (!ok) groupId = -1; disconnect(d->tags, &Zotero::Tags::finishedLoading, this, &ZoteroBrowser::reenableWidget); disconnect(d->items, &Zotero::Items::stoppedSearch, this, &ZoteroBrowser::reenableWidget); disconnect(d->items, &Zotero::Items::foundElement, this, &ZoteroBrowser::showItem); disconnect(d->tagModel, &Zotero::TagModel::modelReset, this, &ZoteroBrowser::modelReset); disconnect(d->collectionModel, &Zotero::CollectionModel::modelReset, this, &ZoteroBrowser::modelReset); d->collection->deleteLater(); d->items->deleteLater(); d->tags->deleteLater(); d->collectionModel->deleteLater(); d->tagModel->deleteLater(); d->api.clear(); const bool makeGroupRequest = d->radioGroupLibrary->isChecked() && groupId > 0; d->api = QSharedPointer<Zotero::API>(new Zotero::API(makeGroupRequest ? Zotero::API::GroupRequest : Zotero::API::UserRequest, makeGroupRequest ? groupId : userId, d->lineEditApiKey->text(), this)); d->items = new Zotero::Items(d->api, this); d->tags = new Zotero::Tags(d->api, this); d->tagModel = new Zotero::TagModel(d->tags, this); d->tagBrowser->setModel(d->tagModel); d->collection = new Zotero::Collection(d->api, this); d->collectionModel = new Zotero::CollectionModel(d->collection, this); d->collectionBrowser->setModel(d->collectionModel); connect(d->collectionModel, &Zotero::CollectionModel::modelReset, this, &ZoteroBrowser::modelReset); connect(d->tagModel, &Zotero::TagModel::modelReset, this, &ZoteroBrowser::modelReset); connect(d->items, &Zotero::Items::foundElement, this, &ZoteroBrowser::showItem); connect(d->items, &Zotero::Items::stoppedSearch, this, &ZoteroBrowser::reenableWidget); connect(d->tags, &Zotero::Tags::finishedLoading, this, &ZoteroBrowser::reenableWidget); d->needToApplyCredentials = false; return true; } else return false; } void ZoteroBrowser::radioButtonsToggled() { d->comboBoxGroupList->setEnabled(d->comboBoxGroupListInitialized && d->comboBoxGroupList->count() > 0 && d->radioGroupLibrary->isChecked()); if (!d->comboBoxGroupListInitialized && d->radioGroupLibrary->isChecked()) retrieveGroupList(); d->needToApplyCredentials = true; } void ZoteroBrowser::groupListChanged() { d->needToApplyCredentials = true; } void ZoteroBrowser::retrieveGroupList() { bool ok = false; const int userId = d->lineEditNumericUserId->text().toInt(&ok); if (ok) { setCursor(Qt::WaitCursor); setEnabled(false); d->comboBoxGroupList->clear(); d->comboBoxGroupListInitialized = false; disconnect(d->groups, &Zotero::Groups::finishedLoading, this, &ZoteroBrowser::gotGroupList); d->groups->deleteLater(); d->api.clear(); d->api = QSharedPointer<Zotero::API>(new Zotero::API(Zotero::API::UserRequest, userId, d->lineEditApiKey->text(), this)); d->groups = new Zotero::Groups(d->api, this); connect(d->groups, &Zotero::Groups::finishedLoading, this, &ZoteroBrowser::gotGroupList); } } void ZoteroBrowser::invalidateGroupList() { d->comboBoxGroupList->clear(); d->comboBoxGroupListInitialized = false; d->comboBoxGroupList->addItem(i18n("No groups available or no permissions")); d->comboBoxGroupList->setEnabled(false); d->radioPersonalLibrary->setChecked(true); } void ZoteroBrowser::gotGroupList() { const QMap<int, QString> groupMap = d->groups->groups(); for (QMap<int, QString>::ConstIterator it = groupMap.constBegin(); it != groupMap.constEnd(); ++it) { d->comboBoxGroupList->addItem(it.value(), QVariant::fromValue<int>(it.key())); } if (groupMap.isEmpty()) { invalidateGroupList(); } else { d->comboBoxGroupListInitialized = true; d->comboBoxGroupList->setEnabled(true); d->needToApplyCredentials = true; } reenableWidget(); } void ZoteroBrowser::getOAuthCredentials() { QPointer<Zotero::OAuthWizard> wizard = new Zotero::OAuthWizard(this); if (wizard->exec() && !wizard->apiKey().isEmpty() && wizard->userId() >= 0) { d->lineEditApiKey->setText(wizard->apiKey()); d->lineEditNumericUserId->setText(QString::number(wizard->userId())); d->queueWriteOAuthCredentials(); updateButtons(); retrieveGroupList(); } delete wizard; } void ZoteroBrowser::readOAuthCredentials(bool ok) { /// Do not call this slot a second time disconnect(d->wallet, &Wallet::walletOpened, this, &ZoteroBrowser::readOAuthCredentials); if (ok && (d->wallet->hasFolder(ZoteroBrowser::Private::walletFolderOAuth) || d->wallet->createFolder(ZoteroBrowser::Private::walletFolderOAuth)) && d->wallet->setFolder(ZoteroBrowser::Private::walletFolderOAuth)) { if (d->wallet->hasEntry(ZoteroBrowser::Private::walletEntryKBibTeXZotero)) { QMap<QString, QString> map; if (d->wallet->readMap(ZoteroBrowser::Private::walletEntryKBibTeXZotero, map) == 0) { if (map.contains(ZoteroBrowser::Private::walletKeyZoteroId) && map.contains(ZoteroBrowser::Private::walletKeyZoteroApiKey)) { d->lineEditNumericUserId->setText(map.value(ZoteroBrowser::Private::walletKeyZoteroId, QString())); d->lineEditApiKey->setText(map.value(ZoteroBrowser::Private::walletKeyZoteroApiKey, QString())); updateButtons(); retrieveGroupList(); } else qCWarning(LOG_KBIBTEX_PROGRAM) << "Failed to locate Zotero Id and/or API key in KWallet"; } else qCWarning(LOG_KBIBTEX_PROGRAM) << "Failed to access Zotero data in KWallet"; } else qCDebug(LOG_KBIBTEX_PROGRAM) << "No Zotero credentials stored in KWallet"; } else qCWarning(LOG_KBIBTEX_PROGRAM) << "Accessing KWallet to sync API key did not succeed"; reenableWidget(); } void ZoteroBrowser::writeOAuthCredentials(bool ok) { disconnect(d->wallet, &Wallet::walletOpened, this, &ZoteroBrowser::writeOAuthCredentials); if (ok && (d->wallet->hasFolder(ZoteroBrowser::Private::walletFolderOAuth) || d->wallet->createFolder(ZoteroBrowser::Private::walletFolderOAuth)) && d->wallet->setFolder(ZoteroBrowser::Private::walletFolderOAuth)) { QMap<QString, QString> map; map.insert(ZoteroBrowser::Private::walletKeyZoteroId, d->lineEditNumericUserId->text()); map.insert(ZoteroBrowser::Private::walletKeyZoteroApiKey, d->lineEditApiKey->text()); if (d->wallet->writeMap(ZoteroBrowser::Private::walletEntryKBibTeXZotero, map) != 0) qCWarning(LOG_KBIBTEX_PROGRAM) << "Writing API key to KWallet failed"; } else qCWarning(LOG_KBIBTEX_PROGRAM) << "Accessing KWallet to sync API key did not succeed"; reenableWidget(); } void ZoteroBrowser::tabChanged(int newTabIndex) { if (newTabIndex > 0 /** tabs after credential tab*/ && d->needToApplyCredentials) { const bool success = applyCredentials(); for (int i = 1; i < d->tabWidget->count(); ++i) d->tabWidget->widget(i)->setEnabled(success); } } diff --git a/src/program/mainwindow.cpp b/src/program/mainwindow.cpp index 79223765..f0c46218 100644 --- a/src/program/mainwindow.cpp +++ b/src/program/mainwindow.cpp @@ -1,481 +1,478 @@ /*************************************************************************** * 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 "mainwindow.h" #include <QDockWidget> #include <QDragEnterEvent> #include <QDropEvent> #include <QLabel> #include <QMimeData> #include <QPointer> #include <QMenu> #include <QTimer> #include <QApplication> #include <QFileDialog> #include <QAction> #include <KActionMenu> #include <KActionCollection> #include <KPluginFactory> #include <KPluginLoader> #include <KLocalizedString> #include <KMessageBox> #include <KBibTeX> #include <preferences/KBibTeXPreferencesDialog> #include <file/FileView> #include <XSLTransform> #include <BibliographyService> #include <BibUtils> #include "docklets/referencepreview.h" #include "docklets/documentpreview.h" #include "docklets/searchform.h" #include "docklets/searchresults.h" #include "docklets/elementform.h" #include "docklets/documentpreview.h" #include "docklets/statistics.h" #include "docklets/filesettings.h" #include "docklets/valuelist.h" #include "docklets/zoterobrowser.h" #include "documentlist.h" #include "mdiwidget.h" class KBibTeXMainWindow::KBibTeXMainWindowPrivate { private: KBibTeXMainWindow *p; public: QAction *actionClose; QDockWidget *dockDocumentList; QDockWidget *dockReferencePreview; QDockWidget *dockDocumentPreview; QDockWidget *dockValueList; QDockWidget *dockZotero; QDockWidget *dockStatistics; QDockWidget *dockSearchForm; QDockWidget *dockSearchResults; QDockWidget *dockElementForm; QDockWidget *dockFileSettings; DocumentList *listDocumentList; MDIWidget *mdiWidget; ReferencePreview *referencePreview; DocumentPreview *documentPreview; FileSettings *fileSettings; ValueList *valueList; ZoteroBrowser *zotero; Statistics *statistics; SearchForm *searchForm; SearchResults *searchResults; ElementForm *elementForm; QMenu *actionMenuRecentFilesMenu; KBibTeXMainWindowPrivate(KBibTeXMainWindow *parent) : p(parent) { mdiWidget = new MDIWidget(p); KActionMenu *showPanelsAction = new KActionMenu(i18n("Show Panels"), p); p->actionCollection()->addAction(QStringLiteral("settings_shown_panels"), showPanelsAction); QMenu *showPanelsMenu = new QMenu(showPanelsAction->text(), p->widget()); showPanelsAction->setMenu(showPanelsMenu); KActionMenu *actionMenuRecentFiles = new KActionMenu(QIcon::fromTheme(QStringLiteral("document-open-recent")), i18n("Recently used files"), p); p->actionCollection()->addAction(QStringLiteral("file_open_recent"), actionMenuRecentFiles); actionMenuRecentFilesMenu = new QMenu(actionMenuRecentFiles->text(), p->widget()); actionMenuRecentFiles->setMenu(actionMenuRecentFilesMenu); /** * Docklets (a.k.a. panels) will be added by default to the following * positions unless otherwise configured by the user. * - "List of Values" on the left * - "Statistics" on the left * - "List of Documents" on the left in the same tab * - "Online Search" on the left in a new tab * - "Reference Preview" on the left in the same tab * - "Search Results" on the bottom * - "Document Preview" is hidden * - "Element Editor" is hidden */ dockDocumentList = new QDockWidget(i18n("List of Documents"), p); dockDocumentList->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); p->addDockWidget(Qt::LeftDockWidgetArea, dockDocumentList); listDocumentList = new DocumentList(dockDocumentList); dockDocumentList->setWidget(listDocumentList); dockDocumentList->setObjectName(QStringLiteral("dockDocumentList")); dockDocumentList->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); connect(listDocumentList, &DocumentList::openFile, p, &KBibTeXMainWindow::openDocument); showPanelsMenu->addAction(dockDocumentList->toggleViewAction()); dockValueList = new QDockWidget(i18n("List of Values"), p); dockValueList->setAllowedAreas(Qt::BottomDockWidgetArea | Qt::TopDockWidgetArea | Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); p->addDockWidget(Qt::LeftDockWidgetArea, dockValueList); valueList = new ValueList(dockValueList); dockValueList->setWidget(valueList); dockValueList->setObjectName(QStringLiteral("dockValueList")); dockValueList->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); showPanelsMenu->addAction(dockValueList->toggleViewAction()); dockStatistics = new QDockWidget(i18n("Statistics"), p); dockStatistics->setAllowedAreas(Qt::BottomDockWidgetArea | Qt::TopDockWidgetArea | Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); p->addDockWidget(Qt::LeftDockWidgetArea, dockStatistics); statistics = new Statistics(dockStatistics); dockStatistics->setWidget(statistics); dockStatistics->setObjectName(QStringLiteral("dockStatistics")); dockStatistics->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); showPanelsMenu->addAction(dockStatistics->toggleViewAction()); dockSearchResults = new QDockWidget(i18n("Search Results"), p); dockSearchResults->setAllowedAreas(Qt::BottomDockWidgetArea | Qt::TopDockWidgetArea | Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); p->addDockWidget(Qt::BottomDockWidgetArea, dockSearchResults); dockSearchResults->hide(); searchResults = new SearchResults(mdiWidget, dockSearchResults); dockSearchResults->setWidget(searchResults); dockSearchResults->setObjectName(QStringLiteral("dockResultsFrom")); dockSearchResults->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); showPanelsMenu->addAction(dockSearchResults->toggleViewAction()); connect(mdiWidget, &MDIWidget::documentSwitched, searchResults, &SearchResults::documentSwitched); dockSearchForm = new QDockWidget(i18n("Online Search"), p); dockSearchForm->setAllowedAreas(Qt::BottomDockWidgetArea | Qt::TopDockWidgetArea | Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); p->addDockWidget(Qt::LeftDockWidgetArea, dockSearchForm); searchForm = new SearchForm(searchResults, dockSearchForm); connect(searchForm, &SearchForm::doneSearching, p, &KBibTeXMainWindow::showSearchResults); dockSearchForm->setWidget(searchForm); dockSearchForm->setObjectName(QStringLiteral("dockSearchFrom")); dockSearchForm->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); showPanelsMenu->addAction(dockSearchForm->toggleViewAction()); dockZotero = new QDockWidget(i18n("Zotero"), p); dockZotero->setAllowedAreas(Qt::BottomDockWidgetArea | Qt::TopDockWidgetArea | Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); p->addDockWidget(Qt::LeftDockWidgetArea, dockZotero); zotero = new ZoteroBrowser(searchResults, dockZotero); connect(dockZotero, &QDockWidget::visibilityChanged, zotero, &ZoteroBrowser::visibiltyChanged); connect(zotero, &ZoteroBrowser::itemToShow, p, &KBibTeXMainWindow::showSearchResults); dockZotero->setWidget(zotero); dockZotero->setObjectName(QStringLiteral("dockZotero")); dockZotero->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); showPanelsMenu->addAction(dockZotero->toggleViewAction()); dockReferencePreview = new QDockWidget(i18n("Reference Preview"), p); dockReferencePreview->setAllowedAreas(Qt::BottomDockWidgetArea | Qt::TopDockWidgetArea | Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); p->addDockWidget(Qt::LeftDockWidgetArea, dockReferencePreview); referencePreview = new ReferencePreview(dockReferencePreview); dockReferencePreview->setWidget(referencePreview); dockReferencePreview->setObjectName(QStringLiteral("dockReferencePreview")); dockReferencePreview->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); showPanelsMenu->addAction(dockReferencePreview->toggleViewAction()); dockDocumentPreview = new QDockWidget(i18n("Document Preview"), p); dockDocumentPreview->setAllowedAreas(Qt::BottomDockWidgetArea | Qt::TopDockWidgetArea | Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); p->addDockWidget(Qt::RightDockWidgetArea, dockDocumentPreview); dockDocumentPreview->hide(); documentPreview = new DocumentPreview(dockDocumentPreview); dockDocumentPreview->setWidget(documentPreview); dockDocumentPreview->setObjectName(QStringLiteral("dockDocumentPreview")); dockDocumentPreview->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); showPanelsMenu->addAction(dockDocumentPreview->toggleViewAction()); p->actionCollection()->setDefaultShortcut(dockDocumentPreview->toggleViewAction(), Qt::CTRL + Qt::SHIFT + Qt::Key_D); dockElementForm = new QDockWidget(i18n("Element Editor"), p); dockElementForm->setAllowedAreas(Qt::BottomDockWidgetArea | Qt::TopDockWidgetArea | Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); p->addDockWidget(Qt::BottomDockWidgetArea, dockElementForm); dockElementForm->hide(); elementForm = new ElementForm(mdiWidget, dockElementForm); dockElementForm->setWidget(elementForm); dockElementForm->setObjectName(QStringLiteral("dockElementFrom")); dockElementForm->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); showPanelsMenu->addAction(dockElementForm->toggleViewAction()); dockFileSettings = new QDockWidget(i18n("File Settings"), p); dockFileSettings->setAllowedAreas(Qt::BottomDockWidgetArea | Qt::TopDockWidgetArea | Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); p->addDockWidget(Qt::LeftDockWidgetArea, dockFileSettings); fileSettings = new FileSettings(dockFileSettings); dockFileSettings->setWidget(fileSettings); dockFileSettings->setObjectName(QStringLiteral("dockFileSettings")); dockFileSettings->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); showPanelsMenu->addAction(dockFileSettings->toggleViewAction()); p->tabifyDockWidget(dockFileSettings, dockSearchForm); p->tabifyDockWidget(dockZotero, dockSearchForm); p->tabifyDockWidget(dockValueList, dockStatistics); p->tabifyDockWidget(dockStatistics, dockFileSettings); p->tabifyDockWidget(dockSearchForm, dockReferencePreview); p->tabifyDockWidget(dockFileSettings, dockDocumentList); QAction *action = p->actionCollection()->addAction(KStandardAction::New); connect(action, &QAction::triggered, p, &KBibTeXMainWindow::newDocument); action = p->actionCollection()->addAction(KStandardAction::Open); connect(action, &QAction::triggered, p, &KBibTeXMainWindow::openDocumentDialog); actionClose = p->actionCollection()->addAction(KStandardAction::Close); - connect(actionClose, &QAction::triggered, p, &KBibTeXMainWindow::closeDocument); + connect(actionClose, &QAction::triggered, p, []() { + OpenFileInfoManager::instance().close(OpenFileInfoManager::instance().currentFile()); + }); actionClose->setEnabled(false); action = p->actionCollection()->addAction(KStandardAction::Quit); connect(action, &QAction::triggered, p, &KBibTeXMainWindow::queryCloseAll); action = p->actionCollection()->addAction(KStandardAction::Preferences); connect(action, &QAction::triggered, p, &KBibTeXMainWindow::showPreferences); } ~KBibTeXMainWindowPrivate() { elementForm->deleteLater(); delete mdiWidget; // TODO other deletes } }; KBibTeXMainWindow::KBibTeXMainWindow(QWidget *parent) : KParts::MainWindow(parent, static_cast<Qt::WindowFlags>(KDE_DEFAULT_WINDOWFLAGS)), d(new KBibTeXMainWindowPrivate(this)) { setObjectName(QStringLiteral("KBibTeXShell")); setXMLFile(QStringLiteral("kbibtexui.rc")); setCentralWidget(d->mdiWidget); connect(d->mdiWidget, &MDIWidget::documentSwitched, this, &KBibTeXMainWindow::documentSwitched); connect(d->mdiWidget, &MDIWidget::activePartChanged, this, &KBibTeXMainWindow::createGUI); ///< actually: KParts::MainWindow::createGUI connect(d->mdiWidget, &MDIWidget::documentNew, this, &KBibTeXMainWindow::newDocument); connect(d->mdiWidget, &MDIWidget::documentOpen, this, &KBibTeXMainWindow::openDocumentDialog); connect(d->mdiWidget, &MDIWidget::documentOpenURL, this, &KBibTeXMainWindow::openDocument); connect(&OpenFileInfoManager::instance(), &OpenFileInfoManager::currentChanged, d->mdiWidget, &MDIWidget::setFile); connect(&OpenFileInfoManager::instance(), &OpenFileInfoManager::flagsChanged, this, &KBibTeXMainWindow::documentListsChanged); connect(d->mdiWidget, &MDIWidget::setCaption, this, static_cast<void(KMainWindow::*)(const QString &)>(&KMainWindow::setCaption)); ///< actually: KMainWindow::setCaption documentListsChanged(OpenFileInfo::RecentlyUsed); /// force initialization of menu of recently used files setupControllers(); setupGUI(KXmlGuiWindow::Create | KXmlGuiWindow::Save | KXmlGuiWindow::Keys | KXmlGuiWindow::ToolBar); setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); setAcceptDrops(true); QTimer::singleShot(500, this, &KBibTeXMainWindow::delayed); } KBibTeXMainWindow::~KBibTeXMainWindow() { delete d; } void KBibTeXMainWindow::setupControllers() { // TODO } void KBibTeXMainWindow::dragEnterEvent(QDragEnterEvent *event) { if (event->mimeData()->hasUrls()) event->acceptProposedAction(); } void KBibTeXMainWindow::dropEvent(QDropEvent *event) { QList<QUrl> urlList = event->mimeData()->urls(); if (urlList.isEmpty()) { const QUrl url(event->mimeData()->text()); if (url.isValid()) urlList << url; } if (!urlList.isEmpty()) for (const QUrl &url : const_cast<const QList<QUrl> &>(urlList)) openDocument(url); } void KBibTeXMainWindow::newDocument() { const QString mimeType = FileInfo::mimetypeBibTeX; OpenFileInfo *openFileInfo = OpenFileInfoManager::instance().createNew(mimeType); if (openFileInfo) OpenFileInfoManager::instance().setCurrentFile(openFileInfo); else KMessageBox::error(this, i18n("Creating a new document of mime type '%1' failed as no editor component could be instantiated.", mimeType), i18n("Creating document failed")); } void KBibTeXMainWindow::openDocumentDialog() { OpenFileInfo *currFile = OpenFileInfoManager::instance().currentFile(); QUrl currFileUrl = currFile == nullptr ? QUrl() : currFile->url(); QString startDir = currFileUrl.isValid() ? QUrl(currFileUrl.url()).path() : QString(); OpenFileInfo *ofi = OpenFileInfoManager::instance().currentFile(); if (ofi != nullptr) { QUrl url = ofi->url(); if (url.isValid()) startDir = url.path(); } /// Assemble list of supported mimetypes QStringList supportedMimeTypes {QStringLiteral("text/x-bibtex"), QStringLiteral("application/x-research-info-systems"), QStringLiteral("application/xml")}; if (BibUtils::available()) { supportedMimeTypes.append(QStringLiteral("application/x-isi-export-format")); supportedMimeTypes.append(QStringLiteral("application/x-endnote-refer")); } supportedMimeTypes.append(QStringLiteral("application/pdf")); supportedMimeTypes.append(QStringLiteral("all/all")); QPointer<QFileDialog> dlg = new QFileDialog(this, i18n("Open file") /* TODO better text */, startDir); dlg->setMimeTypeFilters(supportedMimeTypes); dlg->setFileMode(QFileDialog::ExistingFile); const bool dialogAccepted = dlg->exec() != 0; const QUrl url = (dialogAccepted && !dlg->selectedUrls().isEmpty()) ? dlg->selectedUrls().first() : QUrl(); delete dlg; if (url.isValid()) openDocument(url); } void KBibTeXMainWindow::openDocument(const QUrl &url) { OpenFileInfo *openFileInfo = OpenFileInfoManager::instance().open(url); OpenFileInfoManager::instance().setCurrentFile(openFileInfo); } -void KBibTeXMainWindow::closeDocument() -{ - OpenFileInfoManager::instance().close(OpenFileInfoManager::instance().currentFile()); -} - void KBibTeXMainWindow::closeEvent(QCloseEvent *event) { KMainWindow::closeEvent(event); if (OpenFileInfoManager::instance().queryCloseAll()) event->accept(); else event->ignore(); } void KBibTeXMainWindow::showPreferences() { QPointer<KBibTeXPreferencesDialog> dlg = new KBibTeXPreferencesDialog(this); dlg->exec(); delete dlg; } void KBibTeXMainWindow::documentSwitched(FileView *oldFileView, FileView *newFileView) { OpenFileInfo *openFileInfo = d->mdiWidget->currentFile(); bool validFile = openFileInfo != nullptr; d->actionClose->setEnabled(validFile); setCaption(validFile ? i18n("%1 - KBibTeX", openFileInfo->shortCaption()) : i18n("KBibTeX")); d->fileSettings->setEnabled(newFileView != nullptr); d->referencePreview->setEnabled(newFileView != nullptr); d->elementForm->setEnabled(newFileView != nullptr); d->documentPreview->setEnabled(newFileView != nullptr); if (oldFileView != nullptr) { disconnect(newFileView, &FileView::currentElementChanged, d->referencePreview, &ReferencePreview::setElement); disconnect(newFileView, &FileView::currentElementChanged, d->elementForm, &ElementForm::setElement); disconnect(newFileView, &FileView::currentElementChanged, d->documentPreview, &DocumentPreview::setElement); disconnect(newFileView, &FileView::currentElementChanged, d->searchForm, &SearchForm::setElement); disconnect(newFileView, &FileView::modified, d->valueList, &ValueList::update); disconnect(newFileView, &FileView::modified, d->statistics, &Statistics::update); // FIXME disconnect(oldEditor, SIGNAL(modified()), d->elementForm, SLOT(refreshElement())); disconnect(d->elementForm, &ElementForm::elementModified, newFileView, &FileView::externalModification); } if (newFileView != nullptr) { connect(newFileView, &FileView::currentElementChanged, d->referencePreview, &ReferencePreview::setElement); connect(newFileView, &FileView::currentElementChanged, d->elementForm, &ElementForm::setElement); connect(newFileView, &FileView::currentElementChanged, d->documentPreview, &DocumentPreview::setElement); connect(newFileView, &FileView::currentElementChanged, d->searchForm, &SearchForm::setElement); connect(newFileView, &FileView::modified, d->valueList, &ValueList::update); connect(newFileView, &FileView::modified, d->statistics, &Statistics::update); // FIXME connect(newEditor, SIGNAL(modified()), d->elementForm, SLOT(refreshElement())); connect(d->elementForm, &ElementForm::elementModified, newFileView, &FileView::externalModification); connect(d->elementForm, &ElementForm::elementModified, newFileView, &FileView::externalModification); } d->documentPreview->setBibTeXUrl(validFile ? openFileInfo->url() : QUrl()); d->referencePreview->setElement(QSharedPointer<Element>(), nullptr); d->elementForm->setElement(QSharedPointer<Element>(), nullptr); d->documentPreview->setElement(QSharedPointer<Element>(), nullptr); d->valueList->setFileView(newFileView); d->fileSettings->setFileView(newFileView); d->statistics->setFileView(newFileView); d->referencePreview->setFileView(newFileView); } void KBibTeXMainWindow::showSearchResults() { d->dockSearchResults->show(); } void KBibTeXMainWindow::documentListsChanged(OpenFileInfo::StatusFlags statusFlags) { if (statusFlags.testFlag(OpenFileInfo::RecentlyUsed)) { const OpenFileInfoManager::OpenFileInfoList list = OpenFileInfoManager::instance().filteredItems(OpenFileInfo::RecentlyUsed); d->actionMenuRecentFilesMenu->clear(); for (OpenFileInfo *cur : list) { /// Fixing bug 19511: too long filenames make menu too large, /// therefore squeeze text if it is longer than squeezeLen. const int squeezeLen = 64; const QString squeezedShortCap = squeeze_text(cur->shortCaption(), squeezeLen); const QString squeezedFullCap = squeeze_text(cur->fullCaption(), squeezeLen); QAction *action = new QAction(QString(QStringLiteral("%1 [%2]")).arg(squeezedShortCap, squeezedFullCap), this); action->setData(cur->url()); action->setIcon(QIcon::fromTheme(cur->mimeType().replace(QLatin1Char('/'), QLatin1Char('-')))); d->actionMenuRecentFilesMenu->addAction(action); connect(action, &QAction::triggered, this, &KBibTeXMainWindow::openRecentFile); } } } void KBibTeXMainWindow::openRecentFile() { QAction *action = static_cast<QAction *>(sender()); QUrl url = action->data().toUrl(); openDocument(url); } void KBibTeXMainWindow::queryCloseAll() { if (OpenFileInfoManager::instance().queryCloseAll()) qApp->quit(); } void KBibTeXMainWindow::delayed() { /// Static variable, memorizes the dynamically created /// BibliographyService instance and allows to tell if /// this slot was called for the first or second time. static BibliographyService *bs = nullptr; if (bs == nullptr) { /// First call to this slot bs = new BibliographyService(this); if (!bs->isKBibTeXdefault() && KMessageBox::questionYesNo(this, i18n("KBibTeX is not the default editor for its bibliography formats like BibTeX or RIS."), i18n("Default Bibliography Editor"), KGuiItem(i18n("Set as Default Editor")), KGuiItem(i18n("Keep settings unchanged"))) == KMessageBox::Yes) { bs->setKBibTeXasDefault(); /// QTimer calls this slot again, but as 'bs' will not be NULL, /// the 'if' construct's 'else' path will be followed. QTimer::singleShot(5000, this, &KBibTeXMainWindow::delayed); } else { /// KBibTeX is default application or user doesn't care, /// therefore clean up memory delete bs; bs = nullptr; } } else { /// Second call to this slot. This time, clean up memory. bs->deleteLater(); bs = nullptr; } } diff --git a/src/program/mainwindow.h b/src/program/mainwindow.h index a4e89ea4..f031b2fc 100644 --- a/src/program/mainwindow.h +++ b/src/program/mainwindow.h @@ -1,72 +1,71 @@ /*************************************************************************** - * Copyright (C) 2004-2017 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/>. * ***************************************************************************/ #ifndef KBIBTEX_PROGRAM_MAINWINDOW_H #define KBIBTEX_PROGRAM_MAINWINDOW_H #include <kparts/mainwindow.h> #include <KConfigGroup> #include "openfileinfo.h" class QTextEdit; class QDragEnterEvent; class QDropEvent; class QCloseEvent; class ReferencePreview; class FileView; class KBibTeXMainWindow : public KParts::MainWindow { Q_OBJECT public: explicit KBibTeXMainWindow(QWidget *parent = nullptr); ~KBibTeXMainWindow() override; public slots: void openDocument(const QUrl &url); protected: // KMainWindow API void dragEnterEvent(QDragEnterEvent *event) override; void dropEvent(QDropEvent *event) override; void closeEvent(QCloseEvent *event) override; protected: void setupControllers(); protected slots: void newDocument(); void openDocumentDialog(); - void closeDocument(); void showPreferences(); void documentSwitched(FileView *, FileView *); private slots: void showSearchResults(); void documentListsChanged(OpenFileInfo::StatusFlags statusFlags); void openRecentFile(); void queryCloseAll(); void delayed(); private: class KBibTeXMainWindowPrivate; KBibTeXMainWindowPrivate *d; }; #endif // KBIBTEX_PROGRAM_MAINWINDOW_H diff --git a/src/program/openfileinfo.cpp b/src/program/openfileinfo.cpp index 96d96eba..3a3f50aa 100644 --- a/src/program/openfileinfo.cpp +++ b/src/program/openfileinfo.cpp @@ -1,716 +1,713 @@ /*************************************************************************** * 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 <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); + QTimer::singleShot(300, this, [this]() { + d->readConfig(); + }); } 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) std::sort(result.begin(), result.end(), OpenFileInfoManagerPrivate::byLRULessThan); else if (required == OpenFileInfo::Favorite || required == OpenFileInfo::Open) 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(); -} diff --git a/src/program/openfileinfo.h b/src/program/openfileinfo.h index 73805a4c..2e63be30 100644 --- a/src/program/openfileinfo.h +++ b/src/program/openfileinfo.h @@ -1,168 +1,167 @@ /*************************************************************************** - * 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/>. * ***************************************************************************/ #ifndef KBIBTEX_PROGRAM_OPENFILEINFO_H #define KBIBTEX_PROGRAM_OPENFILEINFO_H #include <QObject> #include <QList> #include <QDateTime> #include <QUrl> #include <QVariant> #include <KService> #include <FileInfo> namespace KParts { class ReadOnlyPart; } class OpenFileInfoManager; class OpenFileInfo : public QObject { Q_OBJECT public: enum StatusFlag { Open = 0x1, RecentlyUsed = 0x2, Favorite = 0x4, HasName = 0x8 }; Q_DECLARE_FLAGS(StatusFlags, StatusFlag) ~OpenFileInfo() override; KParts::ReadOnlyPart *part(QWidget *parent, KService::Ptr servicePtr = KService::Ptr()); QString shortCaption() const; QString fullCaption() const; QString mimeType() const; QUrl url() const; bool isModified() const; bool save(); /** * Close the current file. The user may interrupt the closing * if the file is modified and he/she presses "Cancel" when asked * to close the modified file. * * @return returns true if the closing was successful, otherwise false */ bool close(); StatusFlags flags() const; void setFlags(StatusFlags statusFlags); void addFlags(StatusFlags statusFlags); void removeFlags(StatusFlags statusFlags); QDateTime lastAccess() const; void setLastAccess(const QDateTime &dateTime = QDateTime::currentDateTime()); KService::List listOfServices(); KService::Ptr defaultService(); KService::Ptr currentService(); friend class OpenFileInfoManager; signals: void flagsChanged(OpenFileInfo::StatusFlags statusFlags); protected: OpenFileInfo(OpenFileInfoManager *openFileInfoManager, const QUrl &url); OpenFileInfo(OpenFileInfoManager *openFileInfoManager, const QString &mimeType = FileInfo::mimetypeBibTeX); void setUrl(const QUrl &url); private: class OpenFileInfoPrivate; OpenFileInfoPrivate *d; }; Q_DECLARE_METATYPE(OpenFileInfo *) class OpenFileInfoManager: public QObject { Q_OBJECT public: typedef QVector<OpenFileInfo *> OpenFileInfoList; static OpenFileInfoManager &instance(); ~OpenFileInfoManager() override; OpenFileInfo *createNew(const QString &mimeType = FileInfo::mimetypeBibTeX); /** * Open the given bibliography file as specified in the URL. * If the file is already open, an existing OpenFileInfo pointer will be * returned. If the file was not yet open, a new OpenFileInfo object will * be created and returned. * There shall be no two different OpenFileInfo objects representing * the same file. * @param url URL to bibliography file to open * @return an OpenFileInfo object representing the opened file */ OpenFileInfo *open(const QUrl &url); OpenFileInfo *contains(const QUrl &url) const; OpenFileInfo *currentFile() const; bool changeUrl(OpenFileInfo *openFileInfo, const QUrl &url); bool close(OpenFileInfo *openFileInfo); /** * Try to close all open files. If a file is modified, the user will be asked * if the file shall be saved first. Depending on the KPart, the user may opt * to cancel the closing operation for any modified file. * If the user chooses to cancel, this function quits the closing process and * returns false. Files closed so far stay closed, the remaining open files stay * open. * If the user does not interrupt this function (e.g. no file was modified or * the user confirmed to save or to discard changes), all files get closed. * However, all files that were open before will be marked as opened * again. This could render the user interface inconsistent (therefore this * function should be called only when exiting KBibTeX), but it allows to * easily reopen in the next session all files that were open at the time * this function was called in this session. * @return true if all files could be closed successfully, else false. */ bool queryCloseAll(); void setCurrentFile(OpenFileInfo *openFileInfo, KService::Ptr servicePtr = KService::Ptr()); OpenFileInfoList filteredItems(OpenFileInfo::StatusFlags required, OpenFileInfo::StatusFlags forbidden = nullptr); friend class OpenFileInfo; signals: void currentChanged(OpenFileInfo *, KService::Ptr); void flagsChanged(OpenFileInfo::StatusFlags statusFlags); protected: explicit OpenFileInfoManager(QObject *parent); private: class OpenFileInfoManagerPrivate; OpenFileInfoManagerPrivate *d; private slots: void deferredListsChanged(); - void delayedReadConfig(); }; #endif // KBIBTEX_PROGRAM_OPENFILEINFO_H diff --git a/src/test/kbibtextest.cpp b/src/test/kbibtextest.cpp index 0c4615cf..627640b5 100644 --- a/src/test/kbibtextest.cpp +++ b/src/test/kbibtextest.cpp @@ -1,275 +1,269 @@ /*************************************************************************** * 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 "kbibtextest.h" #include <QProgressBar> #include <QTimer> #include <QLayout> #include <QMenu> #include <QIcon> #include <QPalette> #include <QPushButton> #include <QAction> #include <QListWidget> #include <QApplication> #include <KAboutData> #include <onlinesearch/OnlineSearchAcmPortal> #include <onlinesearch/OnlineSearchArXiv> #include <onlinesearch/OnlineSearchBibsonomy> #include <onlinesearch/OnlineSearchGoogleScholar> #include <onlinesearch/OnlineSearchCERNDS> #include <onlinesearch/OnlineSearchIEEEXplore> #include <onlinesearch/OnlineSearchIngentaConnect> #include <onlinesearch/OnlineSearchInspireHep> #include <onlinesearch/OnlineSearchIDEASRePEc> #include <onlinesearch/OnlineSearchJStor> #include <onlinesearch/OnlineSearchMathSciNet> #include <onlinesearch/OnlineSearchMRLookup> #include <onlinesearch/OnlineSearchPubMed> #include <onlinesearch/OnlineSearchScienceDirect> #include <onlinesearch/OnlineSearchSpringerLink> #include <onlinesearch/OnlineSearchSOANASAADS> #include <onlinesearch/OnlineSearchBioRxiv> #include <onlinesearch/OnlineSearchSemanticScholar> static QColor blendColors(const QColor &color1, const QColor &color2, const qreal ratio) { const int r = static_cast<int>(color1.red() * (1 - ratio) + color2.red() * ratio); const int g = static_cast<int>(color1.green() * (1 - ratio) + color2.green() * ratio); const int b = static_cast<int>(color1.blue() * (1 - ratio) + color2.blue() * ratio); return QColor(r, g, b, 255); } class TestWidget : public QWidget { Q_OBJECT private: KBibTeXTest *m_parent; QPushButton *buttonStartTest; QProgressBar *progressBar; QAction *actionStartOnlineSearchTests; public: QListWidget *messageList; TestWidget(KBibTeXTest *parent) : QWidget(parent), m_parent(parent) { QGridLayout *layout = new QGridLayout(this); buttonStartTest = new QPushButton(QIcon::fromTheme(QStringLiteral("application-x-executable")), QStringLiteral("Start Tests"), this); layout->addWidget(buttonStartTest, 0, 0, 1, 1); progressBar = new QProgressBar(this); layout->addWidget(progressBar, 0, 1, 1, 3); progressBar->setVisible(false); messageList = new QListWidget(this); layout->addWidget(messageList, 1, 0, 4, 4); setupMenus(); } void setProgress(int pos, int total) { if (pos < 0 || total < 0) { progressBar->setVisible(false); progressBar->setMaximum(1); progressBar->setValue(0); } else { progressBar->setVisible(true); progressBar->setMaximum(total); progressBar->setValue(pos); } } void setupMenus() { QMenu *menu = new QMenu(buttonStartTest); buttonStartTest->setMenu(menu); /// ** Online Search ** actionStartOnlineSearchTests = new QAction(QStringLiteral("Online Search"), m_parent); connect(actionStartOnlineSearchTests, &QAction::triggered, m_parent, &KBibTeXTest::startOnlineSearchTests); menu->addAction(actionStartOnlineSearchTests); } void setBusy(bool isBusy) { buttonStartTest->setEnabled(!isBusy); actionStartOnlineSearchTests->setEnabled(!isBusy); } }; KBibTeXTest::KBibTeXTest(QWidget *parent) : QDialog(parent), m_running(false), m_isBusy(false) { m_onlineSearchList << new OnlineSearchAcmPortal(this); m_onlineSearchList << new OnlineSearchArXiv(this); m_onlineSearchList << new OnlineSearchBibsonomy(this); m_onlineSearchList << new OnlineSearchCERNDS(this); m_onlineSearchList << new OnlineSearchGoogleScholar(this); m_onlineSearchList << new OnlineSearchIDEASRePEc(this); m_onlineSearchList << new OnlineSearchIEEEXplore(this); m_onlineSearchList << new OnlineSearchIngentaConnect(this); m_onlineSearchList << new OnlineSearchInspireHep(this); m_onlineSearchList << new OnlineSearchJStor(this); m_onlineSearchList << new OnlineSearchMathSciNet(this); m_onlineSearchList << new OnlineSearchMRLookup(this); m_onlineSearchList << new OnlineSearchPubMed(this); m_onlineSearchList << new OnlineSearchScienceDirect(this); m_onlineSearchList << new OnlineSearchSOANASAADS(this); m_onlineSearchList << new OnlineSearchSpringerLink(this); m_onlineSearchList << new OnlineSearchBioRxiv(this); m_onlineSearchList << new OnlineSearchSemanticScholar(this); m_currentOnlineSearch = m_onlineSearchList.constBegin(); setWindowTitle(QStringLiteral("KBibTeX Test Suite")); m_testWidget = new TestWidget(this); #if QT_VERSION >= 0x050b00 const int fontSize = m_testWidget->fontMetrics().horizontalAdvance(QLatin1Char('a')); #else // QT_VERSION >= 0x050b00 const int fontSize = m_testWidget->fontMetrics().width(QLatin1Char('a')); #endif // QT_VERSION >= 0x050b00 m_testWidget->setMinimumSize(fontSize * 96, fontSize * 48); QBoxLayout *boxLayout = new QVBoxLayout(this); boxLayout->addWidget(m_testWidget); connect(this, &KBibTeXTest::rejected, this, &KBibTeXTest::aboutToQuit); addMessage(QString(QStringLiteral("Compiled for %1")).arg(KAboutData::applicationData().version()), statusInfo); } void KBibTeXTest::addMessage(const QString &message, const MessageStatus messageStatus) { static const QIcon iconINFO = QIcon::fromTheme(QStringLiteral("dialog-information")); static const QIcon iconOK = QIcon::fromTheme(QStringLiteral("dialog-ok-apply")); static const QIcon iconERROR = QIcon::fromTheme(QStringLiteral("dialog-cancel")); static const QIcon iconAUTH = QIcon::fromTheme(QStringLiteral("dialog-cancel")); // FIXME "dialog-cancel" should be overlay on "dialog-password" static const QIcon iconNETWORK = QIcon::fromTheme(QStringLiteral("dialog-cancel")); // FIXME "dialog-cancel" should be overlay on "network-wired" QIcon icon; switch (messageStatus) { case statusInfo: icon = iconINFO; break; case statusOk: icon = iconOK; break; case statusError: icon = iconERROR; break; case statusAuth: icon = iconAUTH; break; case statusNetwork: icon = iconNETWORK; break; } QListWidgetItem *item = icon.isNull() ? new QListWidgetItem(message) : new QListWidgetItem(icon, message); item->setToolTip(item->text()); const QColor originalBgColor = QGuiApplication::palette().color(QPalette::Base); switch (messageStatus) { case statusInfo: break; ///< nothing to do case statusOk: item->setBackground(QBrush(blendColors(originalBgColor, Qt::green, .1))); break; case statusError: item->setBackground(QBrush(blendColors(originalBgColor, Qt::red, .1))); break; case statusAuth: item->setBackground(QBrush(blendColors(originalBgColor, Qt::yellow, .1))); break; case statusNetwork: item->setBackground(QBrush(blendColors(originalBgColor, Qt::yellow, .1))); break; } m_testWidget->messageList->addItem(item); m_testWidget->messageList->scrollToBottom(); qApp->processEvents(); } void KBibTeXTest::setBusy(bool isBusy) { m_testWidget->setBusy(isBusy); if (isBusy && !m_isBusy) { /// changing to busy state QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); } else if (!isBusy && m_isBusy) { /// changing to idle state QApplication::restoreOverrideCursor(); } m_isBusy = isBusy; } void KBibTeXTest::aboutToQuit() { m_running = false; QTimer::singleShot(500, qApp, &QApplication::quit); } void KBibTeXTest::startOnlineSearchTests() { m_running = true; setBusy(true); m_testWidget->messageList->clear(); qApp->processEvents(); processNextSearch(); } void KBibTeXTest::onlineSearchStoppedSearch(int searchResult) { if (searchResult == OnlineSearchAbstract::resultNoError) { if (m_currentOnlineSearchNumFoundEntries == 0) addMessage(QString(QStringLiteral("Got no error message searching '%1', but found NO entries")).arg((*m_currentOnlineSearch)->label()), statusError); else addMessage(QString(QStringLiteral("No error searching '%1', found %2 entries")).arg((*m_currentOnlineSearch)->label()).arg(m_currentOnlineSearchNumFoundEntries), statusOk); } else if (searchResult == OnlineSearchAbstract::resultAuthorizationRequired) { addMessage(QString(QStringLiteral("Authorization required for '%1'")).arg((*m_currentOnlineSearch)->label()), statusAuth); } else if (searchResult == OnlineSearchAbstract::resultNetworkError) { addMessage(QString(QStringLiteral("Network error for '%1'")).arg((*m_currentOnlineSearch)->label()), statusNetwork); } else { addMessage(QString(QStringLiteral("Error searching '%1'")).arg((*m_currentOnlineSearch)->label()), statusError); } m_currentOnlineSearch++; progress(-1, -1); processNextSearch(); } -void KBibTeXTest::onlineSearchFoundEntry() -{ - ++m_currentOnlineSearchNumFoundEntries; -} - void KBibTeXTest::progress(int pos, int total) { m_testWidget->setProgress(pos, total); } -void KBibTeXTest::resetProgress() -{ - m_testWidget->setProgress(-1, -1); -} - void KBibTeXTest::processNextSearch() { if (m_running && m_currentOnlineSearch != m_onlineSearchList.constEnd()) { setBusy(true); m_currentOnlineSearchNumFoundEntries = 0; addMessage(QString(QStringLiteral("Searching '%1'")).arg((*m_currentOnlineSearch)->label()), statusInfo); QMap<QString, QString> query; query.insert(OnlineSearchAbstract::queryKeyAuthor, QStringLiteral("smith")); connect(*m_currentOnlineSearch, &OnlineSearchAbstract::stoppedSearch, this, &KBibTeXTest::onlineSearchStoppedSearch); - connect(*m_currentOnlineSearch, &OnlineSearchAbstract::foundEntry, this, &KBibTeXTest::onlineSearchFoundEntry); + connect(*m_currentOnlineSearch, &OnlineSearchAbstract::foundEntry, this, [this]() { + ++m_currentOnlineSearchNumFoundEntries; + }); connect(*m_currentOnlineSearch, &OnlineSearchAbstract::progress, this, &KBibTeXTest::progress); (*m_currentOnlineSearch)->startSearch(query, 3); } else { addMessage(QStringLiteral("Done testing"), statusInfo); setBusy(false); m_running = false; - QTimer::singleShot(500, this, &KBibTeXTest::resetProgress); + QTimer::singleShot(500, this, [this]() { + m_testWidget->setProgress(-1, -1); + }); } } #include "kbibtextest.moc" diff --git a/src/test/kbibtextest.h b/src/test/kbibtextest.h index ccc311bf..6ca6d094 100644 --- a/src/test/kbibtextest.h +++ b/src/test/kbibtextest.h @@ -1,62 +1,60 @@ /*************************************************************************** - * 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/>. * ***************************************************************************/ #ifndef KBIBTEXTEST_H #define KBIBTEXTEST_H #include <QList> #include <QDialog> #include <QIcon> class OnlineSearchAbstract; class TestWidget; class KBibTeXTest : public QDialog { Q_OBJECT public: explicit KBibTeXTest(QWidget *parent = nullptr); public slots: void startOnlineSearchTests(); private slots: void aboutToQuit(); void onlineSearchStoppedSearch(int); - void onlineSearchFoundEntry(); void progress(int, int); - void resetProgress(); private: enum MessageStatus { statusInfo, statusOk, statusError, statusAuth, statusNetwork }; bool m_running; TestWidget *m_testWidget; bool m_isBusy; QList<OnlineSearchAbstract *> m_onlineSearchList; QList<OnlineSearchAbstract *>::ConstIterator m_currentOnlineSearch; int m_currentOnlineSearchNumFoundEntries; void addMessage(const QString &message, const MessageStatus messageStatus); void setBusy(bool isBusy); void processNextSearch(); }; #endif // KBIBTEXTEST_H