Create dock widgets over Slicer views

Hi.

I’m having problems implementing a floating (unmovable) widget container over the views like in these images:





As you can see the floating widgets appear over the views (3D and slices) and I think they should have a fixed relative position with reference to the corresponding view in case of mainWindow moveEvent or resizeEvent.
They should appear on click of the toolbar on the left but that I think I know how to do.

Let me show you the approaches I tried without success:

  • Use QMenu:
    QMenuTest
#sliceViewMenu
toolButton = qt.QToolButton()
toolButton.setText('R')
slicer.app.layoutManager().sliceWidget("Red").sliceController().barLayout().insertWidget(0,toolButton)

myMenu = qt.QMenu(toolButton)
toolButton.setMenu(myMenu)

toolButton.setPopupMode(qt.QToolButton.InstantPopup)

widget = qt.QWidget()
layout = qt.QVBoxLayout(widget)
pushButtonPlus = qt.QPushButton('+',widget)
slider = qt.QSlider(widget)
pushButtonMinus = qt.QPushButton('-',widget)
layout.addWidget(pushButtonPlus)
layout.addWidget(slider)
layout.setAlignment(slider, qt.Qt.AlignHCenter)
layout.addWidget(pushButtonMinus)

widgetAction = qt.QWidgetAction(myMenu)
widgetAction.setDefaultWidget(widget)

myMenu.addAction(widgetAction)

But you can’t have more that one menu shown at a time. So this doesn’t work.

  • Use ctkPopUpWidget (like the one on the pinButton of the sliceController) and set True to PinUp

ctkPopUpWidgetTest

ControllerLayout = slicer.app.layoutManager().sliceWidget("Yellow").sliceController().layout()

PopupWidget = ctk.ctkPopupWidget(slicer.app.layoutManager().sliceWidget("Yellow").sliceController())
PopupWidget.setSizePolicy(qt.QSizePolicy.Minimum, qt.QSizePolicy.Minimum)
ControllerLayout.addWidget(PopupWidget)
PopupWidget.setWindowFlags(PopupWidget.windowFlags() & ~qt.Qt.ToolTip)

layout = qt.QVBoxLayout(PopupWidget)
pushButtonPlus = qt.QPushButton('+',PopupWidget)
slider = qt.QSlider(PopupWidget)
pushButtonMinus = qt.QPushButton('-',PopupWidget)
layout.addWidget(pushButtonPlus)
layout.addWidget(slider)
layout.setAlignment(slider, qt.Qt.AlignHCenter)
layout.addWidget(pushButtonMinus)
PopupWidget.pinPopup(True)

But the horizontalLayout the popup widget is in makes it as large as the sliceController widget. So this approach doesn’t work either.

There were suggestions to derive the mainWindow class to change resizeEvent and moveEvent, or create a showDockWidgets function but I can’t follow them because the mainWindow is owned by Slicer. This one may also be handy.

QgsFloatingWidget widget appears to describe what I need.

Any guidance regarding the implementation of this would be greatly appreciated.

Floating QWidgets I think is something not primarily in the design structure for Qt. It seems like QWidgets are primarily designed to have a parent that is some QLayout and floating widgets don’t really have a layout parent. I can really only think about using actual QDockWidgets such as the behavior of the Module panel area or the Python Interactor. Those are dock widgets which can be docked or set to float. They would float over the entire main window though and just over some specific widget like an individual slice view.

You showcase actions for

  • Adjusting Window/Level
  • Panning the volume in the view
  • Zooming the volume in the view
  • Rotating the volume in the view
  • Flipping the volume in the view

What are the motivations for the design of floating widgets?

  • Is it to establish familiarity to the other program you showcase?
  • Is it not acceptable to use Slicer’s left module panel area for these actions? The other program simply doesn’t have a design with an area for other widgets so they have to be temporarily floated on top of the slice view.
  • Are you hiding the Slicer left module panel area to maximize the Slice viewers area which then requires you to float widgets on top of the slice views?
  • Is a 3 button mouse not being used which would otherwise support zoom/pan/etc actions without visible widgets?
  • Are you optimizing for a touch screen interface where a mouse is not present?

Can the containers (QDockWidget) be moved, if the mainWindow of Slicer is moved, automatically? or if it is resized adapt to that?

Another way could be to derive qMRMLSliceWidget and be set up the floating widget there. Can you give guidance regarding the implementation of this? I think I would need to remake all the layouts to use my_qMRMLSliceWidget also.

To answer your questions:

Is it to establish familiarity to the other program you showcase?

I think so.

Is it not acceptable to use Slicer’s left module panel area for these actions? The other program simply doesn’t have a design with an area for other widgets so they have to be temporarily floated on top of the slice view.

I think design is more comfortable to the users if they use a touchscreen

Are you hiding the Slicer left module panel area to maximize the Slice viewers area which then requires you to float widgets on top of the slice views?

The left module panel area is already used for the workflow. I will add the toolbar (left-side) that is shown in the images, that will steal little bit of space but is acceptable. We cannot have widgets for the showcase actions that are not floating over the views because there is not space.

Is a 3 button mouse not being used which would otherwise support zoom/pan/etc actions without visible widgets?

The GUI must be compatible with touchscreens and it would require users to buy specific hardware.

Are you optimizing for a touch screen interface where a mouse is not present?

Not optimizing but making it usable enough. So the software is good for mouse and good for touchscreen.

It’s an interesting use case of Slicer if touch is the primary input. I don’t think a lot of Slicer design has considered this in the past.

Does the touchscreen support multi touch gestures? Or is it simple input only?

For example in the following linked post there is a video of pinch-to-zoom and rotating working which may avoid the need for individual buttons that do the same.

On Windows, multitouch gestures work very well in 3D views to rotate, pan, zoom, spin. @Sunderlandkyl worked quite a lot on optimizing usability with a pen, too. On macOS, the rotate, pan, zoom, spin multitouch gestures are available on the touchpad.

It is a very interesting discussion. There are a number of projects where ease of use for newcomers is one of the most important design driver. For these cases, discoverability of the features via large, impossible-to-miss buttons is useful.

Rendering a Qt widget over a VTK render window will be always risky (may break with any VTK or Qt update), but it might work, so it is worth a try. We know that floating windows are problematic (the current floating window has problems with some window managers in certain situations - for example when alt-tabbing between applications). It would be much safer to add a button bar on the top or side of the render window.

@ungi you have implemented a few nice designs that addresses similar requirements. Could you tell about your experiences?

The solution I’ve seen from @Sunderlandkyl is to add a QToolButton to the view controller area, and then create a ctkPopupWidget with the tool button as parent. Then the popup widget does not span across the width of the view but shows floating like in the photos of @mau_igna_06. This is the same as the pin button on the 3D views that pops up a floating widget and the size is limited to its contents.
The problems are of course the same too. Sometimes the floating widget appears on the main monitor when Slicer is opened on the second monitor, they sometimes stay on the screen when Slicer is minimized, they show up earlier/later compared to the view, etc.
These problems can be so annoying that in another project we added “floating” widgets on a widget that has the same background color as the view and positioned right above the view. So unless you have something behind the widgets in the view, it gives the impression that that the buttons are floating in the view. I know that’s not an ideal solution either, but it looks better.
It would be nice if either Qt or VTK offered a stable solution to float widgets in views. It’s quite common in commercial applications. It is possible that those applications don’t have a stable solution either, but they have better control over what application is opened on what screen, they don’t switch between applications, and they keep underlying libraries on the same version.

3 Likes

Hi, sorry to interrupt. I encountered the exactly same problem as you do. I wonder if the problem was fixed in the end? Thank you so much.

It was done like it was explained here by Tamas

1 Like

I want to add some toggle buttons just over 3dView to control some items visibility. It should be something similar but more simple as this.
Qt is bit complex to me to manage and I undertand what @ungi explain but I don´t know how to code it.
Do you have any example to start with?

Thanks in advance!

Maybe try combining a QToolButton with a QMenu

Thanks @mau_igna_06 .

I get stacked getting the main widget of the 3DView to add the button.
Following your code:

controllerWidget = slicer.app.layoutManager().threeDWidget(0).threeDController()
buttonLayout = slicer.util.findChild(controllerWidget, 'qMRMLThreeDViewControllerWidget').layout()
button = qt.QToolButton()
button.text="test"
buttonLayout.addWidget(button)

I get a button in the controllerWidget like this
image

How should be possible to add it to the mainWidget? And how to manage its possition? I really don´t need to move from here any more, maybe just show/hide it.

I think you are in a good path. You’ll get it :slight_smile:

Ok, I´m near but not as good looking as I should like…

My version from @ungi, @Sunderlandkyl and @mau_igna_06 (thanks to all of them):

mainWidget = slicer.app.layoutManager().threeDWidget(0)
bar = slicer.util.findChild(mainWidget, 'BarWidget')
popupLayout = qt.QVBoxLayout()
popupWidget = ctk.ctkPopupWidget(bar)
popupWidget.setLayout(popupLayout)
popupWidget.pinPopup(True)
popupWidget.setVisible(True)
popupWidget.setFixedWidth(80)

playButton = qt.QToolButton()
playButton.text=""
playButton.setCheckable(True)
#playButton.connect('clicked(bool)', self.onPlayButton)
playButton.setIcon(qt.QIcon('c://Icons/play.png'))
playButton.setAutoRaise(True)
playButton.setIconSize(qt.QSize(70, 70))
popupLayout.addWidget(playButton)

pauseButton = qt.QToolButton()
pauseButton.text=""
pauseButton.setCheckable(True)
#pauseButton.connect('clicked(bool)', self.onPauseButton)
pauseButton.setIcon(qt.QIcon('c://Icons/pause.png'))
pauseButton.setAutoRaise(True)
pauseButton.setIconSize(qt.QSize(70, 70))
popupLayout.addWidget(pauseButton)

stopButton = qt.QToolButton()
stopButton.text=""
stopButton.setCheckable(True)
#stopButton.connect('clicked(bool)', self.onStopButton)
stopButton.setIcon(qt.QIcon('c://Icons/stop.png'))
stopButton.setAutoRaise(True)
stopButton.setIconSize(qt.QSize(70, 70))
popupLayout.addWidget(stopButton)

Result:

I liked to be a transparent panel so I tried:

popupWidget.setStyleSheet("QWidget {background-color: transparent;}")

But result was even worst

Any idea how to fix transparency?
Should be possible to move it a bit down and right?
If transparency is not feasible, could be a rounded panel?
Sorry, but Qt is hard to me…
Thanks in advance!

Hi, this works for me to have transparent background (look at the setAttribute function). I’m not sure if you need to move the pop-up widget. You could just add padding and that will create a margin around the buttons because of the transparent background. Note that you may need to change the style of the button in different states, if you want to control the background in checked button state.

mainWidget = slicer.app.layoutManager().threeDWidget(0)
bar = slicer.util.findChild(mainWidget, 'BarWidget')
popupLayout = qt.QVBoxLayout()
popupWidget = ctk.ctkPopupWidget(bar)
popupWidget.setLayout(popupLayout)
popupWidget.pinPopup(True)
popupWidget.setVisible(True)
popupWidget.setFixedWidth(80)
popupWidget.setAttribute(qt.Qt.WA_TranslucentBackground)
popupWidget.setStyleSheet("QWidget {background-color: transparent;}")
playButton = qt.QToolButton()
playButton.text=""
playButton.setCheckable(True)
playButton.setIcon(qt.QIcon('c://Icons/play.png'))
playButton.setAutoRaise(True)
playButton.setIconSize(qt.QSize(70, 70))
popupLayout.addWidget(playButton)
1 Like

Wondwerfull! It looks pretty fine… Thanks so much @ungi .

For manage margins, I´ve made:

popupLayout.setContentsMargins(40,10,0,0) # (left,up,right,down)

playButton.setStyleSheet("QWidget:hover {background-color: transparent; border: none; padding:5px}")

I just find a problem…
When I make:

slicer.util.mainWindow().showFullScreen()

PopupWidget is not visible.
It looks to be in another layer because when I took exit button and slicer.util.confirmOkCancelDisplay is popup, I can see it…

Thanks in advance again…

That is why these popup widgets are generally not recommended. These occlusion problems happen regularly. Most times it is possible to find a workaround. But I don’t have experience with frameless main window. I hope you will find a solution.

1 Like

showFullScreen is a very special display mode, it completely changes the behavior of the application. If you just want your application to use the whole screen then it is better to resize it.