diff --git a/src/gui/element/elementeditor.cpp b/src/gui/element/elementeditor.cpp index 8a0c64e1..349f6516 100644 --- a/src/gui/element/elementeditor.cpp +++ b/src/gui/element/elementeditor.cpp @@ -1,687 +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, [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, [this]() { checkBibTeX(); }); addTabWidgets(); } void updateTabVisibility() { - disconnect(tab, &HidingTabWidget::currentChanged, p, &ElementEditor::tabChanged); + const QSignalBlocker blocker(tab); + 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::childModified(bool m) { if (m) { d->elementUnapplied = true; d->referenceWidgetSetEntryIdByDefault(); } emit modified(m); } diff --git a/src/gui/element/elementwidgets.cpp b/src/gui/element/elementwidgets.cpp index ddeb03eb..f2dadee6 100644 --- a/src/gui/element/elementwidgets.cpp +++ b/src/gui/element/elementwidgets.cpp @@ -1,1399 +1,1391 @@ /*************************************************************************** * 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 "elementwidgets.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 #include #include #include #include #include #include #include #include #include #include "field/fieldinput.h" #include "field/fieldlineedit.h" #include "delayedexecutiontimer.h" #include "logging_gui.h" static const unsigned int interColumnSpace = 16; static const char *PropertyIdSuggestion = "PropertyIdSuggestion"; ElementWidget::ElementWidget(QWidget *parent) : QWidget(parent), isReadOnly(false), m_file(nullptr), m_isModified(false) { /// nothing } bool ElementWidget::isModified() const { return m_isModified; } void ElementWidget::setModified(bool newIsModified) { m_isModified = newIsModified; emit modified(newIsModified); } void ElementWidget::gotModified() { setModified(true); } EntryConfiguredWidget::EntryConfiguredWidget(const QSharedPointer &entryTabLayout, QWidget *parent) : ElementWidget(parent), fieldInputCount(entryTabLayout->singleFieldLayouts.size()), numCols(entryTabLayout->columns), etl(entryTabLayout) { vboxLayout = new QVBoxLayout(this); gridLayout = new QGridLayout(); vboxLayout->addLayout(gridLayout, 100); /// Initialize list of field input widgets plus labels listOfLabeledFieldInput = new LabeledFieldInput*[fieldInputCount]; createGUI(); } EntryConfiguredWidget::~EntryConfiguredWidget() { delete[] listOfLabeledFieldInput; } bool EntryConfiguredWidget::apply(QSharedPointer element) const { if (isReadOnly) return false; /// never save data if in read-only mode QSharedPointer entry = element.dynamicCast(); if (entry.isNull()) return false; for (QMap::ConstIterator it = bibtexKeyToWidget.constBegin(); it != bibtexKeyToWidget.constEnd(); ++it) { Value value; it.value()->apply(value); entry->remove(it.key()); if (!value.isEmpty()) entry->insert(it.key(), value); } return true; } bool EntryConfiguredWidget::reset(QSharedPointer element) { QSharedPointer entry = element.dynamicCast(); if (entry.isNull()) return false; /// clear all widgets for (QMap::Iterator it = bibtexKeyToWidget.begin(); it != bibtexKeyToWidget.end(); ++it) { it.value()->setFile(m_file); it.value()->clear(); } for (Entry::ConstIterator it = entry->constBegin(); it != entry->constEnd(); ++it) { const QString key = it.key().toLower(); if (bibtexKeyToWidget.contains(key)) { FieldInput *fieldInput = bibtexKeyToWidget[key]; fieldInput->setElement(element.data()); fieldInput->reset(it.value()); } } return true; } bool EntryConfiguredWidget::validate(QWidget **widgetWithIssue, QString &message) const { for (int i = fieldInputCount - 1; i >= 0; --i) { const bool v = listOfLabeledFieldInput[i]->fieldInput->validate(widgetWithIssue, message); if (!v) return false; } return true; } void EntryConfiguredWidget::showReqOptWidgets(bool forceVisible, const QString &entryType) { layoutGUI(forceVisible, entryType); } QString EntryConfiguredWidget::identifier() const { return etl->identifier; } void EntryConfiguredWidget::setReadOnly(bool _isReadOnly) { ElementWidget::setReadOnly(_isReadOnly); for (QMap::Iterator it = bibtexKeyToWidget.begin(); it != bibtexKeyToWidget.end(); ++it) it.value()->setReadOnly(_isReadOnly); } QString EntryConfiguredWidget::label() { return etl->uiCaption; } QIcon EntryConfiguredWidget::icon() { return QIcon::fromTheme(etl->iconName); } void EntryConfiguredWidget::setFile(const File *file) { for (QMap::Iterator it = bibtexKeyToWidget.begin(); it != bibtexKeyToWidget.end(); ++it) { it.value()->setFile(file); if (file != nullptr) { /// list of unique values for same field QStringList list = file->uniqueEntryValuesList(it.key()); /// for crossref fields, add all entries' ids if (it.key().toLower() == Entry::ftCrossRef) list.append(file->allKeys(File::etEntry)); /// add macro keys list.append(file->allKeys(File::etMacro)); it.value()->setCompletionItems(list); } } ElementWidget::setFile(file); } bool EntryConfiguredWidget::canEdit(const Element *element) { return Entry::isEntry(*element); } void EntryConfiguredWidget::infoMessageLinkActivated(const QString &contents) { if (contents.startsWith(QStringLiteral("#tab:"))) { const QString tabIdentifier = contents.mid(5); emit requestingTabChange(tabIdentifier); } } void EntryConfiguredWidget::createGUI() { int i = 0; for (const SingleFieldLayout &sfl : const_cast &>(etl->singleFieldLayouts)) { LabeledFieldInput *labeledFieldInput = new LabeledFieldInput; /// create an editing widget for this field const FieldDescription &fd = BibTeXFields::instance().find(sfl.bibtexLabel); labeledFieldInput->fieldInput = new FieldInput(sfl.fieldInputLayout, fd.preferredTypeFlag, fd.typeFlags, this); labeledFieldInput->fieldInput->setFieldKey(sfl.bibtexLabel); bibtexKeyToWidget.insert(sfl.bibtexLabel, labeledFieldInput->fieldInput); connect(labeledFieldInput->fieldInput, &FieldInput::modified, this, &EntryConfiguredWidget::gotModified); /// memorize if field input should grow vertically (e.g. is a list) labeledFieldInput->isVerticallyMinimumExpaning = sfl.fieldInputLayout == KBibTeX::MultiLine || sfl.fieldInputLayout == KBibTeX::List || sfl.fieldInputLayout == KBibTeX::PersonList || sfl.fieldInputLayout == KBibTeX::KeywordList; /// create a label next to the editing widget labeledFieldInput->label = new QLabel(QString(QStringLiteral("%1:")).arg(sfl.uiLabel), this); labeledFieldInput->label->setBuddy(labeledFieldInput->fieldInput->buddy()); /// align label's text vertically to match field input const Qt::Alignment horizontalAlignment = static_cast(labeledFieldInput->label->style()->styleHint(QStyle::SH_FormLayoutLabelAlignment)) & Qt::AlignHorizontal_Mask; labeledFieldInput->label->setAlignment(horizontalAlignment | (labeledFieldInput->isVerticallyMinimumExpaning ? Qt::AlignTop : Qt::AlignVCenter)); listOfLabeledFieldInput[i] = labeledFieldInput; ++i; } if (!etl->infoMessages.isEmpty()) for (const QString &infoMessage : etl->infoMessages) { KMessageWidget *infoMessagesWidget = new KMessageWidget(i18n(infoMessage.toUtf8().constData()), this); connect(infoMessagesWidget, &KMessageWidget::linkActivated, this, &EntryConfiguredWidget::infoMessageLinkActivated); vboxLayout->addWidget(infoMessagesWidget, 1); } layoutGUI(true); } void EntryConfiguredWidget::layoutGUI(bool forceVisible, const QString &entryType) { QStringList visibleItems; if (!forceVisible && !entryType.isEmpty()) { const QString entryTypeLc = entryType.toLower(); for (const auto &ed : BibTeXEntries::instance()) { if (entryTypeLc == ed.upperCamelCase.toLower() || entryTypeLc == ed.upperCamelCaseAlt.toLower()) { /// this ugly conversion is necessary because we have a "^" (xor) and "|" (and/or) /// syntax to differentiate required items (not used yet, but will be used /// later if missing required items are marked). QString visible = ed.requiredItems.join(QStringLiteral(",")); visible += QLatin1Char(',') + ed.optionalItems.join(QStringLiteral(",")); visible = visible.replace(QLatin1Char('|'), QLatin1Char(',')).replace(QLatin1Char('^'), QLatin1Char(',')); visibleItems = visible.split(QStringLiteral(",")); break; } } } else if (!forceVisible) { // TODO is this an error condition? } /// variables to keep track which and how many field inputs will be visible int countVisible = 0; QScopedArrayPointer visible(new bool[fieldInputCount]); /// ... and if any field input is vertically expaning /// (e.g. a list, important for layout) bool anyoneVerticallyExpanding = false; for (int i = fieldInputCount - 1; i >= 0; --i) { listOfLabeledFieldInput[i]->label->setVisible(false); listOfLabeledFieldInput[i]->fieldInput->setVisible(false); gridLayout->removeWidget(listOfLabeledFieldInput[i]->label); gridLayout->removeWidget(listOfLabeledFieldInput[i]->fieldInput); const QString key = bibtexKeyToWidget.key(listOfLabeledFieldInput[i]->fieldInput).toLower(); const FieldDescription &fd = BibTeXFields::instance().find(key); Value value; listOfLabeledFieldInput[i]->fieldInput->apply(value); /// Hide non-required and non-optional type-dependent fields, /// except if the field has content visible[i] = forceVisible || fd.typeIndependent || !value.isEmpty() || visibleItems.contains(key); if (visible[i]) { ++countVisible; anyoneVerticallyExpanding |= listOfLabeledFieldInput[i]->isVerticallyMinimumExpaning; } } int numRows = countVisible / numCols; if (countVisible % numCols > 0) ++numRows; gridLayout->setRowStretch(numRows, anyoneVerticallyExpanding ? 0 : 1000); int col = 0, row = 0; for (int i = 0; i < fieldInputCount; ++i) if (visible[i]) { /// add label and field input to new position in grid layout gridLayout->addWidget(listOfLabeledFieldInput[i]->label, row, col * 3); gridLayout->addWidget(listOfLabeledFieldInput[i]->fieldInput, row, col * 3 + 1); /// set row stretch gridLayout->setRowStretch(row, listOfLabeledFieldInput[i]->isVerticallyMinimumExpaning ? 1000 : 0); /// set column stretch and spacing gridLayout->setColumnStretch(col * 3, 1); gridLayout->setColumnStretch(col * 3 + 1, 1000); if (col > 0) gridLayout->setColumnMinimumWidth(col * 3 - 1, interColumnSpace); /// count rows and columns correctly ++row; if (row >= numRows) { row = 0; ++col; } /// finally, set label and field input visible again listOfLabeledFieldInput[i]->label->setVisible(true); listOfLabeledFieldInput[i]->fieldInput->setVisible(true); // FIXME expensive! } if (countVisible > 0) { /// fix row stretch for (int i = numRows + 1; i < 100; ++i) gridLayout->setRowStretch(i, 0); /// hide unused columns for (int i = (col + (row == 0 ? 0 : 1)) * 3 - 1; i < 100; ++i) { gridLayout->setColumnMinimumWidth(i, 0); gridLayout->setColumnStretch(i, 0); } } } ReferenceWidget::ReferenceWidget(QWidget *parent) : ElementWidget(parent), m_applyElement(nullptr), m_entryIdManuallySet(false), m_element(QSharedPointer()) { createGUI(); } bool ReferenceWidget::apply(QSharedPointer element) const { if (isReadOnly) return false; /// never save data if in read-only mode bool result = false; QSharedPointer entry = element.dynamicCast(); if (!entry.isNull()) { entry->setType(computeType()); entry->setId(entryId->text()); result = true; } else { QSharedPointer macro = element.dynamicCast(); if (!macro.isNull()) { macro->setKey(entryId->text()); result = true; } } return result; } bool ReferenceWidget::reset(QSharedPointer element) { /// if signals are not deactivated, the "modified" signal would be emitted when /// resetting the widgets' values - disconnect(entryType->lineEdit(), &QLineEdit::textChanged, this, &ReferenceWidget::gotModified); - disconnect(entryId, &QLineEdit::textEdited, this, &ReferenceWidget::entryIdManuallyChanged); + const QSignalBlocker blockerEntryTypeLineEdit(entryType->lineEdit()); + const QSignalBlocker blockerEntryIdLineEdit(entryId); bool result = false; QSharedPointer entry = element.dynamicCast(); if (!entry.isNull()) { entryType->setEnabled(!isReadOnly); buttonSuggestId->setEnabled(!isReadOnly); QString type = BibTeXEntries::instance().format(entry->type(), KBibTeX::cUpperCamelCase); int index = entryType->findData(type); if (index == -1) { const QString typeLower(type.toLower()); for (const auto &ed : BibTeXEntries::instance()) if (typeLower == ed.upperCamelCaseAlt.toLower()) { index = entryType->findData(ed.upperCamelCase); break; } } entryType->setCurrentIndex(index); if (index == -1) { /// A customized value not known to KBibTeX entryType->lineEdit()->setText(type); } entryId->setText(entry->id()); /// New entries have no values. Use this fact /// to recognize new entries, for which it is /// allowed to automatic set their ids /// if a default id suggestion had been specified. m_entryIdManuallySet = entry->count() > 0; result = true; } else { entryType->setEnabled(false); buttonSuggestId->setEnabled(false); QSharedPointer macro = element.dynamicCast(); if (!macro.isNull()) { entryType->lineEdit()->setText(i18n("Macro")); entryId->setText(macro->key()); result = true; } } - connect(entryId, &QLineEdit::textEdited, this, &ReferenceWidget::entryIdManuallyChanged); - connect(entryType->lineEdit(), &QLineEdit::textChanged, this, &ReferenceWidget::gotModified); - return result; } bool ReferenceWidget::validate(QWidget **widgetWithIssue, QString &message) const { message.clear(); static const QRegularExpression validTypeRegExp(QStringLiteral("^[a-z]+$"), QRegularExpression::CaseInsensitiveOption); const QString type = computeType(); const QRegularExpressionMatch validTypeMatch = validTypeRegExp.match(type); if (!validTypeMatch.hasMatch() || validTypeMatch.capturedLength() != type.length()) { if (widgetWithIssue != nullptr) *widgetWithIssue = entryType; message = i18n("Element type '%1' is invalid.", type); return false; } static const QRegularExpression validIdRegExp(QStringLiteral("^[a-z0-9][a-z0-9_:.+/$\\\"&-]*$"), QRegularExpression::CaseInsensitiveOption); const QString id = entryId->text(); const QRegularExpressionMatch validIdMatch = validIdRegExp.match(id); if (!validIdMatch.hasMatch() || validIdMatch.capturedLength() != id.length()) { if (widgetWithIssue != nullptr) *widgetWithIssue = entryId; message = i18n("Id '%1' is invalid", id); return false; } return true; } void ReferenceWidget::setReadOnly(bool _isReadOnly) { ElementWidget::setReadOnly(_isReadOnly); entryId->setReadOnly(_isReadOnly); entryType->setEnabled(!_isReadOnly); } QString ReferenceWidget::label() { return QString(); } QIcon ReferenceWidget::icon() { return QIcon(); } bool ReferenceWidget::canEdit(const Element *element) { return Entry::isEntry(*element) || Macro::isMacro(*element); } void ReferenceWidget::setOriginalElement(const QSharedPointer &orig) { m_element = orig; } QString ReferenceWidget::currentId() const { return entryId->text(); } void ReferenceWidget::setCurrentId(const QString &newId) { entryId->setText(newId); } void ReferenceWidget::createGUI() { QHBoxLayout *layout = new QHBoxLayout(this); entryType = new QComboBox(this); entryType->setEditable(true); entryType->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); QLabel *label = new QLabel(i18n("Type:"), this); label->setBuddy(entryType); layout->addWidget(label); layout->addWidget(entryType); layout->addSpacing(interColumnSpace); entryId = new QLineEdit(this); entryId->setClearButtonEnabled(true); label = new QLabel(i18n("Id:"), this); label->setBuddy(entryId); layout->addWidget(label); layout->addWidget(entryId); for (const auto &ed : BibTeXEntries::instance()) entryType->addItem(ed.label, ed.upperCamelCase); /// Sort the combo box locale-aware. Thus we need a SortFilterProxyModel QSortFilterProxyModel *proxy = new QSortFilterProxyModel(entryType); proxy->setSortLocaleAware(true); proxy->setSourceModel(entryType->model()); entryType->model()->setParent(proxy); entryType->setModel(proxy); entryType->model()->sort(0); /// Button with a menu listing a set of preconfigured id suggestions buttonSuggestId = new QPushButton(QIcon::fromTheme(QStringLiteral("view-filter")), QString(), this); buttonSuggestId->setToolTip(i18n("Select a suggested id for this entry")); layout->addWidget(buttonSuggestId); QMenu *suggestionsMenu = new QMenu(buttonSuggestId); buttonSuggestId->setMenu(suggestionsMenu); connect(entryType->lineEdit(), &QLineEdit::textChanged, this, &ReferenceWidget::gotModified); connect(entryId, &QLineEdit::textEdited, this, &ReferenceWidget::entryIdManuallyChanged); connect(entryType->lineEdit(), &QLineEdit::textChanged, this, &ReferenceWidget::entryTypeChanged); connect(suggestionsMenu, &QMenu::aboutToShow, this, &ReferenceWidget::prepareSuggestionsMenu); } void ReferenceWidget::prepareSuggestionsMenu() { /// Collect information on the current entry as it is edited QSharedPointer guiDataEntry(new Entry()); m_applyElement->apply(guiDataEntry); QSharedPointer crossrefResolvedEntry(guiDataEntry->resolveCrossref(m_file)); static const IdSuggestions *idSuggestions = new IdSuggestions(); QMenu *suggestionsMenu = buttonSuggestId->menu(); suggestionsMenu->clear(); /// Keep track of shown suggestions to avoid duplicates QSet knownIdSuggestion; const QString defaultSuggestion = idSuggestions->defaultFormatId(*crossrefResolvedEntry.data()); const auto formatIdList = idSuggestions->formatIdList(*crossrefResolvedEntry.data()); for (const QString &suggestionBase : formatIdList) { bool isDefault = suggestionBase == defaultSuggestion; QString suggestion = suggestionBase; /// Test for duplicate ids, use fallback ids with numeric suffix if (m_file != nullptr && m_file->containsKey(suggestion)) { int suffix = 2; while (m_file->containsKey(suggestion = suggestionBase + QChar('_') + QString::number(suffix))) ++suffix; } /// Keep track of shown suggestions to avoid duplicates if (knownIdSuggestion.contains(suggestion)) continue; else knownIdSuggestion.insert(suggestion); /// Create action for suggestion, use icon depending if default or not QAction *suggestionAction = new QAction(suggestion, suggestionsMenu); suggestionAction->setIcon(QIcon::fromTheme(isDefault ? QStringLiteral("favorites") : QStringLiteral("view-filter"))); /// Mesh action into GUI suggestionsMenu->addAction(suggestionAction); connect(suggestionAction, &QAction::triggered, this, &ReferenceWidget::insertSuggestionFromAction); /// Remember suggestion string for time when action gets triggered suggestionAction->setProperty(PropertyIdSuggestion, suggestion); } } void ReferenceWidget::insertSuggestionFromAction() { QAction *action = qobject_cast(sender()); if (action != nullptr) { const QString suggestion = action->property(PropertyIdSuggestion).toString(); entryId->setText(suggestion); } } void ReferenceWidget::entryIdManuallyChanged() { m_entryIdManuallySet = true; gotModified(); } void ReferenceWidget::setEntryIdByDefault() { if (isReadOnly) { /// Never set the suggestion automatically if in read-only mode return; } if (m_entryIdManuallySet) { /// If user changed entry id manually, /// do not overwrite it by a default value return; } static const IdSuggestions *idSuggestions = new IdSuggestions(); /// If there is a default suggestion format set ... if (idSuggestions->hasDefaultFormat()) { /// Collect information on the current entry as it is edited QSharedPointer guiDataEntry(new Entry()); m_applyElement->apply(guiDataEntry); QSharedPointer crossrefResolvedEntry(guiDataEntry->resolveCrossref(m_file)); /// Determine default suggestion based on current data const QString defaultSuggestion = idSuggestions->defaultFormatId(*crossrefResolvedEntry.data()); if (!defaultSuggestion.isEmpty()) { - disconnect(entryId, &QLineEdit::textEdited, this, &ReferenceWidget::entryIdManuallyChanged); + const QSignalBlocker blocker(entryId); /// Apply default suggestion to widget entryId->setText(defaultSuggestion); - connect(entryId, &QLineEdit::textEdited, this, &ReferenceWidget::entryIdManuallyChanged); } } } QString ReferenceWidget::computeType() const { if (entryType->currentIndex() < 0 || entryType->lineEdit()->isModified()) return BibTeXEntries::instance().format(entryType->lineEdit()->text(), KBibTeX::cUpperCamelCase); else return entryType->itemData(entryType->currentIndex()).toString(); } FilesWidget::FilesWidget(QWidget *parent) : ElementWidget(parent) { QVBoxLayout *layout = new QVBoxLayout(this); fileList = new FieldInput(KBibTeX::UrlList, KBibTeX::tfVerbatim /* eventually ignored, see constructor of UrlListEdit */, KBibTeX::tfVerbatim /* eventually ignored, see constructor of UrlListEdit */, this); fileList->setFieldKey(QStringLiteral("^external")); layout->addWidget(fileList); connect(fileList, &FieldInput::modified, this, &FilesWidget::gotModified); } bool FilesWidget::apply(QSharedPointer element) const { if (isReadOnly) return false; /// never save data if in read-only mode QSharedPointer entry = element.dynamicCast(); if (entry.isNull()) return false; for (const QString &keyStem : keyStart) for (int i = 1; i < 32; ++i) { /// FIXME replace number by constant const QString key = i > 1 ? keyStem + QString::number(i) : keyStem; entry->remove(key); } Value combinedValue; fileList->apply(combinedValue); Value urlValue, doiValue, localFileValue; urlValue.reserve(combinedValue.size()); doiValue.reserve(combinedValue.size()); localFileValue.reserve(combinedValue.size()); for (const auto &valueItem : const_cast(combinedValue)) { const QSharedPointer verbatimText = valueItem.dynamicCast(); if (!verbatimText.isNull()) { const QString text = verbatimText->text(); QRegularExpressionMatch match; if ((match = KBibTeX::urlRegExp.match(text)).hasMatch()) { /// add full URL VerbatimText *newVT = new VerbatimText(match.captured(0)); /// test for duplicates if (urlValue.contains(*newVT)) delete newVT; else urlValue.append(QSharedPointer(newVT)); } else if ((match = KBibTeX::doiRegExp.match(text)).hasMatch()) { /// add DOI VerbatimText *newVT = new VerbatimText(match.captured(0)); /// test for duplicates if (doiValue.contains(*newVT)) delete newVT; else doiValue.append(QSharedPointer(newVT)); } else { /// add anything else (e.g. local file) VerbatimText *newVT = new VerbatimText(*verbatimText); /// test for duplicates if (localFileValue.contains(*newVT)) delete newVT; else localFileValue.append(QSharedPointer(newVT)); } } } if (urlValue.isEmpty()) entry->remove(Entry::ftUrl); else entry->insert(Entry::ftUrl, urlValue); if (localFileValue.isEmpty()) { entry->remove(Entry::ftFile); entry->remove(Entry::ftLocalFile); } else if (Preferences::instance().bibliographySystem() == Preferences::BibLaTeX) { entry->remove(Entry::ftLocalFile); entry->insert(Entry::ftFile, localFileValue); } else if (Preferences::instance().bibliographySystem() == Preferences::BibTeX) { entry->remove(Entry::ftFile); entry->insert(Entry::ftLocalFile, localFileValue); } if (doiValue.isEmpty()) entry->remove(Entry::ftDOI); else entry->insert(Entry::ftDOI, doiValue); return true; } bool FilesWidget::reset(QSharedPointer element) { QSharedPointer entry = element.dynamicCast(); if (entry.isNull()) return false; Value combinedValue; for (const QString &keyStem : keyStart) for (int i = 1; i < 32; ++i) { /// FIXME replace number by constant const QString key = i > 1 ? keyStem + QString::number(i) : keyStem; const Value &value = entry->operator [](key); for (const auto &valueItem : const_cast(value)) combinedValue.append(valueItem); } fileList->setElement(element.data()); fileList->reset(combinedValue); return true; } bool FilesWidget::validate(QWidget **widgetWithIssue, QString &message) const { return fileList->validate(widgetWithIssue, message); } void FilesWidget::setReadOnly(bool _isReadOnly) { ElementWidget::setReadOnly(_isReadOnly); fileList->setReadOnly(_isReadOnly); } QString FilesWidget::label() { return i18n("External"); } QIcon FilesWidget::icon() { return QIcon::fromTheme(QStringLiteral("emblem-symbolic-link")); } void FilesWidget::setFile(const File *file) { ElementWidget::setFile(file); fileList->setFile(file); } bool FilesWidget::canEdit(const Element *element) { return Entry::isEntry(*element); } const QStringList FilesWidget::keyStart {Entry::ftUrl, QStringLiteral("postscript"), Entry::ftLocalFile, Entry::ftDOI, Entry::ftFile, QStringLiteral("ee"), QStringLiteral("biburl")}; OtherFieldsWidget::OtherFieldsWidget(const QStringList &blacklistedFields, QWidget *parent) : ElementWidget(parent), blackListed(blacklistedFields) { internalEntry = QSharedPointer(new Entry()); createGUI(); } OtherFieldsWidget::~OtherFieldsWidget() { delete fieldContent; } bool OtherFieldsWidget::apply(QSharedPointer element) const { if (isReadOnly) return false; /// never save data if in read-only mode QSharedPointer entry = element.dynamicCast(); if (entry.isNull()) return false; for (const QString &key : const_cast(deletedKeys)) entry->remove(key); for (const QString &key : const_cast(modifiedKeys)) { entry->remove(key); entry->insert(key, internalEntry->value(key)); } return true; } bool OtherFieldsWidget::reset(QSharedPointer element) { QSharedPointer entry = element.dynamicCast(); if (entry.isNull()) return false; internalEntry = QSharedPointer(new Entry(*entry.data())); deletedKeys.clear(); // FIXME clearing list may be premature here... modifiedKeys.clear(); // FIXME clearing list may be premature here... updateList(); updateGUI(); return true; } bool OtherFieldsWidget::validate(QWidget **, QString &) const { /// No checks to make here; all actual check will be conducted in actionAddApply(..) return true; } void OtherFieldsWidget::setReadOnly(bool _isReadOnly) { ElementWidget::setReadOnly(_isReadOnly); fieldName->setReadOnly(_isReadOnly); fieldContent->setReadOnly(_isReadOnly); /// will take care of enabled/disabling buttons updateGUI(); updateList(); } QString OtherFieldsWidget::label() { return i18n("Other Fields"); } QIcon OtherFieldsWidget::icon() { return QIcon::fromTheme(QStringLiteral("other")); } bool OtherFieldsWidget::canEdit(const Element *element) { return Entry::isEntry(*element); } void OtherFieldsWidget::listElementExecuted(QTreeWidgetItem *item, int column) { Q_UNUSED(column) /// we do not care which column got clicked QString key = item->text(0); fieldName->setText(key); fieldContent->reset(internalEntry->value(key)); } void OtherFieldsWidget::listCurrentChanged(QTreeWidgetItem *item, QTreeWidgetItem *previous) { Q_UNUSED(previous) bool validUrl = false; bool somethingSelected = item != nullptr; buttonDelete->setEnabled(somethingSelected && !isReadOnly); if (somethingSelected) { currentUrl = QUrl(item->text(1)); validUrl = currentUrl.isValid() && currentUrl.isLocalFile() & QFileInfo::exists(currentUrl.toLocalFile()); if (!validUrl) { const QRegularExpressionMatch urlRegExpMatch = KBibTeX::urlRegExp.match(item->text(1)); if (urlRegExpMatch.hasMatch()) { currentUrl = QUrl(urlRegExpMatch.captured(0)); validUrl = currentUrl.isValid(); buttonOpen->setEnabled(validUrl); } } } if (!validUrl) currentUrl = QUrl(); buttonOpen->setEnabled(validUrl); } void OtherFieldsWidget::actionAddApply() { if (isReadOnly) return; /// never modify anything if in read-only mode QString key = fieldName->text(), message; Value value; if (!fieldContent->validate(nullptr, message)) return; ///< invalid values should not get applied if (!fieldContent->apply(value)) return; if (internalEntry->contains(key)) internalEntry->remove(key); internalEntry->insert(key, value); if (!modifiedKeys.contains(key)) modifiedKeys << key; updateList(); updateGUI(); gotModified(); } void OtherFieldsWidget::actionDelete() { if (isReadOnly) return; /// never modify anything if in read-only mode Q_ASSERT_X(otherFieldsList->currentItem() != nullptr, "OtherFieldsWidget::actionDelete", "otherFieldsList->currentItem() is NULL"); QString key = otherFieldsList->currentItem()->text(0); if (!deletedKeys.contains(key)) deletedKeys << key; internalEntry->remove(key); updateList(); updateGUI(); listCurrentChanged(otherFieldsList->currentItem(), nullptr); gotModified(); } void OtherFieldsWidget::actionOpen() { if (currentUrl.isValid()) { /// Guess mime type for url to open QMimeType mimeType = FileInfo::mimeTypeForUrl(currentUrl); const QString mimeTypeName = mimeType.name(); /// Ask KDE subsystem to open url in viewer matching mime type KRun::runUrl(currentUrl, mimeTypeName, this, KRun::RunFlags()); } } void OtherFieldsWidget::createGUI() { QGridLayout *layout = new QGridLayout(this); /// set row and column stretches based on chosen layout layout->setColumnStretch(0, 0); layout->setColumnStretch(1, 1); layout->setColumnStretch(2, 0); layout->setRowStretch(0, 0); layout->setRowStretch(1, 1); layout->setRowStretch(2, 0); layout->setRowStretch(3, 0); layout->setRowStretch(4, 1); QLabel *label = new QLabel(i18n("Name:"), this); layout->addWidget(label, 0, 0, 1, 1); label->setAlignment(static_cast(label->style()->styleHint(QStyle::SH_FormLayoutLabelAlignment))); fieldName = new QLineEdit(this); layout->addWidget(fieldName, 0, 1, 1, 1); label->setBuddy(fieldName); buttonAddApply = new QPushButton(QIcon::fromTheme(QStringLiteral("list-add")), i18n("Add"), this); buttonAddApply->setEnabled(false); layout->addWidget(buttonAddApply, 0, 2, 1, 1); label = new QLabel(i18n("Content:"), this); layout->addWidget(label, 1, 0, 1, 1); label->setAlignment(static_cast(label->style()->styleHint(QStyle::SH_FormLayoutLabelAlignment))); fieldContent = new FieldInput(KBibTeX::MultiLine, KBibTeX::tfSource, KBibTeX::tfSource, this); layout->addWidget(fieldContent, 1, 1, 1, 2); label->setBuddy(fieldContent->buddy()); label = new QLabel(i18n("List:"), this); layout->addWidget(label, 2, 0, 1, 1); label->setAlignment(static_cast(label->style()->styleHint(QStyle::SH_FormLayoutLabelAlignment))); otherFieldsList = new QTreeWidget(this); otherFieldsList->setHeaderLabels(QStringList {i18n("Key"), i18n("Value")}); otherFieldsList->setRootIsDecorated(false); layout->addWidget(otherFieldsList, 2, 1, 3, 1); label->setBuddy(otherFieldsList); buttonDelete = new QPushButton(QIcon::fromTheme(QStringLiteral("list-remove")), i18n("Delete"), this); buttonDelete->setEnabled(false); layout->addWidget(buttonDelete, 2, 2, 1, 1); buttonOpen = new QPushButton(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Open"), this); buttonOpen->setEnabled(false); layout->addWidget(buttonOpen, 3, 2, 1, 1); connect(otherFieldsList, &QTreeWidget::itemActivated, this, &OtherFieldsWidget::listElementExecuted); connect(otherFieldsList, &QTreeWidget::currentItemChanged, this, &OtherFieldsWidget::listCurrentChanged); connect(otherFieldsList, &QTreeWidget::itemSelectionChanged, this, &OtherFieldsWidget::updateGUI); connect(fieldName, &QLineEdit::textEdited, this, &OtherFieldsWidget::updateGUI); connect(buttonAddApply, &QPushButton::clicked, this, &OtherFieldsWidget::actionAddApply); connect(buttonDelete, &QPushButton::clicked, this, &OtherFieldsWidget::actionDelete); connect(buttonOpen, &QPushButton::clicked, this, &OtherFieldsWidget::actionOpen); } void OtherFieldsWidget::updateList() { const QString selText = otherFieldsList->selectedItems().isEmpty() ? QString() : otherFieldsList->selectedItems().first()->text(0); const QString curText = otherFieldsList->currentItem() == nullptr ? QString() : otherFieldsList->currentItem()->text(0); otherFieldsList->clear(); for (Entry::ConstIterator it = internalEntry->constBegin(); it != internalEntry->constEnd(); ++it) if (!blackListed.contains(it.key().toLower())) { QTreeWidgetItem *item = new QTreeWidgetItem(); item->setText(0, it.key()); item->setText(1, PlainTextValue::text(it.value())); item->setIcon(0, QIcon::fromTheme(QStringLiteral("entry"))); // FIXME otherFieldsList->addTopLevelItem(item); item->setSelected(selText == it.key()); if (it.key() == curText) otherFieldsList->setCurrentItem(item); } } void OtherFieldsWidget::updateGUI() { QString key = fieldName->text(); if (key.isEmpty() || blackListed.contains(key, Qt::CaseInsensitive)) // TODO check for more (e.g. spaces) buttonAddApply->setEnabled(false); else { buttonAddApply->setEnabled(!isReadOnly); buttonAddApply->setText(internalEntry->contains(key) ? i18n("Apply") : i18n("Add")); buttonAddApply->setIcon(internalEntry->contains(key) ? QIcon::fromTheme(QStringLiteral("document-edit")) : QIcon::fromTheme(QStringLiteral("list-add"))); } } MacroWidget::MacroWidget(QWidget *parent) : ElementWidget(parent) { createGUI(); } MacroWidget::~MacroWidget() { delete fieldInputValue; } bool MacroWidget::apply(QSharedPointer element) const { if (isReadOnly) return false; /// never save data if in read-only mode QSharedPointer macro = element.dynamicCast(); if (macro.isNull()) return false; Value value; bool result = fieldInputValue->apply(value); macro->setValue(value); return result; } bool MacroWidget::reset(QSharedPointer element) { QSharedPointer macro = element.dynamicCast(); if (macro.isNull()) return false; return fieldInputValue->reset(macro->value()); } bool MacroWidget::validate(QWidget **widgetWithIssue, QString &message) const { return fieldInputValue->validate(widgetWithIssue, message); } void MacroWidget::setReadOnly(bool _isReadOnly) { ElementWidget::setReadOnly(_isReadOnly); fieldInputValue->setReadOnly(_isReadOnly); } QString MacroWidget::label() { return i18n("Macro"); } QIcon MacroWidget::icon() { return QIcon::fromTheme(QStringLiteral("macro")); } bool MacroWidget::canEdit(const Element *element) { return Macro::isMacro(*element); } void MacroWidget::createGUI() { QBoxLayout *layout = new QHBoxLayout(this); QLabel *label = new QLabel(i18n("Value:"), this); layout->addWidget(label, 0); label->setAlignment(static_cast(label->style()->styleHint(QStyle::SH_FormLayoutLabelAlignment))); fieldInputValue = new FieldInput(KBibTeX::MultiLine, KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfSource, this); layout->addWidget(fieldInputValue, 1); label->setBuddy(fieldInputValue->buddy()); connect(fieldInputValue, &FieldInput::modified, this, &MacroWidget::gotModified); } PreambleWidget::PreambleWidget(QWidget *parent) : ElementWidget(parent) { createGUI(); } bool PreambleWidget::apply(QSharedPointer element) const { if (isReadOnly) return false; /// never save data if in read-only mode QSharedPointer preamble = element.dynamicCast(); if (preamble.isNull()) return false; Value value; bool result = fieldInputValue->apply(value); preamble->setValue(value); return result; } bool PreambleWidget::reset(QSharedPointer element) { QSharedPointer preamble = element.dynamicCast(); if (preamble.isNull()) return false; return fieldInputValue->reset(preamble->value()); } bool PreambleWidget::validate(QWidget **widgetWithIssue, QString &message) const { return fieldInputValue->validate(widgetWithIssue, message); } void PreambleWidget::setReadOnly(bool _isReadOnly) { ElementWidget::setReadOnly(_isReadOnly); fieldInputValue->setReadOnly(_isReadOnly); } QString PreambleWidget::label() { return i18n("Preamble"); } QIcon PreambleWidget::icon() { return QIcon::fromTheme(QStringLiteral("preamble")); } bool PreambleWidget::canEdit(const Element *element) { return Preamble::isPreamble(*element); } void PreambleWidget::createGUI() { QBoxLayout *layout = new QHBoxLayout(this); QLabel *label = new QLabel(i18n("Value:"), this); layout->addWidget(label, 0); label->setAlignment(static_cast(label->style()->styleHint(QStyle::SH_FormLayoutLabelAlignment))); fieldInputValue = new FieldInput(KBibTeX::MultiLine, KBibTeX::tfSource, KBibTeX::tfSource, this); // FIXME: other editing modes beyond Source applicable? layout->addWidget(fieldInputValue, 1); label->setBuddy(fieldInputValue->buddy()); connect(fieldInputValue, &FieldInput::modified, this, &PreambleWidget::gotModified); } class SourceWidget::Private { public: QComboBox *messages; QPushButton *buttonRestore; FileImporterBibTeX *importerBibTeX; DelayedExecutionTimer *delayedExecutionTimer; Private(SourceWidget *parent) : messages(nullptr), buttonRestore(nullptr), importerBibTeX(new FileImporterBibTeX(parent)), delayedExecutionTimer(new DelayedExecutionTimer(1500, 500, parent)) { /// nothing } void addMessage(const FileImporter::MessageSeverity severity, const QString &messageText) { const QIcon icon = severity == FileImporter::SeverityInfo ? QIcon::fromTheme(QStringLiteral("dialog-information")) : (severity == FileImporter::SeverityWarning ? QIcon::fromTheme(QStringLiteral("dialog-warning")) : (severity == FileImporter::SeverityError ? QIcon::fromTheme(QStringLiteral("dialog-error")) : QIcon::fromTheme(QStringLiteral("dialog-question")))); messages->addItem(icon, messageText); } }; SourceWidget::SourceWidget(QWidget *parent) : ElementWidget(parent), elementClass(elementInvalid), d(new SourceWidget::Private(this)) { createGUI(); connect(document, &KTextEditor::Document::textChanged, d->delayedExecutionTimer, &DelayedExecutionTimer::trigger); connect(document, &KTextEditor::Document::textChanged, d->messages, &QComboBox::clear); connect(d->delayedExecutionTimer, &DelayedExecutionTimer::triggered, this, &SourceWidget::updateMessage); } SourceWidget::~SourceWidget() { delete document; delete d; } void SourceWidget::setElementClass(ElementClass elementClass) { this->elementClass = elementClass; updateMessage(); } bool SourceWidget::apply(QSharedPointer element) const { if (isReadOnly) return false; ///< never save data if in read-only mode const QString text = document->text(); const QScopedPointer file(d->importerBibTeX->fromString(text)); if (file.isNull() || file->count() != 1) return false; QSharedPointer entry = element.dynamicCast(); QSharedPointer readEntry = file->first().dynamicCast(); if (!readEntry.isNull() && !entry.isNull()) { if (elementClass != elementEntry) return false; ///< Source widget should only edit Entry objects entry->operator =(*readEntry.data()); //entry = readEntry; return true; } else { QSharedPointer macro = element.dynamicCast(); QSharedPointer readMacro = file->first().dynamicCast(); if (!readMacro.isNull() && !macro.isNull()) { if (elementClass != elementMacro) return false; ///< Source widget should only edit Macro objects macro->operator =(*readMacro.data()); return true; } else { QSharedPointer preamble = element.dynamicCast(); QSharedPointer readPreamble = file->first().dynamicCast(); if (!readPreamble.isNull() && !preamble.isNull()) { if (elementClass != elementPreamble) return false; ///< Source widget should only edit Preamble objects preamble->operator =(*readPreamble.data()); return true; } else { qCWarning(LOG_KBIBTEX_GUI) << "Do not know how to apply source code"; return false; } } } } bool SourceWidget::reset(QSharedPointer element) { /// if signals are not deactivated, the "modified" signal would be emitted when /// resetting the widget's value - disconnect(document, &KTextEditor::Document::textChanged, this, &SourceWidget::gotModified); + const QSignalBlocker blocker(document); FileExporterBibTeX exporter(this); exporter.setEncoding(QStringLiteral("utf-8")); const QString exportedText = exporter.toString(element, m_file); if (!exportedText.isEmpty()) { originalText = exportedText; /// Limitation of KTextEditor: If editor is read-only, no text can be set /// Therefore, temporarily lift read-only status const bool originalReadWriteStatus = document->isReadWrite(); document->setReadWrite(true); const bool settingTextSuccessful = document->setText(originalText); if (!settingTextSuccessful) qCWarning(LOG_KBIBTEX_GUI) << "Could not set BibTeX source code to source editor"; document->setReadWrite(originalReadWriteStatus); } else qCWarning(LOG_KBIBTEX_GUI) << "Converting entry to BibTeX source resulting in empty text"; - connect(document, &KTextEditor::Document::textChanged, this, &SourceWidget::gotModified); - return !exportedText.isEmpty(); } bool SourceWidget::validate(QWidget **widgetWithIssue, QString &message) const { message.clear(); d->messages->clear(); const QString text = document->text(); connect(d->importerBibTeX, &FileImporterBibTeX::message, this, &SourceWidget::addMessage); const QScopedPointer file(d->importerBibTeX->fromString(text)); disconnect(d->importerBibTeX, &FileImporterBibTeX::message, this, &SourceWidget::addMessage); if (file.isNull() || file->count() != 1) { if (widgetWithIssue != nullptr) *widgetWithIssue = document->views().at(0); ///< We create one view initially, so this should never fail message = i18n("Given source code does not parse as one single BibTeX element."); return false; } bool result = false; switch (elementClass) { case elementEntry: { QSharedPointer entry = file->first().dynamicCast(); result = !entry.isNull(); if (!result) message = i18n("Given source code does not parse as one single BibTeX entry."); } break; case elementMacro: { QSharedPointer macro = file->first().dynamicCast(); result = !macro.isNull(); if (!result) message = i18n("Given source code does not parse as one single BibTeX macro."); } break; case elementPreamble: { QSharedPointer preamble = file->first().dynamicCast(); result = !preamble.isNull(); if (!result) message = i18n("Given source code does not parse as one single BibTeX preamble."); } break; // case elementComment // TODO? default: message = QString(QStringLiteral("elementClass is unknown: %1")).arg(elementClass); result = false; } if (!result && widgetWithIssue != nullptr) *widgetWithIssue = document->views().at(0); ///< We create one view initially, so this should never fail if (message.isEmpty() && d->messages->count() == 0) d->addMessage(FileImporter::SeverityInfo, i18n("No issues detected")); return result; } void SourceWidget::setReadOnly(bool _isReadOnly) { ElementWidget::setReadOnly(_isReadOnly); d->buttonRestore->setEnabled(!_isReadOnly); document->setReadWrite(!_isReadOnly); } QString SourceWidget::label() { return i18n("Source"); } QIcon SourceWidget::icon() { return QIcon::fromTheme(QStringLiteral("code-context")); } bool SourceWidget::canEdit(const Element *element) { Q_UNUSED(element) return true; /// source widget should be able to edit any element } void SourceWidget::createGUI() { QGridLayout *layout = new QGridLayout(this); layout->setColumnStretch(0, 1); layout->setColumnStretch(1, 0); layout->setRowStretch(0, 1); layout->setRowStretch(1, 0); KTextEditor::Editor *editor = KTextEditor::Editor::instance(); document = editor->createDocument(this); document->setHighlightingMode(QStringLiteral("BibTeX")); KTextEditor::View *view = document->createView(this); layout->addWidget(view, 0, 0, 1, 2); d->messages = new QComboBox(this); layout->addWidget(d->messages, 1, 0, 1, 1); d->buttonRestore = new QPushButton(QIcon::fromTheme(QStringLiteral("edit-undo")), i18n("Restore"), this); layout->addWidget(d->buttonRestore, 1, 1, 1, 1); connect(d->buttonRestore, &QPushButton::clicked, this, static_cast(&SourceWidget::reset)); connect(document, &KTextEditor::Document::textChanged, this, &SourceWidget::gotModified); } void SourceWidget::reset() { /// if signals are not deactivated, the "modified" signal would be emitted when /// resetting the widget's value - disconnect(document, &KTextEditor::Document::textChanged, this, &SourceWidget::gotModified); + const QSignalBlocker blocker(document); document->setText(originalText); setModified(false); - - connect(document, &KTextEditor::Document::textChanged, this, &SourceWidget::gotModified); } void SourceWidget::addMessage(const FileImporter::MessageSeverity severity, const QString &messageText) { d->addMessage(severity, messageText); } void SourceWidget::updateMessage() { QString message; const bool validationResult = validate(nullptr, message); if (!message.isEmpty()) { if (validationResult) addMessage(FileImporter::SeverityInfo, message); else addMessage(FileImporter::SeverityError, message); } } #include "elementwidgets.moc" diff --git a/src/gui/field/colorlabelwidget.cpp b/src/gui/field/colorlabelwidget.cpp index 449bf7a8..63088734 100644 --- a/src/gui/field/colorlabelwidget.cpp +++ b/src/gui/field/colorlabelwidget.cpp @@ -1,263 +1,255 @@ /*************************************************************************** * 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 "colorlabelwidget.h" #include #include #include #include +#include #include #include #include static const QColor NoColor = Qt::black; class ColorLabelComboBoxModel : public QAbstractItemModel { Q_OBJECT public: enum ColorLabelComboBoxModelRole { /// Color of a color-label pair ColorRole = Qt::UserRole + 1721 }; struct ColorLabelPair { QColor color; QString label; }; QColor userColor; ColorLabelComboBoxModel(QObject *p = nullptr) : QAbstractItemModel(p), userColor(NoColor) { /// nothing } QModelIndex index(int row, int column, const QModelIndex &parent) const override { return parent == QModelIndex() ? createIndex(row, column) : QModelIndex(); } QModelIndex parent(const QModelIndex & = QModelIndex()) const override { return QModelIndex(); } int rowCount(const QModelIndex &parent = QModelIndex()) const override { return parent == QModelIndex() ? 2 + Preferences::instance().colorCodes().count() : 0; } int columnCount(const QModelIndex &parent = QModelIndex()) const override { return parent == QModelIndex() ? 1 : 0; } QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override { if (role == ColorRole) { const int cci = index.row() - 1; if (index.row() == 0 || cci < 0 || cci >= Preferences::instance().colorCodes().count()) return NoColor; else if (index.row() == rowCount() - 1) return userColor; else return Preferences::instance().colorCodes().at(cci).first; } else if (role == Qt::FontRole && (index.row() == 0 || index.row() == rowCount() - 1)) { /// Set first item's text ("No color") and last item's text ("User-defined color") in italics QFont font; font.setItalic(true); return font; } else if (role == Qt::DecorationRole && index.row() > 0 && (index.row() < rowCount() - 1 || userColor != NoColor)) { /// For items that have a color to choose, draw a little square in this chosen color QColor color = data(index, ColorRole).value(); return ColorLabelWidget::createSolidIcon(color); } else if (role == Qt::DisplayRole) if (index.row() == 0) return i18n("No color"); else if (index.row() == rowCount() - 1) return i18n("User-defined color"); else return Preferences::instance().colorCodes().at(index.row() - 1).second; else return QVariant(); } QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override { if (section != 0 || orientation != Qt::Horizontal || role != Qt::DisplayRole) return QVariant(); return i18n("Color & Label"); } void setColor(const QColor &newColor) { userColor = newColor; const QModelIndex idx = index(rowCount() - 1, 0, QModelIndex()); emit dataChanged(idx, idx); } void reset() { beginResetModel(); endResetModel(); } }; class ColorLabelWidget::ColorLabelWidgetPrivate : private NotificationListener { private: ColorLabelWidget *parent; public: ColorLabelComboBoxModel *model; ColorLabelWidgetPrivate(ColorLabelWidget *_parent, ColorLabelComboBoxModel *m) : parent(_parent), model(m) { Q_UNUSED(parent) NotificationHub::registerNotificationListener(this, NotificationHub::EventConfigurationChanged); } void notificationEvent(int eventId) override { if (eventId == NotificationHub::EventConfigurationChanged) { /// Avoid triggering signal when current index is set by the program - disconnect(parent, static_cast(&QComboBox::currentIndexChanged), parent, &ColorLabelWidget::slotCurrentIndexChanged); + const QSignalBlocker blocker(parent); const QColor currentColor = parent->currentColor(); model->reset(); model->userColor = NoColor; selectColor(currentColor); - - /// Re-enable triggering signal after setting current index - connect(parent, static_cast(&QComboBox::currentIndexChanged), parent, &ColorLabelWidget::slotCurrentIndexChanged); } } int selectColor(const QString &color) { return selectColor(QColor(color)); } int selectColor(const QColor &color) { int rowIndex = 0; if (color != NoColor) { /// Find row that matches given color for (rowIndex = 0; rowIndex < model->rowCount(); ++rowIndex) if (model->data(model->index(rowIndex, 0, QModelIndex()), ColorLabelComboBoxModel::ColorRole).value() == color) break; if (rowIndex >= model->rowCount()) { /// Color was not in the list of known colors, so set user color to given color model->userColor = color; rowIndex = model->rowCount() - 1; } } return rowIndex; } }; ColorLabelWidget::ColorLabelWidget(QWidget *parent) : QComboBox(parent), d(new ColorLabelWidgetPrivate(this, new ColorLabelComboBoxModel(this))) { setModel(d->model); connect(this, static_cast(&QComboBox::currentIndexChanged), this, &ColorLabelWidget::slotCurrentIndexChanged); } ColorLabelWidget::~ColorLabelWidget() { delete d; } void ColorLabelWidget::clear() { /// Avoid triggering signal when current index is set by the program - disconnect(this, static_cast(&QComboBox::currentIndexChanged), this, &ColorLabelWidget::slotCurrentIndexChanged); + const QSignalBlocker blocker(this); d->model->userColor = NoColor; setCurrentIndex(0); ///< index 0 should be "no color" - - /// Re-enable triggering signal after setting current index - connect(this, static_cast(&QComboBox::currentIndexChanged), this, &ColorLabelWidget::slotCurrentIndexChanged); } QColor ColorLabelWidget::currentColor() const { return d->model->data(d->model->index(currentIndex(), 0, QModelIndex()), ColorLabelComboBoxModel::ColorRole).value(); } bool ColorLabelWidget::reset(const Value &value) { /// Avoid triggering signal when current index is set by the program - disconnect(this, static_cast(&QComboBox::currentIndexChanged), this, &ColorLabelWidget::slotCurrentIndexChanged); + const QSignalBlocker blocker(this); QSharedPointer verbatimText; int rowIndex = 0; if (value.count() == 1 && !(verbatimText = value.first().dynamicCast()).isNull()) { /// Create QColor instance based on given textual representation rowIndex = d->selectColor(verbatimText->text()); } setCurrentIndex(rowIndex); - /// Re-enable triggering signal after setting current index - connect(this, static_cast(&QComboBox::currentIndexChanged), this, &ColorLabelWidget::slotCurrentIndexChanged); - return true; } bool ColorLabelWidget::apply(Value &value) const { const QColor color = currentColor(); value.clear(); if (color != NoColor) value.append(QSharedPointer(new VerbatimText(color.name()))); return true; } bool ColorLabelWidget::validate(QWidget **, QString &) const { return true; } void ColorLabelWidget::setReadOnly(bool isReadOnly) { setEnabled(!isReadOnly); } void ColorLabelWidget::slotCurrentIndexChanged(int index) { if (index == count() - 1) { const QColor initialColor = d->model->userColor; const QColor newColor = QColorDialog::getColor(initialColor, this); if (newColor.isValid()) d->model->setColor(newColor); } emit modified(); } QPixmap ColorLabelWidget::createSolidIcon(const QColor &color) { QFontMetrics fm = QFontMetrics(QFont()); int h = fm.height() - 4; QPixmap pm(h, h); QPainter painter(&pm); painter.setPen(color); painter.setBrush(QBrush(color)); painter.drawRect(0, 0, h, h); return pm; } #include "colorlabelwidget.moc" diff --git a/src/gui/field/fieldinput.cpp b/src/gui/field/fieldinput.cpp index fbcf5692..73d9bb8a 100644 --- a/src/gui/field/fieldinput.cpp +++ b/src/gui/field/fieldinput.cpp @@ -1,391 +1,382 @@ /*************************************************************************** * 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 "fieldinput.h" #include #include #include #include #include #include #include +#include #include #include #include #include "fieldlineedit.h" #include "fieldlistedit.h" #include "colorlabelwidget.h" #include "widgets/starrating.h" class FieldInput::FieldInputPrivate { private: FieldInput *p; public: ColorLabelWidget *colorWidget; StarRatingFieldInput *starRatingWidget; FieldLineEdit *fieldLineEdit; FieldListEdit *fieldListEdit; KBibTeX::FieldInputType fieldInputType; KBibTeX::TypeFlags typeFlags; KBibTeX::TypeFlag preferredTypeFlag; const File *bibtexFile; const Element *element; FieldInputPrivate(FieldInput *parent) : p(parent), colorWidget(nullptr), starRatingWidget(nullptr), fieldLineEdit(nullptr), fieldListEdit(nullptr), fieldInputType(KBibTeX::SingleLine), preferredTypeFlag(KBibTeX::tfSource), bibtexFile(nullptr), element(nullptr) { /// nothing } ~FieldInputPrivate() { if (colorWidget != nullptr) delete colorWidget; else if (starRatingWidget != nullptr) delete starRatingWidget; else if (fieldLineEdit != nullptr) delete fieldLineEdit; else if (fieldListEdit != nullptr) delete fieldListEdit; } void createGUI() { QHBoxLayout *layout = new QHBoxLayout(p); layout->setMargin(0); switch (fieldInputType) { case KBibTeX::MultiLine: fieldLineEdit = new FieldLineEdit(preferredTypeFlag, typeFlags, true, p); + connect(fieldLineEdit, &FieldLineEdit::textChanged, p, &FieldInput::modified); layout->addWidget(fieldLineEdit); break; case KBibTeX::List: fieldListEdit = new FieldListEdit(preferredTypeFlag, typeFlags, p); + connect(fieldListEdit, &FieldListEdit::modified, p, &FieldInput::modified); layout->addWidget(fieldListEdit); break; case KBibTeX::Month: { fieldLineEdit = new FieldLineEdit(preferredTypeFlag, typeFlags, false, p); + connect(fieldLineEdit, &FieldLineEdit::textChanged, p, &FieldInput::modified); layout->addWidget(fieldLineEdit); QPushButton *monthSelector = new QPushButton(QIcon::fromTheme(QStringLiteral("view-calendar-month")), QString()); monthSelector->setToolTip(i18n("Select a predefined month")); fieldLineEdit->prependWidget(monthSelector); QMenu *monthMenu = new QMenu(monthSelector); for (int i = 1; i <= 12; ++i) { QAction *monthAction = monthMenu->addAction(QLocale::system().standaloneMonthName(i)); connect(monthAction, &QAction::triggered, p, [this, i]() { setMonth(i); }); } monthSelector->setMenu(monthMenu); } break; case KBibTeX::Edition: { fieldLineEdit = new FieldLineEdit(preferredTypeFlag, typeFlags, false, p); + connect(fieldLineEdit, &FieldLineEdit::textChanged, p, &FieldInput::modified); layout->addWidget(fieldLineEdit); QPushButton *editionSelector = new QPushButton(QIcon::fromTheme(QStringLiteral("clock")), QString()); editionSelector->setToolTip(i18n("Select a predefined edition")); fieldLineEdit->prependWidget(editionSelector); QMenu *editionMenu = new QMenu(editionSelector); static const QStringList ordinals{i18n("1st"), i18n("2nd"), i18n("3rd"), i18n("4th"), i18n("5th"), i18n("6th"), i18n("7th"), i18n("8th"), i18n("9th"), i18n("10th"), i18n("11th"), i18n("12th"), i18n("13th"), i18n("14th"), i18n("15th"), i18n("16th")}; for (int i = 0; i < ordinals.length(); ++i) { QAction *editionAction = editionMenu->addAction(ordinals[i]); connect(editionAction, &QAction::triggered, p, [this, i]() { setEdition(i + 1); }); } editionSelector->setMenu(editionMenu); } break; case KBibTeX::CrossRef: { fieldLineEdit = new FieldLineEdit(preferredTypeFlag, typeFlags, false, p); + connect(fieldLineEdit, &FieldLineEdit::textChanged, p, &FieldInput::modified); layout->addWidget(fieldLineEdit); QPushButton *referenceSelector = new QPushButton(QIcon::fromTheme(QStringLiteral("flag-green")), QString()); ///< find better icon referenceSelector->setToolTip(i18n("Select an existing entry")); fieldLineEdit->prependWidget(referenceSelector); connect(referenceSelector, &QPushButton::clicked, p, &FieldInput::selectCrossRef); } break; case KBibTeX::Color: { colorWidget = new ColorLabelWidget(p); + connect(colorWidget, &ColorLabelWidget::modified, p, &FieldInput::modified); layout->addWidget(colorWidget, 0); } break; case KBibTeX::StarRating: { starRatingWidget = new StarRatingFieldInput(8 /* = #stars */, p); + connect(starRatingWidget, &StarRatingFieldInput::modified, p, &FieldInput::modified); layout->addWidget(starRatingWidget, 0); } break; case KBibTeX::PersonList: fieldListEdit = new PersonListEdit(preferredTypeFlag, typeFlags, p); + connect(fieldLineEdit, &FieldLineEdit::textChanged, p, &FieldInput::modified); layout->addWidget(fieldListEdit); break; case KBibTeX::UrlList: fieldListEdit = new UrlListEdit(p); + connect(fieldListEdit, &FieldListEdit::modified, p, &FieldInput::modified); layout->addWidget(fieldListEdit); break; case KBibTeX::KeywordList: fieldListEdit = new KeywordListEdit(p); + connect(fieldListEdit, &FieldListEdit::modified, p, &FieldInput::modified); layout->addWidget(fieldListEdit); break; default: fieldLineEdit = new FieldLineEdit(preferredTypeFlag, typeFlags, false, p); + connect(fieldLineEdit, &FieldLineEdit::textChanged, p, &FieldInput::modified); layout->addWidget(fieldLineEdit); } - - enableModifiedSignal(); } void clear() { - disableModifiedSignal(); - if (fieldLineEdit != nullptr) + if (fieldLineEdit != nullptr) { + const QSignalBlocker blocker(fieldLineEdit); fieldLineEdit->setText(QString()); - else if (fieldListEdit != nullptr) + } else if (fieldListEdit != nullptr) { + const QSignalBlocker blocker(fieldListEdit); fieldListEdit->clear(); - else if (colorWidget != nullptr) + } else if (colorWidget != nullptr) { + const QSignalBlocker blocker(colorWidget); colorWidget->clear(); - else if (starRatingWidget != nullptr) + } else if (starRatingWidget != nullptr) { + const QSignalBlocker blocker(starRatingWidget); starRatingWidget->unsetValue(); - enableModifiedSignal(); + } } bool reset(const Value &value) { - /// if signals are not deactivated, the "modified" signal would be emitted when - /// resetting the widget's value - disableModifiedSignal(); - bool result = false; - if (fieldLineEdit != nullptr) + if (fieldLineEdit != nullptr) { + const QSignalBlocker blocker(fieldLineEdit); result = fieldLineEdit->reset(value); - else if (fieldListEdit != nullptr) + } else if (fieldListEdit != nullptr) { + const QSignalBlocker blocker(fieldListEdit); result = fieldListEdit->reset(value); - else if (colorWidget != nullptr) + } else if (colorWidget != nullptr) { + const QSignalBlocker blocker(colorWidget); result = colorWidget->reset(value); - else if (starRatingWidget != nullptr) + } else if (starRatingWidget != nullptr) { + const QSignalBlocker blocker(starRatingWidget); result = starRatingWidget->reset(value); + } - enableModifiedSignal(); return result; } bool apply(Value &value) const { bool result = false; if (fieldLineEdit != nullptr) result = fieldLineEdit->apply(value); else if (fieldListEdit != nullptr) result = fieldListEdit->apply(value); else if (colorWidget != nullptr) result = colorWidget->apply(value); else if (starRatingWidget != nullptr) result = starRatingWidget->apply(value); return result; } bool validate(QWidget **widgetWithIssue, QString &message) const { if (fieldLineEdit != nullptr) return fieldLineEdit->validate(widgetWithIssue, message); else if (fieldListEdit != nullptr) return fieldListEdit->validate(widgetWithIssue, message); else if (colorWidget != nullptr) return colorWidget->validate(widgetWithIssue, message); else if (starRatingWidget != nullptr) return starRatingWidget->validate(widgetWithIssue, message); return false; } void setReadOnly(bool isReadOnly) { if (fieldLineEdit != nullptr) fieldLineEdit->setReadOnly(isReadOnly); else if (fieldListEdit != nullptr) fieldListEdit->setReadOnly(isReadOnly); else if (colorWidget != nullptr) colorWidget->setReadOnly(isReadOnly); else if (starRatingWidget != nullptr) starRatingWidget->setReadOnly(isReadOnly); } void setFile(const File *file) { bibtexFile = file; if (fieldLineEdit != nullptr) fieldLineEdit->setFile(file); if (fieldListEdit != nullptr) fieldListEdit->setFile(file); } void setElement(const Element *element) { this->element = element; if (fieldLineEdit != nullptr) fieldLineEdit->setElement(element); if (fieldListEdit != nullptr) fieldListEdit->setElement(element); } void setFieldKey(const QString &fieldKey) { if (fieldLineEdit != nullptr) fieldLineEdit->setFieldKey(fieldKey); if (fieldListEdit != nullptr) fieldListEdit->setFieldKey(fieldKey); } void setCompletionItems(const QStringList &items) { if (fieldLineEdit != nullptr) fieldLineEdit->setCompletionItems(items); if (fieldListEdit != nullptr) fieldListEdit->setCompletionItems(items); } bool selectCrossRef() { Q_ASSERT_X(fieldLineEdit != nullptr, "void FieldInput::FieldInputPrivate::selectCrossRef()", "fieldLineEdit is invalid"); if (bibtexFile == nullptr) return false; /// create a standard input dialog with a list of all keys (ids of entries) bool ok = false; QStringList list = bibtexFile->allKeys(File::etEntry); list.sort(); /// remove own id const Entry *entry = dynamic_cast(element); if (entry != nullptr) list.removeOne(entry->id()); QString crossRef = QInputDialog::getItem(p, i18n("Select Cross Reference"), i18n("Select the cross reference to another entry:"), list, 0, false, &ok); if (ok && !crossRef.isEmpty()) { /// insert selected cross reference into edit widget Value value; value.append(QSharedPointer(new VerbatimText(crossRef))); reset(value); return true; } return false; } - void enableModifiedSignal() { - if (fieldLineEdit != nullptr) - connect(fieldLineEdit, &FieldLineEdit::textChanged, p, &FieldInput::modified); - if (fieldListEdit != nullptr) - connect(fieldListEdit, &FieldListEdit::modified, p, &FieldInput::modified); - if (colorWidget != nullptr) - connect(colorWidget, &ColorLabelWidget::modified, p, &FieldInput::modified); - if (starRatingWidget != nullptr) - connect(starRatingWidget, &StarRatingFieldInput::modified, p, &FieldInput::modified); - } - - void disableModifiedSignal() { - if (fieldLineEdit != nullptr) - disconnect(fieldLineEdit, &FieldLineEdit::textChanged, p, &FieldInput::modified); - if (fieldListEdit != nullptr) - disconnect(fieldListEdit, &FieldListEdit::modified, p, &FieldInput::modified); - if (colorWidget != nullptr) - disconnect(colorWidget, &ColorLabelWidget::modified, p, &FieldInput::modified); - if (starRatingWidget != nullptr) - disconnect(starRatingWidget, &StarRatingFieldInput::modified, p, &FieldInput::modified); - } - void setMonth(int month) { Value value; value.append(QSharedPointer(new MacroKey(KBibTeX::MonthsTriple[month - 1]))); reset(value); /// Instead of an 'emit' ... QMetaObject::invokeMethod(p, "modified", Qt::DirectConnection, QGenericReturnArgument()); } void setEdition(int edition) { Value value; value.append(QSharedPointer(new MacroKey(QString::number(edition)))); reset(value); /// Instead of an 'emit' ... QMetaObject::invokeMethod(p, "modified", Qt::DirectConnection, QGenericReturnArgument()); } }; FieldInput::FieldInput(KBibTeX::FieldInputType fieldInputType, KBibTeX::TypeFlag preferredTypeFlag, KBibTeX::TypeFlags typeFlags, QWidget *parent) : QWidget(parent), d(new FieldInputPrivate(this)) { d->fieldInputType = fieldInputType; d->typeFlags = typeFlags; d->preferredTypeFlag = preferredTypeFlag; d->createGUI(); } FieldInput::~FieldInput() { delete d; } void FieldInput::clear() { d->clear(); } bool FieldInput::reset(const Value &value) { return d->reset(value); } bool FieldInput::apply(Value &value) const { return d->apply(value); } bool FieldInput::validate(QWidget **widgetWithIssue, QString &message) const { return d->validate(widgetWithIssue, message); } void FieldInput::setReadOnly(bool isReadOnly) { d->setReadOnly(isReadOnly); } void FieldInput::setFile(const File *file) { d->setFile(file); } void FieldInput::setElement(const Element *element) { d->setElement(element); } void FieldInput::setFieldKey(const QString &fieldKey) { d->setFieldKey(fieldKey); } void FieldInput::setCompletionItems(const QStringList &items) { d->setCompletionItems(items); } QWidget *FieldInput::buddy() { if (d->fieldLineEdit != nullptr) return d->fieldLineEdit->buddy(); // TODO fieldListEdit else if (d->colorWidget != nullptr) return d->colorWidget; else if (d->starRatingWidget != nullptr) return d->starRatingWidget; return nullptr; } void FieldInput::selectCrossRef() { if (d->selectCrossRef()) emit modified(); }