1 /*
2  * Copyright (c) 2015-2020, The University of Oxford
3  * All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions are met:
7  * 1. Redistributions of source code must retain the above copyright notice,
8  *    this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright notice,
10  *    this list of conditions and the following disclaimer in the documentation
11  *    and/or other materials provided with the distribution.
12  * 3. Neither the name of the University of Oxford nor the names of its
13  *    contributors may be used to endorse or promote products derived from this
14  *    software without specific prior written permission.
15  *
16  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
20  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26  * POSSIBILITY OF SUCH DAMAGE.
27  */
28 
29 #include "gui/oskar_SettingsModel.h"
30 #include "settings/oskar_settings_types.h"
31 #include "settings/oskar_SettingsTree.h"
32 #include "settings/oskar_SettingsNode.h"
33 #include "settings/oskar_SettingsKey.h"
34 #include "settings/oskar_SettingsValue.h"
35 #include <QApplication>
36 #include <QFile>
37 #include <QFileInfo>
38 #include <QFontMetrics>
39 #include <QIcon>
40 #include <QModelIndex>
41 #include <QPalette>
42 #include <QSize>
43 #include <QStringList>
44 #include <QVariant>
45 
46 using namespace std;
47 
48 namespace oskar {
49 
SettingsModel(SettingsTree * settings,QObject * parent)50 SettingsModel::SettingsModel(SettingsTree* settings, QObject* parent)
51 : QAbstractItemModel(parent),
52   settings_(settings),
53   iconOpen_(QIcon(":/icons/open.png")),
54   iconSave_(QIcon(":/icons/save.png")),
55   lastModified_(QDateTime::currentDateTime()),
56   displayKey_(false)
57 {
58 }
59 
~SettingsModel()60 SettingsModel::~SettingsModel()
61 {
62 }
63 
beginReset()64 void SettingsModel::beginReset()
65 {
66     beginResetModel();
67 }
68 
endReset()69 void SettingsModel::endReset()
70 {
71     endResetModel();
72 }
73 
columnCount(const QModelIndex &) const74 int SettingsModel::columnCount(const QModelIndex& /*parent*/) const
75 {
76     return 2;
77 }
78 
data(const QModelIndex & index,int role) const79 QVariant SettingsModel::data(const QModelIndex& index, int role) const
80 {
81     // Get a pointer to the item.
82     if (!index.isValid())
83         return QVariant();
84 
85     const SettingsNode* node = get_node(index);
86     const SettingsValue& value = node->settings_value();
87     const SettingsValue::TypeId type = value.type();
88     const char* key = node->key();
89 
90     // Check for roles common to all columns.
91     switch (role)
92     {
93     case Qt::ForegroundRole:
94     {
95         QPalette palette = QApplication::palette((QWidget*) 0);
96         if (!settings_->dependencies_satisfied(key))
97             return palette.color(QPalette::Disabled, QPalette::Text);
98         if (settings_->is_critical(key))
99             return QColor(Qt::white);
100         if (node->value_or_child_set())
101             return palette.color(QPalette::Normal, QPalette::Link);
102         return palette.color(QPalette::Normal, QPalette::Text);
103     }
104     case Qt::BackgroundRole:
105     {
106         bool disabled = !settings_->dependencies_satisfied(key);
107         if (settings_->is_critical(key) && !disabled)
108         {
109             if (index.column() == 0)
110                 return QColor(0, 48, 255, 160);
111             else if (node->item_type() != SettingsItem::LABEL)
112                 return QColor(255, 64, 64, 255);
113         }
114         if (index.column() == 1)
115             return QColor(0, 0, 192, 12);
116         break;
117     }
118     case Qt::ToolTipRole:
119     {
120         QString tooltip = QString(node->description());
121         if (!tooltip.isEmpty())
122         {
123             tooltip = "<p>" + tooltip + "</p>";
124             if (node->is_required())
125                 tooltip.append(" [Required]");
126 //            if (node->item_type() == SettingsItem::SETTING)
127 //                tooltip.append(" [" + QString::fromStdString(
128 //                        node->value().type_name()) + "]");
129         }
130         return tooltip;
131     }
132     case Qt::EditRole:
133     {
134         if (node->item_type() == SettingsItem::SETTING)
135             return QString(node->value());
136         break;
137     }
138     case KeyRole:
139         return QString(key);
140     case ValueRole:
141         return QString(node->value());
142     case DefaultRole:
143         return QString(node->default_value());
144     case TypeRole:
145         return type;
146     case ItemTypeRole:
147         return node->item_type();
148     case RangeRole:
149     {
150         QList<QVariant> range;
151         switch (type)
152         {
153         case SettingsValue::INT_RANGE:
154         {
155             range.append(value.get<IntRange>().min());
156             range.append(value.get<IntRange>().max());
157             return range;
158         }
159         case SettingsValue::DOUBLE_RANGE:
160         {
161             range.append(value.get<DoubleRange>().min());
162             range.append(value.get<DoubleRange>().max());
163             return range;
164         }
165         default:
166             break;
167         }
168         return range;
169     }
170     case ExtRangeRole:
171     {
172         QList<QVariant> range;
173         switch (type)
174         {
175         case SettingsValue::INT_RANGE_EXT:
176         {
177             range.append(value.get<IntRangeExt>().min());
178             range.append(value.get<IntRangeExt>().max());
179             range.append(QString(value.get<IntRangeExt>().ext_min()));
180             range.append(QString(value.get<IntRangeExt>().ext_max()));
181             return range;
182         }
183         case SettingsValue::DOUBLE_RANGE_EXT:
184         {
185             range.append(value.get<DoubleRangeExt>().min());
186             range.append(value.get<DoubleRangeExt>().max());
187             range.append(QString(value.get<DoubleRangeExt>().ext_min()));
188             range.append(QString(value.get<DoubleRangeExt>().ext_max()));
189             return range;
190         }
191         default:
192             break;
193         }
194         return range;
195     }
196     case OptionsRole:
197     {
198         QStringList options;
199         if (type == SettingsValue::OPTION_LIST)
200         {
201             const OptionList& l = value.get<OptionList>();
202             for (int i = 0; i < l.size(); ++i)
203                 options.push_back(QString(l.option(i)));
204         }
205         return options;
206     }
207     default:
208         break;
209     }
210 
211     // Check for roles in specific columns.
212     if (index.column() == 0)
213     {
214         switch (role)
215         {
216         case Qt::SizeHintRole:
217         {
218             int width = QApplication::fontMetrics().width(
219                     (data(index, Qt::DisplayRole)).toString()) + 20;
220             return QSize(width, 26);
221         }
222         case Qt::DisplayRole:
223             return displayKey_ ? QString(key) : QString(node->label());
224         default:
225             break;
226         }
227     }
228     else if (index.column() == 1)
229     {
230         switch (role)
231         {
232         case Qt::DisplayRole:
233         {
234             if (node->item_type() == SettingsItem::SETTING)
235                 return QString(node->value());
236             break;
237         }
238         case Qt::CheckStateRole:
239         {
240             if (type == SettingsValue::BOOL)
241                 return value.get<Bool>().value() ? Qt::Checked : Qt::Unchecked;
242             break;
243         }
244         case Qt::DecorationRole:
245         {
246             if (type == SettingsValue::INPUT_FILE ||
247                     type == SettingsValue::INPUT_FILE_LIST ||
248                     type == SettingsValue::INPUT_DIRECTORY)
249                 return iconOpen_;
250             else if (type == SettingsValue::OUTPUT_FILE)
251                 return iconSave_;
252             break;
253         }
254         default:
255             break;
256         }
257     }
258 
259     return QVariant();
260 }
261 
flags(const QModelIndex & index) const262 Qt::ItemFlags SettingsModel::flags(const QModelIndex& index) const
263 {
264     if (!index.isValid())
265         return 0;
266 
267     const SettingsNode* node = get_node(index);
268     if (!settings_->dependencies_satisfied(node->key()))
269         return Qt::ItemIsSelectable;
270     int column = index.column();
271     if (column == 0 || node->item_type() == SettingsItem::LABEL)
272         return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
273     if (column == 1 && node->settings_value().type() == SettingsValue::BOOL)
274         return Qt::ItemIsEnabled | Qt::ItemIsSelectable |  Qt::ItemIsUserCheckable;
275 
276     return Qt::ItemIsEditable | Qt::ItemIsEnabled | Qt::ItemIsSelectable;
277 }
278 
headerData(int section,Qt::Orientation orientation,int role) const279 QVariant SettingsModel::headerData(int section,
280         Qt::Orientation orientation, int role) const
281 {
282     if (orientation == Qt::Horizontal && role == Qt::DisplayRole)
283     {
284         if (section == 0)
285             return "Setting";
286         else if (section == 1)
287             return "Value";
288     }
289 
290     return QVariant();
291 }
292 
index(int row,int column,const QModelIndex & parent) const293 QModelIndex SettingsModel::index(int row, int column,
294         const QModelIndex& parent) const
295 {
296     if (parent.isValid() && parent.column() != 0)
297         return QModelIndex();
298 
299     const SettingsNode* child_node = get_node(parent)->child(row);
300     // This is from the Qt documentation: const_cast is unavoidable here.
301     SettingsNode* node = const_cast<SettingsNode*>(child_node);
302     if (child_node)
303         return createIndex(row, column, static_cast<void*>(node));
304     else
305         return QModelIndex();
306 }
307 
load_settings_file(const QString & filename)308 void SettingsModel::load_settings_file(const QString& filename)
309 {
310     if (!filename.isEmpty()) filename_ = filename;
311     lastModified_ = QDateTime::currentDateTime();
312     settings_->load(filename.toLatin1().constData());
313     refresh(QModelIndex());
314 }
315 
save_settings_file(const QString & filename)316 void SettingsModel::save_settings_file(const QString& filename)
317 {
318     if (!filename.isEmpty()) filename_ = filename;
319     lastModified_ = QDateTime::currentDateTime();
320     settings_->save(filename.toLatin1().constData());
321 }
322 
parent(const QModelIndex & index) const323 QModelIndex SettingsModel::parent(const QModelIndex& index) const
324 {
325     if (!index.isValid())
326         return QModelIndex();
327 
328     const SettingsNode* parent = get_node(index)->parent();
329     if (parent == settings_->root_node())
330         return QModelIndex();
331 
332     SettingsNode* node = const_cast<SettingsNode*>(parent);
333     return createIndex(parent->child_number(), 0, static_cast<void*>(node));
334 }
335 
refresh()336 void SettingsModel::refresh()
337 {
338     refresh(QModelIndex());
339 }
340 
rowCount(const QModelIndex & parent) const341 int SettingsModel::rowCount(const QModelIndex& parent) const
342 {
343     return get_node(parent)->num_children();
344 }
345 
setData(const QModelIndex & idx,const QVariant & value,int role)346 bool SettingsModel::setData(const QModelIndex& idx, const QVariant& value,
347                             int role)
348 {
349     // Check for roles that do not depend on the index.
350     if (role == CheckExternalChangesRole)
351     {
352         if (!QFile::exists(filename_)) return false;
353         QFileInfo fileInfo(filename_);
354         if (fileInfo.lastModified() > lastModified_.addMSecs(200))
355         {
356             load_settings_file(filename_);
357             lastModified_ = QDateTime::currentDateTime();
358             emit fileReloaded();
359         }
360         return true;
361     }
362     else if (role == DisplayKeysRole)
363     {
364         displayKey_ = value.toBool();
365         refresh(QModelIndex());
366         return true;
367     }
368 
369     if (!idx.isValid())
370         return false;
371 
372     // Get a pointer to the item.
373     const SettingsNode* node = get_node(idx);
374 
375     // Get model indexes for the row.
376     QModelIndex topLeft = idx.sibling(idx.row(), 0);
377     QModelIndex bottomRight = idx.sibling(idx.row(), columnCount() - 1);
378 
379     // Check for role type.
380     if (role == Qt::EditRole || role == Qt::CheckStateRole)
381     {
382         QVariant data = value;
383         if (role == Qt::CheckStateRole)
384             data = value.toBool() ? QString("true") : QString("false");
385 
386         if (idx.column() == 1)
387         {
388             lastModified_ = QDateTime::currentDateTime();
389             QString value;
390             if (node->settings_value().type() == SettingsValue::INPUT_FILE_LIST)
391             {
392                 QStringList l = data.toStringList();
393                 for (int i = 0; i < l.size(); ++i) {
394                     value += l[i];
395                     if (i < l.size()) value += ",";
396                 }
397             }
398             else {
399                 value = data.toString();
400             }
401             settings_->set_value(node->key(), value.toLatin1().constData());
402 
403             QModelIndex i(idx);
404             while (i.isValid())
405             {
406                 emit dataChanged(i.sibling(i.row(), 0),
407                                  i.sibling(i.row(), columnCount()-1));
408                 i = i.parent();
409             }
410             emit dataChanged(topLeft, bottomRight);
411             return true;
412         }
413     }
414     else if (role == SettingsModel::ResetGroupRole)
415     {
416         if (idx.column() == 1) {
417             reset_group_(node);
418             lastModified_ = QDateTime::currentDateTime();
419             // TODO(BM) call dataChanged on all children and parents too.
420             // seems to work at the moment on the basis of luck or the right
421             // click action used to call reset calling a redraw.
422             emit dataChanged(topLeft, bottomRight);
423 //            QModelIndex i(idx);
424 //            while (i.isValid())
425 //            {
426 //                emit dataChanged(i.sibling(i.row(), 0),
427 //                                 i.sibling(i.row(), columnCount()-1));
428 //                i = i.parent();
429 //            }
430             return true;
431         }
432     }
433     return false;
434 }
435 
436 
437 // Private methods.
438 
reset_group_(const SettingsNode * node)439 void SettingsModel::reset_group_(const SettingsNode* node)
440 {
441     for (int i = 0; i < node->num_children(); ++i) {
442         const SettingsNode* child = node->child(i);
443         settings_->set_value(child->key(), child->default_value());
444         reset_group_(child);
445     }
446 }
447 
get_node(const QModelIndex & index) const448 const SettingsNode* SettingsModel::get_node(const QModelIndex& index) const
449 {
450     if (index.isValid())
451     {
452         SettingsNode* node = static_cast<SettingsNode*>(index.internalPointer());
453         if (node) return node;
454     }
455     return settings_->root_node();
456 }
457 
refresh(const QModelIndex & parent)458 void SettingsModel::refresh(const QModelIndex& parent)
459 {
460     int rows = rowCount(parent);
461     for (int i = 0; i < rows; ++i)
462     {
463         QModelIndex idx = index(i, 0, parent);
464         if (idx.isValid())
465         {
466             emit dataChanged(idx, idx.sibling(idx.row(), 1));
467             refresh(idx);
468         }
469     }
470 }
471 
472 
SettingsModelFilter(QObject * parent)473 SettingsModelFilter::SettingsModelFilter(QObject* parent)
474 : QSortFilterProxyModel(parent)
475 {
476     setDynamicSortFilter(true);
477 }
478 
~SettingsModelFilter()479 SettingsModelFilter::~SettingsModelFilter()
480 {
481 }
482 
data(const QModelIndex & index,int role) const483 QVariant SettingsModelFilter::data(const QModelIndex& index, int role) const
484 {
485     if (!filterRegExp().isEmpty())
486     {
487         if (role == Qt::BackgroundRole && index.column() == 0)
488         {
489             QString label = QSortFilterProxyModel::data(index,
490                     Qt::DisplayRole).toString();
491             if (label.contains(filterRegExp().pattern(), Qt::CaseInsensitive))
492                 return QColor("#FFFF9F");
493         }
494     }
495     return QSortFilterProxyModel::data(index, role);
496 }
497 
setFilterRegExp(const QString & pattern)498 void SettingsModelFilter::setFilterRegExp(const QString& pattern)
499 {
500     QSortFilterProxyModel::setFilterRegExp(pattern);
501     invalidate();
502 }
503 
504 // Protected methods.
505 
filterAcceptsChildren(int sourceRow,const QModelIndex & sourceParent) const506 bool SettingsModelFilter::filterAcceptsChildren(int sourceRow,
507         const QModelIndex& sourceParent) const
508 {
509     QModelIndex idx = sourceModel()->index(sourceRow, 0, sourceParent);
510     if (!idx.isValid())
511         return false;
512 
513     int childCount = idx.model()->rowCount(idx);
514     for (int i = 0; i < childCount; ++i)
515     {
516         if (filterAcceptsCurrentRow(i, idx))
517             return true;
518         if (filterAcceptsChildren(i, idx))
519             return true;
520     }
521     return false;
522 }
523 
filterAcceptsCurrentRow(const QModelIndex & idx) const524 bool SettingsModelFilter::filterAcceptsCurrentRow(const QModelIndex& idx) const
525 {
526     QString labelCurrent = sourceModel()->data(idx, Qt::DisplayRole).toString();
527     return labelCurrent.contains(filterRegExp().pattern(), Qt::CaseInsensitive);
528 }
529 
filterAcceptsCurrentRow(int sourceRow,const QModelIndex & sourceParent) const530 bool SettingsModelFilter::filterAcceptsCurrentRow(int sourceRow,
531             const QModelIndex& sourceParent) const
532 {
533     QModelIndex idx = sourceModel()->index(sourceRow, 0, sourceParent);
534     return filterAcceptsCurrentRow(idx);
535 }
536 
filterAcceptsRow(int sourceRow,const QModelIndex & sourceParent) const537 bool SettingsModelFilter::filterAcceptsRow(int sourceRow,
538             const QModelIndex& sourceParent) const
539 {
540     // Check if filter accepts this row.
541     QModelIndex idx = sourceModel()->index(sourceRow, 0, sourceParent);
542     if (filterAcceptsCurrentRow(idx))
543         return true;
544 
545     // Check if filter accepts any parent.
546     QModelIndex parent = sourceParent;
547     while (parent.isValid())
548     {
549         if (filterAcceptsCurrentRow(parent.row(), parent.parent()))
550             return true;
551         parent = parent.parent();
552     }
553 
554     // Check if filter accepts any child.
555     if (filterAcceptsChildren(sourceRow, sourceParent))
556         return true;
557 
558     return false;
559 }
560 
561 } // namespace oskar
562