1 /***************************************************************************
2  *   Copyright (C) 2004-2018 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 "elementform.h"
19 
20 #include <QLayout>
21 #include <QDockWidget>
22 #include <QLabel>
23 #include <QCheckBox>
24 #include <QPushButton>
25 
26 #include <KLocalizedString>
27 #include <KIconLoader>
28 #include <KConfigGroup>
29 #include <KSharedConfig>
30 #include <KMessageBox>
31 
32 #include "entry.h"
33 #include "elementeditor.h"
34 #include "mdiwidget.h"
35 
36 class ElementForm::ElementFormPrivate
37 {
38 private:
39     ElementForm *p;
40     QGridLayout *layout;
41     const File *file;
42 
43 public:
44     ElementEditor *elementEditor;
45     MDIWidget *mdiWidget;
46     QCheckBox *checkBoxAutoApply;
47     QPushButton *buttonApply, *buttonReset;
48     QWidget *widgetUnmodifiedChanges;
49     bool gotModified;
50     QSharedPointer<Element> element;
51 
52     KSharedConfigPtr config;
53     /// Group name in configuration file for all settings for this form
54     static const QString configGroupName;
55     /// Key to store/retrieve setting whether changes in form should be automatically applied to element or not
56     static const QString configKeyAutoApply;
57 
58     ElementFormPrivate(MDIWidget *_mdiWidget, ElementForm *parent)
59             : p(parent), file(nullptr), mdiWidget(_mdiWidget), gotModified(false), config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))) {
60         KConfigGroup configGroup(config, configGroupName);
61 
62         layout = new QGridLayout(p);
63         layout->setColumnStretch(0, 10);
64         layout->setColumnStretch(1, 0);
65         layout->setColumnStretch(2, 0);
66         layout->setColumnStretch(3, 0);
67 
68         elementEditor = new ElementEditor(true, p);
69         layout->addWidget(elementEditor, 0, 0, 1, 4);
70         elementEditor->setEnabled(false);
71         elementEditor->layout()->setMargin(0);
72         connect(elementEditor, &ElementEditor::modified, p, &ElementForm::modified);
73 
74         /// Checkbox enabling/disabling setting to automatically apply changes in form to element
75         checkBoxAutoApply = new QCheckBox(i18n("Automatically apply changes"), p);
76         checkBoxAutoApply->setChecked(configGroup.readEntry(configKeyAutoApply, false));
77         layout->addWidget(checkBoxAutoApply, 1, 0, 1, 1);
78 
79         /// Create a special widget that shows a small icon and a text
80         /// stating that there are unsaved changes. It will be shown
81         /// simultaneously when the Apply and Reset buttons are enabled.
82         // TODO nearly identical code as in SearchResultsPrivate constructor, create common class
83         widgetUnmodifiedChanges = new QWidget(p);
84         layout->addWidget(widgetUnmodifiedChanges, 1, 1, 1, 1);
85         QBoxLayout *layoutUnmodifiedChanges = new QHBoxLayout(widgetUnmodifiedChanges);
86         layoutUnmodifiedChanges->addSpacing(32);
87         QLabel *label = new QLabel(widgetUnmodifiedChanges);
88         label->setAlignment(Qt::AlignCenter | Qt::AlignVCenter);
89         label->setPixmap(KIconLoader::global()->loadIcon(QStringLiteral("dialog-information"), KIconLoader::Dialog, KIconLoader::SizeSmall));
90         layoutUnmodifiedChanges->addWidget(label);
91         label = new QLabel(i18n("There are unsaved changes. Please press either 'Apply' or 'Reset'."), widgetUnmodifiedChanges);
92         label->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
93         layoutUnmodifiedChanges->addWidget(label);
94 
95         buttonApply = new QPushButton(QIcon::fromTheme(QStringLiteral("dialog-ok-apply")), i18n("Apply"), p);
96         layout->addWidget(buttonApply, 1, 2, 1, 1);
97 
98         buttonReset = new QPushButton(QIcon::fromTheme(QStringLiteral("edit-undo")), i18n("Reset"), p);
99         layout->addWidget(buttonReset, 1, 3, 1, 1);
100 
101         connect(checkBoxAutoApply, &QCheckBox::toggled, p, &ElementForm::autoApplyToggled);
102         connect(buttonApply, &QPushButton::clicked, p, &ElementForm::validateAndOnlyThenApply);
103         connect(buttonReset, &QPushButton::clicked, p, &ElementForm::reset);
104     }
105 
106     ~ElementFormPrivate() {
107         delete elementEditor;
108     }
109 
110     void refreshElement() {
111         loadElement(element, file);
112     }
113 
114     void loadElement(QSharedPointer<Element> element, const File *file) {
115         /// store both element and file for later refresh
116         this->element = element;
117         this->file = file;
118 
119         /// skip whole process of loading an element if not visible
120         if (isVisible())
121             p->setEnabled(true);
122         else {
123             p->setEnabled(false);
124             return;
125         }
126 
127         elementEditor->setElement(element, file);
128         elementEditor->setEnabled(!element.isNull());
129 
130         /// make apply and reset buttons aware of new element editor
131         buttonApply->setEnabled(false);
132         buttonReset->setEnabled(false);
133         widgetUnmodifiedChanges->setVisible(false);
134         gotModified = false;
135     }
136 
137     bool isVisible() {
138         /// get dock where this widget is inside
139         /// static cast is save as constructor requires parent to be QDockWidget
140         QDockWidget *pp = static_cast<QDockWidget *>(p->parent());
141         return pp != nullptr && !pp->isHidden();
142     }
143 
144     void apply() {
145         elementEditor->apply();
146         buttonApply->setEnabled(false);
147         buttonReset->setEnabled(false);
148         gotModified = false;
149         widgetUnmodifiedChanges->setVisible(false);
150     }
151 
152     void reset() {
153         elementEditor->reset();
154         buttonApply->setEnabled(false);
155         buttonReset->setEnabled(false);
156         gotModified = false;
157         widgetUnmodifiedChanges->setVisible(false);
158     }
159 };
160 
161 const QString ElementForm::ElementFormPrivate::configGroupName = QStringLiteral("ElementForm");
162 const QString ElementForm::ElementFormPrivate::configKeyAutoApply = QStringLiteral("AutoApply");
163 
164 ElementForm::ElementForm(MDIWidget *mdiWidget, QDockWidget *parent)
165         : QWidget(parent), d(new ElementFormPrivate(mdiWidget, this))
166 {
167     connect(parent, &QDockWidget::visibilityChanged, this, &ElementForm::visibilityChanged);
168 }
169 
170 ElementForm::~ElementForm()
171 {
172     delete d;
173 }
174 
175 void ElementForm::setElement(QSharedPointer<Element> element, const File *file)
176 {
177     /// Test if previous element (1) got modified, (2) the new element isn't
178     /// the same as the new one, and (3) the user confirms to apply those
179     /// changes rather than to discard them -> apply changes in previous element.
180     /// FIXME If the previous element got delete from the file and therefore a different
181     /// element gets set, changes will be still applied to the element to-be-deleted.
182     if (d->gotModified && element != d->element && KMessageBox::questionYesNo(this, i18n("The current element got modified.\nApply or discard changes?"), i18n("Element modified"), KGuiItem(i18n("Apply changes"), QIcon::fromTheme(QStringLiteral("dialog-ok-apply"))), KGuiItem(i18n("Discard changes"), QIcon::fromTheme(QStringLiteral("edit-undo")))) == KMessageBox::Yes) {
183         d->apply();
184     }
185     if (element != d->element) {
186         /// Ignore loading the same element again
187         d->loadElement(element, file);
188     }
189 }
190 
191 void ElementForm::refreshElement()
192 {
193     d->refreshElement();
194 }
195 
196 /**
197  * Fetch the modified signal from the editing widget.
198  * @param gotModified true if widget was modified by user, false if modified status was reset by e.g. apply operation
199  */
200 void ElementForm::modified(bool gotModified)
201 {
202     /// Only interested in modifications, not resets of modified status
203     if (!gotModified) return;
204 
205     if (d->checkBoxAutoApply->isChecked()) {
206         /// User wants to automatically apply changes, so do it
207         // FIXME validateAndOnlyThenApply();
208         apply();
209     } else {
210         /// No automatic apply, therefore enable buttons where user can
211         /// apply or reset changes, plus show warning label about unsaved changes
212         d->buttonApply->setEnabled(true);
213         d->buttonReset->setEnabled(true);
214         d->widgetUnmodifiedChanges->setVisible(true);
215         d->gotModified = true;
216     }
217 }
218 
219 void ElementForm::apply()
220 {
221     d->apply();
222 
223     /// Notify rest of program (esp. main list) about changes
224     emit elementModified();
225 
226 }
227 
228 bool ElementForm::validateAndOnlyThenApply()
229 {
230     const bool isValid = d->elementEditor->validate();
231     if (isValid)
232         apply();
233     return isValid;
234 }
235 
236 void ElementForm::reset()
237 {
238     d->reset();
239 }
240 
241 void ElementForm::visibilityChanged(bool)
242 {
243     d->refreshElement();
244 }
245 
246 /**
247  * React on toggles of checkbox for auto-apply.
248  * @param isChecked true if checkbox got checked, false if checkbox got unchecked
249  */
250 void ElementForm::autoApplyToggled(bool isChecked)
251 {
252     if (isChecked) {
253         /// Got toggled to check state
254         if (!d->element.isNull()) {
255             validateAndOnlyThenApply();
256         } else {
257             /// The following settings would happen when calling apply(),
258             /// but as no valid element is edited, perform settings here instead
259             d->buttonApply->setEnabled(false);
260             d->buttonReset->setEnabled(false);
261             d->widgetUnmodifiedChanges->setVisible(false);
262             d->gotModified = false;
263         }
264     }
265 
266     /// Save changed status of checkbox in configuration settings
267     KConfigGroup configGroup(d->config, ElementFormPrivate::configGroupName);
268     configGroup.writeEntry(ElementFormPrivate::configKeyAutoApply, d->checkBoxAutoApply->isChecked());
269     configGroup.sync();
270 }
271