Dock widgets

Met.3D uses Qt-Advanced-Docking-System (ADS) for the main-window layout. It allows users to move dock widgets, tab them together, float them in separate windows, place them in auto-hide sidebars, and hide or close them depending on the dock’s configuration.

This page explains how Met.3D uses ADS and how to create and manage dock widgets.

The Qt-Advanced-Docking-System

General information about the docking system can be found in their user guide. It explains different configuration settings for dock widgets. We recommend reading the user guide before continuing with this page.

The docking system divides a window into several docking areas. New docking areas can be created by splitting an existing one.

Met.3D’s main window uses a docking layout with a single, central dock area. It contains a dummy dock widget for the central scene view. It has the NoTab flag, so that its tab is not displayed in the tab bar.

By default, the central dock widget is used for scene views. If no scene view is placed in that dock area, a button is displayed to create a new view.

When working with Met.3D’s docking system, distinguish between the following three objects:

  • Content widget – the actual Met.3D UI, implemented as a QWidget.

  • Dock widget – an ads::CDockWidget that provides the docking UI around the content widget.

  • Dock area – an ads::CDockAreaWidget containing one or more dock widgets, usually as tabs.

Content widgets

Each dock widget displays exactly one content widget at a time. It can be any class inheriting from QWidget or its descendants. The content widget can be changed any time.

Dock widgets

Dock widgets are the main component of the docking system. They hold one content widget.

Dock widgets are generally owned by MMainWindow and can outlive their content widgets. There are exceptions to this rule, but few. When a content widget is destroyed, make sure its dock widget is removed from the layout and deleted.

Dock Areas

The window is subdivided into multiple dock areas. A dock area can contain more than a single dock widget. Dock areas are not persistent. If the last dock widget of an area is removed, the area is destroyed. Therefore, never store a ads::CDockAreaWidget * for later use, unless you know that at least one dock widget will keep it alive. If you need to refer to an area later, obtain it from an existing dock widget that is currently in that area.

Each dock widget exposes the area it is currently placed in:

dockWidget->dockAreaWidget();

Custom docking components

The Qt-Advanced-Docking-System allows us to create custom components for its docking system. Met.3D provides custom implementations for the title bar and the individual dock widget tabs.

The custom title bar is designed to allow users to dynamically create more dock widgets of a certain type. For this purpose a button is added to the title bar. It appears as a + next to all tabs.

An example of such a dock widget would be the scene view. The user can create more than one scene view dynamically. If a scene view is docked to an area, the button is added to the title bar and the user can create more scene views when pressing it.

The custom tab implementation allows a dock widget’s content widget to provide its registered actions as context-menu options of the dock widget tab.

MDockWidget

MDockWidget is an extension of the standard ads::CDockWidget.

It provides a MDynamicDockCreationAction for our custom title bar. If a dock widget of this type is added to a dock area, the action is used to let the user create new dock widgets for this content.

For example, the scene view uses dock widgets of MDockWidget, and provides a MDynamicDockCreationAction that creates a new scene view in the current session. Since the session’s signal is used to automatically dock the scene view, the scene view will be placed into the dock layout.

When the user presses the + button in a dock area’s title bar, the action records that dock area as its triggering area. After the new content has been created, takeTriggeringArea() can be used to retrieve that area and place the new dock widget there. This will also reset the stored area to null.

1auto area = creationAction->takeTriggeringArea();

Because dock areas are transient, the returned pointer should only be used during the corresponding creation operation. Do not retain it for later use.

Ownership and lifetime

In general, MMainWindow owns the dock widgets it creates. The content widgets are owned by their respective application/session objects and do not necessarily have the main window as their owner. Consequently, deleting a content widget does not necessarily mean that its dock widget should be deleted automatically, and closing a dock widget does not necessarily mean that its underlying session object should be deleted.

For dynamically created session content, use the session and undo framework to manage the lifetime of the content. MMainWindow reacts to session signals by creating or removing the corresponding dock widgets.

Creating new dock widgets

This section will explain how to create new dock widgets. You should already have created the content widget for your dock widget.

First, create a new dock widget and set it up to display your content widget:

1// ...
2yourDock = new ads::CDockWidget(dockManager, "Display name", this);
3yourDock->setObjectName("<your unique id>");
4yourDock->setWidget(yourContentWidget);
5// ...

Give every dock widget a stable, unique objectName. The object name is used by the docking system when identifying dock widgets and is particularly important for saving and restoring layouts. It has to be unique between all dock widgets within a session. When creating a unique ID, prefer to use the session-unique IDs of their content widgets instead of global ID counters. This makes sure, that the dock layout can properly be restored.

An example is scene views. They have a global ID, that is counted up over the lifetime of the Met.3D application. Additionally, they have a session-local ID, that changes depending on how many scene views are present in the session. With three scene views open, removing view two causes view three to take ID two in its place. See MSceneViewGLWidget::getID() for the session local ID and MSceneViewGLWidget::updateSessionIndex() where it is updated.

The dock widget either has to be created in MMainWindow, or utilize MMainWindow::getDockManager() to get access to the dock manager. Normally, dock widgets should be created in MMainWindow, as it represents our window.

Do not make session/content classes responsible for manipulating the main-window docking layout. Session objects should manage their domain objects and emit signals. MMainWindow translates those signals into dock widgets and docking operations.

For example, a session contains scene views. When they are created inside a session, a signal is emitted that is connected to a slot in the main window, MMainWindow::onSceneViewRegistered. It creates the dock widget for the scene view.

Adding dock widgets to the dock layout

Once your dock widget is created, you can add it to the dock layout. There are several ways to do so, depending on your use-case.

Note

We currently store all our dock widgets as class members of MMainWindow. This does not mean that all content widgets are part of MMainWindow. They can be stored where appropriate.

Adding dock widget to dock area

The standard way to add a dock widget would be to add it to an existing dock area or create a new one.

To create a new dock area, based on the central dock area, you can use:

1dockManager->addDockWidget(ads::LeftDockWidgetArea, yourDock);

This would add your dock widget to the left dock area of the central dock area. If it does not exist, the central dock area is split to create the left dock area.

To add your dock widget to an existing dock area, you can use:

1dockManager->addDockWidgetTabToArea(yourDock, existingArea);

This adds your dock widget as a tab to the existing dock area. When using dock areas, always get them from existing dock widgets. Since the user can modify the dock layout at will, it is not guaranteed that a saved dock area still exists.

An example of this is the system manager dock widget.

Adding dock widget floating

Another way to add your dock widget is as a floating dock widget. It is initially placed in a separate floating dock container rather than being docked into the main window. The user can then drag the window and drop it where they want.

Alternatively, the user can also dock other dock widgets inside the created floating window, much like the main window.

To add your dock widget as a floating dock widget, use:

1dockManager->addDockWidgetFloating(yourDock);

This will create a new floating window with one dock area containing the dock widget.

An example of a floating dock widget is the analysis control, opened in analysis mode, when the user analyzes an iso-surface.

Adding auto-hide dock widgets

Auto-hide dock widgets are placed on sidebars around the window. They only show a tab with their name. Hovering over a tab temporarily opens the dock widget as an overlay above the window. The dock widget remains part of the docking system, only its presentation is changed when it is auto-hidden.

The user can also pin these docks, which adds them to a dock area at their corresponding sidebar.

To add a dock widget as an auto-hide container, use:

1dockManager->addAutoHideDockWidget(ads::SideBarLeft, yourDock);

This adds your dock widget to the left sidebar as an auto-hide dock.

An example of an auto-hide dock widget is the bounding box dock widget. It is placed on the bottom sidebar, and can briefly be opened by hovering its tab.

Default dock widget layout

When creating a new, empty session, Met.3D resets the dock widget layout to its default layout. This also happens for sessions without a stored window layout. All dock widgets are placed at their default locations.

This is implemented in MMainWindow::resetDockLayout(). It defines the default arrangement of all docks. The user can modify this layout as part of their session and save it with the session.

When creating a new dock widget, as explained above, make sure to also add logic on where it is placed in the default layout.

For example, to create a dock widget that is placed as an auto-hide dock widget on the bottom sidebar:

 1void MMainWindow::resetDockLayout()
 2{
 3    // Remove yourDock from the dock manager
 4    dockManager->removeDockWidget(yourDock);
 5
 6    //...
 7
 8    // Add it to its default location after all docks have been removed.
 9    dockManager->addAutoHideDockWidget(ads::SideBarBottom, yourDock);
10}

Remove all relevant dock widgets before rebuilding the default layout. This avoids lingering dock widgets in areas that might no longer exist and ensures that the default layout can be restored properly.

Important

All docks that are removed but not re-added will be hidden.

Closing dock widgets

It is important to consider what should happen when the user is closing your dock widget. For some, such as the scene view dock widgets, they should delete their content widget from the session. Others, such as the bounding box dock widget, should not be closable at all, and instead should be hidden from the layout. These should have an action in the View menu that toggles visibility.

Custom close handling

Dock widgets only hide by default when closed. Setting the DockWidgetDeleteOnClose flag will delete the dock widget and its content when the dock widget is closed. This is not always desired, as some of our content widgets are part of the session.

You can specify custom close handling when creating your dock widget as follows:

 1// After creating your dock, set the custom close handling feature.
 2yourDock->setFeature(ads::CDockWidget::CustomCloseHandling, true);
 3
 4// Set the content widget.
 5yourDock->setWidget(contentWidget);
 6
 7// Connect callback to handle closing the dock
 8connect(yourDock, &ads::CDockWidget::closeRequested, [this, contentWidget]()
 9{
10    // Check for user confirmation
11    if (...)
12    {
13        // Handle the deletion of the content widget.
14    }
15});

The dock widget is not removed, but instead emits the close request. Usually, this should use Undo command to make the deletion of the dock widget’s content undoable. Afterwards, the dock widget itself also needs to be deleted. This can happen as part of your signal / slot connections automatically, or if that is not the case, should be done manually after running the undo command.

Unclosable dock widgets

For dock widgets that the user should not be able to close, but merely hide, you should add a so-called toggle view action to the View menu. This action can be used by the user to either hide or show the dock widget. If the user clicks the close button of the dock widget, it is hidden instead, and can be shown again via this action.

This is useful for tool or editor dock widgets, such as the bounding box dock widget.

Adding it to the view menu of MMainWindow does not require much:

1ui->menu_View->addAction(yourDock->toggleViewAction());

Note

Since the view menu contains multiple sections, one of which for toggle-view actions, use insertAction() to place it relative to existing toggle-view actions for dock widgets.

The dynamic creation action

This section explains in detail how to utilize MDockWidget and MDynamicDockCreationAction to let the user create more dock widgets for a certain content.

What you generally need to implement such a widget is the following:

  • A session object for your content widget – See Storing objects in a session for more info on what that requires.

  • MDynamicDockCreationAction allows the user to create new instances of your content widget.

  • A slot to actually create the new instance of your content widget.

  • Slots to react to creation / deletion of your content widget in the session. These will create your dock widget.

  • An MDockWidget that actually displays your content widget.

First, you need to create the MDynamicDockCreationAction. This is usually done in the MMainWindow:

 1MMainWindow::MMainWindow()
 2{
 3    // ...
 4    yourAction = new MDynamicDockCreationAction("Create your content");
 5    connect(yourAction, &QAction::triggered, this, &MMainWindow::onCreateYourContent);
 6    ui->menu_Edit->addAction(yourAction); // Optionally add it to the edit menu.
 7    // ...
 8}
 9
10void MMainWindow::onCreateYourContent()
11{
12    // Create the new content widget.
13}

It also requires a slot that creates the new widget and places it into a new dock widget. The action should trigger the creation of the content widget. This is usually a session object, see Sessions. The creation of the content widget should be undoable, see Undo framework. Once the session object was created, the session should emit a signal. Connect this signal to a slot in MMainWindow that creates the corresponding dock widget:

 1void MMainWindow::switchSession()
 2{
 3    // ...
 4    connect(currentSession, &MSession::yourContentWidgetCreated,
 5            this, &MMainWindow::onYourContentWidgetCreated);
 6    // ...
 7}
 8
 9void MMainWindow::onYourContentWidgetCreated()
10{
11    // Create dock widget
12}

Creating the dock widget itself is straightforward:

1auto *yourNewDock = new MDockWidget(dockManager, "Display name", this);
2yourNewDock->setObjectName("<your unique id>");
3yourNewDock->setWidget(yourNewWidget);
4yourNewDock->setDynamicDockCreationAction(yourAction);
5
6yourDocks.append(yourNewDock);

Met.3D currently keeps references to all main-window dock widgets in MMainWindow. This makes it possible to reset their layout and clean them up when switching sessions.

You may want to utilize MDynamicDockCreationAction::takeTriggeringArea() here to insert it into the correct area. Alternatively, re-insert the dock widget in your MMainWindow::onCreateYourContent() slot after running your undo-command that creates the content widget. Next, add your dynamically created dock widgets to their default intended layout in MMainWindow::resetDockLayout():

 1void MMainWindow::resetDockLayout()
 2{
 3    // Remove your docks from the dock manager
 4    for (auto dock : yourDocks)
 5    {
 6        dockManager->removeDockWidget(dock);
 7    }
 8
 9    //...
10
11    for (auto dock : yourDocks)
12    {
13        // Dock to their default location.
14    }
15}

Lastly, remove all your created dock widgets in MMainWindow::closeCurrentSession(). Otherwise, some of your docks might persist across sessions.

Action -> MMainWindow: action triggered
MMainWindow -> UndoStack: run CreateYourContentCommand
UndoStack -> Session: create new YourContent
Session -> MMainWindow: new content created
MMainWindow -> MMainWindow: create dock widget
MMainWindow -> Action: take area
Action -> MMainWindow: return area
MMainWindow -> MMainWindow: dock the dock widget to area

Fig. 43 Sequence diagram for a dynamically created dock widget and the steps involved to create it for a session object.

Note

You should probably use custom close handling, so that your dynamically created content widgets can be removed by the user. This should undoably remove the content widget from the current session, if it is a session object.