Friday, February 21, 2014

Getting mouse position within QAbstractItemView

Spent a few hours trying to make a presenter for a context menu that I am making within my UI. What I wanted to do was to allow for the context menu to be dynamic to what it appears above, regardless of what is currently selected. The problem I faced was that I needed to get the position of the mouse, and then map that global position to the view that I am using, and get the index of the model that is at the position I am looking at.

In order, here is how I proceeded.

When I create the context menu for a view, there is signal triggered, that emits a QPoint of where the mouse has been clicked:
# Connect the signal and the slot
self.view_context_menu = QtGui.QMenu()

# The function to show the menu
def show_view_context_menu(self, pos):
    # We need to take the point we got from the signal, and map it to the world
    mapped_point = self.view.viewport().mapToGlobal(pos)
    # Now show the menu in the global space
All seems well and simple, we got our menu, and we displayed it. But now, I want to get the index that might be at that point, and change the context menu accordingly. Lets say for now, I just want to see if the index is a valid one or not.
# Connect this to run right before the menu appears

def set_menu_action_states(self):
    point = QtGui.QCursor.pos()
    index = self.get_item_at_point(point)
    print index.isValid()

def get_item_at_point(self, point):
    return self.view.indexAt(point)
Seems straightforward. However, what happens here, the point we get with QtGui.QCursor.pos() is in the world space of our monitor(s). So, whenever you will be trying to run this code, you will pretty much never get a valid index, because, as far as the view is concerned, you are not within it. So to solve this issue one has to transform the cursor position back into the view's coordinate system. But, you have to also remember something - all views in QT inherit the QAbstractItemView class, which in turn inherits the QAbstractScrollArea. What this means, is, the coordinate system of the actual view widget is a bit different then what we get when we are displaying the menu with customContextMenuRequested. The scroll area creates little widgets to drive the area display if some items in your view are hidden, allowing, well, for scrolling. Additionally, there are headers that the view posses, and those are different from what we see within the rest of the view. To be exact, everything we see withing the view, such as the view's items, are all sitting within the view's viewport coordinate system. If one has paid attention when we were showing the menu, we use the self.view.viewport().mapToGlobal() function to determine where the menu should appear. We have to do the same, but in reverse, when we are trying to find the indices under our mouse. So the corrected function looks like this:
def get_item_at_point(self, point):
    mapped_point = self.view.viewport().mapFromGlobal(point)
    return self.view.indexAt(mapped_point)
And that's it!

No comments:

Post a Comment