Gui: enable dock/undock/fullscreen for all MDI widgets

This commit is contained in:
jffmichi
2025-07-01 20:29:50 +02:00
parent ef9378bd5b
commit 05045d6f10
8 changed files with 209 additions and 190 deletions

View File

@@ -1682,7 +1682,7 @@ void StdViewDock::activated(int iMsg)
bool StdViewDock::isActive()
{
MDIView* view = getMainWindow()->activeWindow();
return (qobject_cast<View3DInventor*>(view) ? true : false);
return view != nullptr;
}
//===========================================================================
@@ -1711,7 +1711,7 @@ void StdViewUndock::activated(int iMsg)
bool StdViewUndock::isActive()
{
MDIView* view = getMainWindow()->activeWindow();
return (qobject_cast<View3DInventor*>(view) ? true : false);
return view != nullptr;
}
//===========================================================================
@@ -1773,7 +1773,7 @@ void StdViewFullscreen::activated(int iMsg)
bool StdViewFullscreen::isActive()
{
MDIView* view = getMainWindow()->activeWindow();
return (qobject_cast<View3DInventor*>(view) ? true : false);
return view != nullptr;
}
//===========================================================================
@@ -1825,67 +1825,61 @@ void StdViewDockUndockFullscreen::activated(int iMsg)
if (!view) // no active view
return;
// nothing to do when the view is docked and 'Docked' is pressed
if (iMsg == 0 && view->currentViewMode() == MDIView::Child)
const auto oldmode = view->currentViewMode();
auto mode = (MDIView::ViewMode)iMsg;
// Pressing the same button again toggles the view back to docked.
if (mode == oldmode) {
mode = MDIView::Child;
}
if (mode == oldmode) {
return;
}
// Change the view mode after an mdi view was already visible doesn't
// work well with Qt5 any more because of some strange OpenGL behaviour.
// A workaround is to clone the mdi view, set its view mode and delete
// the original view.
Gui::Document* doc = Gui::Application::Instance->activeDocument();
if (doc) {
Gui::MDIView* clone = doc->cloneView(view);
if (!clone)
return;
const char* ppReturn = nullptr;
if (view->onMsg("GetCamera", &ppReturn)) {
std::string sMsg = "SetCamera ";
sMsg += ppReturn;
bool needsClone = mode == MDIView::Child || oldmode == MDIView::Child;
Gui::MDIView* clone = needsClone ? view->clone() : nullptr;
const char** pReturnIgnore=nullptr;
clone->onMsg(sMsg.c_str(), pReturnIgnore);
}
if (iMsg==0) {
if (clone) {
if (mode == MDIView::Child) {
getMainWindow()->addWindow(clone);
}
else if (iMsg==1) {
if (view->currentViewMode() == MDIView::TopLevel)
getMainWindow()->addWindow(clone);
else
clone->setCurrentViewMode(MDIView::TopLevel);
}
else if (iMsg==2) {
if (view->currentViewMode() == MDIView::FullScreen)
getMainWindow()->addWindow(clone);
else
clone->setCurrentViewMode(MDIView::FullScreen);
else {
clone->setCurrentViewMode(mode);
}
// destroy the old view
view->deleteSelf();
}
else {
// no clone needed, simply change the view mode
view->setCurrentViewMode(mode);
}
}
bool StdViewDockUndockFullscreen::isActive()
{
MDIView* view = getMainWindow()->activeWindow();
if (qobject_cast<View3DInventor*>(view)) {
// update the action group if needed
auto pActGrp = qobject_cast<ActionGroup*>(_pcAction);
if (pActGrp) {
int index = pActGrp->checkedAction();
int mode = (int)(view->currentViewMode());
if (index != mode) {
// active window has changed with another view mode
pActGrp->setCheckedAction(mode);
}
}
if (!view)
return false;
return true;
// update the action group if needed
auto pActGrp = qobject_cast<ActionGroup*>(_pcAction);
if (pActGrp) {
int index = pActGrp->checkedAction();
int mode = (int)(view->currentViewMode());
if (index != mode) {
// active window has changed with another view mode
pActGrp->setCheckedAction(mode);
}
}
return false;
return true;
}

View File

@@ -2016,7 +2016,7 @@ void Document::addRootObjectsToGroup(const std::vector<App::DocumentObject*>& ob
}
}
MDIView *Document::createView(const Base::Type& typeId)
MDIView* Document::createView(const Base::Type& typeId, CreateViewMode mode)
{
if (!typeId.isDerivedFrom(MDIView::getClassTypeId()))
return nullptr;
@@ -2062,11 +2062,16 @@ MDIView *Document::createView(const Base::Type& typeId)
for (App::DocumentObject* obj : child_vps)
view3D->getViewer()->removeViewProvider(getViewProvider(obj));
const char* name = getDocument()->Label.getValue();
QString title = QStringLiteral("%1 : %2[*]")
.arg(QString::fromUtf8(name)).arg(d->_iWinCount++);
// When cloning the view, don't increment the window counter as the old view will be deleted
// shortly after.
if (mode != CreateViewMode::Clone) {
const char* name = getDocument()->Label.getValue();
QString title =
QStringLiteral("%1 : %2[*]").arg(QString::fromUtf8(name)).arg(d->_iWinCount++);
view3D->setWindowTitle(title);
}
view3D->setWindowTitle(title);
view3D->setWindowModified(this->isModified());
view3D->resize(400, 300);
@@ -2075,65 +2080,19 @@ MDIView *Document::createView(const Base::Type& typeId)
view3D->onMsg(cameraSettings.c_str(),&ppReturn);
}
getMainWindow()->addWindow(view3D);
// When cloning the view, don't add the view to the main window. The whole purpose of the
// workaround using cloned views is that the view can be shown in undocked/fullscreen mode
// without having been docked before.
if (mode != CreateViewMode::Clone) {
getMainWindow()->addWindow(view3D);
}
view3D->getViewer()->redraw();
return view3D;
}
return nullptr;
}
Gui::MDIView* Document::cloneView(Gui::MDIView* oldview)
{
if (!oldview)
return nullptr;
if (oldview->is<View3DInventor>()) {
auto view3D = new View3DInventor(this, getMainWindow());
auto firstView = static_cast<View3DInventor*>(oldview);
std::string overrideMode = firstView->getViewer()->getOverrideMode();
view3D->getViewer()->setOverrideMode(overrideMode);
view3D->getViewer()->setAxisCross(firstView->getViewer()->hasAxisCross());
// attach the viewproviders. we need to make sure that we only attach the toplevel ones
// and not viewproviders which are claimed by other providers. To ensure this we first
// add all providers and then remove the ones already claimed
std::map<const App::DocumentObject*,ViewProviderDocumentObject*>::const_iterator It1;
std::vector<App::DocumentObject*> child_vps;
for (It1=d->_ViewProviderMap.begin();It1!=d->_ViewProviderMap.end();++It1) {
view3D->getViewer()->addViewProvider(It1->second);
std::vector<App::DocumentObject*> children = It1->second->claimChildren3D();
child_vps.insert(child_vps.end(), children.begin(), children.end());
}
std::map<std::string,ViewProvider*>::const_iterator It2;
for (It2=d->_ViewProviderMapAnnotation.begin();It2!=d->_ViewProviderMapAnnotation.end();++It2) {
view3D->getViewer()->addViewProvider(It2->second);
std::vector<App::DocumentObject*> children = It2->second->claimChildren3D();
child_vps.insert(child_vps.end(), children.begin(), children.end());
}
for (App::DocumentObject* obj : child_vps)
view3D->getViewer()->removeViewProvider(getViewProvider(obj));
view3D->setWindowTitle(oldview->windowTitle());
view3D->setWindowModified(oldview->isWindowModified());
view3D->setWindowIcon(oldview->windowIcon());
view3D->resize(oldview->size());
// FIXME: Add parameter to define behaviour by the calling instance
// View provider editing
if (d->_editViewProvider) {
firstView->getViewer()->resetEditingViewProvider();
view3D->getViewer()->setEditingViewProvider(d->_editViewProvider, d->_editMode);
}
return view3D;
}
return nullptr;
}
const char *Document::getCameraSettings() const {
return cameraSettings.size()>10?cameraSettings.c_str()+10:cameraSettings.c_str();
}

View File

@@ -58,6 +58,12 @@ class Application;
class DocumentPy;
class TransactionViewProvider;
enum class CreateViewMode
{
Normal,
Clone
};
/** The Gui Document
* This is the document on GUI level. Its main responsibility is keeping
* track off open windows for a document and warning on unsaved closes.
@@ -186,9 +192,7 @@ public:
Gui::MDIView* getViewOfViewProvider(const Gui::ViewProvider*) const;
Gui::MDIView* getViewOfNode(SoNode*) const;
/// Create a new view
MDIView *createView(const Base::Type& typeId);
/// Create a clone of the given view
Gui::MDIView* cloneView(Gui::MDIView*);
MDIView* createView(const Base::Type& typeId, CreateViewMode mode = CreateViewMode::Normal);
/** send messages to the active view
* Send a specific massage to the active view and is able to receive a
* return message

View File

@@ -25,6 +25,7 @@
#ifndef _PreComp_
# include <boost/signals2.hpp>
# include <boost/core/ignore_unused.hpp>
# include <QAction>
# include <QApplication>
# include <QEvent>
# include <QCloseEvent>
@@ -129,6 +130,11 @@ void MDIView::deleteSelf()
_pcDocument = nullptr;
}
MDIView* MDIView::clone()
{
return nullptr;
}
PyObject* MDIView::getPyObject()
{
if (!pythonObject)
@@ -207,7 +213,7 @@ bool MDIView::canClose()
return true;
}
void MDIView::closeEvent(QCloseEvent *e)
void MDIView::closeEvent(QCloseEvent* e)
{
if (canClose()) {
e->accept();
@@ -353,7 +359,8 @@ QSize MDIView::minimumSizeHint () const
return {400, 300};
}
void MDIView::changeEvent(QEvent *e)
void MDIView::changeEvent(QEvent* e)
{
switch (e->type()) {
case QEvent::ActivationChange:
@@ -377,6 +384,29 @@ void MDIView::changeEvent(QEvent *e)
}
}
bool MDIView::eventFilter(QObject* watched, QEvent* event)
{
// As long as this widget is a top-level window (either in 'TopLevel' or 'FullScreen' mode) we
// need to be notified when an action is added to a widget. This action must also be added to
// this window to allow one to make use of its shortcut (if defined).
// Note: We don't need to care about removing an action if its parent widget gets destroyed.
// This does the action itself for us.
if (watched != this && event->type() == QEvent::ActionAdded) {
auto actionEvent = static_cast<QActionEvent*>(event);
QAction* action = actionEvent->action();
if (!action->isSeparator()) {
QList<QAction*> acts = actions();
if (!acts.contains(action)) {
addAction(action);
}
}
}
return false;
}
#if defined(Q_WS_X11)
// To fix bug #0000345 move function declaration to here
extern void qt_x11_wait_for_window_manager( QWidget* w ); // defined in qwidget_x11.cpp
@@ -384,36 +414,43 @@ extern void qt_x11_wait_for_window_manager( QWidget* w ); // defined in qwidget_
void MDIView::setCurrentViewMode(ViewMode mode)
{
ViewMode oldmode = MDIView::currentViewMode();
if (oldmode == mode) {
return;
}
switch (mode) {
// go to normal mode
case Child:
{
if (this->currentMode == FullScreen) {
if (currentMode == FullScreen) {
showNormal();
setWindowFlags(windowFlags() & ~Qt::Window);
}
else if (this->currentMode == TopLevel) {
this->wstate = windowState();
else if (currentMode == TopLevel) {
wstate = windowState();
setWindowFlags( windowFlags() & ~Qt::Window );
}
if (this->currentMode != Child) {
this->currentMode = Child;
if (currentMode != Child) {
currentMode = Child;
getMainWindow()->addWindow(this);
getMainWindow()->activateWindow();
update();
}
} break;
// go to top-level mode
case TopLevel:
{
if (this->currentMode == Child) {
if (qobject_cast<QMdiSubWindow*>(this->parentWidget()))
getMainWindow()->removeWindow(this,false);
if (currentMode == Child) {
if (qobject_cast<QMdiSubWindow*>(parentWidget()))
getMainWindow()->removeWindow(this, false);
setWindowFlags(windowFlags() | Qt::Window);
setParent(nullptr, Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint |
Qt::WindowMinMaxButtonsHint);
if (this->wstate & Qt::WindowMaximized)
setParent(nullptr,
Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint
| Qt::WindowMinMaxButtonsHint);
if (wstate & Qt::WindowMaximized)
showMaximized();
else
showNormal();
@@ -424,35 +461,64 @@ void MDIView::setCurrentViewMode(ViewMode mode)
#endif
activateWindow();
}
else if (this->currentMode == FullScreen) {
if (this->wstate & Qt::WindowMaximized)
else if (currentMode == FullScreen) {
if (wstate & Qt::WindowMaximized)
showMaximized();
else
showNormal();
}
this->currentMode = TopLevel;
currentMode = TopLevel;
update();
} break;
// go to fullscreen mode
case FullScreen:
{
if (this->currentMode == Child) {
if (qobject_cast<QMdiSubWindow*>(this->parentWidget()))
getMainWindow()->removeWindow(this,false);
if (currentMode == Child) {
if (qobject_cast<QMdiSubWindow*>(parentWidget()))
getMainWindow()->removeWindow(this, false);
setWindowFlags(windowFlags() | Qt::Window);
setParent(nullptr, Qt::Window);
showFullScreen();
}
else if (this->currentMode == TopLevel) {
this->wstate = windowState();
else if (currentMode == TopLevel) {
wstate = windowState();
showFullScreen();
}
this->currentMode = FullScreen;
currentMode = FullScreen;
update();
} break;
}
if (oldmode == Child) {
// To make a global shortcut working from this window we need to add
// all existing actions from the mainwindow and its sub-widgets
QList<QAction*> acts = getMainWindow()->findChildren<QAction*>();
addActions(acts);
// To be notfified for new actions
qApp->installEventFilter(this);
}
else if (mode == Child) {
qApp->removeEventFilter(this);
QList<QAction*> acts = actions();
for (QAction* it : acts) {
removeAction(it);
}
// When switching from undocked to docked mode, the widget position is somehow not updated
// correctly. In this case mapToGlobal(Point()) returns {0, 0} even though the widget is
// clearly not at the top-left corner of the screen. We fix this by briefly changing the
// maximum size of the widget.
const auto oldsize = maximumSize();
setMaximumSize({1, 1});
setMaximumSize(oldsize);
}
}
QString MDIView::buildWindowTitle() const

View File

@@ -70,6 +70,8 @@ public:
*/
~MDIView() override;
virtual MDIView* clone();
/// get called when the document is updated
void onRelabel(Gui::Document *pDoc) override;
virtual void viewAll();
@@ -177,9 +179,11 @@ protected Q_SLOTS:
virtual void windowStateChanged(QWidget*);
protected:
void closeEvent(QCloseEvent *e) override;
void closeEvent(QCloseEvent* e) override;
/** \internal */
void changeEvent(QEvent *e) override;
void changeEvent(QEvent* e) override;
bool eventFilter(QObject* watched, QEvent* e) override;
protected:
PyObject* pythonObject;

View File

@@ -1234,6 +1234,7 @@ void MainWindow::removeWindow(Gui::MDIView* view, bool close)
auto subwindow = qobject_cast<QMdiSubWindow*>(parent);
if(subwindow && d->mdiArea->subWindowList().contains(subwindow)) {
subwindow->setParent(nullptr);
subwindow->deleteLater();
assert(!d->mdiArea->subWindowList().contains(subwindow));
}

View File

@@ -24,7 +24,6 @@
#ifndef _PreComp_
# include <string>
# include <QAction>
# include <QApplication>
# include <QKeyEvent>
# include <QEvent>
@@ -39,6 +38,7 @@
# include <QPrintDialog>
# include <QPrintPreviewDialog>
# include <QStackedWidget>
# include <QSurfaceFormat>
# include <QTimer>
# include <QUrl>
# include <QWindow>
@@ -94,9 +94,12 @@ void GLOverlayWidget::paintEvent(QPaintEvent*)
TYPESYSTEM_SOURCE_ABSTRACT(Gui::View3DInventor,Gui::MDIView)
View3DInventor::View3DInventor(Gui::Document* pcDocument, QWidget* parent,
const QOpenGLWidget* sharewidget, Qt::WindowFlags wflags)
: MDIView(pcDocument, parent, wflags), _viewerPy(nullptr)
View3DInventor::View3DInventor(Gui::Document* pcDocument,
QWidget* parent,
const QOpenGLWidget* sharewidget,
Qt::WindowFlags wflags)
: MDIView(pcDocument, parent, wflags)
, _viewerPy(nullptr)
{
stack = new QStackedWidget(this);
// important for highlighting
@@ -189,6 +192,30 @@ void View3DInventor::deleteSelf()
MDIView::deleteSelf();
}
View3DInventor* View3DInventor::clone()
{
auto mdiView = _pcDocument->createView(getClassTypeId(), CreateViewMode::Clone);
auto view3D = static_cast<View3DInventor*>(mdiView);
view3D->getViewer()->setAxisCross(getViewer()->hasAxisCross());
view3D->setWindowTitle(windowTitle());
view3D->setWindowIcon(windowIcon());
view3D->resize(size());
// FIXME: Add parameter to define behaviour by the calling instance
// View provider editing
int editMode;
ViewProvider* editViewProvider = _pcDocument->getInEdit(nullptr, nullptr, &editMode);
if (editViewProvider) {
getViewer()->resetEditingViewProvider();
view3D->getViewer()->setEditingViewProvider(editViewProvider, editMode);
}
return view3D;
}
PyObject *View3DInventor::getPyObject()
{
if (!_viewerPy)
@@ -722,33 +749,27 @@ void View3DInventor::dragEnterEvent (QDragEnterEvent * e)
e->ignore();
}
void View3DInventor::setCurrentViewMode(ViewMode newmode)
void View3DInventor::setCurrentViewMode(ViewMode mode)
{
ViewMode oldmode = MDIView::currentViewMode();
if (oldmode == newmode)
ViewMode oldmode = currentViewMode();
if (mode == oldmode) {
return;
if (newmode == Child) {
// Fix in two steps:
// The mdi view got a QWindow when it became a top-level widget and when resetting it to a child widget
// the QWindow must be deleted because it has an impact on resize events and may break the layout of
// mdi view inside the QMdiSubWindow.
// In the second step below the layout must be invalidated after it's again a child widget to make sure
// the mdi view fits into the QMdiSubWindow.
QWindow* winHandle = this->windowHandle();
if (winHandle)
winHandle->destroy();
}
MDIView::setCurrentViewMode(newmode);
if (mode == Child) {
// Fix in two steps:
// The mdi view got a QWindow when it became a top-level widget and when resetting it to a
// child widget the QWindow must be deleted because it has an impact on resize events and
// may break the layout of mdi view inside the QMdiSubWindow. In the second step below the
// layout must be invalidated after it's again a child widget to make sure the mdi view fits
// into the QMdiSubWindow.
QWindow* winHandle = this->windowHandle();
if (winHandle) {
winHandle->destroy();
}
}
// Internally the QOpenGLWidget switches of the multi-sampling and there is no
// way to switch it on again. So as a workaround we just re-create a new viewport
// The method is private but defined as slot to avoid to call it by accident.
//int index = _viewer->metaObject()->indexOfMethod("replaceViewport()");
//if (index >= 0) {
// _viewer->qt_metacall(QMetaObject::InvokeMetaMethod, index, 0);
//}
MDIView::setCurrentViewMode(mode);
// This widget becomes the focus proxy of the embedded GL widget if we leave
// the 'Child' mode. If we reenter 'Child' mode the focus proxy is reset to 0.
@@ -760,26 +781,18 @@ void View3DInventor::setCurrentViewMode(ViewMode newmode)
//
// It is important to set the focus proxy to get all key events otherwise we would lose
// control after redirecting the first key event to the GL widget.
if (oldmode == Child) {
// To make a global shortcut working from this window we need to add
// all existing actions from the mainwindow and its sub-widgets
QList<QAction*> acts = getMainWindow()->findChildren<QAction*>();
this->addActions(acts);
_viewer->getGLWidget()->setFocusProxy(this);
// To be notfified for new actions
qApp->installEventFilter(this);
}
else if (newmode == Child) {
else if (mode == Child) {
_viewer->getGLWidget()->setFocusProxy(nullptr);
qApp->removeEventFilter(this);
QList<QAction*> acts = this->actions();
for (QAction* it : acts)
this->removeAction(it);
// Step two
auto mdi = qobject_cast<QMdiSubWindow*>(parentWidget());
if (mdi && mdi->layout())
if (mdi && mdi->layout()) {
mdi->layout()->invalidate();
}
}
}
@@ -874,27 +887,6 @@ RayPickInfo View3DInventor::getObjInfoRay(Base::Vector3d* startvec, Base::Vector
return ret;
}
bool View3DInventor::eventFilter(QObject* watched, QEvent* e)
{
// As long as this widget is a top-level window (either in 'TopLevel' or 'FullScreen' mode) we
// need to be notified when an action is added to a widget. This action must also be added to
// this window to allow one to make use of its shortcut (if defined).
// Note: We don't need to care about removing an action if its parent widget gets destroyed.
// This does the action itself for us.
if (watched != this && e->type() == QEvent::ActionAdded) {
auto a = static_cast<QActionEvent*>(e);
QAction* action = a->action();
if (!action->isSeparator()) {
QList<QAction*> actions = this->actions();
if (!actions.contains(action))
this->addAction(action);
}
}
return false;
}
void View3DInventor::keyPressEvent (QKeyEvent* e)
{
// See StdViewDockUndockFullscreen::activated()

View File

@@ -85,6 +85,8 @@ public:
View3DInventor(Gui::Document* pcDocument, QWidget* parent, const QOpenGLWidget* sharewidget = nullptr, Qt::WindowFlags wflags=Qt::WindowFlags());
~View3DInventor() override;
View3DInventor* clone() override;
/// Message handler
bool onMsg(const char* pMsg, const char** ppReturn) override;
bool onHasMsg(const char* pMsg) const override;
@@ -132,9 +134,6 @@ public Q_SLOTS:
protected Q_SLOTS:
void stopAnimating();
public:
bool eventFilter(QObject*, QEvent* ) override;
private:
void applySettings();