/****************************************************************************** * The MIT License (MIT) * * Copyright (c) 2020-2021 Baldur Karlsson * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. ******************************************************************************/ #include "ConfigEditor.h" #include #include #include #include #include "Code/QRDUtils.h" #include "Code/Resources.h" #include "Widgets/Extended/RDHeaderView.h" #include "Widgets/Extended/RDLineEdit.h" #include "Widgets/OrderedListEditor.h" #include "ui_ConfigEditor.h" static QString valueString(const SDObject *o) { if(o->type.basetype == SDBasic::String) return o->data.str; if(o->type.basetype == SDBasic::UnsignedInteger) return Formatter::Format(o->data.basic.u); if(o->type.basetype == SDBasic::SignedInteger) return Formatter::Format(o->data.basic.i); if(o->type.basetype == SDBasic::Float) return Formatter::Format(o->data.basic.d); if(o->type.basetype == SDBasic::Boolean) return o->data.basic.b ? lit("True") : lit("False"); if(o->type.basetype == SDBasic::Array) return lit("{...}"); return lit("??"); } static bool anyChildChanged(const SDObject *o) { const SDObject *def = o->FindChild("default"); const SDObject *val = o->FindChild("value"); if(val && def) return !val->HasEqualValue(def); for(const SDObject *c : *o) { if(anyChildChanged(c)) return true; } return false; } class SettingModel : public QAbstractItemModel { public: SettingModel(ConfigEditor *view) : QAbstractItemModel(view), m_Viewer(view) { populateParents(m_Viewer->m_Config, QModelIndex()); } void refresh() { emit beginResetModel(); emit endResetModel(); } QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override { SDObject *o = obj(parent); if(row < 0 || row > rowCount(parent)) return QModelIndex(); return createIndex(row, column, o->GetChild(row)); } QModelIndex parent(const QModelIndex &index) const override { SDObject *o = obj(index); if(o == m_Viewer->m_Config) return QModelIndex(); QModelIndex ret = parents[o]; if(!ret.isValid()) return ret; return createIndex(ret.row(), index.column(), ret.internalPointer()); } int rowCount(const QModelIndex &parent = QModelIndex()) const override { SDObject *o = obj(parent); // values don't have children if(o->FindChild("value")) return 0; return (int)o->NumChildren(); } enum Columns { Column_Name, Column_Value, Column_ResetButton, Column_Count, }; Qt::ItemFlags flags(const QModelIndex &index) const override { if(!index.isValid()) return 0; Qt::ItemFlags ret = QAbstractItemModel::flags(index); if(index.column() == Column_Value) { SDObject *o = obj(index); SDObject *value = o->FindChild("value"); if(value) { ret |= Qt::ItemIsEditable; if(value->type.basetype == SDBasic::Boolean) ret |= Qt::ItemIsUserCheckable; } } return ret; } int columnCount(const QModelIndex &parent = QModelIndex()) const override { return Column_Count; } QVariant headerData(int section, Qt::Orientation orientation, int role) const override { if(orientation == Qt::Horizontal && role == Qt::DisplayRole) { switch(section) { case Column_Name: return lit("Setting"); case Column_Value: return lit("Value"); case Column_ResetButton: return lit("Reset"); default: break; } } return QVariant(); } bool setData(const QModelIndex &index, const QVariant &val, int role) override { SDObject *o = NULL; if(role == Qt::UserRole) { // if we have setData for user role, that means we got reset. Just need to emit data changed o = obj(index); } else if(index.column() == Column_Value) { o = obj(index); SDObject *value = o->FindChild("value"); if(role == Qt::CheckStateRole && value) { value->data.basic.b = (val.toInt() == Qt::CheckState::Checked); } else { // didn't change anything we care about o = NULL; } } if(o) { // dataChanged this index and all parents (in case a section became non-customised, or // customised, and it wasn't before) QModelIndex idx = index; while(idx.isValid()) { o = obj(idx); emit dataChanged(createIndex(idx.row(), 0, o), createIndex(idx.row(), Column_Count, o), {Qt::DisplayRole, Qt::CheckStateRole, Qt::FontRole}); idx = parents[o]; } return true; } return false; } QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override { if(index.isValid()) { SDObject *o = obj(index); int col = index.column(); SDObject *value = o->FindChild("value"); if(role == Qt::UserRole) { return QVariant::fromValue((quintptr)o); } else if(role == Qt::DisplayRole) { switch(col) { case Column_Name: return o->name; case Column_Value: return value && value->type.basetype != SDBasic::Boolean ? valueString(value) : QVariant(); case Column_ResetButton: return anyChildChanged(o) ? lit("...") : QVariant(); default: break; } } else if(role == Qt::CheckStateRole && col == Column_Value) { if(value && value->type.basetype == SDBasic::Boolean) return value->data.basic.b ? Qt::CheckState::Checked : Qt::CheckState::Unchecked; return QVariant(); } else if(role == Qt::TextAlignmentRole && col == Column_ResetButton) { return Qt::AlignHCenter + Qt::AlignTop; } else if(role == Qt::ToolTipRole) { SDObject *desc = o->FindChild("description"); if(desc) { rdcstr ret = desc->AsString(); if(o->FindChild("key") == NULL) { ret = "WARNING: Unknown setting, possibly it has been removed or from a different " "build.\n\n" + ret; } return ret; } } else if(role == Qt::FontRole) { if(anyChildChanged(o)) { QFont font; font.setBold(true); return font; } // if this is a value but has no key, it's an unrecognised setting (stale/removed, or from // a different or future build). if(o->FindChild("description") && o->FindChild("key") == NULL) { QFont font; font.setItalic(true); return font; } } } return QVariant(); } private: SDObject *obj(const QModelIndex &parent) const { SDObject *ret = (SDObject *)parent.internalPointer(); if(ret == NULL) ret = m_Viewer->m_Config; return ret; } // Qt models need child->parent relationships. We don't have that with SDObject but they are // immutable so we can cache them QMap parents; void populateParents(SDObject *o, QModelIndex parent) { if(o->FindChild("value")) return; int i = 0; for(SDObject *c : *o) { parents[c] = parent; populateParents(c, index(i++, 0, parent)); } } ConfigEditor *m_Viewer; }; class SettingFilterModel : public QSortFilterProxyModel { public: explicit SettingFilterModel(ConfigEditor *view) : QSortFilterProxyModel(view), m_Viewer(view) {} void setFilter(QString text) { m_Text = text; m_KeyText = m_Text; m_KeyText.replace(QLatin1Char('.'), QLatin1Char('_')); emit beginResetModel(); emit endResetModel(); } protected: virtual bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override { if(m_Text.isEmpty()) return true; SDObject *o = obj(source_parent); return matchesAnyChild(o->GetChild(source_row)); } bool matchesAnyChild(SDObject *o) const { if(!o) return false; if(QString(o->name).contains(m_Text, Qt::CaseInsensitive)) return true; if(o->FindChild("value")) { if(o->FindChild("key") && QString(o->FindChild("key")->AsString()).contains(m_KeyText, Qt::CaseInsensitive)) return true; return false; } for(SDObject *c : *o) if(matchesAnyChild(c)) return true; return false; } private: SDObject *obj(const QModelIndex &parent) const { SDObject *ret = (SDObject *)parent.internalPointer(); if(ret == NULL) ret = m_Viewer->m_Config; return ret; } ConfigEditor *m_Viewer; QString m_Text; QString m_KeyText; }; SettingDelegate::SettingDelegate(ConfigEditor *editor, RDTreeView *parent) : QStyledItemDelegate(parent), m_Editor(editor), m_View(parent) { } SettingDelegate::~SettingDelegate() { } void SettingDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { if(index.column() == SettingModel::Column_ResetButton) { SDObject *o = (SDObject *)index.data(Qt::UserRole).toULongLong(); SDObject *def = o->FindChild("default"); SDObject *val = o->FindChild("value"); if(val && def && !val->HasEqualValue(def)) { // draw the item without text, so we get the proper background/selection/etc. // we'd like to be able to use the parent delegate's paint here, but either it calls to // QStyledItemDelegate which will re-fetch the text (bleh), or it calls to the manual // delegate which could do anything. So for this case we just use the style and skip the // delegate and hope it works out. QStyleOptionViewItem opt = option; QStyledItemDelegate::initStyleOption(&opt, index); opt.text.clear(); m_Editor->style()->drawControl(QStyle::CE_ItemViewItem, &opt, painter, m_Editor); QStyleOptionToolButton buttonOpt; int size = m_Editor->style()->pixelMetric(QStyle::PM_SmallIconSize, 0, m_Editor); buttonOpt.iconSize = QSize(size, size); buttonOpt.subControls = 0; buttonOpt.activeSubControls = 0; buttonOpt.features = QStyleOptionToolButton::None; buttonOpt.arrowType = Qt::NoArrow; buttonOpt.state = QStyle::State_Active | QStyle::State_Enabled | QStyle::State_AutoRaise; buttonOpt.rect = option.rect.adjusted(0, 0, -1, -1); buttonOpt.icon = Icons::arrow_undo(); if(m_View->currentHoverIndex() == index) buttonOpt.state |= QStyle::State_MouseOver; m_Editor->style()->drawComplexControl(QStyle::CC_ToolButton, &buttonOpt, painter, m_Editor); return; } } return QStyledItemDelegate::paint(painter, option, index); } QSize SettingDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { return QStyledItemDelegate::sizeHint(option, index); } bool SettingDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) { if(event->type() == QEvent::MouseButtonRelease && index.column() == SettingModel::Column_ResetButton) { SDObject *o = (SDObject *)index.data(Qt::UserRole).toULongLong(); SDObject *def = o->FindChild("default"); SDObject *val = o->FindChild("value"); if(def && val) { val->data.str = def->data.str; memcpy(&val->data.basic, &def->data.basic, sizeof(val->data.basic)); val->DeleteChildren(); for(size_t c = 0; c < def->NumChildren(); c++) val->DuplicateAndAddChild(def->GetChild(c)); // call setData() to emit the dataChanged for this element and all parents model->setData(index, QVariant(), Qt::UserRole); return true; } } return QStyledItemDelegate::editorEvent(event, model, option, index); } QWidget *SettingDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const { QWidget *ret = NULL; SDObject *o = (SDObject *)index.data(Qt::UserRole).toULongLong(); SDObject *val = o->FindChild("value"); if(val) { // bools should have checkboxes if(val->type.basetype == SDBasic::Boolean) { qWarning() << "Unexpected createEditor for boolean " << QString(o->name); return ret; } QString settingName; SDObject *key = o->FindChild("key"); if(key) settingName = key->AsString(); else settingName = tr("Unknown Setting %1").arg(o->name); // for numbers, provide a spinbox if(val->type.basetype == SDBasic::UnsignedInteger || val->type.basetype == SDBasic::SignedInteger) { QSpinBox *spin = new QSpinBox(parent); ret = spin; if(val->type.basetype == SDBasic::UnsignedInteger) spin->setMinimum(0); else spin->setMinimum(INT_MIN); spin->setMaximum(INT_MAX); } else if(val->type.basetype == SDBasic::Float) { QDoubleSpinBox *spin = new QDoubleSpinBox(parent); ret = spin; spin->setSingleStep(0.1); spin->setMinimum(-FLT_MAX); spin->setMaximum(FLT_MAX); } else if(val->type.basetype == SDBasic::String) { if(QString(o->name).contains(lit("DirPath"), Qt::CaseSensitive)) { QString dir = RDDialog::getExistingDirectory(m_Editor, tr("Browse for %1").arg(settingName)); if(!dir.isEmpty()) { val->data.str = dir; // we've handled the edit synchronously, don't create an edit widget ret = NULL; // call setData() to emit the dataChanged for this element and all parents m_View->model()->setData(index, QVariant(), Qt::UserRole); } } else if(QString(o->name).contains(lit("Path"), Qt::CaseSensitive)) { QString file = RDDialog::getOpenFileName(m_Editor, tr("Browse for %1").arg(settingName)); if(!file.isEmpty()) { val->data.str = file; // we've handled the edit synchronously, don't create an edit widget ret = NULL; // call setData() to emit the dataChanged for this element and all parents m_View->model()->setData(index, QVariant(), Qt::UserRole); } } else { RDLineEdit *line = new RDLineEdit(parent); ret = line; QObject::connect(line, &RDLineEdit::keyPress, this, &SettingDelegate::editorKeyPress); } } else if(val->type.basetype == SDBasic::Array) { // only support arrays of strings. Pop up a separate editor to handle this QDialog listEditor; listEditor.setWindowTitle(tr("Edit values of %1").arg(QString(settingName))); listEditor.setWindowFlags(listEditor.windowFlags() & ~Qt::WindowContextHelpButtonHint); BrowseMode mode = BrowseMode::None; if(QString(o->name).contains(lit("DirPath"), Qt::CaseSensitive)) mode = BrowseMode::Folder; else if(QString(o->name).contains(lit("Path"), Qt::CaseSensitive)) mode = BrowseMode::File; OrderedListEditor list(tr("Entry"), mode); QVBoxLayout layout; QDialogButtonBox okCancel; okCancel.setStandardButtons(QDialogButtonBox::Cancel | QDialogButtonBox::Ok); layout.addWidget(&list); layout.addWidget(&okCancel); QObject::connect(&okCancel, &QDialogButtonBox::accepted, &listEditor, &QDialog::accept); QObject::connect(&okCancel, &QDialogButtonBox::rejected, &listEditor, &QDialog::reject); listEditor.setLayout(&layout); QStringList items; for(SDObject *c : *val) items << c->data.str; list.setItems(items); int res = RDDialog::show(&listEditor); if(res) { items = list.getItems(); val->DeleteChildren(); val->ReserveChildren(items.size()); for(int i = 0; i < items.size(); i++) val->AddAndOwnChild(makeSDString("$el"_lit, items[i])); } // we've handled the edit synchronously, don't create an edit widget ret = NULL; // call setData() to emit the dataChanged for this element and all parents m_View->model()->setData(index, QVariant(), Qt::UserRole); } else { qWarning() << "Unexpected type of " << QString(o->name) << " to edit: " << ToQStr(val->type.basetype); } } return ret; } void SettingDelegate::editorKeyPress(QKeyEvent *ev) { QLineEdit *line = qobject_cast(QObject::sender()); if(ev->key() == Qt::Key_Return || ev->key() == Qt::Key_Enter) { commitData(line); closeEditor(line); } else if(ev->key() == Qt::Key_Escape) { closeEditor(line); } } void SettingDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const { SDObject *o = (SDObject *)index.data(Qt::UserRole).toULongLong(); SDObject *val = o->FindChild("value"); if(val) { if(val->type.basetype == SDBasic::Boolean) { qWarning() << "Unexpected setEditorData for boolean " << QString(o->name); return; } if(val->type.basetype == SDBasic::UnsignedInteger) qobject_cast(editor)->setValue(val->AsUInt32() & 0x7fffffffU); else if(val->type.basetype == SDBasic::SignedInteger) qobject_cast(editor)->setValue(val->AsInt32()); else if(val->type.basetype == SDBasic::Float) qobject_cast(editor)->setValue(val->AsDouble()); else if(val->type.basetype == SDBasic::String) qobject_cast(editor)->setText(val->AsString()); else qWarning() << "Unexpected type of " << QString(o->name) << ": " << ToQStr(val->type.basetype); } } void SettingDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const { SDObject *o = (SDObject *)index.data(Qt::UserRole).toULongLong(); SDObject *val = o->FindChild("value"); if(val) { if(val->type.basetype == SDBasic::Boolean) { qWarning() << "Unexpected setModelData for boolean " << QString(o->name); return; } if(val->type.basetype == SDBasic::UnsignedInteger) val->data.basic.u = qMax(0, qobject_cast(editor)->value()); else if(val->type.basetype == SDBasic::SignedInteger) val->data.basic.i = qobject_cast(editor)->value(); else if(val->type.basetype == SDBasic::Float) val->data.basic.d = qobject_cast(editor)->value(); else if(val->type.basetype == SDBasic::String) val->data.str = qobject_cast(editor)->text(); else qWarning() << "Unexpected type of " << QString(o->name) << ": " << ToQStr(val->type.basetype); } } ConfigEditor::ConfigEditor(QWidget *parent) : QDialog(parent), ui(new Ui::ConfigEditor) { ui->setupUi(this); setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); m_Config = RENDERDOC_SetConfigSetting(""); m_SettingModel = new SettingModel(this); m_FilterModel = new SettingFilterModel(this); m_FilterModel->setSourceModel(m_SettingModel); ui->settings->setModel(m_FilterModel); { RDHeaderView *header = new RDHeaderView(Qt::Horizontal, ui->settings); ui->settings->setHeader(header); header->setColumnStretchHints({-1, 1, -1}); } ui->settings->setItemDelegate(new SettingDelegate(this, ui->settings)); } ConfigEditor::~ConfigEditor() { delete ui; } void ConfigEditor::on_filter_textChanged(const QString &text) { RDTreeViewExpansionState state; ui->settings->saveExpansion(state, 0); m_FilterModel->setFilter(text); ui->settings->applyExpansion(state, 0); } void ConfigEditor::keyPressEvent(QKeyEvent *e) { if(e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) return; QDialog::keyPressEvent(e); }