From 05045d6f10d809727e3a0293ddba1c6130e16d55 Mon Sep 17 00:00:00 2001 From: jffmichi <> Date: Tue, 1 Jul 2025 20:29:50 +0200 Subject: [PATCH] Gui: enable dock/undock/fullscreen for all MDI widgets --- src/Gui/CommandView.cpp | 80 +++++++++++++-------------- src/Gui/Document.cpp | 75 ++++++------------------- src/Gui/Document.h | 10 +++- src/Gui/MDIView.cpp | 110 +++++++++++++++++++++++++++++-------- src/Gui/MDIView.h | 8 ++- src/Gui/MainWindow.cpp | 1 + src/Gui/View3DInventor.cpp | 110 +++++++++++++++++-------------------- src/Gui/View3DInventor.h | 5 +- 8 files changed, 209 insertions(+), 190 deletions(-) diff --git a/src/Gui/CommandView.cpp b/src/Gui/CommandView.cpp index 26eb5434fc..30dfdb0911 100644 --- a/src/Gui/CommandView.cpp +++ b/src/Gui/CommandView.cpp @@ -1682,7 +1682,7 @@ void StdViewDock::activated(int iMsg) bool StdViewDock::isActive() { MDIView* view = getMainWindow()->activeWindow(); - return (qobject_cast(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(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(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(view)) { - // update the action group if needed - auto pActGrp = qobject_cast(_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(_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; } diff --git a/src/Gui/Document.cpp b/src/Gui/Document.cpp index 50d97b3b17..8fe1118b88 100644 --- a/src/Gui/Document.cpp +++ b/src/Gui/Document.cpp @@ -2016,7 +2016,7 @@ void Document::addRootObjectsToGroup(const std::vector& 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()) { - auto view3D = new View3DInventor(this, getMainWindow()); - - auto firstView = static_cast(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_iterator It1; - std::vector child_vps; - for (It1=d->_ViewProviderMap.begin();It1!=d->_ViewProviderMap.end();++It1) { - view3D->getViewer()->addViewProvider(It1->second); - std::vector children = It1->second->claimChildren3D(); - child_vps.insert(child_vps.end(), children.begin(), children.end()); - } - std::map::const_iterator It2; - for (It2=d->_ViewProviderMapAnnotation.begin();It2!=d->_ViewProviderMapAnnotation.end();++It2) { - view3D->getViewer()->addViewProvider(It2->second); - std::vector 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(); } diff --git a/src/Gui/Document.h b/src/Gui/Document.h index ae27120a6b..d1d186796a 100644 --- a/src/Gui/Document.h +++ b/src/Gui/Document.h @@ -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 diff --git a/src/Gui/MDIView.cpp b/src/Gui/MDIView.cpp index 253e8bb9b2..2ce4e7d40d 100644 --- a/src/Gui/MDIView.cpp +++ b/src/Gui/MDIView.cpp @@ -25,6 +25,7 @@ #ifndef _PreComp_ # include # include +# include # include # include # include @@ -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(event); + QAction* action = actionEvent->action(); + + if (!action->isSeparator()) { + QList 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(this->parentWidget())) - getMainWindow()->removeWindow(this,false); + if (currentMode == Child) { + if (qobject_cast(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(this->parentWidget())) - getMainWindow()->removeWindow(this,false); + if (currentMode == Child) { + if (qobject_cast(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 acts = getMainWindow()->findChildren(); + addActions(acts); + + // To be notfified for new actions + qApp->installEventFilter(this); + } + else if (mode == Child) { + qApp->removeEventFilter(this); + QList 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 diff --git a/src/Gui/MDIView.h b/src/Gui/MDIView.h index 64c92ee4d8..49dd1d254c 100644 --- a/src/Gui/MDIView.h +++ b/src/Gui/MDIView.h @@ -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; diff --git a/src/Gui/MainWindow.cpp b/src/Gui/MainWindow.cpp index 20d1ba7c98..e620ef8729 100644 --- a/src/Gui/MainWindow.cpp +++ b/src/Gui/MainWindow.cpp @@ -1234,6 +1234,7 @@ void MainWindow::removeWindow(Gui::MDIView* view, bool close) auto subwindow = qobject_cast(parent); if(subwindow && d->mdiArea->subWindowList().contains(subwindow)) { subwindow->setParent(nullptr); + subwindow->deleteLater(); assert(!d->mdiArea->subWindowList().contains(subwindow)); } diff --git a/src/Gui/View3DInventor.cpp b/src/Gui/View3DInventor.cpp index 7b2f2b241f..00a8a790ed 100644 --- a/src/Gui/View3DInventor.cpp +++ b/src/Gui/View3DInventor.cpp @@ -24,7 +24,6 @@ #ifndef _PreComp_ # include -# include # include # include # include @@ -39,6 +38,7 @@ # include # include # include +# include # include # include # include @@ -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(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 acts = getMainWindow()->findChildren(); - 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 acts = this->actions(); - for (QAction* it : acts) - this->removeAction(it); // Step two auto mdi = qobject_cast(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(e); - QAction* action = a->action(); - - if (!action->isSeparator()) { - QList actions = this->actions(); - if (!actions.contains(action)) - this->addAction(action); - } - } - - return false; -} - void View3DInventor::keyPressEvent (QKeyEvent* e) { // See StdViewDockUndockFullscreen::activated() diff --git a/src/Gui/View3DInventor.h b/src/Gui/View3DInventor.h index efa6416844..de36d6b1c9 100644 --- a/src/Gui/View3DInventor.h +++ b/src/Gui/View3DInventor.h @@ -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();