Files
o3de/Gems/EMotionFX/Code/MysticQt/Source/DialogStack.cpp
T
2021-03-08 14:30:57 -08:00

719 lines
25 KiB
C++

/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
// 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::Dialog::Dialog()
{
mButton = nullptr;
mFrame = nullptr;
mWidget = nullptr;
mDialogWidget = nullptr;
mSplitter = nullptr;
mClosable = true;
}
// destructor
DialogStack::Dialog::~Dialog()
{
delete mDialogWidget;
}
// the constructor
DialogStack::DialogStack(QWidget* parent)
: QScrollArea(parent)
{
// set the memory category of the dialog array
mDialogs.SetMemoryCategory(MEMCATEGORY_MYSTICQT);
// 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.GetLength() == 0)
{
// 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.GetLast().mSplitter->count() == 1)
{
// add the dialog widget
dialogSplitter = mDialogs.GetLast().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.GetLast().mFrame->isHidden())
{
mDialogs.GetLast().mSplitter->handle(1)->setFixedHeight(1);
mDialogs.GetLast().mSplitter->setStyleSheet("QSplitter::handle{ height: 1px; background: transparent; }");
}
// disable the splitter
if (mDialogs.GetLast().mFrame->isHidden())
{
mDialogs.GetLast().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.GetLast().mDialogWidget);
dialogSplitter->addWidget(dialogWidget);
// stretch if needed
if (mDialogs.GetLast().mMaximizeSize && mDialogs.GetLast().mStretchWhenMaximize)
{
dialogSplitter->setStretchFactor(0, 1);
}
// less space used by the splitter when the last dialog is closed
if (mDialogs.GetLast().mFrame->isHidden())
{
dialogSplitter->handle(1)->setFixedHeight(1);
dialogSplitter->setStyleSheet("QSplitter::handle{ height: 1px; background: transparent; }");
}
// disable the splitter
if (mDialogs.GetLast().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.GetLast().mSplitter->addWidget(dialogSplitter);
// disable the splitter
const uint32 lastDialogIndex = mDialogs.GetLength() - 1;
if (mDialogs[lastDialogIndex - 1].mFrame->isHidden())
{
mDialogs.GetLast().mSplitter->handle(1)->setDisabled(true);
}
// stretch the splitter if needed
// the correct behavior is found after experimentations
if ((mDialogs.GetLast().mMaximizeSize && mDialogs.GetLast().mStretchWhenMaximize) || (mDialogs[lastDialogIndex - 1].mMaximizeSize && mDialogs[lastDialogIndex - 1].mStretchWhenMaximize == false))
{
mDialogs.GetLast().mSplitter->setStretchFactor(1, 1);
}
// set the new splitter of the last dialog
mDialogs.GetLast().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.AddEmpty();
mDialogs.GetLast().mButton = headerButton;
mDialogs.GetLast().mFrame = frame;
mDialogs.GetLast().mWidget = widget;
mDialogs.GetLast().mDialogWidget = dialogWidget;
mDialogs.GetLast().mSplitter = dialogSplitter;
mDialogs.GetLast().mClosable = closable;
mDialogs.GetLast().mMaximizeSize = maximizeSize;
mDialogs.GetLast().mStretchWhenMaximize = stretchWhenMaximize;
mDialogs.GetLast().mLayout = layout;
mDialogs.GetLast().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.GetLength();
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.Remove(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.GetLength();
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.GetLength() - 1))
{
mDialogs[dialogIndex].mSplitter->handle(1)->setFixedHeight(4);
mDialogs[dialogIndex].mSplitter->setStyleSheet("QSplitter::handle{ height: 4px; background: transparent; }");
}
// enable the splitter
if (dialogIndex < (mDialogs.GetLength() - 1))
{
mDialogs[dialogIndex].mSplitter->handle(1)->setEnabled(true);
}
// maximize the size if it's needed
if (mDialogs.GetLength() > 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.GetLength() - 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.GetLength() - 1))
{
mDialogs[dialogIndex].mSplitter->handle(1)->setFixedHeight(1);
mDialogs[dialogIndex].mSplitter->setStyleSheet("QSplitter::handle{ height: 1px; background: transparent; }");
}
// disable the splitter
if (dialogIndex < (mDialogs.GetLength() - 1))
{
mDialogs[dialogIndex].mSplitter->handle(1)->setDisabled(true);
}
// set the first splitter to the min if needed
if (dialogIndex < (mDialogs.GetLength() - 1))
{
static_cast<DialogStackSplitter*>(mDialogs[dialogIndex].mSplitter)->MoveFirstSplitterToMin();
}
// maximize the first needed to avoid empty space
bool findPreviousMaximizedDialogNeeded = true;
const uint32 numDialogs = mDialogs.GetLength();
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.GetLength();
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.GetLength(); ++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.GetLength() - 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>