// SPDX-FileCopyrightText: Copyright (c) Kitware Inc.
// SPDX-FileCopyrightText: Copyright (c) Sandia Corporation
// SPDX-License-Identifier: BSD-3-Clause
#include "pqMultiViewWidget.h"
#include "ui_pqPopoutPlaceholder.h"

#include "pqActiveObjects.h"
#include "pqApplicationCore.h"
#include "pqEventDispatcher.h"
#include "pqHierarchicalGridLayout.h"
#include "pqHierarchicalGridWidget.h"
#include "pqInterfaceTracker.h"
#include "pqObjectBuilder.h"
#include "pqPropertyLinks.h"
#include "pqQVTKWidget.h"
#include "pqServerManagerModel.h"
#include "pqUndoStack.h"
#include "pqView.h"
#include "pqViewFrame.h"
#include "pqViewFrameActionsInterface.h"
#include "vtkCommand.h"
#include "vtkErrorCode.h"
#include "vtkImageData.h"
#include "vtkLogger.h"
#include "vtkNew.h"
#include "vtkSMParaViewPipelineControllerWithRendering.h"
#include "vtkSMProperty.h"
#include "vtkSMPropertyHelper.h"
#include "vtkSMSaveScreenshotProxy.h"
#include "vtkSMSession.h"
#include "vtkSMSessionProxyManager.h"
#include "vtkSMTrace.h"
#include "vtkSMUtilities.h"
#include "vtkSMViewLayoutProxy.h"
#include "vtkSMViewProxy.h"
#include "vtkWeakPointer.h"

#include <QApplication>
#include <QFrame>
#include <QHBoxLayout>
#include <QLabel>
#include <QMap>
#include <QPushButton>
#include <QVBoxLayout>
#include <QVariant>
#include <QVector>
#include <cassert>
#include <functional>

namespace
{
static const int PARAVIEW_DEFAULT_LAYOUT_SPACING = 4;
}

class pqMultiViewWidget::pqInternals
{
public:
  QVector<QPointer<pqViewFrame>> Frames;

  // This map is used to avoid reassigning frames. Once a view is assigned a
  // frame, we preserve that frame as long as possible.
  QMap<vtkSMViewProxy*, QPointer<pqViewFrame>> ViewFrames;

  // This is a collection for empty frames.
  QVector<QPointer<pqViewFrame>> EmptyFrames;

  std::vector<unsigned long> ObserverIds;
  vtkWeakPointer<vtkSMViewLayoutProxy> LayoutManager;
  QPointer<pqViewFrame> ActiveFrame;

  // Set to true to place views in a separate popout widget.
  bool Popout;
  QScopedPointer<QWidget> PopoutWindow;
  QPointer<pqHierarchicalGridWidget> Container;

  QScopedPointer<QWidget> PopoutPlaceholder;

  pqPropertyLinks Links;

  double CustomDevicePixelRatio = 0.0;

  pqInternals(pqMultiViewWidget* self)
    : Popout(false)
    , SavedButtons(pqViewFrame::NoButton)
  {
    this->Container = new pqHierarchicalGridWidget(self);
    this->Container->setObjectName("Container");
    this->Container->setAutoFillBackground(true);
    auto hlayout = new pqHierarchicalGridLayout(this->Container);
    hlayout->setSpacing(PARAVIEW_DEFAULT_LAYOUT_SPACING);

    QObject::connect(this->Container.data(), &pqHierarchicalGridWidget::splitterMoved,
      [self](int location, double fraction) {
        if (auto vlayout = self->layoutManager())
        {
          BEGIN_UNDO_SET(QCoreApplication::translate("pqInternals", "Resize Frame"));
          vlayout->SetSplitFraction(location, fraction);
          END_UNDO_SET();
        }
      });

    QVBoxLayout* slayout = new QVBoxLayout(self);
    slayout->setContentsMargins(0, 0, 0, 0);
    slayout->addWidget(this->Container);

    this->PopoutPlaceholder.reset(new QWidget());
    Ui::PopoutPlaceholder ui;
    ui.setupUi(this->PopoutPlaceholder.data());
    QObject::connect(
      ui.restoreButton, &QPushButton::clicked, [self](bool) { self->togglePopout(); });

    this->CustomDevicePixelRatio = 0.0;
  }

  ~pqInternals()
  {
    if (this->LayoutManager)
    {
      for (auto id : this->ObserverIds)
      {
        this->LayoutManager->RemoveObserver(id);
      }
    }
    delete this->Container;
  }

  void setMaximizedWidget(QWidget* wdg)
  {
    pqViewFrame* frame = qobject_cast<pqViewFrame*>(wdg);
    if (frame)
    {
      this->SavedButtons = frame->standardButtons();
      frame->setStandardButtons(pqViewFrame::Restore);
    }
    if (this->MaximizedWidget)
    {
      this->MaximizedWidget->setStandardButtons(this->SavedButtons);
      this->SavedButtons = pqViewFrame::NoButton;
    }
    this->MaximizedWidget = frame;
  }

  void createViewFrames(const std::vector<vtkSMViewProxy*>& viewProxies, pqMultiViewWidget* self)
  {
    for (auto proxy : viewProxies)
    {
      if (!this->ViewFrames.contains(proxy))
      {
        this->ViewFrames.insert(proxy, self->newFrame(proxy));
      }
    }
  }

  void setPreviewSpacing(int spacing)
  {
    this->PreviewSpacing = spacing;
    if (!this->PreviewSize.isEmpty())
    {
      this->Container->layout()->setSpacing(spacing);
    }
  }

  void setPreviewSeparatorColor(const QColor& color)
  {
    this->PreviewColor = color;
    if (!this->PreviewSize.isEmpty())
    {
      auto palette = this->Container->palette();
      palette.setColor(QPalette::Window, color);
      this->Container->setPalette(palette);
    }
  }

  void preview(const QSize& size)
  {
    vtkLogScopeF(TRACE, "preview (%d, %d)", size.width(), size.height());
    this->PreviewSize = size;
    if (size.isEmpty())
    {
      this->setCustomDevicePixelRatio(0.0); // set to 0 to not use custom value.
      this->Container->setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX);
      this->Container->layout()->setSpacing(PARAVIEW_DEFAULT_LAYOUT_SPACING);

      auto palette = this->Container->parentWidget()->palette();
      this->Container->setPalette(palette);
    }
    else
    {
      this->Container->layout()->setSpacing(this->PreviewSpacing);
      auto palette = this->Container->palette();
      palette.setColor(QPalette::Window, this->PreviewColor);
      this->Container->setPalette(palette);

      // ensure that the max size we set on the dialog is less that what we have
      // available.
      vtkVector2i tsize(size.width(), size.height());
      const QRect crect = this->Container->parentWidget()->contentsRect();
      vtkVector2i csize(crect.width(), crect.height());
      const int magnification = vtkSMSaveScreenshotProxy::ComputeMagnification(tsize, csize);
      this->setCustomDevicePixelRatio(magnification);
      this->Container->setMaximumSize(csize[0], csize[1]);
      vtkLogF(
        TRACE, "cur=(%d, %d), new=(%d, %d)", crect.width(), crect.height(), csize[0], csize[1]);
    }
    this->updateDecorations();
  }

  const QSize& previewSize() const { return this->PreviewSize; }

  void setCustomDevicePixelRatio(double sf)
  {
    if (this->CustomDevicePixelRatio != sf)
    {
      this->CustomDevicePixelRatio = sf;
      for (auto renderWidget : this->Container->findChildren<pqQVTKWidget*>())
      {
        renderWidget->setCustomDevicePixelRatio(sf);
        // need to disable font-scaling if custom ratio is being used.
        renderWidget->setEnableHiDPI(sf == 0.0 ? true : false);
      }
    }
  }

  void setDecorationsVisibility(bool val)
  {
    this->DecorationsVisibility = val;
    this->updateDecorations();
  }

  bool decorationsVisibility() const { return this->DecorationsVisibility; }

  void updateDecorations()
  {
    // we show decorations if explicitly requested *and* we're not in preview mode.
    const bool showDecorations = this->DecorationsVisibility && this->PreviewSize.isEmpty();
    for (auto iter = this->ViewFrames.begin(); iter != this->ViewFrames.end(); ++iter)
    {
      if (iter.value() != nullptr)
      {
        iter.value()->setDecorationsVisibility(showDecorations);
      }
    }
  }

  void lockViewSize(const QSize& size)
  {
    this->LockedSize = size.isEmpty() ? QSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX) : size;
    for (pqViewFrame* frame : this->Frames)
    {
      if (auto centralWidget = frame ? frame->centralWidget() : nullptr)
      {
        centralWidget->setMaximumSize(this->LockedSize);
      }
    }
  }
  const QSize& lockedSize() const { return this->LockedSize; }

private:
  QPointer<pqViewFrame> MaximizedWidget;
  pqViewFrame::StandardButtons SavedButtons;

  int PreviewSpacing = 1;
  QColor PreviewColor;
  QSize PreviewSize;
  bool DecorationsVisibility = true;

  QSize LockedSize;
};

//=============================================================================
namespace
{
pqView* getPQView(vtkSMProxy* view)
{
  if (view)
  {
    pqServerManagerModel* smmodel = pqApplicationCore::instance()->getServerManagerModel();
    return smmodel->findItem<pqView*>(view);
  }
  return nullptr;
}

void ConnectFrameToView(pqViewFrame* frame, pqView* pqview)
{
  assert(frame);
  // if pqview == nullptr, then the frame is either being assigned to a empty
  // view, or pqview for a view-proxy just isn't present yet.
  // it's possible that pqview is nullptr, if the view proxy hasn't been registered
  // yet. This happens often when initialization state is being loaded in
  // collaborative sessions.
  if (pqview != nullptr)
  {
    QWidget* viewWidget = pqview->widget();
    frame->setCentralWidget(viewWidget, pqview);
  }
}
}

//=============================================================================
pqMultiViewWidget::pqMultiViewWidget(QWidget* parentObject, Qt::WindowFlags f)
  : Superclass(parentObject, f)
  , Internals(new pqInternals(this))
{
  qApp->installEventFilter(this);

  QObject::connect(
    &pqActiveObjects::instance(), SIGNAL(viewChanged(pqView*)), this, SLOT(markActive(pqView*)));

  pqApplicationCore* core = pqApplicationCore::instance();
  pqServerManagerModel* smmodel = core->getServerManagerModel();
  QObject::connect(smmodel, SIGNAL(proxyRemoved(pqProxy*)), this, SLOT(proxyRemoved(pqProxy*)));
  QObject::connect(smmodel, SIGNAL(viewAdded(pqView*)), this, SLOT(viewAdded(pqView*)));
}

//-----------------------------------------------------------------------------
pqMultiViewWidget::~pqMultiViewWidget()
{
  delete this->Internals;
  this->Internals = nullptr;
}

//-----------------------------------------------------------------------------
bool pqMultiViewWidget::isViewAssigned(pqView* view) const
{
  return (view && this->Internals->LayoutManager &&
    this->Internals->LayoutManager->GetViewLocation(view->getViewProxy()) != -1);
}

//-----------------------------------------------------------------------------
void pqMultiViewWidget::viewAdded(pqView* view)
{
  if (view && this->Internals->LayoutManager &&
    this->Internals->LayoutManager->GetViewLocation(view->getViewProxy()) != -1)
  {
    // a pqview was created for a view that this layout knows about. we have to
    // reload the layout to ensure that the view gets placed correctly.
    pqViewFrame* frame = this->Internals->ViewFrames[view->getViewProxy()];
    if (frame)
    {
      ConnectFrameToView(frame, view);
    }
    else
    {
      this->reload();
    }
  }
}

//-----------------------------------------------------------------------------
void pqMultiViewWidget::setLayoutManager(vtkSMViewLayoutProxy* vlayout)
{
  pqInternals& internals = (*this->Internals);
  if (internals.LayoutManager != vlayout)
  {
    if (internals.LayoutManager)
    {
      for (auto id : this->Internals->ObserverIds)
      {
        internals.LayoutManager->RemoveObserver(id);
      }
    }
    internals.Links.clear();
    internals.ObserverIds.clear();
    if (vlayout)
    {
      internals.ObserverIds.push_back(
        vlayout->AddObserver(vtkCommand::ConfigureEvent, this, &pqMultiViewWidget::reload));
      internals.ObserverIds.push_back(vlayout->AddObserver(
        vtkCommand::PropertyModifiedEvent, this, &pqMultiViewWidget::layoutPropertyModified));

      // explicitly call `layoutPropertyModified` for all properties we care
      // about to ensure our state is initialized to the current values from the
      // layout proxy.
      this->layoutPropertyModified(
        vlayout, vtkCommand::PropertyModifiedEvent, const_cast<char*>("SeparatorWidth"));
      this->layoutPropertyModified(
        vlayout, vtkCommand::PropertyModifiedEvent, const_cast<char*>("SeparatorColor"));
      this->layoutPropertyModified(
        vlayout, vtkCommand::PropertyModifiedEvent, const_cast<char*>("PreviewMode"));
    }
  }

  // we delay the setting of the LayoutManager to avoid the duplicate `reload`
  // call when `addPropertyLink` is called if the window decorations
  // visibility changed.
  internals.LayoutManager = vlayout;
  this->reload();
}

//-----------------------------------------------------------------------------
vtkSMViewLayoutProxy* pqMultiViewWidget::layoutManager() const
{
  return this->Internals->LayoutManager;
}

//-----------------------------------------------------------------------------
void pqMultiViewWidget::layoutPropertyModified(
  vtkObject* sender, unsigned long eventid, void* vdata)
{
  assert(eventid == vtkCommand::PropertyModifiedEvent);
  Q_UNUSED(eventid);

  auto& internals = (*this->Internals);
  auto vlayout = vtkSMViewLayoutProxy::SafeDownCast(sender);
  assert(vlayout);
  if (const char* pname = reinterpret_cast<const char*>(vdata))
  {
    if (strcmp(pname, "SeparatorWidth") == 0)
    {
      internals.setPreviewSpacing(vtkSMPropertyHelper(vlayout, "SeparatorWidth").GetAsInt());
    }
    else if (strcmp(pname, "SeparatorColor") == 0)
    {
      double color[3];
      vtkSMPropertyHelper(vlayout, "SeparatorColor").Get(color, 3);
      internals.setPreviewSeparatorColor(QColor::fromRgbF(color[0], color[1], color[2]));
    }
    else if (strcmp(pname, "PreviewMode") == 0)
    {
      int size[2];
      vtkSMPropertyHelper(vlayout, "PreviewMode").Get(size, 2);
      internals.preview(QSize(size[0], size[1]));
    }
  }
}

//-----------------------------------------------------------------------------
bool pqMultiViewWidget::eventFilter(QObject* caller, QEvent* evt)
{
  if (evt->type() == QEvent::MouseButtonPress)
  {
    QWidget* wdg = qobject_cast<QWidget*>(caller);
    if (wdg &&
      ((!this->Internals->Popout && this->isAncestorOf(wdg)) ||
        (this->Internals->Popout && this->Internals->PopoutWindow->isAncestorOf(wdg))))
    {
      // If the new widget that is getting the focus is a child widget of any of the
      // frames, then the frame should be made active.
      for (pqViewFrame* frame : this->Internals->Frames)
      {
        if (frame && frame->isAncestorOf(wdg))
        {
          this->makeActive(frame);
        }
      }
    }
  }
  else if (evt->type() == QEvent::Close && caller == this->Internals->PopoutWindow.data())
  {
    // the popout-frame is being closed. We interpret that as "un-popping" the
    // frame.
    this->togglePopout();
  }

  return this->Superclass::eventFilter(caller, evt);
}

//-----------------------------------------------------------------------------
void pqMultiViewWidget::resizeEvent(QResizeEvent* evt)
{
  this->Superclass::resizeEvent(evt);
  auto& internals = (*this->Internals);
  const auto& psize = internals.previewSize();
  if (!psize.isEmpty())
  {
    internals.preview(psize);
  }
}

//-----------------------------------------------------------------------------
void pqMultiViewWidget::proxyRemoved(pqProxy* proxy)
{
  vtkSMViewProxy* view = vtkSMViewProxy::SafeDownCast(proxy->getProxy());
  if (view && this->Internals->ViewFrames.contains(view) && this->layoutManager())
  {
    this->layoutManager()->RemoveView(view);
  }
}

//-----------------------------------------------------------------------------
void pqMultiViewWidget::makeFrameActive()
{
  /// note pqMultiViewWidget::markActive(pqViewFrame*) fires a signal that
  /// results in this method being called. So we need to ensure that we don't
  /// do anything silly here that could cause infinite recursion.
  if (!this->Internals->ActiveFrame)
  {
    for (const auto& frame : this->Internals->Frames)
    {
      if (frame)
      {
        this->makeActive(frame);
        break;
      }
    }
  }

  if (this->layoutManager())
  {
    this->layoutManager()->ShowViewsOnTileDisplay();
  }
}

//-----------------------------------------------------------------------------
void pqMultiViewWidget::markActive(pqView* view)
{
  if (view && this->Internals->ViewFrames.contains(view->getViewProxy()))
  {
    this->markActive(this->Internals->ViewFrames[view->getViewProxy()]);
  }
  else
  {
    this->markActive(static_cast<pqViewFrame*>(nullptr));
  }
}

//-----------------------------------------------------------------------------
void pqMultiViewWidget::markActive(pqViewFrame* frame)
{
  if (this->Internals->ActiveFrame)
  {
    this->Internals->ActiveFrame->setBorderVisibility(false);
  }
  this->Internals->ActiveFrame = frame;
  if (frame)
  {
    frame->setBorderVisibility(true);
    // indicate to the world that a frame on this widget has been activated.
    // pqTabbedMultiViewWidget listens to this signal to raise that tab.
    Q_EMIT this->frameActivated();
    // NOTE: this signal will result in call to makeFrameActive().
  }
}

//-----------------------------------------------------------------------------
void pqMultiViewWidget::makeActive(pqViewFrame* frame)
{
  if (this->Internals->ActiveFrame != frame)
  {
    pqView* view = nullptr;
    if (frame)
    {
      int index = frame->property("FRAME_INDEX").toInt();
      view = getPQView(this->layoutManager()->GetView(index));
    }
    pqActiveObjects::instance().setActiveView(view);
    // this needs to called only when view == nullptr since in that case when
    // markActive(pqView*) slot is called, we have no idea what frame is really
    // to be made active.
    this->markActive(frame);
  }
}

//-----------------------------------------------------------------------------
void pqMultiViewWidget::lockViewSize(const QSize& viewSize)
{
  pqInternals& internals = (*this->Internals);
  internals.lockViewSize(viewSize);
}

//-----------------------------------------------------------------------------
pqViewFrame* pqMultiViewWidget::newFrame(vtkSMProxy* view)
{
  pqViewFrame* frame = new pqViewFrame();
  QObject::connect(frame, SIGNAL(buttonPressed(int)), this, SLOT(standardButtonPressed(int)));
  QObject::connect(frame, SIGNAL(swapPositions(const QString&)), this,
    SLOT(swapPositions(const QString&)), Qt::QueuedConnection);

  pqServerManagerModel* smmodel = pqApplicationCore::instance()->getServerManagerModel();
  pqView* pqview = smmodel->findItem<pqView*>(view);
  // it's possible that pqview is nullptr, if the view proxy hasn't been registered
  // yet. This happens often when initialization state is being loaded in
  // collaborative sessions.
  ConnectFrameToView(frame, pqview);

  // Search for view frame actions plugins and allow them to decide
  // whether to add their actions to this view type's frame or not.
  pqInterfaceTracker* tracker = pqApplicationCore::instance()->interfaceTracker();
  Q_FOREACH (pqViewFrameActionsInterface* vfai, tracker->interfaces<pqViewFrameActionsInterface*>())
  {
    vfai->frameConnected(frame, pqview);
  }
  return frame;
}

//-----------------------------------------------------------------------------
void pqMultiViewWidget::reload()
{
  vtkSMViewLayoutProxy* vlayout = this->layoutManager();
  if (!vlayout)
  {
    return;
  }

  auto& internals = (*this->Internals);
  auto hlayout = qobject_cast<pqHierarchicalGridLayout*>(internals.Container->layout());
  assert(hlayout != nullptr);

  internals.Frames.clear();

  // for all non-nullptr views known to vlayout, let's make sure we have created pqViewFrame
  // for each of them. No need to delete any obsolete view frames just yet, they'll get cleaned
  // up following `pqHierarchicalGridLayout::rearrange()`
  internals.createViewFrames(vlayout->GetViews(), this);

  // Make sure the `hlayout` matches `vlayout`.
  QVector<pqHierarchicalGridLayout::Item> hitems;

  QVector<QPointer<pqViewFrame>> empty_frames;
  empty_frames = std::move(internals.EmptyFrames);

  auto& all_frames = internals.Frames;

  std::function<void(int)> builder = [&](int location) {
    const auto vdirection = vlayout->GetSplitDirection(location);
    if (hitems.size() <= location)
    {
      hitems.resize(location + 1);
    }
    if (all_frames.size() <= location)
    {
      all_frames.resize(location + 1);
    }

    if (vdirection != vtkSMViewLayoutProxy::NONE)
    {
      hitems[location] = pqHierarchicalGridLayout::Item(
        vdirection == vtkSMViewLayoutProxy::VERTICAL ? Qt::Vertical : Qt::Horizontal,
        vlayout->GetSplitFraction(location));
      builder(vtkSMViewLayoutProxy::GetFirstChild(location));
      builder(vtkSMViewLayoutProxy::GetSecondChild(location));
    }
    else
    {
      auto viewProxy = vlayout->GetView(location); // may be nullptr.
      auto frame = internals.ViewFrames.value(viewProxy, nullptr);
      assert(viewProxy == nullptr || frame != nullptr);
      if (viewProxy == nullptr && frame == nullptr)
      {
        if (!empty_frames.empty())
        {
          frame = empty_frames.takeLast();
        }
        else
        {
          frame = this->newFrame(nullptr);
        }
        internals.EmptyFrames.push_back(frame);
      }
      frame->setObjectName(QString("Frame.%1").arg(location));
      frame->setProperty("FRAME_INDEX", QVariant(location));
      frame->setDecorationsVisibility(internals.decorationsVisibility());
      hitems[location] = pqHierarchicalGridLayout::Item(frame);
      all_frames[location] = frame;
    }
  };
  builder(0);

  auto obsoleteWidgets = hlayout->rearrange(hitems);

  // delete frames no longer in the layout.
  for (auto wdg : obsoleteWidgets)
  {
    delete wdg;
  }

  // delete empty frames no longer needed.
  for (const auto& eframe : empty_frames)
  {
    delete eframe;
  }

  // cleanup ViewFrames map to remove empty frames
  for (auto iter = internals.ViewFrames.begin(); iter != internals.ViewFrames.end();)
  {
    if (iter.value() == nullptr)
    {
      iter = internals.ViewFrames.erase(iter);
    }
    else
    {
      ++iter;
    }
  }
  assert(internals.ViewFrames.size() == static_cast<int>(vlayout->GetViews().size()));

  // handle maximization state.
  const int maximized_cell = vlayout->GetMaximizedCell();
  if (maximized_cell >= 0 && internals.Frames.value(maximized_cell, nullptr))
  {
    internals.setMaximizedWidget(internals.Frames.value(maximized_cell));
    hlayout->maximize(maximized_cell);
  }
  else
  {
    internals.setMaximizedWidget(nullptr);
    hlayout->maximize(0);
  }

  // let's make sure maximum size on all `pqViewFrame`s is set correctly.
  internals.lockViewSize(internals.lockedSize());

  // post reload, the active frames may have changed.
  // so we ensure we mark the right one active.
  this->markActive(pqActiveObjects::instance().activeView());

  // we let the GUI updated immediately. This is needed since when a new view is
  // created (for example), it may depend on the size of the view during its
  // initialization to ensure camera is reset correctly.
  // We have gone back and forth between whether we should let the qt app
  // process events on just process posted events. Calling `processEvents` was
  // finally chosen to address issues like paraview/paraview#18963.
  pqEventDispatcher::processEvents();
}

//-----------------------------------------------------------------------------
void pqMultiViewWidget::standardButtonPressed(int button)
{
  pqViewFrame* frame = qobject_cast<pqViewFrame*>(this->sender());
  QVariant index = frame ? frame->property("FRAME_INDEX") : QVariant();
  if (!index.isValid() || this->layoutManager() == nullptr)
  {
    return;
  }

  switch (button)
  {
    case pqViewFrame::SplitVertical:
    case pqViewFrame::SplitHorizontal:
    {
      BEGIN_UNDO_SET(tr("Split View"));
      int new_index = this->layoutManager()->Split(index.toInt(),
        (button == pqViewFrame::SplitVertical ? vtkSMViewLayoutProxy::VERTICAL
                                              : vtkSMViewLayoutProxy::HORIZONTAL),
        0.5);
      this->makeActive(this->Internals->Frames[new_index + 1]);
      END_UNDO_SET();
    }
    break;

    case pqViewFrame::Maximize:
      // ensure that the frame being maximized is active.
      this->makeActive(frame);
      this->layoutManager()->MaximizeCell(index.toInt());
      break;

    case pqViewFrame::Restore:
      // ensure that the frame being minimized is active.
      this->makeActive(frame);
      this->layoutManager()->RestoreMaximizedState();
      break;

    case pqViewFrame::Close:
    {
      BEGIN_UNDO_SET(tr("Close View"));
      vtkSmartPointer<vtkSMViewProxy> viewProxy = this->layoutManager()->GetView(index.toInt());
      if (viewProxy)
      {
        auto session = viewProxy->GetSession();
        // trigger progress request here delays render requests that happen as
        // the UI is being updated to remove the view. That fixes #18077.
        session->PrepareProgress();
        this->layoutManager()->RemoveView(viewProxy);
        pqObjectBuilder* builder = pqApplicationCore::instance()->getObjectBuilder();
        builder->destroy(getPQView(viewProxy));
        session->CleanupPendingProgress();
      }
      if (index.toInt() != 0)
      {
        int location = index.toInt();
        int parent_idx = vtkSMViewLayoutProxy::GetParent(location);
        this->layoutManager()->Collapse(location);
        pqViewFrame* frameToActivate = this->Internals->Frames[parent_idx];
        this->makeActive(frameToActivate);
      }
      END_UNDO_SET();
    }
    break;
  }
}

//-----------------------------------------------------------------------------
void pqMultiViewWidget::destroyAllViews()
{
  BEGIN_UNDO_SET(tr("Destroy all views"));
  pqObjectBuilder* builder = pqApplicationCore::instance()->getObjectBuilder();
  QList<vtkSMViewProxy*> views = this->viewProxies();
  Q_FOREACH (vtkSMViewProxy* view, views)
  {
    if (view)
    {
      builder->destroy(getPQView(view));
    }
  }
  END_UNDO_SET();
}

//-----------------------------------------------------------------------------
void pqMultiViewWidget::setDecorationsVisibility(bool val)
{
  auto& internals = (*this->Internals);
  internals.setDecorationsVisibility(val);
  Q_EMIT this->decorationsVisibilityChanged(val);
}

//-----------------------------------------------------------------------------
bool pqMultiViewWidget::decorationsVisibility() const
{
  auto& internals = (*this->Internals);
  return internals.decorationsVisibility();
}

//-----------------------------------------------------------------------------
void pqMultiViewWidget::swapPositions(const QString& uid_str)
{
  QUuid other(uid_str);

  vtkSMViewLayoutProxy* vlayout = this->layoutManager();
  pqViewFrame* senderFrame = qobject_cast<pqViewFrame*>(this->sender());
  if (!senderFrame || !vlayout)
  {
    return;
  }

  pqViewFrame* swapWith = nullptr;
  for (pqViewFrame* frame : this->Internals->Frames)
  {
    if (frame && frame->uniqueID() == other)
    {
      swapWith = frame;
      break;
    }
  }

  if (!swapWith)
  {
    return;
  }

  int id1 = senderFrame->property("FRAME_INDEX").toInt();
  int id2 = swapWith->property("FRAME_INDEX").toInt();
  vtkSMViewProxy* view1 = vlayout->GetView(id1);
  vtkSMViewProxy* view2 = vlayout->GetView(id2);

  if (view1 == nullptr && view2 == nullptr)
  {
    return;
  }

  BEGIN_UNDO_SET(tr("Swap Views"));
  vlayout->SwapCells(id1, id2);
  END_UNDO_SET();
  this->reload();
}

//-----------------------------------------------------------------------------
void pqMultiViewWidget::reset()
{
  this->layoutManager()->Reset();
}

//-----------------------------------------------------------------------------
QList<vtkSMViewProxy*> pqMultiViewWidget::viewProxies() const
{
  return this->Internals->ViewFrames.keys();
}

//-----------------------------------------------------------------------------
bool pqMultiViewWidget::togglePopout()
{
  auto& internals = (*this->Internals);
  internals.Popout = !internals.Popout;
  if (internals.Popout)
  {
    if (internals.PopoutWindow == nullptr)
    {
      internals.PopoutWindow.reset(new QWidget(this,
        Qt::Window | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowMaximizeButtonHint |
          Qt::WindowCloseButtonHint));
      internals.PopoutWindow->setObjectName("PopoutWindow");
      auto l = new QVBoxLayout(internals.PopoutWindow.data());
      l->setContentsMargins(0, 0, 0, 0);
      internals.PopoutWindow->resize(this->size());
    }

    if (vtkSMViewLayoutProxy* vlayout = this->layoutManager())
    {
      QString layoutName = vlayout->GetSessionProxyManager()->GetProxyName("layouts", vlayout);
      internals.PopoutWindow->setWindowTitle(layoutName);
    }

    this->layout()->removeWidget(internals.Container);
    this->layout()->addWidget(internals.PopoutPlaceholder.data());
    internals.PopoutPlaceholder->show();

    internals.PopoutWindow->layout()->addWidget(internals.Container);
    internals.PopoutWindow->show();
  }
  else
  {
    assert(internals.PopoutWindow != nullptr);
    internals.PopoutWindow->hide();
    internals.PopoutWindow->layout()->removeWidget(internals.Container);

    internals.PopoutPlaceholder->hide();
    this->layout()->removeWidget(internals.PopoutPlaceholder.data());
    this->layout()->addWidget(internals.Container);
  }
  return internals.Popout;
}

//-----------------------------------------------------------------------------
QSize pqMultiViewWidget::preview(const QSize& nsize)
{
  if (vtkSMViewLayoutProxy* vlayout = this->layoutManager())
  {
    int resolution[2];
    if (nsize.isEmpty())
    {
      resolution[0] = 0;
      resolution[1] = 0;
    }
    else
    {
      resolution[0] = nsize.width();
      resolution[1] = nsize.height();
    }
    SM_SCOPED_TRACE(PropertiesModified)
      .arg("proxy", vlayout)
      .arg("comment",
        nsize.isEmpty() ? qPrintable(tr("Exit preview mode"))
                        : qPrintable(tr("Enter preview mode")));

    vtkSMPropertyHelper(vlayout, "PreviewMode").Set(resolution, 2);
    //< results in a call to "layoutPropertyModified" if changed.
    vlayout->UpdateVTKObjects();
  }
  return this->Internals->Container->maximumSize();
}

//-----------------------------------------------------------------------------
pqViewFrame* pqMultiViewWidget::activeFrame()
{
  return this->Internals->ActiveFrame;
}

//-----------------------------------------------------------------------------
int pqMultiViewWidget::activeFrameLocation() const
{
  if (auto frame = this->Internals->ActiveFrame)
  {
    return frame->property("FRAME_INDEX").toInt();
  }
  return -1;
}
