1 /***************************************************************************
2  *   Copyright (C) 2004-2020 by Thomas Fischer <fischer@unix-ag.uni-kl.de> *
3  *                                                                         *
4  *   This program is free software; you can redistribute it and/or modify  *
5  *   it under the terms of the GNU General Public License as published by  *
6  *   the Free Software Foundation; either version 2 of the License, or     *
7  *   (at your option) any later version.                                   *
8  *                                                                         *
9  *   This program is distributed in the hope that it will be useful,       *
10  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
11  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
12  *   GNU General Public License for more details.                          *
13  *                                                                         *
14  *   You should have received a copy of the GNU General Public License     *
15  *   along with this program; if not, see <https://www.gnu.org/licenses/>. *
16  ***************************************************************************/
17 
18 #include "elementeditor.h"
19 
20 #include <typeinfo>
21 
22 #include <QCheckBox>
23 #include <QLabel>
24 #include <QLayout>
25 #include <QBuffer>
26 #include <QTextStream>
27 #include <QApplication>
28 #include <QFileInfo>
29 #include <QMenu>
30 #include <QScrollArea>
31 #include <QPushButton>
32 
33 #include <KMessageBox>
34 #include <KLocalizedString>
35 #include <KSharedConfig>
36 #include <KConfigGroup>
37 
38 #include "menulineedit.h"
39 #include "entry.h"
40 #include "comment.h"
41 #include "macro.h"
42 #include "preamble.h"
43 #include "element.h"
44 #include "file.h"
45 #include "elementwidgets.h"
46 #include "checkbibtex.h"
47 #include "hidingtabwidget.h"
48 
49 class ElementEditor::ElementEditorPrivate : public ElementEditor::ApplyElementInterface
50 {
51 private:
52     typedef QVector<ElementWidget *> WidgetList;
53     WidgetList widgets;
54     const File *file;
55     QSharedPointer<Entry> internalEntry;
56     QSharedPointer<Macro> internalMacro;
57     QSharedPointer<Preamble> internalPreamble;
58     QSharedPointer<Comment> internalComment;
59     ElementEditor *p;
60     ElementWidget *previousWidget;
61     ReferenceWidget *referenceWidget;
62     SourceWidget *sourceWidget;
63     QPushButton *buttonCheckWithBibTeX;
64 
65     /// Settings management through a push button with menu
66     KSharedConfigPtr config;
67     QPushButton *buttonOptions;
68     QAction *actionForceShowAllWidgets, *actionLimitKeyboardTabStops;
69 
70 public:
71     QSharedPointer<Element> element;
72     HidingTabWidget *tab;
73     bool elementChanged, elementUnapplied;
74 
ElementEditorPrivate(bool scrollable,ElementEditor * parent)75     ElementEditorPrivate(bool scrollable, ElementEditor *parent)
76             : file(nullptr), p(parent), previousWidget(nullptr), config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))), elementChanged(false), elementUnapplied(false) {
77         internalEntry = QSharedPointer<Entry>();
78         internalMacro = QSharedPointer<Macro>();
79         internalComment = QSharedPointer<Comment>();
80         internalPreamble = QSharedPointer<Preamble>();
81 
82         createGUI(scrollable);
83     }
84 
~ElementEditorPrivate()85     ~ElementEditorPrivate() override {
86         clearWidgets();
87     }
88 
clearWidgets()89     void clearWidgets() {
90         for (int i = widgets.count() - 1; i >= 0; --i) {
91             QWidget *w = widgets[i];
92             w->deleteLater();
93         }
94         widgets.clear();
95     }
96 
setElement(QSharedPointer<Element> element,const File * file)97     void setElement(QSharedPointer<Element> element, const File *file) {
98         this->element = element;
99         this->file = file;
100         referenceWidget->setOriginalElement(element);
101         updateTabVisibility();
102     }
103 
addTabWidgets()104     void addTabWidgets() {
105         for (const auto &etl : EntryLayout::instance()) {
106             ElementWidget *widget = new EntryConfiguredWidget(etl, tab);
107             connect(widget, &ElementWidget::modified, p, &ElementEditor::childModified);
108             widgets << widget;
109             if (previousWidget == nullptr)
110                 previousWidget = widget; ///< memorize the first tab
111             int index = tab->addTab(widget, widget->icon(), widget->label());
112             tab->hideTab(index);
113         }
114 
115         ElementWidget *widget = new PreambleWidget(tab);
116         connect(widget, &ElementWidget::modified, p, &ElementEditor::childModified);
117         widgets << widget;
118         int index = tab->addTab(widget, widget->icon(), widget->label());
119         tab->hideTab(index);
120 
121         widget = new MacroWidget(tab);
122         connect(widget, &ElementWidget::modified, p, &ElementEditor::childModified);
123         widgets << widget;
124         index = tab->addTab(widget, widget->icon(), widget->label());
125         tab->hideTab(index);
126 
127         FilesWidget *filesWidget = new FilesWidget(tab);
128         connect(filesWidget, &FilesWidget::modified, p, &ElementEditor::childModified);
129         widgets << filesWidget;
130         index = tab->addTab(filesWidget, filesWidget->icon(), filesWidget->label());
131         tab->hideTab(index);
132 
133         QStringList blacklistedFields;
134 
135         /// blacklist fields covered by EntryConfiguredWidget
136         for (const auto &etl : EntryLayout::instance())
137             for (const auto &sfl : const_cast<const QList<SingleFieldLayout> &>(etl->singleFieldLayouts))
138                 blacklistedFields << sfl.bibtexLabel;
139 
140         /// blacklist fields covered by FilesWidget
141         blacklistedFields << QString(Entry::ftUrl) << QString(Entry::ftLocalFile) << QString(Entry::ftFile) << QString(Entry::ftDOI) << QStringLiteral("ee") << QStringLiteral("biburl") << QStringLiteral("postscript");
142         for (int i = 2; i < 256; ++i) // FIXME replace number by constant
143             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);
144 
145         widget = new OtherFieldsWidget(blacklistedFields, tab);
146         connect(widget, &ElementWidget::modified, p, &ElementEditor::childModified);
147         widgets << widget;
148         index = tab->addTab(widget, widget->icon(), widget->label());
149         tab->hideTab(index);
150 
151         sourceWidget = new SourceWidget(tab);
152         connect(sourceWidget, &ElementWidget::modified, p, &ElementEditor::childModified);
153         widgets << sourceWidget;
154         index = tab->addTab(sourceWidget, sourceWidget->icon(), sourceWidget->label());
155         tab->hideTab(index);
156     }
157 
createGUI(bool scrollable)158     void createGUI(bool scrollable) {
159         /// load configuration for options push button
160         static const QString configGroupName = QStringLiteral("User Interface");
161         static const QString keyEnableAllWidgets = QStringLiteral("EnableAllWidgets");
162         KConfigGroup configGroup(config, configGroupName);
163         const bool showAll = configGroup.readEntry(keyEnableAllWidgets, true);
164         const bool limitKeyboardTabStops = configGroup.readEntry(MenuLineEdit::keyLimitKeyboardTabStops, false);
165 
166         QBoxLayout *vLayout = new QVBoxLayout(p);
167 
168         referenceWidget = new ReferenceWidget(p);
169         referenceWidget->setApplyElementInterface(this);
170         connect(referenceWidget, &ElementWidget::modified, p, &ElementEditor::childModified);
171         connect(referenceWidget, &ReferenceWidget::entryTypeChanged, p, &ElementEditor::updateReqOptWidgets);
172         vLayout->addWidget(referenceWidget, 0);
173         widgets << referenceWidget;
174 
175         if (scrollable) {
176             QScrollArea *sa = new QScrollArea(p);
177             tab = new HidingTabWidget(sa);
178             sa->setFrameStyle(0);
179             sa->setWidget(tab);
180             sa->setWidgetResizable(true);
181             sa->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
182             sa->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
183             vLayout->addWidget(sa, 10);
184         } else {
185             tab = new HidingTabWidget(p);
186             vLayout->addWidget(tab, 10);
187         }
188 
189         QBoxLayout *hLayout = new QHBoxLayout();
190         vLayout->addLayout(hLayout, 0);
191 
192         /// Push button with menu to toggle various options
193         buttonOptions = new QPushButton(QIcon::fromTheme(QStringLiteral("configure")), i18n("Options"), p);
194         hLayout->addWidget(buttonOptions, 0);
195         QMenu *menuOptions = new QMenu(buttonOptions);
196         buttonOptions->setMenu(menuOptions);
197 
198         /// Option to show all fields or only those require for current entry type
199         actionForceShowAllWidgets = menuOptions->addAction(i18n("Show all fields"), p, SLOT(updateReqOptWidgets()));
200         actionForceShowAllWidgets->setCheckable(true);
201         actionForceShowAllWidgets->setChecked(showAll);
202 
203         /// Option to disable tab key focus to reach/visit various non-editable widgets
204         actionLimitKeyboardTabStops = menuOptions->addAction(i18n("Tab key visits only editable fields"), p, SLOT(limitKeyboardTabStops()));
205         actionLimitKeyboardTabStops->setCheckable(true);
206         actionLimitKeyboardTabStops->setChecked(limitKeyboardTabStops);
207 
208         hLayout->addStretch(10);
209 
210         buttonCheckWithBibTeX = new QPushButton(QIcon::fromTheme(QStringLiteral("tools-check-spelling")), i18n("Check with BibTeX"), p);
211         hLayout->addWidget(buttonCheckWithBibTeX, 0);
212         connect(buttonCheckWithBibTeX, &QPushButton::clicked, p, &ElementEditor::checkBibTeX);
213 
214         addTabWidgets();
215     }
216 
updateTabVisibility()217     void updateTabVisibility() {
218         disconnect(tab, &HidingTabWidget::currentChanged, p, &ElementEditor::tabChanged);
219         if (element.isNull()) {
220             p->setEnabled(false);
221         } else {
222             p->setEnabled(true);
223             int firstEnabledTab = 1024;
224 
225             for (ElementWidget *widget : const_cast<const WidgetList &>(widgets)) {
226                 const int index = tab->indexOf(widget);
227                 const bool canEdit = widget->canEdit(element.data());
228 
229                 if (widget == referenceWidget) {
230                     /// Reference widget
231                     widget->setVisible(canEdit);
232                     widget->setEnabled(canEdit);
233                 } else {
234                     if (canEdit) tab->showTab(widget);
235                     else if (index >= 0) tab->hideTab(index);
236                     if (canEdit && index >= 0 && index < firstEnabledTab)
237                         firstEnabledTab = index;
238                 }
239             }
240             if (firstEnabledTab < 1024)
241                 tab->setCurrentIndex(firstEnabledTab);
242         }
243         connect(tab, &HidingTabWidget::currentChanged, p, &ElementEditor::tabChanged);
244     }
245 
246     /**
247      * If this element editor makes use of a reference widget
248      * (e.g. where entry type and entry id/macro key can be edited),
249      * then return the current value of the entry id/macro key
250      * editing widget.
251      * Otherwise, return an empty string.
252      *
253      * @return Current value of entry id/macro key if any, otherwise empty string
254      */
currentId() const255     QString currentId() const {
256         if (referenceWidget != nullptr)
257             return referenceWidget->currentId();
258         return QString();
259     }
260 
setCurrentId(const QString & newId)261     void setCurrentId(const QString &newId) {
262         if (referenceWidget != nullptr)
263             return referenceWidget->setCurrentId(newId);
264     }
265 
266     /**
267      * Return the current File object set for this element editor.
268      * May be NULL if nothing has been set or if it has been cleared.
269      *
270      * @return Current File object, may be nullptr
271      */
currentFile() const272     const File *currentFile() const {
273         return file;
274     }
275 
apply()276     void apply() {
277         elementChanged = true;
278         elementUnapplied = false;
279         apply(element);
280     }
281 
apply(QSharedPointer<Element> element)282     void apply(QSharedPointer<Element> element) override {
283         QSharedPointer<Entry> e = element.dynamicCast<Entry>();
284         QSharedPointer<Macro> m = e.isNull() ? element.dynamicCast<Macro>() : QSharedPointer<Macro>();
285         QSharedPointer<Comment> c = e.isNull() && m.isNull() ? element.dynamicCast<Comment>() : QSharedPointer<Comment>();
286         QSharedPointer<Preamble> p = e.isNull() && m.isNull() && c.isNull() ? element.dynamicCast<Preamble>() : QSharedPointer<Preamble>();
287 
288         if (tab->currentWidget() == sourceWidget) {
289             /// Very simple if source view is active: BibTeX code contains
290             /// all necessary data
291             if (!e.isNull())
292                 sourceWidget->setElementClass(SourceWidget::elementEntry);
293             else if (!m.isNull())
294                 sourceWidget->setElementClass(SourceWidget::elementMacro);
295             else if (!p.isNull())
296                 sourceWidget->setElementClass(SourceWidget::elementPreamble);
297             else if (!c.isNull())
298                 sourceWidget->setElementClass(SourceWidget::elementComment);
299             else
300                 sourceWidget->setElementClass(SourceWidget::elementInvalid);
301             sourceWidget->apply(element);
302         } else {
303             /// Start by assigning the current internal element's
304             /// data to the output element
305             if (!e.isNull())
306                 *e = *internalEntry;
307             else {
308                 if (!m.isNull())
309                     *m = *internalMacro;
310                 else {
311                     if (!c.isNull())
312                         *c = *internalComment;
313                     else {
314                         if (!p.isNull())
315                             *p = *internalPreamble;
316                         else
317                             Q_ASSERT_X(element.isNull(), "ElementEditor::ElementEditorPrivate::apply(QSharedPointer<Element> element)", "element is not NULL but could not be cast on a valid Element sub-class");
318                     }
319                 }
320             }
321 
322             /// The internal element may be outdated (only updated on tab switch),
323             /// so apply the reference widget's data on the output element
324             if (referenceWidget != nullptr)
325                 referenceWidget->apply(element);
326             /// The internal element may be outdated (only updated on tab switch),
327             /// so apply the current widget's data on the output element
328             ElementWidget *currentElementWidget = qobject_cast<ElementWidget *>(tab->currentWidget());
329             if (currentElementWidget != nullptr)
330                 currentElementWidget->apply(element);
331         }
332     }
333 
validate(QWidget ** widgetWithIssue,QString & message) const334     bool validate(QWidget **widgetWithIssue, QString &message) const override {
335         if (tab->currentWidget() == sourceWidget) {
336             /// Source widget must check its textual content for being valid BibTeX code
337             return sourceWidget->validate(widgetWithIssue, message);
338         } else {
339             /// All widgets except for the source widget must validate their values
340             for (WidgetList::ConstIterator it = widgets.begin(); it != widgets.end(); ++it) {
341                 if ((*it) == sourceWidget) continue;
342                 const bool v = (*it)->validate(widgetWithIssue, message);
343                 /// A single widget failing to validate lets the whole validation fail
344                 if (!v) return false;
345             }
346             return true;
347         }
348     }
349 
reset()350     void reset() {
351         elementChanged = false;
352         elementUnapplied = false;
353         reset(element);
354 
355         /// show checkbox to enable all fields only if editing an entry
356         actionForceShowAllWidgets->setVisible(!internalEntry.isNull());
357         /// Disable widgets if necessary
358         if (!actionForceShowAllWidgets->isChecked())
359             updateReqOptWidgets();
360     }
361 
reset(QSharedPointer<const Element> element)362     void reset(QSharedPointer<const Element> element) {
363         for (WidgetList::Iterator it = widgets.begin(); it != widgets.end(); ++it) {
364             (*it)->setFile(file);
365             (*it)->reset(element);
366             (*it)->setModified(false);
367         }
368 
369         QSharedPointer<const Entry> e = element.dynamicCast<const Entry>();
370         if (!e.isNull()) {
371             internalEntry = QSharedPointer<Entry>(new Entry(*e.data()));
372             sourceWidget->setElementClass(SourceWidget::elementEntry);
373         } else {
374             QSharedPointer<const Macro> m = element.dynamicCast<const Macro>();
375             if (!m.isNull()) {
376                 internalMacro = QSharedPointer<Macro>(new Macro(*m.data()));
377                 sourceWidget->setElementClass(SourceWidget::elementMacro);
378             } else {
379                 QSharedPointer<const Comment> c = element.dynamicCast<const Comment>();
380                 if (!c.isNull()) {
381                     internalComment = QSharedPointer<Comment>(new Comment(*c.data()));
382                     sourceWidget->setElementClass(SourceWidget::elementComment);
383                 } else {
384                     QSharedPointer<const Preamble> p = element.dynamicCast<const Preamble>();
385                     if (!p.isNull()) {
386                         internalPreamble = QSharedPointer<Preamble>(new Preamble(*p.data()));
387                         sourceWidget->setElementClass(SourceWidget::elementPreamble);
388                     } else
389                         Q_ASSERT_X(element.isNull(), "ElementEditor::ElementEditorPrivate::reset(QSharedPointer<const Element> element)", "element is not NULL but could not be cast on a valid Element sub-class");
390                 }
391             }
392         }
393 
394         buttonCheckWithBibTeX->setEnabled(!internalEntry.isNull());
395     }
396 
setReadOnly(bool isReadOnly)397     void setReadOnly(bool isReadOnly) {
398         for (WidgetList::Iterator it = widgets.begin(); it != widgets.end(); ++it)
399             (*it)->setReadOnly(isReadOnly);
400     }
401 
updateReqOptWidgets()402     void updateReqOptWidgets() {
403         /// this function is only relevant if editing an entry (and not e.g. a comment)
404         if (internalEntry.isNull()) return; /// quick-and-dirty test if editing an entry
405 
406         /// make a temporary snapshot of the current state
407         QSharedPointer<Entry> tempEntry = QSharedPointer<Entry>(new Entry());
408         apply(tempEntry);
409 
410         /// update the enabled/disabled state of required and optional widgets/fields
411         bool forceVisible = actionForceShowAllWidgets->isChecked();
412         for (ElementWidget *elementWidget : const_cast<const WidgetList &>(widgets)) {
413             elementWidget->showReqOptWidgets(forceVisible, tempEntry->type());
414         }
415 
416         /// save configuration
417         static const QString configGroupName = QStringLiteral("User Interface");
418         static const QString keyEnableAllWidgets = QStringLiteral("EnableAllWidgets");
419         KConfigGroup configGroup(config, configGroupName);
420         configGroup.writeEntry(keyEnableAllWidgets, actionForceShowAllWidgets->isChecked());
421         config->sync();
422     }
423 
limitKeyboardTabStops()424     void limitKeyboardTabStops() {
425         /// save configuration
426         static const QString configGroupName = QStringLiteral("User Interface");
427         KConfigGroup configGroup(config, configGroupName);
428         configGroup.writeEntry(MenuLineEdit::keyLimitKeyboardTabStops, actionLimitKeyboardTabStops->isChecked());
429         config->sync();
430 
431         /// notify all listening MenuLineEdit widgets to change their behavior
432         NotificationHub::publishEvent(MenuLineEdit::MenuLineConfigurationChangedEvent);
433     }
434 
switchTo(QWidget * futureTab)435     void switchTo(QWidget *futureTab) {
436         /// Switched from source widget to another widget?
437         const bool isToSourceWidget = futureTab == sourceWidget;
438         /// Switch from some widget to the source widget?
439         const bool isFromSourceWidget = previousWidget == sourceWidget;
440         /// Interprete future widget as an ElementWidget
441         ElementWidget *futureWidget = qobject_cast<ElementWidget *>(futureTab);
442         /// Past and future ElementWidget values are valid?
443         if (previousWidget != nullptr && futureWidget != nullptr) {
444             /// Assign to temp wihch internal variable holds current state
445             QSharedPointer<Element> temp;
446             if (!internalEntry.isNull())
447                 temp = internalEntry;
448             else if (!internalMacro.isNull())
449                 temp = internalMacro;
450             else if (!internalComment.isNull())
451                 temp = internalComment;
452             else if (!internalPreamble.isNull())
453                 temp = internalPreamble;
454             Q_ASSERT_X(!temp.isNull(), "void ElementEditor::ElementEditorPrivate::switchTo(QWidget *newTab)", "temp is NULL");
455 
456             /// Past widget writes its state to the internal state
457             previousWidget->apply(temp);
458             /// Before switching to source widget, store internally reference widget's state
459             if (isToSourceWidget && referenceWidget != nullptr) referenceWidget->apply(temp);
460             /// Tell future widget to initialize itself based on internal state
461             futureWidget->reset(temp);
462             /// When switchin from source widget to another widget, initialize reference widget
463             if (isFromSourceWidget && referenceWidget != nullptr)
464                 referenceWidget->reset(temp);
465         }
466         previousWidget = futureWidget;
467 
468         /// Enable/disable tabs
469         for (WidgetList::Iterator it = widgets.begin(); it != widgets.end(); ++it)
470             (*it)->setEnabled(!isToSourceWidget || *it == futureTab);
471     }
472 
473     /**
474       * Test current entry if it compiles with BibTeX.
475       * Show warnings and errors in message box.
476       */
checkBibTeX()477     void checkBibTeX() {
478         /// disable GUI under process
479         p->setEnabled(false);
480         QSharedPointer<Entry> entry = QSharedPointer<Entry>(new Entry());
481         apply(entry);
482         CheckBibTeX::checkBibTeX(entry, file, p);
483         p->setEnabled(true);
484     }
485 
setModified(bool newIsModified)486     void setModified(bool newIsModified) {
487         for (WidgetList::Iterator it = widgets.begin(); it != widgets.end(); ++it)
488             (*it)->setModified(newIsModified);
489     }
490 
referenceWidgetSetEntryIdByDefault()491     void referenceWidgetSetEntryIdByDefault() {
492         referenceWidget->setEntryIdByDefault();
493     }
494 };
495 
ElementEditor(bool scrollable,QWidget * parent)496 ElementEditor::ElementEditor(bool scrollable, QWidget *parent)
497         : QWidget(parent), d(new ElementEditorPrivate(scrollable, this))
498 {
499     connect(d->tab, &HidingTabWidget::currentChanged, this, &ElementEditor::tabChanged);
500 }
501 
~ElementEditor()502 ElementEditor::~ElementEditor()
503 {
504     disconnect(d->tab, &HidingTabWidget::currentChanged, this, &ElementEditor::tabChanged);
505     delete d;
506 }
507 
apply()508 void ElementEditor::apply()
509 {
510     /// The prime problem to tackle in this function is to cope with
511     /// invalid/problematic entry ids or macro keys, respectively:
512     ///  - empty ids/keys
513     ///  - ids/keys that are duplicates of already used ids/keys
514 
515     QSharedPointer<Entry> entry = d->element.dynamicCast<Entry>();
516     QSharedPointer<Macro> macro = d->element.dynamicCast<Macro>();
517     /// Only for entry or macro bother with duplicate ids (but not for preamble or comment)
518     if (!entry.isNull() || !macro.isNull()) {
519         /// Determine id/key as it was set before the current editing started
520         const QString originalId = !entry.isNull() ? entry->id() : (!macro.isNull() ? macro->key() : QString());
521         /// Get the id/key as it is in the editing widget right now
522         const QString newId = d->currentId();
523         /// Keep track whether the 'original' id/key or the 'new' id/key will eventually be used
524         enum IdToUse {UseOriginalId, UseNewId};
525         IdToUse idToUse = UseNewId;
526 
527         if (newId.isEmpty() && !originalId.isEmpty()) {
528             /// New id/key is empty (invalid by definition), so just notify use and revert back to original id/key
529             /// (assuming that original id/key is valid)
530             KMessageBox::sorry(this, i18n("No id was entered, so the previous id '%1' will be restored.", originalId), i18n("No id given"));
531             idToUse = UseOriginalId;
532         } else if (!newId.isEmpty()) { // FIXME test if !originalId.isEmpty() ?
533             /// If new id/key is not empty, then check if it is identical to another entry/macro in the current file
534             const QSharedPointer<Element> knownElementWithSameId = d->currentFile() != nullptr ? d->currentFile()->containsKey(newId) : QSharedPointer<Element>();
535             if (!knownElementWithSameId.isNull() && d->element != knownElementWithSameId) {
536                 /// Some other, different element (entry or macro) uses same id/key, so ask user how to proceed
537                 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")));
538                 idToUse = msgBoxResult == KMessageBox::Continue ? UseNewId : UseOriginalId;
539             }
540         }
541 
542         if (idToUse == UseOriginalId) {
543             /// As 'apply()' above set the 'new' id/key but the 'original' id/key is to be used,
544             /// now UI must be updated accordingly. Changes will propagate to the entry id or
545             /// macro key, respectively, when invoking apply() further down
546             d->setCurrentId(originalId);
547         }
548         /// Case idToUse == UseNewId does not need to get handled as newId == d->currentId()
549     }
550 
551     d->apply();
552     d->setModified(false);
553     emit modified(false);
554 }
555 
reset()556 void ElementEditor::reset()
557 {
558     d->reset();
559     emit modified(false);
560 }
561 
validate()562 bool ElementEditor::validate() {
563     QWidget *widgetWithIssue = nullptr;
564     QString message;
565     if (!validate(&widgetWithIssue, message)) {
566         const QString msgBoxMessage = message.isEmpty() ? i18n("Validation for the current element failed.") : i18n("Validation for the current element failed:\n%1", message);
567         KMessageBox::error(this, msgBoxMessage, i18n("Element validation failed"));
568         if (widgetWithIssue != nullptr) {
569             /// Probe if widget with issue is inside a QTabWiget; if yes, make parenting tab the current tab
570             QWidget *cur = widgetWithIssue;
571             do {
572                 QTabWidget *tabWidget = cur->parent() != nullptr && cur->parent()->parent() != nullptr ? qobject_cast<QTabWidget *>(cur->parent()->parent()) : nullptr;
573                 if (tabWidget != nullptr) {
574                     tabWidget->setCurrentWidget(cur);
575                     break;
576                 }
577                 cur = qobject_cast<QWidget *>(cur->parent());
578             } while (cur != nullptr);
579             /// Set focus to widget with issue
580             widgetWithIssue->setFocus();
581         }
582         return false;
583     }
584     return true;
585 }
586 
setElement(QSharedPointer<Element> element,const File * file)587 void ElementEditor::setElement(QSharedPointer<Element> element, const File *file)
588 {
589     d->setElement(element, file);
590     d->reset();
591     emit modified(false);
592 }
593 
setElement(QSharedPointer<const Element> element,const File * file)594 void ElementEditor::setElement(QSharedPointer<const Element> element, const File *file)
595 {
596     QSharedPointer<Element> clone;
597     QSharedPointer<const Entry> entry = element.dynamicCast<const Entry>();
598     if (!entry.isNull())
599         clone = QSharedPointer<Entry>(new Entry(*entry.data()));
600     else {
601         QSharedPointer<const Macro> macro = element.dynamicCast<const Macro>();
602         if (!macro.isNull())
603             clone = QSharedPointer<Macro>(new Macro(*macro.data()));
604         else {
605             QSharedPointer<const Preamble> preamble = element.dynamicCast<const Preamble>();
606             if (!preamble.isNull())
607                 clone = QSharedPointer<Preamble>(new Preamble(*preamble.data()));
608             else {
609                 QSharedPointer<const Comment> comment = element.dynamicCast<const Comment>();
610                 if (!comment.isNull())
611                     clone = QSharedPointer<Comment>(new Comment(*comment.data()));
612                 else
613                     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");
614             }
615         }
616     }
617 
618     d->setElement(clone, file);
619     d->reset();
620 }
621 
setReadOnly(bool isReadOnly)622 void ElementEditor::setReadOnly(bool isReadOnly)
623 {
624     d->setReadOnly(isReadOnly);
625 }
626 
elementChanged()627 bool ElementEditor::elementChanged()
628 {
629     return d->elementChanged;
630 }
631 
elementUnapplied()632 bool ElementEditor::elementUnapplied()
633 {
634     return d->elementUnapplied;
635 }
636 
validate(QWidget ** widgetWithIssue,QString & message)637 bool ElementEditor::validate(QWidget **widgetWithIssue, QString &message)
638 {
639     return d->validate(widgetWithIssue, message);
640 }
641 
currentPage() const642 QWidget *ElementEditor::currentPage() const
643 {
644     return d->tab->currentWidget();
645 }
646 
setCurrentPage(QWidget * page)647 void ElementEditor::setCurrentPage(QWidget *page)
648 {
649     if (d->tab->indexOf(page) >= 0)
650         d->tab->setCurrentWidget(page);
651 }
652 
tabChanged()653 void ElementEditor::tabChanged()
654 {
655     d->switchTo(d->tab->currentWidget());
656 }
657 
checkBibTeX()658 void ElementEditor::checkBibTeX()
659 {
660     d->checkBibTeX();
661 }
662 
childModified(bool m)663 void ElementEditor::childModified(bool m)
664 {
665     if (m) {
666         d->elementUnapplied = true;
667         d->referenceWidgetSetEntryIdByDefault();
668     }
669     emit modified(m);
670 }
671 
updateReqOptWidgets()672 void ElementEditor::updateReqOptWidgets()
673 {
674     d->updateReqOptWidgets();
675 }
676 
limitKeyboardTabStops()677 void ElementEditor::limitKeyboardTabStops()
678 {
679     d->limitKeyboardTabStops();
680 }
681