Files
o3de/Gems/EMotionFX/Code/MysticQt/Source/DialogStack.cpp
T
Chris Burel 8884227fe6 Remove MCore::Array
This translates all usages of MCore::Array to AZStd::vector. It is
designed to be as minimal of a change as possible (no changing to
range-for loops or other C++11 stuff).

We can decide to submit this wholesale, or submit it to a separate
branch that we can then integrate individual files from once we're ready
to do a specific class's transition.

It does not completely solve the `uint32`->`size_t` transition.

One important finding from doing this: `MCore::Array` uses a `memcpy`
when it reallocates. `AZStd::vector` will use the contained type's copy
or move constructor, per element. This is a significant change in
behavior. If you have type, `SomeStruct` that defines a destructor, that
type is copyable and not movable. So if you have a
`MCore::Array<SomeStruct>`, and you call `Add(); Add(); Add()`, that
reallocates 3 times, copying the contents using `memcpy`, and never
invokes `SomeStruct`'s copy constructor or destructor. Translating that
to `AZStd::vector<SomeStruct>` and calling `push_back(); push_back();
push_back();` will still reallocate 3 times, but it sees that
`SomeStruct` is non-movable, and uses the copy constructor to make the
copies, and then the destructor on the previous values. This call to the
destructor wasn't there before, and can cause things to be deleted that
weren't before. The solution to this is to make that struct be a
move-only type. Where possible, this was done by changing that type to
use `AZStd::unique_ptr` instead of a raw pointer, to get the proper move
behavior. Where that is not possible (types that inherit from
`MCore::MemoryObject`), a hand-written move constructor was created.

In general:
GetLength() becomes size()
GetMaxLength() becomes capacity()
GetIsEmpty() becomes empty()
Reserve() becomes reserve()
ReserveExact() becomes reserve()
Resize() becomes resize()
ResizeFast() becomes resize_no_construct()
Add() becomes emplace_back()
AddExact() becomes emplace_back()
AddEmpty() becomes emplace_back()
AddEmptyExact() becomes emplace_back()
GetPtr() becomes data()
GetItem() becomes at()
Shrink() becomes shrink_to_fit()
GetFirst() becomes front()
GetLast() becomes back()
Remove() becomes erase()
RemoveFirst() becomes erase()
RemoveLast() becomes pop_back()
RemoveByValue() becomes if (const auto it = AZStd::find(...); it != end(container)) container.erase(it);
Insert() becomes emplace()
Swap() becomes swap()
Clear(true) becomes clear(); shrink_to_fit()
Clear() becomes clear(); shrink_to_fit()
Clear(false) becomes clear()
Swap() becomes swap()
Find() becomes AZStd::find
MoveElements() becomes AZStd::move
SetMemoryCategory() is removed

Signed-off-by: Chris Burel <burelc@amazon.com>
2021-08-09 08:36:43 -07:00

700 lines
24 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 the required headers
#include "DialogStack.h"
#include "MysticQtManager.h"
#include <QPushButton>
#include <QVBoxLayout>
#include <QFrame>
#include <QScrollArea>
#include <QMouseEvent>
#include <QResizeEvent>
#include <QScrollBar>
#include <QSplitter>
#include <QApplication>
namespace MysticQt
{
// dialog stack splitter
class DialogStackSplitter
: public QSplitter
{
public:
DialogStackSplitter()
{
setStyleSheet("QSplitter::handle{ height: 4px; background: transparent; }");
}
void MoveFirstSplitterToMin()
{
moveSplitter(0, 1);
}
void MoveFirstSplitterToMax()
{
moveSplitter(INT_MAX, 1);
}
void MoveFirstSplitter(int pos)
{
moveSplitter(pos, 1);
}
};
// the constructor
DialogStack::DialogStack(QWidget* parent)
: QScrollArea(parent)
{
// set the object name
setObjectName("DialogStack");
// create the root splitter
mRootSplitter = new DialogStackSplitter();
mRootSplitter->setOrientation(Qt::Vertical);
mRootSplitter->setChildrenCollapsible(false);
// set the widget resizable to have the scrollarea resizing it
setWidgetResizable(true);
// set the scrollarea widget
setWidget(mRootSplitter);
}
// destructor
DialogStack::~DialogStack()
{
}
// get rid of all dialogs and their allocated memory
void DialogStack::Clear()
{
// destroy the dialogs
mDialogs.clear();
// update the scroll bars
UpdateScrollBars();
}
// add an item to the stack
void DialogStack::Add(QWidget* widget, const QString& headerTitle, bool closed, bool maximizeSize, bool closable, bool stretchWhenMaximize)
{
// create the dialog widget
QWidget* dialogWidget = new QWidget();
QVBoxLayout* dialogLayout = new QVBoxLayout();
dialogLayout->setAlignment(Qt::AlignTop);
dialogWidget->setLayout(dialogLayout);
dialogLayout->setSpacing(0);
dialogLayout->setMargin(0);
// add the dialog widget
// the splitter is hierarchical : {a, {b, c}}
QSplitter* dialogSplitter;
if (mDialogs.empty())
{
// add the dialog widget
dialogSplitter = mRootSplitter;
dialogSplitter->addWidget(dialogWidget);
// stretch if needed
if (maximizeSize && stretchWhenMaximize)
{
dialogSplitter->setStretchFactor(0, 1);
}
}
else
{
// check if one space is free on the last splitter
if (mDialogs.back().mSplitter->count() == 1)
{
// add the dialog widget
dialogSplitter = mDialogs.back().mSplitter;
dialogSplitter->addWidget(dialogWidget);
// stretch if needed
if (maximizeSize && stretchWhenMaximize)
{
dialogSplitter->setStretchFactor(1, 1);
}
// less space used by the splitter when the last dialog is closed
if (mDialogs.back().mFrame->isHidden())
{
mDialogs.back().mSplitter->handle(1)->setFixedHeight(1);
mDialogs.back().mSplitter->setStyleSheet("QSplitter::handle{ height: 1px; background: transparent; }");
}
// disable the splitter
if (mDialogs.back().mFrame->isHidden())
{
mDialogs.back().mSplitter->handle(1)->setDisabled(true);
}
}
else // already two dialogs in the splitter
{
// create the new splitter
dialogSplitter = new DialogStackSplitter();
dialogSplitter->setOrientation(Qt::Vertical);
dialogSplitter->setChildrenCollapsible(false);
// add the current last dialog and the new dialog after
dialogSplitter->addWidget(mDialogs.back().mDialogWidget.get());
dialogSplitter->addWidget(dialogWidget);
// stretch if needed
if (mDialogs.back().mMaximizeSize && mDialogs.back().mStretchWhenMaximize)
{
dialogSplitter->setStretchFactor(0, 1);
}
// less space used by the splitter when the last dialog is closed
if (mDialogs.back().mFrame->isHidden())
{
dialogSplitter->handle(1)->setFixedHeight(1);
dialogSplitter->setStyleSheet("QSplitter::handle{ height: 1px; background: transparent; }");
}
// disable the splitter
if (mDialogs.back().mFrame->isHidden())
{
dialogSplitter->handle(1)->setDisabled(true);
}
// stretch if needed
if (maximizeSize && stretchWhenMaximize)
{
dialogSplitter->setStretchFactor(1, 1);
}
// replace the last dialog by the new splitter
mDialogs.back().mSplitter->addWidget(dialogSplitter);
// disable the splitter
if (mDialogs.size() > 1)
{
const auto previousDialogIt = mDialogs.end() - 2;
if (previousDialogIt->mFrame->isHidden())
{
mDialogs.back().mSplitter->handle(1)->setDisabled(true);
}
// stretch the splitter if needed
// the correct behavior is found after experimentations
if ((mDialogs.back().mMaximizeSize && mDialogs.back().mStretchWhenMaximize) || (previousDialogIt->mMaximizeSize && previousDialogIt->mStretchWhenMaximize == false))
{
mDialogs.back().mSplitter->setStretchFactor(1, 1);
}
}
// set the new splitter of the last dialog
mDialogs.back().mSplitter = dialogSplitter;
}
}
// create the header button that we can click to open/close this dialog
QPushButton* headerButton = new QPushButton(headerTitle);
headerButton->setObjectName("HeaderButton");
if (closed)
{
headerButton->setIcon(GetMysticQt()->FindIcon("Images/Icons/ArrowRightGray.png"));
}
else
{
headerButton->setIcon(GetMysticQt()->FindIcon("Images/Icons/ArrowDownGray.png"));
}
// add the header button in the layout
dialogLayout->addWidget(headerButton);
connect(headerButton, &QPushButton::clicked, this, &MysticQt::DialogStack::OnHeaderButton);
// create the frame where the dialog/widget will be inside
QWidget* frame = new QWidget();
frame->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
frame->setObjectName("StackFrame");
dialogLayout->addWidget(frame);
// create the layout thats inside of the frame
QVBoxLayout* layout = new QVBoxLayout();
layout->addWidget(widget, Qt::AlignTop | Qt::AlignLeft);
layout->setSpacing(0);
layout->setMargin(3);
// set the frame layout
frame->setLayout(layout);
// adjust size of the button
headerButton->adjustSize();
// adjust size of the widget
widget->adjustSize();
// set the constraints
if (maximizeSize)
{
// set the layout size constraint
layout->setSizeConstraint(QLayout::SetMaximumSize);
}
else
{
// set the layout size constraint
layout->setSizeConstraint(QLayout::SetMinimumSize);
// set the frame height
frame->setFixedHeight(layout->minimumSize().height());
// set the dialog height
dialogWidget->setFixedHeight(dialogLayout->minimumSize().height());
}
// adjust size of the dialog widget
dialogWidget->adjustSize();
// register it, so that we know which frame is linked to which header button
mDialogs.emplace_back(Dialog{
/*.mButton =*/ headerButton,
/*.mFrame =*/ frame,
/*.mWidget =*/ widget,
/*.mDialogWidget =*/ AZStd::unique_ptr<QWidget>{dialogWidget},
/*.mSplitter =*/ dialogSplitter,
/*.mClosable =*/ closable,
/*.mMaximizeSize =*/ maximizeSize,
/*.mStretchWhenMaximize =*/ stretchWhenMaximize,
/*.mMinimumHeightBeforeClose =*/ 0,
/*.mMaximumHeightBeforeClose =*/ 0,
/*.mLayout =*/ layout,
/*.mDialogLayout =*/ dialogLayout,
});
// check if the dialog is closed
if (closed)
{
Close(headerButton);
}
// update the scroll bars
UpdateScrollBars();
}
// add an item to the stack
void DialogStack::Add(QLayout* layout, const QString& headerTitle, bool closed, bool maximizeSize, bool closable, bool stretchWhenMaximize)
{
// create the widget
QWidget* widget = new QWidget();
// set the layout on this widget
widget->setLayout(layout);
// add the dialog using the created widget
Add(widget, headerTitle, closed, maximizeSize, closable, stretchWhenMaximize);
}
bool DialogStack::Remove(QWidget* widget)
{
const uint32 numDialogs = mDialogs.size();
for (uint32 i = 0; i < numDialogs; ++i)
{
QLayout* layout = mDialogs[i].mFrame->layout();
int index = layout->indexOf(widget);
// if the widget is located in the current layout, remove it
// all next dialogs has to be moved to the previous splitter and delete if the last splitter is empty
if (index != -1)
{
// remove the dialog
// TODO : shift all dialogs needed as explained on the previous comment
mDialogs[i].mDialogWidget->hide();
mDialogs[i].mDialogWidget->deleteLater();
mDialogs.erase(AZStd::next(begin(mDialogs), i));
// update the scroll bars
UpdateScrollBars();
// done
return true;
}
}
// not found
return false;
}
// on header button press
void DialogStack::OnHeaderButton()
{
QPushButton* button = (QPushButton*)sender();
const uint32 dialogIndex = FindDialog(button);
if (mDialogs[dialogIndex].mFrame->isHidden())
{
Open(button);
}
else
{
Close(button);
}
}
// find the dialog that goes with the given button
uint32 DialogStack::FindDialog(QPushButton* pushButton)
{
const uint32 numDialogs = mDialogs.size();
for (uint32 i = 0; i < numDialogs; ++i)
{
if (mDialogs[i].mButton == pushButton)
{
return i;
}
}
return MCORE_INVALIDINDEX32;
}
// open the dialog
void DialogStack::Open(QPushButton* button)
{
// find the dialog index
const uint32 dialogIndex = FindDialog(button);
if (dialogIndex == MCORE_INVALIDINDEX32)
{
return;
}
// show the widget inside the dialog
mDialogs[dialogIndex].mFrame->show();
// set the previous minimum and maximum height before closed
mDialogs[dialogIndex].mDialogWidget->setMinimumHeight(mDialogs[dialogIndex].mMinimumHeightBeforeClose);
mDialogs[dialogIndex].mDialogWidget->setMaximumHeight(mDialogs[dialogIndex].mMaximumHeightBeforeClose);
// change the stylesheet and the icon
button->setStyleSheet("");
button->setIcon(GetMysticQt()->FindIcon("Images/Icons/ArrowDownGray.png"));
// more space used by the splitter when the dialog is open
if (dialogIndex < (mDialogs.size() - 1))
{
mDialogs[dialogIndex].mSplitter->handle(1)->setFixedHeight(4);
mDialogs[dialogIndex].mSplitter->setStyleSheet("QSplitter::handle{ height: 4px; background: transparent; }");
}
// enable the splitter
if (dialogIndex < (mDialogs.size() - 1))
{
mDialogs[dialogIndex].mSplitter->handle(1)->setEnabled(true);
}
// maximize the size if it's needed
if (mDialogs.size() > 1)
{
if (mDialogs[dialogIndex].mMaximizeSize)
{
// special case if it's the first dialog
if (dialogIndex == 0)
{
// if it's the first dialog and stretching is enabled, it expand to the max, all others expand to the min
if (mDialogs[dialogIndex].mStretchWhenMaximize == false && mDialogs[dialogIndex + 1].mMaximizeSize && mDialogs[dialogIndex + 1].mFrame->isHidden() == false)
{
static_cast<DialogStackSplitter*>(mDialogs[dialogIndex].mSplitter)->MoveFirstSplitterToMin();
}
else
{
static_cast<DialogStackSplitter*>(mDialogs[dialogIndex].mSplitter)->MoveFirstSplitterToMax();
}
}
else // not the first dialog
{
// set the previous dialog to the min to have this dialog expanded to the top
if (mDialogs[dialogIndex - 1].mFrame->isHidden() || mDialogs[dialogIndex - 1].mMaximizeSize == false || (mDialogs[dialogIndex - 1].mMaximizeSize && mDialogs[dialogIndex - 1].mStretchWhenMaximize == false))
{
static_cast<DialogStackSplitter*>(mDialogs[dialogIndex - 1].mSplitter)->MoveFirstSplitterToMin();
}
// special case if it's not the last dialog
if (dialogIndex < (mDialogs.size() - 1))
{
// if the next dialog is closed, it's needed to expand to the max too
if (mDialogs[dialogIndex + 1].mFrame->isHidden())
{
static_cast<DialogStackSplitter*>(mDialogs[dialogIndex].mSplitter)->MoveFirstSplitterToMax();
}
}
}
}
}
// update the scrollbars
UpdateScrollBars();
}
// close the dialog
void DialogStack::Close(QPushButton* button)
{
// find the dialog index
const uint32 dialogIndex = FindDialog(button);
if (dialogIndex == MCORE_INVALIDINDEX32)
{
return;
}
// only closable dialog can be closed
if (mDialogs[dialogIndex].mClosable == false)
{
return;
}
// keep the min and max height before close
mDialogs[dialogIndex].mMinimumHeightBeforeClose = mDialogs[dialogIndex].mDialogWidget->minimumHeight();
mDialogs[dialogIndex].mMaximumHeightBeforeClose = mDialogs[dialogIndex].mDialogWidget->maximumHeight();
// hide the widget inside the dialog
mDialogs[dialogIndex].mFrame->hide();
// set the widget to fixed size to not have it possible to resize
mDialogs[dialogIndex].mDialogWidget->setMinimumHeight(mDialogs[dialogIndex].mButton->height());
mDialogs[dialogIndex].mDialogWidget->setMaximumHeight(mDialogs[dialogIndex].mButton->height());
// change the stylesheet and the icon
button->setStyleSheet("border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; border: 1px solid rgb(40,40,40);"); // TODO: link to the real style sheets
button->setIcon(GetMysticQt()->FindIcon("Images/Icons/ArrowRightGray.png"));
// less space used by the splitter when the dialog is closed
if (dialogIndex < (mDialogs.size() - 1))
{
mDialogs[dialogIndex].mSplitter->handle(1)->setFixedHeight(1);
mDialogs[dialogIndex].mSplitter->setStyleSheet("QSplitter::handle{ height: 1px; background: transparent; }");
}
// disable the splitter
if (dialogIndex < (mDialogs.size() - 1))
{
mDialogs[dialogIndex].mSplitter->handle(1)->setDisabled(true);
}
// set the first splitter to the min if needed
if (dialogIndex < (mDialogs.size() - 1))
{
static_cast<DialogStackSplitter*>(mDialogs[dialogIndex].mSplitter)->MoveFirstSplitterToMin();
}
// maximize the first needed to avoid empty space
bool findPreviousMaximizedDialogNeeded = true;
const uint32 numDialogs = mDialogs.size();
for (uint32 i = dialogIndex + 1; i < numDialogs; ++i)
{
if (mDialogs[i].mMaximizeSize && mDialogs[i].mFrame->isHidden() == false)
{
if (i < (numDialogs - 1) && mDialogs[i + 1].mFrame->isHidden())
{
static_cast<DialogStackSplitter*>(mDialogs[i].mSplitter)->MoveFirstSplitterToMax();
}
else
{
static_cast<DialogStackSplitter*>(mDialogs[i - 1].mSplitter)->MoveFirstSplitterToMin();
}
findPreviousMaximizedDialogNeeded = false;
break;
}
}
if (findPreviousMaximizedDialogNeeded)
{
for (int32 i = dialogIndex - 1; i >= 0; --i)
{
if (mDialogs[i].mMaximizeSize && mDialogs[i].mFrame->isHidden() == false)
{
static_cast<DialogStackSplitter*>(mDialogs[i].mSplitter)->MoveFirstSplitterToMax();
break;
}
}
}
// update the scrollbars
UpdateScrollBars();
}
// when we press the mouse
void DialogStack::mousePressEvent(QMouseEvent* event)
{
if (event->buttons() & Qt::LeftButton)
{
// keep the mouse pos
mPrevMouseX = event->globalX();
mPrevMouseY = event->globalY();
// set the cursor if the scrollbar is visible
if ((horizontalScrollBar()->maximum() > 0) || (verticalScrollBar()->maximum() > 0))
{
QApplication::setOverrideCursor(Qt::ClosedHandCursor);
}
}
}
// when we double click
// without this event handled the hand cursor is not set when double click
void DialogStack::mouseDoubleClickEvent(QMouseEvent* event)
{
mousePressEvent(event);
}
// when the mouse button is released
void DialogStack::mouseReleaseEvent(QMouseEvent* event)
{
// action only for the left button
if (event->button() != Qt::LeftButton)
{
return;
}
// reset the cursor if the scrollbar is visible
if ((horizontalScrollBar()->maximum() > 0) || (verticalScrollBar()->maximum() > 0))
{
QApplication::restoreOverrideCursor();
}
}
// when a mouse button was clicked and we're moving the mouse
void DialogStack::mouseMoveEvent(QMouseEvent* event)
{
// action only for the left button
if ((event->buttons() & Qt::LeftButton) == false)
{
return;
}
// calculate the delta mouse movement
const int32 deltaX = event->globalX() - mPrevMouseX;
const int32 deltaY = event->globalY() - mPrevMouseY;
// now apply this delta movement to the scroller
int32 newX = horizontalScrollBar()->value() - deltaX;
int32 newY = verticalScrollBar()->value() - deltaY;
horizontalScrollBar()->setSliderPosition(newX);
verticalScrollBar()->setSliderPosition(newY);
// store the current value as previous value
mPrevMouseX = event->globalX();
mPrevMouseY = event->globalY();
}
// update the scroll bars ranges
void DialogStack::UpdateScrollBars()
{
// compute the new range
const QSize areaSize = viewport()->size();
const QSize widgetSize = widget()->size();
const int32 rangeX = widgetSize.width() - areaSize.width();
const int32 rangeY = widgetSize.height() - areaSize.height();
// set the new ranges
horizontalScrollBar()->setRange(0, rangeX);
verticalScrollBar()->setRange(0, rangeY);
}
// when resizing
void DialogStack::resizeEvent(QResizeEvent* event)
{
// update the scrollbar ranges
UpdateScrollBars();
// update the scroll area widget
QScrollArea::resizeEvent(event);
// maximize the first dialog needed
const uint32 numDialogs = mDialogs.size();
const int32 lastDialogIndex = static_cast<int32>(numDialogs) - 1;
for (int32 i = lastDialogIndex; i >= 0; --i)
{
if (mDialogs[i].mMaximizeSize && mDialogs[i].mFrame->isHidden() == false)
{
if (i < lastDialogIndex)
{
static_cast<DialogStackSplitter*>(mDialogs[i].mSplitter)->MoveFirstSplitterToMax();
}
break;
}
}
}
// replace an internal widget
void DialogStack::ReplaceWidget(QWidget* oldWidget, QWidget* newWidget)
{
for (uint32 i = 0; i < mDialogs.size(); ++i)
{
// go next if the widget is not the same
if (mDialogs[i].mWidget != oldWidget)
{
continue;
}
// replace the widget
mDialogs[i].mFrame->layout()->replaceWidget(oldWidget, newWidget);
mDialogs[i].mWidget = newWidget;
// adjust size of the new widget
newWidget->adjustSize();
// set the constraints
if (mDialogs[i].mMaximizeSize == false)
{
// get margins
const QMargins frameMargins = mDialogs[i].mLayout->contentsMargins();
const QMargins dialogMargins = mDialogs[i].mDialogLayout->contentsMargins();
const int frameMarginTopBottom = frameMargins.top() + frameMargins.bottom();
const int dialogMarginTopBottom = dialogMargins.top() + dialogMargins.bottom();
const int allMarginsTopBottom = frameMarginTopBottom + dialogMarginTopBottom;
// set the frame height
mDialogs[i].mFrame->setFixedHeight(newWidget->height() + frameMarginTopBottom);
// compute the dialog height
const int dialogHeight = newWidget->height() + allMarginsTopBottom + mDialogs[i].mButton->height();
// set the maximum height in case the dialog is not closed, if it's closed update the stored height
if (mDialogs[i].mFrame->isHidden() == false)
{
// set the dialog height
mDialogs[i].mDialogWidget->setFixedHeight(dialogHeight);
// set the first splitter to the min if needed
if (i < (mDialogs.size() - 1))
{
static_cast<DialogStackSplitter*>(mDialogs[i].mSplitter)->MoveFirstSplitterToMin();
}
}
else // dialog closed
{
// update the minimum and maximum stored height
mDialogs[i].mMinimumHeightBeforeClose = dialogHeight;
mDialogs[i].mMaximumHeightBeforeClose = dialogHeight;
}
}
// stop here
return;
}
}
} // namespace MysticQt
#include <MysticQt/Source/moc_DialogStack.cpp>