You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Gems/PhysX/Code/Editor/CollisionGroupsWidget.cpp

598 lines
20 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 <AzFramework/Physics/SystemBus.h>
#include <QBoxLayout>
#include <QCheckBox>
#include <QPushButton>
#include <QLineEdit>
#include <QScrollArea>
#include <Editor/CollisionGroupsWidget.h>
namespace PhysX
{
namespace Editor
{
static const int s_columnWidthBuffer = 15;
static const int s_rowHeight = 25;
static const int s_rowHeaderWidth = 100;
static const int s_rowHeaderWidthBuffer = 15;
static const int s_buttonWidth = 100;
Cell::Cell(QWidget* parent, const Data& cell)
: QWidget(parent)
, m_cell(cell)
{
bool isEnabled = cell.row.m_group.IsSet(cell.column.m_layer);
m_checkBox = new QCheckBox();
m_checkBox->setChecked(isEnabled);
m_checkBox->setContentsMargins(0, 0, 0, 0);
m_checkBox->setEnabled(!cell.row.m_readOnly);
connect(m_checkBox, &QCheckBox::stateChanged, this, &Cell::OnCheckboxChanged);
QHBoxLayout* layout = new QHBoxLayout();
layout->setAlignment(Qt::AlignHCenter);
layout->addWidget(m_checkBox);
setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
setLayout(layout);
}
QSize Cell::sizeHint() const
{
return m_checkBox->sizeHint();
}
void Cell::OnCheckboxChanged(int state)
{
bool enabled = state == Qt::CheckState::Checked;
emit OnLayerChanged(m_cell.row.m_groupId, m_cell.column.m_layer, enabled);
}
ColumnHeader::ColumnHeader(QWidget* parent, const Data& column)
: QWidget(parent)
, m_col(column)
{
m_label = new QLabel();
m_label->setText(m_col.m_name.c_str());
m_label->setAlignment(Qt::AlignHCenter);
QHBoxLayout* layout = new QHBoxLayout();
layout->setAlignment(Qt::AlignHCenter);
layout->addWidget(m_label);
setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed);
setLayout(layout);
}
QSize ColumnHeader::sizeHint() const
{
QSize size = m_label->sizeHint();
size.setWidth(size.width() + s_columnWidthBuffer);
return size;
}
QSize ColumnHeader::minimumSizeHint() const
{
QSize size = m_label->sizeHint();
size.setWidth(size.width() + s_columnWidthBuffer);
size.setHeight(s_rowHeight);
return size;
}
const AZStd::string RowHeader::defaultGroupName = "NewGroup";
bool RowHeader::ForceUniqueGroupName(AZStd::string& groupNameOut)
{
if (m_nameSet->find(groupNameOut) == m_nameSet->end())
{
return false;
}
Physics::Utils::MakeUniqueString(*m_nameSet
, groupNameOut
, s_maxCollisionGroupNameLength);
return true;
}
bool RowHeader::SanitizeGroupName(AZStd::string& groupNameOut)
{
bool nameModified = false;
if (groupNameOut.length() == 0)
{
groupNameOut = defaultGroupName;
nameModified = true;
}
else if (groupNameOut.length() > s_maxCollisionGroupNameLength)
{
groupNameOut = groupNameOut.substr(0, s_maxCollisionGroupNameLength);
nameModified = true;
}
nameModified = nameModified || ForceUniqueGroupName(groupNameOut);
if (nameModified)
{
AZ_Warning("PhysX Collision Groups"
, false
, "Invalid collision group name used. Collision group automatically renamed to: %s"
, groupNameOut.c_str());
}
return nameModified;
}
RowHeader::RowHeader(QWidget* parent
, const Data& row
, Physics::Utils::NameSet& nameSet)
: QWidget(parent)
, m_row(row)
, m_nameSet(&nameSet)
{
m_text = new QLineEdit(this);
SanitizeGroupName(m_row.m_groupName);
m_nameSet->insert(m_row.m_groupName);
m_text->setText(m_row.m_groupName.c_str());
m_text->setEnabled(!m_row.m_readOnly);
m_text->setMaxLength(s_maxCollisionGroupNameLength);
connect(m_text, &QLineEdit::textEdited, this, &RowHeader::OnTextChanged);
QWidget::connect(m_text, &QLineEdit::editingFinished, this, &RowHeader::OnEditingFinished);
setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed);
}
void RowHeader::OnEditingFinished()
{
if (m_nameBeforeEdit == "")
{
return;
}
m_nameSet->erase(m_nameBeforeEdit);
AZStd::string groupName = m_text->text().toUtf8().data();
if (SanitizeGroupName(groupName))
{
m_text->setText(QString(groupName.c_str()));
}
m_row.m_groupName = groupName;
m_nameSet->insert(m_row.m_groupName);
m_nameBeforeEdit = "";
emit OnGroupRenamed(m_row.m_groupId, m_row.m_groupName.c_str());
}
void RowHeader::OnTextChanged(const QString& newText)
{
if (m_nameBeforeEdit == "")
{
m_nameBeforeEdit = m_row.m_groupName;
}
m_row.m_groupName = newText.toUtf8().data();
emit OnGroupRenamed(m_row.m_groupId, m_row.m_groupName.c_str());
}
QSize RowHeader::sizeHint() const
{
return QSize(s_rowHeaderWidth + s_rowHeaderWidthBuffer, s_rowHeight);
}
QSize RowHeader::minimumSizeHint() const
{
return QSize(s_rowHeaderWidth + s_rowHeaderWidthBuffer, s_rowHeight);
}
AzPhysics::CollisionGroups::Id RowHeader::GetGroupId() const
{
return m_row.m_groupId;
}
const AZStd::string& RowHeader::GetGroupName() const
{
return m_row.m_groupName;
}
CollisionGroupsWidget::CollisionGroupsWidget(QWidget* parent)
: QWidget(parent)
{
CreateLayout();
}
void CollisionGroupsWidget::SetValue(const AzPhysics::CollisionGroups& groups, const AzPhysics::CollisionLayers& layers)
{
blockSignals(true);
m_groups = groups;
m_layers = layers;
PopulateTableView();
blockSignals(false);
}
const AzPhysics::CollisionGroups& CollisionGroupsWidget::GetValue() const
{
return m_groups;
}
void CollisionGroupsWidget::CreateLayout()
{
// Hierarchy goes like this:
// -ThisWidget (CollisionGroupsWidget)
// -ScrollLayout (VBox)
// -ScrollArea (Widget)
// -ScrollContainer (Widget)
// -MainLayout (VBox)
// -GridLayout
// -PushButton
QVBoxLayout* scrollLayout = new QVBoxLayout();
scrollLayout->setContentsMargins(0, 0, 0, 0);
QScrollArea* scrollArea = new QScrollArea();
QWidget* scrollContainer = new QWidget();
// Grid layout
m_gridLayout = new QGridLayout();
// New group button
QPushButton* addNewGroup = new QPushButton();
addNewGroup->setText("Add");
addNewGroup->setFixedSize(s_buttonWidth + s_rowHeaderWidthBuffer, s_rowHeight);
m_mainLayout = new QVBoxLayout();
m_mainLayout->setMargin(0);
m_mainLayout->setSpacing(0);
m_mainLayout->setContentsMargins(0, 0, 0, 0);
m_mainLayout->addLayout(m_gridLayout);
m_mainLayout->addWidget(addNewGroup,Qt::AlignTop);
m_mainLayout->addStretch();
m_mainLayout->setSizeConstraint(QLayout::SizeConstraint::SetMinimumSize);
scrollLayout->addWidget(scrollArea);
scrollArea->setWidget(scrollContainer);
scrollContainer->setLayout(m_mainLayout);
setLayout(scrollLayout);
setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
setContentsMargins(0, 0, 0, 0);
// Connect signals
connect(addNewGroup, &QPushButton::clicked, this, &CollisionGroupsWidget::AddGroup);
}
void CollisionGroupsWidget::RecreateGridLayout()
{
m_mainLayout->removeItem(m_gridLayout);
delete m_gridLayout;
m_gridLayout = new QGridLayout();
m_mainLayout->insertLayout(0, m_gridLayout);
}
void CollisionGroupsWidget::ClearWidgets()
{
qDeleteAll(m_widgets);
m_widgets.clear();
m_nameSet.clear();
}
void CollisionGroupsWidget::PopulateTableView()
{
RecreateGridLayout();
ClearWidgets();
auto rows = GetRows();
auto columns = GetColumns();
for (int row = 0; row <= rows.size(); ++row)
{
const int columnCount = static_cast<int>(columns.size() + 1); // 2 extra columns for row header and 'Remove' button. This value is only meant for for-loop iteration.
for (int col = 0; col <= columnCount; ++col)
{
if (row == 0 && col == 0)
{
// Topleft cell is empty
}
else if (col == columnCount)
{
if (row == 0)
{
continue;
}
else
{
AZ_Assert(row > 0, "Unexpected row index.");
AddWidgetRemoveButton(rows[row - 1], row, col);
}
}
else if (row == 0)
{
AZ_Assert(col > 0, "Unexpected column index.");
AddWidgetColumnHeader(columns[col - 1], row, col);
}
else if (col == 0)
{
AZ_Assert(row > 0, "Unexpected row index.");
AddWidgetRowHeader(rows[row - 1], row, col);
}
else
{
AZ_Assert(row > 0 && col > 0, "Unexpected row or column index.");
AddWidgetCell(rows[row - 1], columns[col - 1], row, col);
}
}
}
}
void CollisionGroupsWidget::AddGroupTableView()
{
AZStd::vector<PhysX::Editor::RowHeader::Data> rows = GetRows();
AZStd::vector<PhysX::Editor::ColumnHeader::Data> columns = GetColumns();
if (rows.size() < 1 || columns.size() < 1)
{
return;
}
int row = static_cast<int>(rows.size());
for (int col = 0; col <= columns.size() + 1; ++col)
{
auto& rowData = rows[row - 1];
if (col == columns.size() + 1) // Column for 'Remove' buttons
{
AddWidgetRemoveButton(rowData, row, col);
}
else if (col == 0) // Column for row headers
{
AddWidgetRowHeader(rowData, row, col);
}
else
{
AddWidgetCell(rowData, columns[col - 1], row, col);
}
}
}
void CollisionGroupsWidget::RemoveGroupTableView(AzPhysics::CollisionGroups::Id groupId)
{
// Search for index of row to delete
RowHeader* rowHeader = nullptr;
for (int row = 1; row < m_gridLayout->rowCount(); ++row)
{
QLayoutItem* layoutItem = m_gridLayout->itemAtPosition(row, 0);
if (layoutItem == nullptr)
{
continue;
}
QWidget* widget = layoutItem->widget();
if (widget == nullptr)
{
continue;
}
rowHeader = qobject_cast<RowHeader*>(widget);
if (rowHeader != nullptr)
{
if (rowHeader->GetGroupId() == groupId)
{
break;
}
}
}
// If row to delete cannot be found, return.
if (rowHeader == nullptr || rowHeader->GetGroupId() != groupId)
{
return;
}
blockSignals(true);
m_nameSet.erase(rowHeader->GetGroupName());
m_mainLayout->removeItem(m_gridLayout); // Detach grid layout from main layout
for (AZ::u32 widgetIndex = 0; aznumeric_cast<int>(widgetIndex) < m_widgets.size(); ++widgetIndex)
{
if (m_widgets[widgetIndex] == rowHeader)
{
AZ::u64 columnCount = GetColumnCount() + 2; // +2 for RowHeader and last column for 'Remove' button
// Delete and remove references to widget pointers in deleted row
for (AZ::u32 deleteIndex = 0; deleteIndex < columnCount; ++deleteIndex)
{
delete m_widgets[widgetIndex + deleteIndex];
}
m_widgets.erase(m_widgets.begin() + widgetIndex, m_widgets.begin() + widgetIndex + columnCount);
break;
}
}
delete m_gridLayout;
m_gridLayout = new QGridLayout();
m_mainLayout->insertLayout(0, m_gridLayout);
// Place widget pointers of undeleted rows in new grid layout, i.e. reuse them.
auto rows = GetRows();
auto columns = GetColumns();
AZ::u32 widgetIndex = 0;
for (int row = 0; row <= rows.size(); ++row)
{
for (int col = 0; col <= columns.size() + 1; ++col)
{
if (row == 0 && col == 0)
{
// Topleft cell is empty
}
else if (col == columns.size() + 1)
{
if (row == 0)
{
continue;
}
else
{
auto& rowData = rows[row - 1];
if (!rowData.m_readOnly)
{
m_gridLayout->addWidget(m_widgets[widgetIndex++], row, col);
}
}
}
else
{
m_gridLayout->addWidget(m_widgets[widgetIndex++], row, col);
}
}
}
blockSignals(false);
}
void CollisionGroupsWidget::AddWidgetRemoveButton(const RowHeader::Data& rowData
, int row
, int column)
{
if (rowData.m_readOnly)
{
return;
}
QPushButton* deleteRow = new QPushButton("Remove");
deleteRow->setFixedSize(s_buttonWidth, s_rowHeight);
m_gridLayout->addWidget(deleteRow, row, column);
m_widgets.push_back(deleteRow);
auto groupId = rowData.m_groupId;
connect(deleteRow, &QPushButton::clicked, this, [this, groupId]() {
RemoveGroup(groupId);
});
}
void CollisionGroupsWidget::AddWidgetColumnHeader(const ColumnHeader::Data& columnData
, int row
, int column)
{
ColumnHeader* colHeader = new ColumnHeader(nullptr, columnData);
m_gridLayout->addWidget(colHeader, row, column);
m_widgets.push_back(colHeader);
}
void CollisionGroupsWidget::AddWidgetRowHeader(RowHeader::Data& rowData
, int row
, int column)
{
RowHeader* rowHeader = new RowHeader(nullptr, rowData, m_nameSet);
if (rowHeader->GetGroupName() != rowData.m_groupName)
{
RenameGroup(rowData.m_groupId, rowHeader->GetGroupName());
}
m_gridLayout->addWidget(rowHeader, row, column);
m_widgets.push_back(rowHeader);
connect(rowHeader, &RowHeader::OnGroupRenamed, this, &CollisionGroupsWidget::RenameGroup);
}
void CollisionGroupsWidget::AddWidgetCell(const RowHeader::Data& rowData
, const ColumnHeader::Data& columnData
, int row
, int column)
{
Cell::Data cellData = {
columnData,
rowData
};
Cell* cell = new Cell(nullptr, cellData);
m_gridLayout->addWidget(cell, row, column);
m_widgets.push_back(cell);
connect(cell, &Cell::OnLayerChanged, this, &CollisionGroupsWidget::EnableLayer);
}
void CollisionGroupsWidget::AddGroup()
{
m_groups.CreateGroup("NewGroup", AzPhysics::CollisionGroup::All, AzPhysics::CollisionGroups::Id::Create(), false);
AddGroupTableView();
emit onValueChanged(m_groups);
}
void CollisionGroupsWidget::RemoveGroup(AzPhysics::CollisionGroups::Id groupId)
{
m_groups.DeleteGroup(groupId);
RemoveGroupTableView(groupId);
emit onValueChanged(m_groups);
}
void CollisionGroupsWidget::RenameGroup(AzPhysics::CollisionGroups::Id groupId, const AZStd::string& newName)
{
m_groups.SetGroupName(groupId, newName);
emit onValueChanged(m_groups);
}
void CollisionGroupsWidget::EnableLayer(AzPhysics::CollisionGroups::Id groupId, const AzPhysics::CollisionLayer& layer, bool enabled)
{
m_groups.SetLayer(groupId, layer, enabled);
emit onValueChanged(m_groups);
}
AZStd::vector<RowHeader::Data> CollisionGroupsWidget::GetRows()
{
AZStd::vector<RowHeader::Data> m_rows;
for (auto& group : m_groups.GetPresets())
{
RowHeader::Data row =
{
group.m_id,
group.m_name,
group.m_group,
group.m_readOnly
};
m_rows.push_back(row);
}
return m_rows;
}
AZStd::vector<ColumnHeader::Data> CollisionGroupsWidget::GetColumns()
{
AZStd::vector<ColumnHeader::Data> m_cols;
for(AZ::u8 i = 0; i < AzPhysics::CollisionLayers::MaxCollisionLayers; ++i)
{
auto layerName = m_layers.GetName(i);
if (layerName.empty())
{
continue;
}
ColumnHeader::Data col =
{
layerName,
static_cast<AZ::u8>(i)
};
m_cols.push_back(col);
}
return m_cols;
}
AZ::u64 CollisionGroupsWidget::GetColumnCount() const
{
AZ::u64 columnCount = 0;
for (AZ::u8 i = 0; i < AzPhysics::CollisionLayers::MaxCollisionLayers; ++i)
{
auto layerName = m_layers.GetName(i);
if (!layerName.empty())
{
columnCount++;
}
}
return columnCount;
}
} // namespace Editor
} // namespace PhysX
#include <Editor/moc_CollisionGroupsWidget.cpp>