I’ve set up multiple shortcuts for each view, but they only work if I click to activate the view first. I’d like to improve my workflow by activating views on mouse hover instead, so shortcuts are immediately available without extra clicks.
Is there a way to enable view activation on hover?
Generally the views act like other widgets, like text entry, with a click-to-focus model. You can listen for window enter events and force the focus to change, but on some OSs in the past we found this also raised the application window if you moved the mouse through the view even if there was another application in front, which was annoying to users. Maybe you can find a way to have focus follow the mouse only if the Slicer application is in the foreground.
If, for example I’m in paint mode, what I do is use the space bar to toggle to the arrow cursor, then click in the view, then space bar again so that keyboard and mouse events to to the view of choice.
I’ve created a FocusOnHover class to set focus when the mouse enters or leaves the view. The print("enter") and print("leave") statements work as expected, but setFocus doesn’t seem to activate the view, and vtk shortcuts still aren’t working.
Here’s the code I’m using:
import qt
class FocusOnHover(qt.QObject):
def __init__(self, widget):
super().__init__()
self.widget = widget
# Install event filter
self.widget.installEventFilter(self)
def eventFilter(self, obj, event):
# Check event type
if obj == self.widget:
if event.type() == qt.QEvent.Enter:
# Set focus when mouse enters
print("enter")
self.widget.setFocus(qt.Qt.MouseFocusReason)
elif event.type() == qt.QEvent.Leave:
# Clear focus when mouse leaves
print("leave")
self.widget.clearFocus()
return False
layoutManager = slicer.app.layoutManager()
threeDView = layoutManager.threeDWidget(0).threeDView() # Select specific 3D view
hover_focus = FocusOnHover(threeDView)
I agree that it would be nice if some keyboard shortcuts could be applied to views without requiring clicking.
However, changing the focus by mouse hover is an operating system/window manager level decision that should not be overridden at application level. On Windows, “Activate a window by hovering over it with the mouse” is an accessibility feature that can be enabled/disabled in the operating system (it works only at application window level though, so it does not set the focus to a particular widget).
For a custom application, activating a view widget on hover could be fine. If we want to improve Slicer core behavior, we could do something like installing an event filter and capture unprocessed keyboard events. If an unprocessed event occurs then the focus can be set to the last active view widget (we should probably hightlight this widget with a frame) and the event could be sent to that widget.
Would you be able to spend a couple of weeks on this, developing mainly in C++? It would be great. We could help you along the way.
This could be done in 3 steps:
Implement the concept of “active” view node. The user would change which view is active by clicking in it, and it would be indicated by some decoration (probably a colored border around the view). Node ID of the active view would be stored in the selection node in the scene.
Implement an event filter that can detect events that are not processed by the Qt widget that currently has the focus. For example, if you are in a textbox and press r then you want to add that letter to the textbox, but if a button or slider has the focus then the active view should get a chance to process the event.
Implement changing of widget focus to the active view and forward the event to the active view, when there is an event that the currently focused Qt widget cannot process but the currently active view can process.
The first step would be to implement drawing of focus frame around a view. I quite like how it looks in ParaView:
Since ParaView uses the exact same libraries (Qt for GUI, VTK for rendering) and it is open-source software with permissive license, we can have a look how they implemented this frame and implement the same solution in Slicer. It seems that they simply have a Qt frame around the view and change the border visibility.
In Slicer, activeViewFrame() method (that would return the frame widget that can be used to show a border around the active view) could be added to qMRMLAbstractViewWidget.h (similarly to viewWidget() method). activeViewFrame() would be implemented in qMRMLSliceWidget, qMRMLThreeDWidget, qMRMLPlotWidget, and qMRMLTableWidget. The frame widget has to be added in qMRMLSliceWidget.ui, qMRMLThreeDWidget.cxx, qMRMLPlotWidget.cxx, and qMRMLTableWidget.cxx.
Once each widget can display a border, the next step is to connect it to MRML. The selection node stores the currently active view node. Each qMRMLAbstractViewWidget could observe the selection node and update border visibility (show the border if the active view node in the selection node is the same as the widget’s view node). Whenever a view widget receives the focus then it would updates the active node in the selection node.