Files
o3de/Code/Editor/UndoDropDown.cpp
T
Dennis Brakhane 7f75dc6dee Fix "index out of range" error in AssetProcessor (#4324)
* Fix "index out of range" error

When the parent is the tree root element, beginInsertRows must be
called with an invalid (but legal) index. A QModelIndex with a row
index of zero when the parent has no children is an illegal index
and will result in "undefined behavior", like the "index out of
range" one.

Therefore, if our parent is the tree root element, we use QModelIndex()
instead.

Fixes #2343

Signed-off-by: Dennis Brakhane <brakhane@gmail.com>

* Use QModelIndex() instead of createIndex(-1, -1)

Both do the same, but the former is Qt best practise.

Signed-off-by: Dennis Brakhane <brakhane@gmail.com>

* add some sanity checks in debug mode

Using illegal ModelIndices can result in hard to debug problems later
on, so add a few checks to help spotting them sooner.

Signed-off-by: Dennis Brakhane <brakhane@gmail.com>
2021-10-04 17:41:35 -07:00

248 lines
7.1 KiB
C++

/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include "EditorDefs.h"
#include "UndoDropDown.h"
#include <vector> // for std::vector
#include <algorithm> // for std::reverse
// Qt
#include <QVBoxLayout>
#include <QListView>
#include <QAbstractListModel>
#include <QPushButton>
// Editor
#include "Undo/Undo.h" // for CUndoManager
/////////////////////////////////////////////////////////////////////////////
// Turns listener callbacks into signals
UndoStackStateAdapter::UndoStackStateAdapter(QObject* parent /* = nullptr */)
: QObject(parent)
{
GetIEditor()->GetUndoManager()->AddListener(this);
}
UndoStackStateAdapter::~UndoStackStateAdapter()
{
GetIEditor()->GetUndoManager()->RemoveListener(this);
}
/////////////////////////////////////////////////////////////////////////////
// The model that holds the list of undo/redo actions
class UndoDropDownListModel
: public QAbstractListModel
, IUndoManagerListener
{
public:
UndoDropDownListModel(CUndoManager& manager, UndoRedoDirection direction, QObject* parent = nullptr)
: QAbstractListModel(parent)
, m_manager(manager)
, m_direction(direction)
{
m_stackNames = m_direction == UndoRedoDirection::Undo ? m_manager.GetUndoStackNames() : m_manager.GetRedoStackNames();
m_manager.AddListener(this);
}
~UndoDropDownListModel() override
{
m_manager.RemoveListener(this);
}
int rowCount(const QModelIndex& parent = QModelIndex()) const override
{
if (parent.model() != this && parent != QModelIndex())
{
return 0;
}
return static_cast<int>(m_stackNames.size());
}
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override
{
if (index.row() < 0 || index.row() >= rowCount() || role != Qt::DisplayRole)
{
return {};
}
return m_stackNames[index.row()];
}
void SignalNumUndoRedo(const unsigned int& numUndo, const unsigned int& numRedo) override
{
std::vector<QString> fresh;
if (UndoRedoDirection::Undo == m_direction && m_stackNames.size() != numUndo)
{
fresh = m_manager.GetUndoStackNames();
}
else if (UndoRedoDirection::Redo == m_direction && m_stackNames.size() != numRedo)
{
fresh = m_manager.GetRedoStackNames();
}
else
{
return;
}
std::reverse(fresh.begin(), fresh.end());
if (fresh.size() < m_stackNames.size())
{
beginRemoveRows(QModelIndex(), static_cast<int>(fresh.size()), static_cast<int>(m_stackNames.size() - 1));
m_stackNames = fresh;
endRemoveRows();
}
else
{
beginInsertRows(QModelIndex(), static_cast<int>(m_stackNames.size()), static_cast<int>(fresh.size() - 1));
m_stackNames = fresh;
endInsertRows();
}
}
private:
CUndoManager& m_manager;
UndoRedoDirection m_direction;
std::vector<QString> m_stackNames;
};
/////////////////////////////////////////////////////////////////////////////
// Custom selection model we use for the list of undo/redo actions
void UndoStackItemSelectionModel::select(const QModelIndex& index, QItemSelectionModel::SelectionFlags command)
{
if (!model())
{
return;
}
if (index.isValid())
{
QItemSelectionModel::select({ model()->index(0, 0, {}), index }, command);
}
clearCurrentIndex();
}
void UndoStackItemSelectionModel::select([[maybe_unused]] const QItemSelection& selection, QItemSelectionModel::SelectionFlags command)
{
QPoint mouse = m_view->mapFromGlobal(QCursor::pos());
QModelIndex first = model()->index(0, 0, {});
QModelIndex last = model()->index(model()->rowCount() - 1, 0, {});
QModelIndex underMouse = m_view->indexAt(mouse);
if (underMouse.isValid())
{
last = underMouse;
}
QItemSelectionModel::select({ first, last }, command);
clearCurrentIndex();
}
/////////////////////////////////////////////////////////////////////////////
// CUndoDropDown dialog
CUndoDropDown::CUndoDropDown(UndoRedoDirection direction, QWidget* pParent /* = nullptr */)
: QDialog(pParent)
, m_direction(direction)
{
auto layout = new QVBoxLayout(this);
setLayout(layout);
// Model & view
m_model = new UndoDropDownListModel(*GetIEditor()->GetUndoManager(), direction, this);
m_view = new QListView(this);
m_view->setModel(m_model);
m_view->setSelectionModel(new UndoStackItemSelectionModel(m_view, m_model));
m_view->setSelectionMode(QAbstractItemView::ContiguousSelection);
layout->addWidget(m_view);
// The buttons along the bottom of the dropdown
auto buttonBox = new QHBoxLayout();
m_doButton = new QPushButton(this);
buttonBox->addWidget(m_doButton);
buttonBox->addStretch(1);
auto clearButton = new QPushButton(this);
clearButton->setText(tr("Clear"));
buttonBox->addWidget(clearButton);
layout->addLayout(buttonBox);
// Connections
connect(m_doButton, &QPushButton::clicked, this, &CUndoDropDown::OnUndoButton);
connect(clearButton, &QPushButton::clicked, this, &CUndoDropDown::OnUndoClear);
connect(m_view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &CUndoDropDown::SelectionChanged);
setMinimumWidth(450);
}
void CUndoDropDown::Prepare()
{
m_view->selectionModel()->select(m_model->index(0, 0, {}), QItemSelectionModel::ClearAndSelect);
m_view->setFocus();
show();
}
void CUndoDropDown::OnUndoButton()
{
int numSelected = m_view->selectionModel()->selectedIndexes().size();
if (m_direction == UndoRedoDirection::Undo)
{
GetIEditor()->GetUndoManager()->Undo(numSelected);
}
else
{
GetIEditor()->GetUndoManager()->Redo(numSelected);
}
accept();
}
void CUndoDropDown::OnUndoClear()
{
if (m_direction == UndoRedoDirection::Undo)
{
GetIEditor()->GetUndoManager()->ClearUndoStack();
}
else
{
GetIEditor()->GetUndoManager()->ClearRedoStack();
}
accept();
}
void CUndoDropDown::SelectionChanged([[maybe_unused]] const QItemSelection& selected, [[maybe_unused]] const QItemSelection& deselected)
{
QString label = QString("%1 %2 action%3")
.arg(m_direction == UndoRedoDirection::Undo ? tr("Undo") : tr("Redo"))
.arg(m_view->selectionModel()->selectedIndexes().size())
.arg(m_view->selectionModel()->selectedIndexes().size() > 1 ? "s" : "");
m_doButton->setText(label);
}
void CUndoDropDown::contextMenuEvent(QContextMenuEvent*)
{
// Inhibit QDialog::contextMenuEvent() as this will trigger the "What's this" popup.
// That happens because we're a child of a QMenu, and menus have Qt::WA_CustomWhatsThis
// which make the popup show even if QWidget::whatsThis() text is empty.
}
#include <moc_UndoDropDown.cpp>