1 /****************************************************************************
2 **
3 ** Copyright (C) 2018 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25 
26 #include "preseteditor.h"
27 
28 #include "canvas.h"
29 #include "easingcurve.h"
30 #include "timelineicons.h"
31 
32 #include <QAbstractButton>
33 #include <QApplication>
34 #include <QContextMenuEvent>
35 #include <QMenu>
36 #include <QMessageBox>
37 #include <QPainter>
38 #include <QPixmap>
39 #include <QSettings>
40 #include <QStandardItemModel>
41 #include <QString>
42 
43 #include <coreplugin/icore.h>
44 #include <theme.h>
45 
46 namespace QmlDesigner {
47 
48 constexpr int iconWidth = 86;
49 constexpr int iconHeight = 86;
50 constexpr int itemFrame = 3;
51 constexpr int itemWidth = iconWidth + 2 * itemFrame;
52 constexpr int unsavedMarkSize = 18;
53 
54 constexpr int spacingg = 5;
55 
56 const QColor background = Qt::white;
57 
PresetItemDelegate(const QColor & background)58 PresetItemDelegate::PresetItemDelegate(const QColor& background)
59     : QStyledItemDelegate()
60     , m_background(background)
61 {}
62 
paint(QPainter * painter,const QStyleOptionViewItem & opt,const QModelIndex & index) const63 void PresetItemDelegate::paint(QPainter *painter,
64                                const QStyleOptionViewItem &opt,
65                                const QModelIndex &index) const
66 {
67     QStyleOptionViewItem option = opt;
68     initStyleOption(&option, index);
69 
70     auto *w = option.widget;
71     auto *style = w == nullptr ? qApp->style() : w->style();
72 
73     QSize textSize = QSize(option.rect.width(),
74                            style->subElementRect(QStyle::SE_ItemViewItemText, &option, w).height());
75 
76     auto textRect = QRect(option.rect.topLeft(), textSize);
77     textRect.moveBottom(option.rect.bottom());
78 
79     option.font.setPixelSize(Theme::instance()->smallFontPixelSize());
80 
81     painter->save();
82     painter->fillRect(option.rect, m_background);
83 
84     if (option.text.isEmpty())
85         painter->fillRect(textRect, m_background);
86     else
87         painter->fillRect(textRect, Theme::instance()->qmlDesignerButtonColor());
88 
89     style->drawControl(QStyle::CE_ItemViewItem, &option, painter, option.widget);
90 
91     QVariant dirty = option.index.data(PresetList::ItemRole_Dirty);
92     if (dirty.isValid()) {
93         if (dirty.toBool()) {
94             QRect asteriskRect(option.rect.right() - unsavedMarkSize,
95                                itemFrame,
96                                unsavedMarkSize,
97                                unsavedMarkSize);
98 
99             QFont font = painter->font();
100             font.setPixelSize(unsavedMarkSize);
101             painter->setFont(font);
102 
103             auto pen = painter->pen();
104             pen.setColor(Qt::white);
105             painter->setPen(pen);
106 
107             painter->drawText(asteriskRect, Qt::AlignTop | Qt::AlignRight, "*");
108         }
109     }
110     painter->restore();
111 }
112 
sizeHint(const QStyleOptionViewItem & opt,const QModelIndex & index) const113 QSize PresetItemDelegate::sizeHint(const QStyleOptionViewItem &opt, const QModelIndex &index) const
114 {
115     QSize size = QStyledItemDelegate::sizeHint(opt, index);
116     size.rwidth() = itemWidth;
117     return size;
118 }
119 
paintPreview(const QColor & background)120 QIcon paintPreview(const QColor& background)
121 {
122     QPixmap pm(iconWidth, iconHeight);
123     pm.fill(background);
124     return QIcon(pm);
125 }
126 
paintPreview(const EasingCurve & curve,const QColor & background,const QColor & curveColor)127 QIcon paintPreview(const EasingCurve &curve, const QColor& background, const QColor& curveColor)
128 {
129     QPixmap pm(iconWidth, iconHeight);
130     pm.fill(background);
131 
132     QPainter painter(&pm);
133     painter.setRenderHint(QPainter::Antialiasing, true);
134 
135     Canvas canvas(iconWidth, iconHeight, 2, 2, 9, 6, 0, 1);
136     canvas.paintCurve(&painter, curve, curveColor);
137 
138     return QIcon(pm);
139 }
140 
141 namespace Internal {
142 
143 static const char settingsKey[] = "EasingCurveList";
144 static const char settingsFileName[] = "EasingCurves.ini";
145 
settingsFullFilePath(const QSettings::Scope & scope)146 QString settingsFullFilePath(const QSettings::Scope &scope)
147 {
148     if (scope == QSettings::SystemScope)
149         return Core::ICore::installerResourcePath(settingsFileName).toString();
150 
151     return Core::ICore::userResourcePath(settingsFileName).toString();
152 }
153 
154 } // namespace Internal
155 
PresetList(QSettings::Scope scope,QWidget * parent)156 PresetList::PresetList(QSettings::Scope scope, QWidget *parent)
157     : QListView(parent)
158     , m_scope(scope)
159     , m_index(-1)
160     , m_filename(Internal::settingsFullFilePath(scope))
161     , m_background(Theme::getColor(Theme::DSsectionHeadBackground ))
162     , m_curveColor(Theme::getColor(Theme::DStextColor))
163 {
164     int magic = 4;
165     int scrollBarWidth = this->style()->pixelMetric(QStyle::PM_ScrollBarExtent);
166     const int width = 3 * itemWidth + 4 * spacingg + scrollBarWidth + magic;
167 
168     setFixedWidth(width);
169 
170     setModel(new QStandardItemModel);
171 
172     setItemDelegate(new PresetItemDelegate(m_background));
173 
174     setSpacing(spacingg);
175 
176     setUniformItemSizes(true);
177 
178     setIconSize(QSize(iconWidth, iconHeight));
179 
180     setSelectionMode(QAbstractItemView::SingleSelection);
181 
182     setViewMode(QListView::IconMode);
183 
184     setFlow(QListView::LeftToRight);
185 
186     setMovement(QListView::Static);
187 
188     setWrapping(true);
189 
190     setTextElideMode(Qt::ElideMiddle);
191 
192     setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
193 }
194 
selectionChanged(const QItemSelection & selected,const QItemSelection & deselected)195 void PresetList::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
196 {
197     for (const QModelIndex &index : deselected.indexes()) {
198         if (dirty(index)) {
199             QMessageBox msgBox;
200             msgBox.setText("The preset has been modified.");
201             msgBox.setInformativeText("Do you want to save your changes?");
202             msgBox.setStandardButtons(QMessageBox::Save | QMessageBox::Discard
203                                       | QMessageBox::Cancel);
204             msgBox.setDefaultButton(QMessageBox::Save);
205 
206             if (QAbstractButton *button = msgBox.button(QMessageBox::Discard))
207                 button->setText("Discard Changes");
208 
209             if (QAbstractButton *button = msgBox.button(QMessageBox::Cancel))
210                 button->setText("Cancel Selection");
211 
212             int ret = msgBox.exec();
213 
214             switch (ret) {
215             case QMessageBox::Save:
216                 // Save the preset and continue selection.
217                 writePresets();
218                 break;
219             case QMessageBox::Discard:
220                 // Discard changes to the curve and continue selection.
221                 revert(index);
222                 break;
223 
224             case QMessageBox::Cancel:
225                 // Cancel selection operation and leave the curve untouched.
226                 selectionModel()->select(index, QItemSelectionModel::ClearAndSelect);
227                 return;
228 
229             default:
230                 // should never be reachedDiscard
231                 break;
232             }
233         }
234     }
235 
236     for (const auto &index : selected.indexes()) {
237         QVariant curveData = model()->data(index, ItemRole_Data);
238         if (curveData.isValid())
239             emit presetChanged(curveData.value<EasingCurve>());
240     }
241 }
242 
hasSelection() const243 bool PresetList::hasSelection() const
244 {
245     return selectionModel()->hasSelection();
246 }
247 
dirty(const QModelIndex & index) const248 bool PresetList::dirty(const QModelIndex &index) const
249 {
250     return model()->data(index, ItemRole_Dirty).toBool();
251 }
252 
index() const253 int PresetList::index() const
254 {
255     return m_index;
256 }
257 
isEditable(const QModelIndex & index) const258 bool PresetList::isEditable(const QModelIndex &index) const
259 {
260     QFlags<Qt::ItemFlag> flags(model()->flags(index));
261     return flags.testFlag(Qt::ItemIsEditable);
262 }
263 
backgroundColor() const264 QColor PresetList::backgroundColor() const
265 {
266     return m_background;
267 }
268 
curveColor() const269 QColor PresetList::curveColor() const
270 {
271     return m_curveColor;
272 }
273 
initialize(int index)274 void PresetList::initialize(int index)
275 {
276     m_index = index;
277 
278     readPresets();
279 }
280 
readPresets()281 void PresetList::readPresets()
282 {
283     auto *simodel = qobject_cast<QStandardItemModel *>(model());
284 
285     simodel->clear();
286 
287     QList<NamedEasingCurve> curves = storedCurves();
288 
289     for (int i = 0; i < curves.size(); ++i) {
290         QVariant curveData = QVariant::fromValue(curves[i].curve());
291 
292         auto *item = new QStandardItem(paintPreview(curves[i].curve(), m_background, m_curveColor), curves[i].name());
293         item->setData(curveData, ItemRole_Data);
294         item->setEditable(m_scope == QSettings::UserScope);
295         item->setToolTip(curves[i].name());
296 
297         simodel->setItem(i, item);
298     }
299 }
300 
writePresets()301 void PresetList::writePresets()
302 {
303     QList<QVariant> presets;
304     for (int i = 0; i < model()->rowCount(); ++i) {
305         QModelIndex index = model()->index(i, 0);
306 
307         QVariant nameData = model()->data(index, Qt::DisplayRole);
308         QVariant curveData = model()->data(index, ItemRole_Data);
309 
310         if (nameData.isValid() && curveData.isValid()) {
311             NamedEasingCurve curve(nameData.toString(), curveData.value<QmlDesigner::EasingCurve>());
312 
313             presets << QVariant::fromValue(curve);
314         }
315 
316         model()->setData(index, false, ItemRole_Dirty);
317     }
318 
319     QSettings settings(m_filename, QSettings::IniFormat);
320     settings.clear();
321     settings.setValue(Internal::settingsKey, QVariant::fromValue(presets));
322 }
323 
revert(const QModelIndex & index)324 void PresetList::revert(const QModelIndex &index)
325 {
326     auto *simodel = qobject_cast<QStandardItemModel *>(model());
327     if (auto *item = simodel->itemFromIndex(index)) {
328         QString name = item->data(Qt::DisplayRole).toString();
329         QList<NamedEasingCurve> curves = storedCurves();
330 
331         for (const auto &curve : curves) {
332             if (curve.name() == name) {
333                 item->setData(false, ItemRole_Dirty);
334                 item->setData(paintPreview(curve.curve(), m_background, m_curveColor), Qt::DecorationRole);
335                 item->setData(QVariant::fromValue(curve.curve()), ItemRole_Data);
336                 item->setToolTip(name);
337                 return;
338             }
339         }
340     }
341 }
342 
updateCurve(const EasingCurve & curve)343 void PresetList::updateCurve(const EasingCurve &curve)
344 {
345     if (!selectionModel()->hasSelection())
346         return;
347 
348     QVariant icon = QVariant::fromValue(paintPreview(curve, m_background, m_curveColor));
349     QVariant curveData = QVariant::fromValue(curve);
350 
351     for (const auto &index : selectionModel()->selectedIndexes())
352         setItemData(index, curveData, icon);
353 }
354 
contextMenuEvent(QContextMenuEvent * event)355 void PresetList::contextMenuEvent(QContextMenuEvent *event)
356 {
357     event->accept();
358 
359     if (m_scope == QSettings::SystemScope)
360         return;
361 
362     auto *menu = new QMenu(this);
363 
364     QAction *addAction = menu->addAction(tr("Add Preset"));
365 
366     connect(addAction, &QAction::triggered, [&]() { createItem(); });
367 
368     if (selectionModel()->hasSelection()) {
369         QAction *removeAction = menu->addAction(tr("Delete Selected Preset"));
370         connect(removeAction, &QAction::triggered, [&]() { removeSelectedItem(); });
371     }
372 
373     menu->exec(event->globalPos());
374     menu->deleteLater();
375 }
376 
dataChanged(const QModelIndex & topLeft,const QModelIndex & bottomRight,const QVector<int> & roles)377 void PresetList::dataChanged(const QModelIndex &topLeft,
378                              const QModelIndex &bottomRight,
379                              const QVector<int> &roles)
380 {
381     if (topLeft == bottomRight && roles.contains(0)) {
382         QVariant name = model()->data(topLeft, 0);
383         model()->setData(topLeft, name, Qt::ToolTipRole);
384     }
385 }
386 
createItem()387 void PresetList::createItem()
388 {
389     EasingCurve curve;
390     curve.makeDefault();
391     createItem(createUniqueName(), curve);
392 }
393 
createItem(const QString & name,const EasingCurve & curve)394 void PresetList::createItem(const QString &name, const EasingCurve &curve)
395 {
396     auto *item = new QStandardItem(paintPreview(curve, m_background, m_curveColor), name);
397     item->setData(QVariant::fromValue(curve), ItemRole_Data);
398     item->setToolTip(name);
399 
400     int row = model()->rowCount();
401     qobject_cast<QStandardItemModel *>(model())->setItem(row, item);
402 
403     QModelIndex index = model()->index(row, 0);
404 
405     // Why is that needed? SingleSelection is specified.
406     selectionModel()->clear();
407     selectionModel()->select(index, QItemSelectionModel::Select);
408 }
409 
removeSelectedItem()410 void PresetList::removeSelectedItem()
411 {
412     for (const auto &index : selectionModel()->selectedIndexes())
413         model()->removeRow(index.row());
414 
415     writePresets();
416 }
417 
setItemData(const QModelIndex & index,const QVariant & curve,const QVariant & icon)418 void PresetList::setItemData(const QModelIndex &index, const QVariant &curve, const QVariant &icon)
419 {
420     if (isEditable(index)) {
421         model()->setData(index, curve, PresetList::ItemRole_Data);
422         model()->setData(index, true, PresetList::ItemRole_Dirty);
423         model()->setData(index, icon, Qt::DecorationRole);
424     }
425 }
426 
createUniqueName() const427 QString PresetList::createUniqueName() const
428 {
429     QStringList names = allNames();
430     auto nameIsUnique = [&](const QString &name) {
431         auto iter = std::find(names.begin(), names.end(), name);
432         if (iter == names.end())
433             return true;
434         else
435             return false;
436     };
437 
438     int counter = 0;
439     QString tmp("Default");
440     QString name = tmp;
441 
442     while (!nameIsUnique(name))
443         name = tmp + QString(" %1").arg(counter++);
444 
445     return name;
446 }
447 
allNames() const448 QStringList PresetList::allNames() const
449 {
450     QStringList names;
451     for (int i = 0; i < model()->rowCount(); ++i) {
452         QModelIndex index = model()->index(i, 0);
453         QVariant nameData = model()->data(index, Qt::DisplayRole);
454         if (nameData.isValid())
455             names << nameData.toString();
456     }
457 
458     return names;
459 }
460 
storedCurves() const461 QList<NamedEasingCurve> PresetList::storedCurves() const
462 {
463     QSettings settings(m_filename, QSettings::IniFormat);
464     QVariant presetSettings = settings.value(Internal::settingsKey);
465 
466     if (!presetSettings.isValid())
467         return QList<NamedEasingCurve>();
468 
469     QList<QVariant> presets = presetSettings.toList();
470 
471     QList<NamedEasingCurve> out;
472     for (const QVariant &preset : presets)
473         if (preset.isValid())
474             out << preset.value<NamedEasingCurve>();
475 
476     return out;
477 }
478 
PresetEditor(QWidget * parent)479 PresetEditor::PresetEditor(QWidget *parent)
480     : QStackedWidget(parent)
481     , m_presets(new PresetList(QSettings::SystemScope))
482     , m_customs(new PresetList(QSettings::UserScope))
483 {
484     setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
485 
486     addWidget(m_presets);
487     addWidget(m_customs);
488 
489     connect(m_presets, &PresetList::presetChanged, this, &PresetEditor::presetChanged);
490     connect(m_customs, &PresetList::presetChanged, this, &PresetEditor::presetChanged);
491 }
492 
initialize(QTabBar * bar)493 void PresetEditor::initialize(QTabBar *bar)
494 {
495     m_presets->initialize(bar->addTab("Presets"));
496     m_customs->initialize(bar->addTab("Custom"));
497 
498     connect(bar, &QTabBar::currentChanged, this, &PresetEditor::activate);
499     connect(this, &PresetEditor::currentChanged, bar, &QTabBar::setCurrentIndex);
500 
501     m_presets->selectionModel()->clear();
502     m_customs->selectionModel()->clear();
503 
504     activate(m_presets->index());
505 }
506 
activate(int id)507 void PresetEditor::activate(int id)
508 {
509     if (id == m_presets->index())
510         setCurrentWidget(m_presets);
511     else
512         setCurrentWidget(m_customs);
513 }
514 
update(const EasingCurve & curve)515 void PresetEditor::update(const EasingCurve &curve)
516 {
517     if (isCurrent(m_presets))
518         m_presets->selectionModel()->clear();
519     else {
520         if (m_customs->selectionModel()->hasSelection()) {
521             QVariant icon = QVariant::fromValue(
522                 paintPreview(curve, m_presets->backgroundColor(), m_presets->curveColor()));
523             QVariant curveData = QVariant::fromValue(curve);
524             for (const QModelIndex &index : m_customs->selectionModel()->selectedIndexes())
525                 m_customs->setItemData(index, curveData, icon);
526         }
527     }
528 }
529 
writePresets(const EasingCurve & curve)530 bool PresetEditor::writePresets(const EasingCurve &curve)
531 {
532     if (!curve.isLegal()) {
533         QMessageBox msgBox;
534         msgBox.setText("Attempting to save invalid curve");
535         msgBox.setInformativeText("Please solve the issue before proceeding.");
536         msgBox.setStandardButtons(QMessageBox::Ok);
537         msgBox.exec();
538         return false;
539     }
540 
541     if (auto current = qobject_cast<const PresetList *>(currentWidget())) {
542         if (current->index() == m_presets->index()
543             || (current->index() == m_customs->index() && !m_customs->hasSelection())) {
544             bool ok;
545             QString name = QInputDialog::getText(this,
546                                                  tr("Save Preset"),
547                                                  tr("Name"),
548                                                  QLineEdit::Normal,
549                                                  QString(),
550                                                  &ok);
551 
552             if (ok && !name.isEmpty()) {
553                 activate(m_customs->index());
554                 m_customs->createItem(name, curve);
555             }
556         }
557 
558         m_customs->writePresets();
559         return true;
560     }
561 
562     return false;
563 }
564 
isCurrent(PresetList * list)565 bool PresetEditor::isCurrent(PresetList *list)
566 {
567     if (auto current = qobject_cast<const PresetList *>(currentWidget()))
568         return list->index() == current->index();
569 
570     return false;
571 }
572 
573 } // namespace QmlDesigner
574