// Copyright (C) 2021 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "annotationtableview.h" #include "defaultannotations.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace QmlDesigner { struct ColumnId { enum Column { Title = 0, Author = 1, Value = 2, }; }; CommentDelegate::CommentDelegate(QObject *parent) : QItemDelegate(parent) , m_completer(std::make_unique()) {} CommentDelegate::~CommentDelegate() {} DefaultAnnotationsModel *CommentDelegate::defaultAnnotations() const { return m_defaults; } void CommentDelegate::setDefaultAnnotations(DefaultAnnotationsModel *defaults) { m_defaults = defaults; m_completer->setModel(m_defaults); } QCompleter *CommentDelegate::completer() const { return m_completer.get(); } void CommentDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, [[maybe_unused]] const QModelIndex &index) const { editor->setGeometry(option.rect); } Comment CommentDelegate::comment(QModelIndex const &index) { auto *model = index.model(); return model->data(model->index(index.row(), ColumnId::Title), CommentRole).value(); } CommentTitleDelegate::CommentTitleDelegate(QObject *parent) : CommentDelegate(parent) {} CommentTitleDelegate::~CommentTitleDelegate() {} QWidget *CommentTitleDelegate::createEditor(QWidget *parent, [[maybe_unused]] const QStyleOptionViewItem &option, [[maybe_unused]] const QModelIndex &index) const { auto *editor = new QComboBox(parent); editor->setEditable(true); editor->setCompleter(completer()); editor->setFrame(false); editor->setFocusPolicy(Qt::StrongFocus); return editor; } void CommentTitleDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const { QString text = index.model()->data(index, Qt::DisplayRole).toString(); auto *comboBox = qobject_cast(editor); comboBox->setModel(defaultAnnotations()); comboBox->setCurrentText(text); } void CommentTitleDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const { auto *comboBox = qobject_cast(editor); auto oldText = model->data(index, Qt::EditRole).toString(); auto newText = comboBox->currentText(); if (oldText != newText) { model->setData(index, comboBox->currentText(), Qt::EditRole); auto comment = model->data(index, CommentRole).value(); comment.setTitle(newText); model->setData(index, QVariant::fromValue(comment), CommentRole); // Set default value to data item auto colIdx = model->index(index.row(), ColumnId::Value); if (defaultAnnotations()->hasDefault(comment)) model->setData(colIdx, defaultAnnotations()->defaultValue(comment), Qt::DisplayRole); else // Reset to rich text when there is no default item model->setData(colIdx, QVariant::fromValue({comment.text()}), Qt::DisplayRole); } } CommentValueDelegate::CommentValueDelegate(QObject *parent) : CommentDelegate(parent) {} CommentValueDelegate::~CommentValueDelegate() {} void CommentValueDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { auto data = index.model()->data(index, Qt::DisplayRole); if (data.userType() == qMetaTypeId()) drawDisplay(painter, option, option.rect, data.value().plainText()); else if (data.userType() == QMetaType::QColor) painter->fillRect(option.rect, data.value()); else QItemDelegate::paint(painter, option, index); } void CommentValueDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const { auto data = index.model()->data(index, Qt::DisplayRole); if (data.userType() == qMetaTypeId()) { auto richText = data.value(); auto *e = qobject_cast(editor); e->setText(richText.plainText()); e->setupSignal(index.row(), comment(index).title()); connect(e, &RichTextCellEditor::richTextClicked, this, &CommentValueDelegate::richTextEditorRequested, Qt::UniqueConnection); } else if (data.userType() == QMetaType::QString) { auto *e = qobject_cast(editor); e->setText(data.toString()); } else if (data.userType() == QMetaType::QColor) { auto *e = qobject_cast(editor); e->setColor(data.value()); e->installEventFilter(e); connect(e, &AnnotationTableColorButton::editorFinished, this, &CommentValueDelegate::slotEditorFinished, Qt::UniqueConnection); connect(e, &AnnotationTableColorButton::editorCanceled, this, &CommentValueDelegate::slotEditorCanceled, Qt::UniqueConnection); } else QItemDelegate::setEditorData(editor, index); } bool AnnotationTableColorButton::eventFilter(QObject *object, QEvent *event) { AnnotationTableColorButton *editor = qobject_cast(object); if (editor && event->type() == QEvent::FocusOut && editor->isDialogOpen()) return true; return QObject::eventFilter(object, event); } void CommentValueDelegate::slotEditorCanceled(QWidget *editor) { emit closeEditor(editor); } void CommentValueDelegate::slotEditorFinished(QWidget *editor) { AnnotationTableColorButton* e = qobject_cast(editor); if (e) { emit commitData(editor); emit closeEditor(editor, QAbstractItemDelegate::SubmitModelCache); } } void CommentValueDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const { auto data = model->data(index, Qt::EditRole); if (data.userType() == qMetaTypeId()) return; else if (data.userType() == QMetaType::QColor) { model->setData(index, qobject_cast(editor)->color(), Qt::DisplayRole); } else if (data.userType() == QMetaType::QString) model->setData(index, qobject_cast(editor)->text(), Qt::DisplayRole); else QItemDelegate::setModelData(editor, model, index); } RichTextCellEditor::RichTextCellEditor(QWidget *parent) : QLabel(parent) {} RichTextCellEditor::~RichTextCellEditor() {} RichTextProxy RichTextCellEditor::richText() const { return m_richText; } void RichTextCellEditor::setRichText(const RichTextProxy &richText) { if (richText.text == m_richText.text) return; m_richText = richText; setText(richText.plainText()); emit richTextChanged(); } void RichTextCellEditor::setupSignal(int index, const QString &commentTitle) { if (m_connection) disconnect(m_connection); m_connection = connect(this, &RichTextCellEditor::clicked, this, [=]() { emit richTextClicked(index, commentTitle); }); } void RichTextCellEditor::mouseReleaseEvent(QMouseEvent *) { emit clicked(); } AnnotationTableColorButton::AnnotationTableColorButton(QWidget *parent) : Utils::QtColorButton(parent) { connect(this, &Utils::QtColorButton::colorChangeStarted, this, [this](){emit editorStarted(this);}); connect(this, &Utils::QtColorButton::colorChanged, this, [this](QColor){emit editorFinished(this);}); connect(this, &Utils::QtColorButton::colorUnchanged, this, [this](){emit editorCanceled(this);}); } AnnotationTableColorButton::~AnnotationTableColorButton() {} AnnotationTableView::AnnotationTableView(QWidget *parent) : QTableView(parent) , m_model(std::make_unique()) , m_editorFactory(std::make_unique()) { setSelectionBehavior(QAbstractItemView::SelectRows); setSelectionMode(QAbstractItemView::ContiguousSelection); setModel(m_model.get()); connect(m_model.get(), &QStandardItemModel::itemChanged, this, [this](QStandardItem *item) { if (item->isCheckable()) m_model->setData(item->index(), item->checkState() == Qt::Checked); if (this->m_modelUpdating) return; auto *valueItem = m_model->item(item->row(), ColumnId::Value); // When comment title was edited, make value item editable if (item->column() == ColumnId::Title && valueItem) { valueItem->setEditable(!item->text().isEmpty()); valueItem->setCheckable(valueItem->data(Qt::DisplayRole).userType() == QMetaType::Bool); } m_modelUpdating = true; if (!rowIsEmpty(m_model->rowCount() - 1)) addEmptyRow(); m_modelUpdating = false; }); horizontalHeader()->setStretchLastSection(true); horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive); m_editorFactory->registerEditor(qMetaTypeId(), new QItemEditorCreator("richText")); m_editorFactory->registerEditor(QMetaType::QColor, new QItemEditorCreator("color")); m_valueDelegate.setItemEditorFactory(m_editorFactory.get()); connect(&m_valueDelegate, &CommentValueDelegate::richTextEditorRequested, this, &AnnotationTableView::richTextEditorRequested); verticalHeader()->hide(); } AnnotationTableView::~AnnotationTableView() {} QVector AnnotationTableView::fetchComments() const { QVector comments; for (int i = 0; i < m_model->rowCount(); ++i) { Comment comment = fetchComment(i); if (!comment.isEmpty()) comments.push_back(comment); } return comments; } Comment AnnotationTableView::fetchComment(int row) const { const auto *item = m_model->item(row, ColumnId::Title); Comment comment = item->data().value(); if (comment.isEmpty()) return {}; comment.setTitle(item->text()); comment.setAuthor(m_model->item(row, ColumnId::Author)->text()); comment.setText(dataToCommentText(m_model->item(row, ColumnId::Value)->data(Qt::DisplayRole))); return comment; } void AnnotationTableView::setupComments(QVector const &comments) { m_model->clear(); m_modelUpdating = true; m_model->setColumnCount(3); m_model->setHeaderData(ColumnId::Title, Qt::Horizontal, tr("Title")); m_model->setHeaderData(ColumnId::Author, Qt::Horizontal, tr("Author")); m_model->setHeaderData(ColumnId::Value, Qt::Horizontal, tr("Value")); setItemDelegateForColumn(ColumnId::Title, &m_titleDelegate); setItemDelegateForColumn(ColumnId::Value, &m_valueDelegate); for (auto &comment : comments) { if (comment.isEmpty()) continue; addEmptyRow(); changeRow(m_model->rowCount() - 1, comment); } addEmptyRow(); m_modelUpdating = false; } DefaultAnnotationsModel *AnnotationTableView::defaultAnnotations() const { return m_defaults; } void AnnotationTableView::setDefaultAnnotations(DefaultAnnotationsModel *defaults) { m_defaults = defaults; m_titleDelegate.setDefaultAnnotations(defaults); m_valueDelegate.setDefaultAnnotations(defaults); } void AnnotationTableView::changeRow(int index, Comment const &comment) { auto *titleItem = m_model->item(index, ColumnId::Title); auto *authorItem = m_model->item(index, ColumnId::Author); auto *textItem = m_model->item(index, ColumnId::Value); titleItem->setText(comment.title()); titleItem->setData(QVariant::fromValue(comment)); authorItem->setText(comment.author()); QVariant data = commentToData(comment, m_defaults ? m_defaults->defaultType(comment) : QMetaType::UnknownType); textItem->setEditable(data.isValid()); textItem->setCheckable(data.userType() == QMetaType::Bool); textItem->setData(data, Qt::DisplayRole); } void AnnotationTableView::removeRow(int index) { m_model->removeRow(index); } void AnnotationTableView::removeSelectedRows() { const auto selRows = selectionModel()->selectedRows(); for (auto it = selRows.rbegin(); it != selRows.rend(); ++it) removeRow(it->row()); } void AnnotationTableView::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Delete) removeSelectedRows(); } void AnnotationTableView::addEmptyRow() { auto *valueItem = new QStandardItem; valueItem->setEditable(false); m_model->appendRow({new QStandardItem, new QStandardItem, valueItem}); } bool AnnotationTableView::rowIsEmpty(int row) const { auto itemText = [&](int col) { return m_model->item(row, col) ? m_model->item(row, col)->text() : QString(); }; return QString(itemText(0) + itemText(1) + itemText(2)).isEmpty(); } QString AnnotationTableView::dataToCommentText(QVariant const &data) { auto type = data.userType(); if (type == qMetaTypeId()) return data.value().text; switch (type) { case QMetaType::QColor: return data.value().name(); case QMetaType::Bool: return data.toBool() ? QStringLiteral("true") : QStringLiteral("false"); case QMetaType::QString: return data.toString(); } return {}; } QVariant AnnotationTableView::commentToData(Comment const& comment, QMetaType::Type type) { switch (type) { case QMetaType::Bool: return QVariant::fromValue(comment.deescapedText().toLower().trimmed() == "true"); case QMetaType::QColor: return QVariant::fromValue(QColor(comment.deescapedText().toLower().trimmed())); break; case QMetaType::QString: return QVariant::fromValue(comment.text()); break; default: if (type == qMetaTypeId() || type == QMetaType::UnknownType) return QVariant::fromValue({comment.text()}); } return {}; } } // namespace QmlDesigner