qtpyside2qlistwidgetqtreewidget

PySide2 - Qt - Disable drag selection when using ExtendedSelection on QListWidget / QTreeWidget / QTableWidget


I am trying to find a solution to use the ExtendedSelection (single selection and multiple selection by using ctrl and shift keys), but I want to prevent using the multiple selection by dragging the mouse up&down after a mouse click.

Working example:

import sys
from PySide2.QtWidgets import QListWidget, QApplication, QAbstractItemView

NAMES = ["sparrow", "robin", "crow", "raven", "woodpecker", "hummingbird"]


class CustomListWidget(QListWidget):

  def __init__(self):
    super().__init__()

    self.setSelectionMode(QAbstractItemView.ExtendedSelection)
    for name in NAMES:
        self.addItem(name)

  def mouseMoveEvent(self, event: QMouseEvent) -> None:
      if self.state() != QAbstractItemView.DragSelectingState:
          super().mouseMoveEvent(event)


if __name__ == '__main__':
  app = QApplication(sys.argv)
  custom_list = CustomListWidget()
  custom_list.show()
  sys.exit(app.exec_())

I tried by overriding the mouseMoveEvent to avoid propagating the mouseMove when the state is on DragSelectingState. This works most of the times, but if the user moves the mouse "very-quick", a couple of items can be selected at once. I'd like to completely disable selection by dragging and keep the ctrl + shift functionality.


Solution

  • Since the ExtendedSelection mode also updates the selection on mouse movements, this means that the update also happens as soon as the very first mouse move event is handled.

    At that point, the state() is still NoState: the user has just clicked some item, but has not moved the mouse before.

    Remember that mouse move event are not continuous, as there's no real movement involved (a physical distance that is actually traveled), it's an abstraction: we have the perception of "movement", but the cursor actually "jumps" to a different position, giving the illusion of movement, similarly to an animation in a cartoon.

    So, if the mouse is moved very fast after being clicked, even the first event could actually be on a new index. Note that this also happens if you click at the exact edge of an index and slightly move the mouse outside of that index.

    Now, what actually updates the selection in extended mode when the mouse is moved, is a call to setSelection(), which uses the visualRect() that contains the clicked index and the one under the new mouse position.

    Luckily, setSelection() is a virtual function, meaning that we can override it and Qt will call our own implementation: we just call the base implementation using a rectangle that has 0 height.

    Note that, theoretically, we should not update the given rectangle (it might be used by some other function, including the one that actually called setSelection()), so it's better to use a new one based on it:

        def setSelection(self, rect, flags):
            super().setSelection(QRect(rect.topLeft(), QSize(0, 0)), flags)