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