123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676 |
- /*
- * 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 "AzCore/std/iterator.h"
- #include "AzCore/std/limits.h"
- #include "AzQtComponents/Components/Widgets/CardHeader.h"
- #include "MysticQtManager.h"
- #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
- m_rootSplitter = new DialogStackSplitter();
- m_rootSplitter->setOrientation(Qt::Vertical);
- m_rootSplitter->setChildrenCollapsible(false);
- // set the widget resizable to have the scrollarea resizing it
- setWidgetResizable(true);
- // We don't want the default 1px frame, headers should fully span the
- // widget
- setFrameStyle(QFrame::Shape::NoFrame);
- // set the scrollarea widget
- setWidget(m_rootSplitter);
- }
- DialogStack::~DialogStack()
- {
- }
- // get rid of all dialogs and their allocated memory
- void DialogStack::Clear()
- {
- for (Dialog& dialog : m_dialogs)
- {
- if (dialog.m_dialogWidget)
- {
- dialog.m_dialogWidget->deleteLater();
- }
- }
- // destroy the dialogs
- m_dialogs.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}}
- DialogStackSplitter* dialogSplitter;
- if (m_dialogs.empty())
- {
- // add the dialog widget
- dialogSplitter = m_rootSplitter;
- 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 (m_dialogs.back().m_splitter->count() == 1)
- {
- // add the dialog widget
- dialogSplitter = m_dialogs.back().m_splitter;
- 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 (m_dialogs.back().m_frame->isHidden())
- {
- m_dialogs.back().m_splitter->handle(1)->setFixedHeight(1);
- m_dialogs.back().m_splitter->setStyleSheet("QSplitter::handle{ height: 1px; background: transparent; }");
- }
- // disable the splitter
- if (m_dialogs.back().m_frame->isHidden())
- {
- m_dialogs.back().m_splitter->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(m_dialogs.back().m_dialogWidget);
- dialogSplitter->addWidget(dialogWidget);
- // stretch if needed
- if (m_dialogs.back().m_maximizeSize && m_dialogs.back().m_stretchWhenMaximize)
- {
- dialogSplitter->setStretchFactor(0, 1);
- }
- // less space used by the splitter when the last dialog is closed
- if (m_dialogs.back().m_frame->isHidden())
- {
- dialogSplitter->handle(1)->setFixedHeight(1);
- dialogSplitter->setStyleSheet("QSplitter::handle{ height: 1px; background: transparent; }");
- }
- // disable the splitter
- if (m_dialogs.back().m_frame->isHidden())
- {
- dialogSplitter->handle(1)->setDisabled(true);
- }
- // stretch if needed
- if (maximizeSize && stretchWhenMaximize)
- {
- dialogSplitter->setStretchFactor(1, 1);
- }
- // replace the last dialog by the new splitter
- m_dialogs.back().m_splitter->addWidget(dialogSplitter);
- // disable the splitter
- if (m_dialogs.size() > 1)
- {
- const auto previousDialogIt = m_dialogs.end() - 2;
- if (previousDialogIt->m_frame->isHidden())
- {
- m_dialogs.back().m_splitter->handle(1)->setDisabled(true);
- }
- // stretch the splitter if needed
- // the correct behavior is found after experimentations
- if ((m_dialogs.back().m_maximizeSize && m_dialogs.back().m_stretchWhenMaximize) || (previousDialogIt->m_maximizeSize && previousDialogIt->m_stretchWhenMaximize == false))
- {
- m_dialogs.back().m_splitter->setStretchFactor(1, 1);
- }
- }
- // set the new splitter of the last dialog
- m_dialogs.back().m_splitter = dialogSplitter;
- }
- }
- // create the container card header that we can click to open/close this dialog
- auto* header = new AzQtComponents::CardHeader();
- AzQtComponents::CardHeader::applyContainerStyle(header);
- header->setTitle(headerTitle);
- header->setObjectName("CardHeader");
- // add the separator card header in the layout
- dialogLayout->addWidget(header);
- connect(header, &AzQtComponents::CardHeader::expanderChanged, this, &MysticQt::DialogStack::OnExpandedChanged);
- // 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 header
- header->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
- m_dialogs.emplace_back(Dialog{
- /*.m_header =*/header,
- /*.m_frame =*/ frame,
- /*.m_widget =*/ widget,
- /*.m_dialogWidget =*/ dialogWidget,
- /*.m_splitter =*/ dialogSplitter,
- /*.m_closable =*/ closable,
- /*.m_maximizeSize =*/ maximizeSize,
- /*.m_stretchWhenMaximize =*/ stretchWhenMaximize,
- /*.m_minimumHeightBeforeClose =*/ 0,
- /*.m_maximumHeightBeforeClose =*/ 0,
- /*.m_layout =*/ layout,
- /*.m_dialogLayout =*/ dialogLayout,
- });
- // check if the dialog is closed
- if (closed)
- {
- Close(header);
- }
- // 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 auto foundDialog = AZStd::find_if(begin(m_dialogs), end(m_dialogs), [widget](const Dialog& dialog)
- {
- return dialog.m_frame->layout()->indexOf(widget) != -1;
- });
- if (foundDialog == end(m_dialogs))
- {
- return false;
- }
- // 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
- // TODO : shift all dialogs needed as explained on the previous comment
- foundDialog->m_dialogWidget->hide();
- foundDialog->m_dialogWidget->deleteLater();
- m_dialogs.erase(foundDialog);
- // update the scroll bars
- UpdateScrollBars();
- return true;
- }
- // on header card expansions
- void DialogStack::OnExpandedChanged(bool expanded)
- {
- auto* header = qobject_cast<AzQtComponents::CardHeader*>(sender());
- if (expanded)
- {
- Open(header);
- }
- else
- {
- Close(header);
- }
- }
- // find the dialog that goes with the given header
- size_t DialogStack::FindDialog(AzQtComponents::CardHeader* header)
- {
- const auto foundDialog = AZStd::find_if(begin(m_dialogs), end(m_dialogs), [header](const Dialog& dialog)
- {
- return dialog.m_header == header;
- });
- return foundDialog != end(m_dialogs) ? AZStd::distance(begin(m_dialogs), foundDialog) : MCore::InvalidIndex;
- }
- // open the dialog
- void DialogStack::Open(AzQtComponents::CardHeader* header)
- {
- const auto dialog = AZStd::find_if(begin(m_dialogs), end(m_dialogs), [header](const Dialog& dialog)
- {
- return dialog.m_header == header;
- });
- if (dialog == end(m_dialogs))
- {
- return;
- }
- // show the widget inside the dialog
- dialog->m_frame->show();
- // set the previous minimum and maximum height before closed
- dialog->m_dialogWidget->setMinimumHeight(dialog->m_minimumHeightBeforeClose);
- dialog->m_dialogWidget->setMaximumHeight(dialog->m_maximumHeightBeforeClose);
- // more space used by the splitter when the dialog is open
- if (dialog != m_dialogs.end() - 1)
- {
- dialog->m_splitter->handle(1)->setFixedHeight(4);
- dialog->m_splitter->setStyleSheet("QSplitter::handle{ height: 4px; background: transparent; }");
- }
- // enable the splitter
- if (dialog != m_dialogs.end() - 1)
- {
- dialog->m_splitter->handle(1)->setEnabled(true);
- }
- // maximize the size if it's needed
- if (m_dialogs.size() > 1)
- {
- if (dialog->m_maximizeSize)
- {
- // special case if it's the first dialog
- if (dialog == m_dialogs.begin())
- {
- // if it's the first dialog and stretching is enabled, it expands to the max, all others expand to the min
- if (dialog->m_stretchWhenMaximize == false && (dialog + 1)->m_maximizeSize && (dialog + 1)->m_frame->isHidden() == false)
- {
- static_cast<DialogStackSplitter*>(dialog->m_splitter)->MoveFirstSplitterToMin();
- }
- else
- {
- static_cast<DialogStackSplitter*>(dialog->m_splitter)->MoveFirstSplitterToMax();
- }
- }
- else // not the first dialog
- {
- // set the previous dialog to the min to have this dialog expanded to the top
- if ((dialog - 1)->m_frame->isHidden() || (dialog - 1)->m_maximizeSize == false || ((dialog - 1)->m_maximizeSize && (dialog - 1)->m_stretchWhenMaximize == false))
- {
- static_cast<DialogStackSplitter*>((dialog - 1)->m_splitter)->MoveFirstSplitterToMin();
- }
- // special case if it's not the last dialog
- if (dialog != m_dialogs.end() - 1)
- {
- // if the next dialog is closed, it's needed to expand to the max too
- if ((dialog + 1)->m_frame->isHidden())
- {
- static_cast<DialogStackSplitter*>(dialog->m_splitter)->MoveFirstSplitterToMax();
- }
- }
- }
- }
- }
- // update the scrollbars
- UpdateScrollBars();
- }
- // close the dialog
- void DialogStack::Close(AzQtComponents::CardHeader* header)
- {
- const auto dialog = AZStd::find_if(begin(m_dialogs), end(m_dialogs), [header](const Dialog& dialog)
- {
- return dialog.m_header == header;
- });
- if (dialog == end(m_dialogs))
- {
- return;
- }
- // only closable dialog can be closed
- if (dialog->m_closable == false)
- {
- return;
- }
- // keep the min and max height before close
- dialog->m_minimumHeightBeforeClose = dialog->m_dialogWidget->minimumHeight();
- dialog->m_maximumHeightBeforeClose = dialog->m_dialogWidget->maximumHeight();
- // hide the widget inside the dialog
- dialog->m_frame->hide();
- // set the widget to fixed size to not have it possible to resize
- dialog->m_dialogWidget->setMinimumHeight(dialog->m_header->height());
- dialog->m_dialogWidget->setMaximumHeight(dialog->m_header->height());
- // less space used by the splitter when the dialog is closed
- if (dialog < m_dialogs.end() - 1)
- {
- dialog->m_splitter->handle(1)->setFixedHeight(1);
- dialog->m_splitter->setStyleSheet("QSplitter::handle{ height: 1px; background: transparent; }");
- dialog->m_splitter->handle(1)->setDisabled(true);
- static_cast<DialogStackSplitter*>(dialog->m_splitter)->MoveFirstSplitterToMin();
- }
- // maximize the first needed to avoid empty space
- bool findPreviousMaximizedDialogNeeded = true;
- for (auto curDialog = dialog + 1; curDialog != m_dialogs.end(); ++curDialog)
- {
- if (curDialog->m_maximizeSize && curDialog->m_frame->isHidden() == false)
- {
- if (curDialog != (m_dialogs.end() - 1) && (curDialog + 1)->m_frame->isHidden())
- {
- static_cast<DialogStackSplitter*>(curDialog->m_splitter)->MoveFirstSplitterToMax();
- }
- else
- {
- static_cast<DialogStackSplitter*>((curDialog - 1)->m_splitter)->MoveFirstSplitterToMin();
- }
- findPreviousMaximizedDialogNeeded = false;
- break;
- }
- }
- if (findPreviousMaximizedDialogNeeded)
- {
- for (auto curDialog = AZStd::make_reverse_iterator(dialog); curDialog != m_dialogs.rend(); ++curDialog)
- {
- if (curDialog->m_maximizeSize && curDialog->m_frame->isHidden() == false)
- {
- static_cast<DialogStackSplitter*>(curDialog->m_splitter)->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
- m_prevMouseX = event->globalX();
- m_prevMouseY = 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() - m_prevMouseX;
- const int32 deltaY = event->globalY() - m_prevMouseY;
- // 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
- m_prevMouseX = event->globalX();
- m_prevMouseY = 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
- if (m_dialogs.empty() || m_dialogs.size() == 1)
- {
- return;
- }
- for (auto dialog = m_dialogs.rbegin() + 1; dialog != m_dialogs.rend(); ++dialog)
- {
- if (dialog->m_maximizeSize && dialog->m_frame->isHidden() == false)
- {
- static_cast<DialogStackSplitter*>(dialog->m_splitter)->MoveFirstSplitterToMax();
- break;
- }
- }
- }
- // replace an internal widget
- void DialogStack::ReplaceWidget(QWidget* oldWidget, QWidget* newWidget)
- {
- for (auto dialog = m_dialogs.begin(); dialog != m_dialogs.end(); ++dialog)
- {
- // go next if the widget is not the same
- if (dialog->m_widget != oldWidget)
- {
- continue;
- }
- // replace the widget
- dialog->m_frame->layout()->replaceWidget(oldWidget, newWidget);
- dialog->m_widget = newWidget;
- // adjust size of the new widget
- newWidget->adjustSize();
- // set the constraints
- if (dialog->m_maximizeSize == false)
- {
- // get margins
- const QMargins frameMargins = dialog->m_layout->contentsMargins();
- const QMargins dialogMargins = dialog->m_dialogLayout->contentsMargins();
- const int frameMarginTopBottom = frameMargins.top() + frameMargins.bottom();
- const int dialogMarginTopBottom = dialogMargins.top() + dialogMargins.bottom();
- const int allMarginsTopBottom = frameMarginTopBottom + dialogMarginTopBottom;
- // set the frame height
- dialog->m_frame->setFixedHeight(newWidget->height() + frameMarginTopBottom);
- // compute the dialog height
- const int dialogHeight = newWidget->height() + allMarginsTopBottom + dialog->m_header->height();
- // set the maximum height in case the dialog is not closed, if it's closed update the stored height
- if (dialog->m_frame->isHidden() == false)
- {
- // set the dialog height
- dialog->m_dialogWidget->setFixedHeight(dialogHeight);
- // set the first splitter to the min if needed
- if (dialog != m_dialogs.end() - 1)
- {
- static_cast<DialogStackSplitter*>(dialog->m_splitter)->MoveFirstSplitterToMin();
- }
- }
- else // dialog closed
- {
- // update the minimum and maximum stored height
- dialog->m_minimumHeightBeforeClose = dialogHeight;
- dialog->m_maximumHeightBeforeClose = dialogHeight;
- }
- }
- // stop here
- return;
- }
- }
- } // namespace MysticQt
|